/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.pip; import static android.util.TypedValue.COMPLEX_UNIT_DIP; import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.TaskSnapshot; /** * Represents the content overlay used during the entering PiP animation. */ public abstract class PipContentOverlay { // Fixed string used in WMShellFlickerTests protected static final String LAYER_NAME = "PipContentOverlay"; protected SurfaceControl mLeash; /** Attaches the internal {@link #mLeash} to the given parent leash. */ public abstract void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash); /** Detaches the internal {@link #mLeash} from its parent by removing itself. */ public void detach(SurfaceControl.Transaction tx) { if (mLeash != null && mLeash.isValid()) { tx.remove(mLeash); tx.apply(); } } @Nullable public SurfaceControl getLeash() { return mLeash; } /** * Animates the internal {@link #mLeash} by a given fraction. * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly * call apply on this transaction, it should be applied on the caller side. * @param currentBounds {@link Rect} of the current animation bounds. * @param fraction progress of the animation ranged from 0f to 1f. */ public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction); /** * Callback when reaches the end of animation on the internal {@link #mLeash}. * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly * call apply on this transaction, it should be applied on the caller side. * @param destinationBounds {@link Rect} of the final bounds. */ public abstract void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds); /** A {@link PipContentOverlay} uses solid color. */ public static final class PipColorOverlay extends PipContentOverlay { private static final String TAG = PipColorOverlay.class.getSimpleName(); private final Context mContext; public PipColorOverlay(Context context) { mContext = context; mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) .setName(LAYER_NAME) .setColorLayer() .build(); } @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); tx.setColor(mLeash, getContentOverlayColor(mContext)); tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); } @Override public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction) { atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } @Override public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { // Do nothing. Color overlay should be fully opaque by now. } private float[] getContentOverlayColor(Context context) { final TypedArray ta = context.obtainStyledAttributes(new int[] { android.R.attr.colorBackground }); try { int colorAccent = ta.getColor(0, 0); return new float[] { Color.red(colorAccent) / 255f, Color.green(colorAccent) / 255f, Color.blue(colorAccent) / 255f }; } finally { ta.recycle(); } } } /** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */ public static final class PipSnapshotOverlay extends PipContentOverlay { private static final String TAG = PipSnapshotOverlay.class.getSimpleName(); private final TaskSnapshot mSnapshot; private final Rect mSourceRectHint; public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { mSnapshot = snapshot; mSourceRectHint = new Rect(sourceRectHint); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) .setName(LAYER_NAME) .build(); } @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { final float taskSnapshotScaleX = (float) mSnapshot.getTaskSize().x / mSnapshot.getHardwareBuffer().getWidth(); final float taskSnapshotScaleY = (float) mSnapshot.getTaskSize().y / mSnapshot.getHardwareBuffer().getHeight(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer()); // Relocate the content to parentLeash's coordinates. tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top); tx.setScale(mLeash, taskSnapshotScaleX, taskSnapshotScaleY); tx.reparent(mLeash, parentLeash); tx.apply(); } @Override public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction) { // Do nothing. Keep the snapshot till animation ends. } @Override public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { atomicTx.remove(mLeash); } } /** A {@link PipContentOverlay} shows app icon on solid color background. */ public static final class PipAppIconOverlay extends PipContentOverlay { private static final String TAG = PipAppIconOverlay.class.getSimpleName(); // The maximum size for app icon in pixel. private static final int MAX_APP_ICON_SIZE_DP = 72; private final Context mContext; private final int mAppIconSizePx; private final Rect mAppBounds; private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; private Bitmap mBitmap; public PipAppIconOverlay(Context context, Rect appBounds, Drawable appIcon, int appIconSizePx) { mContext = context; final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx); mAppBounds = new Rect(appBounds); mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(), Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); mLeash = new SurfaceControl.Builder(new SurfaceSession()) .setCallsite(TAG) .setName(LAYER_NAME) .build(); } @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); tx.reparent(mLeash, parentLeash); tx.apply(); } @Override public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction) { mTmpTransform.reset(); // Scale back the bitmap with the pivot point at center. mTmpTransform.postScale( (float) mAppBounds.width() / currentBounds.width(), (float) mAppBounds.height() / currentBounds.height(), mAppBounds.centerX(), mAppBounds.centerY()); atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } @Override public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) { atomicTx.remove(mLeash); } @Override public void detach(SurfaceControl.Transaction tx) { super.detach(tx); if (mBitmap != null && !mBitmap.isRecycled()) { mBitmap.recycle(); } } private void prepareAppIconOverlay(Drawable appIcon) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); final TypedArray ta = mContext.obtainStyledAttributes(new int[] { android.R.attr.colorBackground }); try { int colorAccent = ta.getColor(0, 0); canvas.drawRGB( Color.red(colorAccent), Color.green(colorAccent), Color.blue(colorAccent)); } finally { ta.recycle(); } final Rect appIconBounds = new Rect( mAppBounds.centerX() - mAppIconSizePx / 2, mAppBounds.centerY() - mAppIconSizePx / 2, mAppBounds.centerX() + mAppIconSizePx / 2, mAppBounds.centerY() + mAppIconSizePx / 2); appIcon.setBounds(appIconBounds); appIcon.draw(canvas); mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); } } }