/** * Copyright (c) 2017 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 android.app; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; import static android.view.Display.INVALID_DISPLAY; import android.annotation.NonNull; import android.annotation.TestApi; import android.app.ActivityManager.StackInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.input.InputManager; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.IWindowManager; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceSession; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.inputmethod.InputMethodManager; import dalvik.system.CloseGuard; import java.util.List; /** * Activity container that allows launching activities into itself. *

Activity launching into this container is restricted by the same rules that apply to launching * on VirtualDisplays. * @hide */ @TestApi public class ActivityView extends ViewGroup { private static final String DISPLAY_NAME = "ActivityViewVirtualDisplay"; private static final String TAG = "ActivityView"; private VirtualDisplay mVirtualDisplay; private final SurfaceView mSurfaceView; /** * This is the root surface for the VirtualDisplay. The VirtualDisplay child surfaces will be * re-parented to this surface. This will also be a child of the SurfaceView's SurfaceControl. */ private SurfaceControl mRootSurfaceControl; private final SurfaceCallback mSurfaceCallback; private StateCallback mActivityViewCallback; private IActivityTaskManager mActivityTaskManager; // Temp container to store view coordinates in window. private final int[] mLocationInWindow = new int[2]; // The latest tap exclude region that we've sent to WM. private final Region mTapExcludeRegion = new Region(); private TaskStackListener mTaskStackListener; private final CloseGuard mGuard = CloseGuard.get(); private boolean mOpened; // Protected by mGuard. private final SurfaceControl.Transaction mTmpTransaction = new SurfaceControl.Transaction(); /** The ActivityView is only allowed to contain one task. */ private final boolean mSingleTaskInstance; private Insets mForwardedInsets; public ActivityView(Context context) { this(context, null /* attrs */); } public ActivityView(Context context, AttributeSet attrs) { this(context, attrs, 0 /* defStyle */); } public ActivityView(Context context, AttributeSet attrs, int defStyle) { this(context, attrs, defStyle, false /*singleTaskInstance*/); } public ActivityView( Context context, AttributeSet attrs, int defStyle, boolean singleTaskInstance) { super(context, attrs, defStyle); mSingleTaskInstance = singleTaskInstance; mActivityTaskManager = ActivityTaskManager.getService(); mSurfaceView = new SurfaceView(context); mSurfaceCallback = new SurfaceCallback(); mSurfaceView.getHolder().addCallback(mSurfaceCallback); addView(mSurfaceView); mOpened = true; mGuard.open("release"); } /** Callback that notifies when the container is ready or destroyed. */ public abstract static class StateCallback { /** * Called when the container is ready for launching activities. Calling * {@link #startActivity(Intent)} prior to this callback will result in an * {@link IllegalStateException}. * * @see #startActivity(Intent) */ public abstract void onActivityViewReady(ActivityView view); /** * Called when the container can no longer launch activities. Calling * {@link #startActivity(Intent)} after this callback will result in an * {@link IllegalStateException}. * * @see #startActivity(Intent) */ public abstract void onActivityViewDestroyed(ActivityView view); /** * Called when a task is created inside the container. * This is a filtered version of {@link TaskStackListener} */ public void onTaskCreated(int taskId, ComponentName componentName) { } /** * Called when a task is moved to the front of the stack inside the container. * This is a filtered version of {@link TaskStackListener} */ public void onTaskMovedToFront(int taskId) { } /** * Called when a task is about to be removed from the stack inside the container. * This is a filtered version of {@link TaskStackListener} */ public void onTaskRemovalStarted(int taskId) { } } /** * Set the callback to be notified about state changes. *

This class must finish initializing before {@link #startActivity(Intent)} can be called. *

Note: If the instance was ready prior to this call being made, then * {@link StateCallback#onActivityViewReady(ActivityView)} will be called from within * this method call. * * @param callback The callback to report events to. * * @see StateCallback * @see #startActivity(Intent) */ public void setCallback(StateCallback callback) { mActivityViewCallback = callback; if (mVirtualDisplay != null && mActivityViewCallback != null) { mActivityViewCallback.onActivityViewReady(this); } } /** * Sets the corner radius for the Activity displayed here. The corners will be * cropped from the window painted by the contained Activity. * * @param cornerRadius the radius for the corners, in pixels * @hide */ public void setCornerRadius(float cornerRadius) { mSurfaceView.setCornerRadius(cornerRadius); } /** * Launch a new activity into this container. *

Activity resolved by the provided {@link Intent} must have * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be * launched here. Also, if activity is not owned by the owner of this container, it must allow * embedding and the caller must have permission to embed. *

Note: This class must finish initializing and * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before * this method can be called. * * @param intent Intent used to launch an activity. * * @see StateCallback * @see #startActivity(PendingIntent) */ public void startActivity(@NonNull Intent intent) { final ActivityOptions options = prepareActivityOptions(); getContext().startActivity(intent, options.toBundle()); } /** * Launch a new activity into this container. *

Activity resolved by the provided {@link Intent} must have * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be * launched here. Also, if activity is not owned by the owner of this container, it must allow * embedding and the caller must have permission to embed. *

Note: This class must finish initializing and * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before * this method can be called. * * @param intent Intent used to launch an activity. * @param user The UserHandle of the user to start this activity for. * * * @see StateCallback * @see #startActivity(PendingIntent) */ public void startActivity(@NonNull Intent intent, UserHandle user) { final ActivityOptions options = prepareActivityOptions(); getContext().startActivityAsUser(intent, options.toBundle(), user); } /** * Launch a new activity into this container. *

Activity resolved by the provided {@link PendingIntent} must have * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be * launched here. Also, if activity is not owned by the owner of this container, it must allow * embedding and the caller must have permission to embed. *

Note: This class must finish initializing and * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before * this method can be called. * * @param pendingIntent Intent used to launch an activity. * * @see StateCallback * @see #startActivity(Intent) */ public void startActivity(@NonNull PendingIntent pendingIntent) { final ActivityOptions options = prepareActivityOptions(); try { pendingIntent.send(null /* context */, 0 /* code */, null /* intent */, null /* onFinished */, null /* handler */, null /* requiredPermission */, options.toBundle()); } catch (PendingIntent.CanceledException e) { throw new RuntimeException(e); } } /** * Launch a new activity into this container. *

Activity resolved by the provided {@link PendingIntent} must have * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be * launched here. Also, if activity is not owned by the owner of this container, it must allow * embedding and the caller must have permission to embed. *

Note: This class must finish initializing and * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before * this method can be called. * * @param pendingIntent Intent used to launch an activity. * @param options options for the activity * * @see StateCallback * @see #startActivity(Intent) */ public void startActivity(@NonNull PendingIntent pendingIntent, @NonNull ActivityOptions options) { options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId()); try { pendingIntent.send(null /* context */, 0 /* code */, null /* intent */, null /* onFinished */, null /* handler */, null /* requiredPermission */, options.toBundle()); } catch (PendingIntent.CanceledException e) { throw new RuntimeException(e); } } /** * Check if container is ready to launch and create {@link ActivityOptions} to target the * virtual display. */ private ActivityOptions prepareActivityOptions() { if (mVirtualDisplay == null) { throw new IllegalStateException( "Trying to start activity before ActivityView is ready."); } final ActivityOptions options = ActivityOptions.makeBasic(); options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId()); return options; } /** * Release this container. Activity launching will no longer be permitted. *

Note: Calling this method is allowed after * {@link StateCallback#onActivityViewReady(ActivityView)} callback was triggered and before * {@link StateCallback#onActivityViewDestroyed(ActivityView)}. * * @see StateCallback */ public void release() { if (mVirtualDisplay == null) { throw new IllegalStateException( "Trying to release container that is not initialized."); } performRelease(); } /** * Triggers an update of {@link ActivityView}'s location in window to properly set tap exclude * regions and avoid focus switches by touches on this view. */ public void onLocationChanged() { updateLocationAndTapExcludeRegion(); } private void clearActivityViewGeometryForIme() { if (mVirtualDisplay == null) { return; } final int displayId = mVirtualDisplay.getDisplay().getDisplayId(); mContext.getSystemService(InputMethodManager.class).reportActivityView(displayId, null); } @Override public void onLayout(boolean changed, int l, int t, int r, int b) { mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */); } @Override public boolean gatherTransparentRegion(Region region) { // The tap exclude region may be affected by any view on top of it, so we detect the // possible change by monitoring this function. updateLocationAndTapExcludeRegion(); return super.gatherTransparentRegion(region); } /** * Sends current location in window and tap exclude region to WM for this view. */ private void updateLocationAndTapExcludeRegion() { if (mVirtualDisplay == null || !isAttachedToWindow()) { return; } try { int x = mLocationInWindow[0]; int y = mLocationInWindow[1]; getLocationInWindow(mLocationInWindow); if (x != mLocationInWindow[0] || y != mLocationInWindow[1]) { x = mLocationInWindow[0]; y = mLocationInWindow[1]; final int displayId = mVirtualDisplay.getDisplay().getDisplayId(); WindowManagerGlobal.getWindowSession().updateDisplayContentLocation( getWindow(), x, y, displayId); // Also report this geometry information to InputMethodManagerService. // TODO(b/115693908): Unify this logic into the above WMS-based one. final Matrix matrix = new Matrix(); matrix.set(getMatrix()); matrix.postTranslate(x, y); mContext.getSystemService(InputMethodManager.class) .reportActivityView(displayId, matrix); } updateTapExcludeRegion(x, y); } catch (RemoteException e) { e.rethrowAsRuntimeException(); } } /** Computes and sends current tap exclude region to WM for this view. */ private void updateTapExcludeRegion(int x, int y) throws RemoteException { if (!canReceivePointerEvents()) { cleanTapExcludeRegion(); return; } mTapExcludeRegion.set(x, y, x + getWidth(), y + getHeight()); // There might be views on top of us. We need to subtract those areas from the tap // exclude region. final ViewParent parent = getParent(); if (parent != null) { parent.subtractObscuredTouchableRegion(mTapExcludeRegion, this); } WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(), mTapExcludeRegion); } private class SurfaceCallback implements SurfaceHolder.Callback { @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { if (mVirtualDisplay == null) { initVirtualDisplay(new SurfaceSession()); if (mVirtualDisplay != null && mActivityViewCallback != null) { mActivityViewCallback.onActivityViewReady(ActivityView.this); } } else { mTmpTransaction.reparent(mRootSurfaceControl, mSurfaceView.getSurfaceControl()).apply(); } if (mVirtualDisplay != null) { mVirtualDisplay.setDisplayState(true); } updateLocationAndTapExcludeRegion(); } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) { if (mVirtualDisplay != null) { mVirtualDisplay.resize(width, height, getBaseDisplayDensity()); } updateLocationAndTapExcludeRegion(); } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { if (mVirtualDisplay != null) { mVirtualDisplay.setDisplayState(false); } clearActivityViewGeometryForIme(); cleanTapExcludeRegion(); } } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); mSurfaceView.setVisibility(visibility); } /** * @return the display id of the virtual display. */ public int getVirtualDisplayId() { if (mVirtualDisplay != null) { return mVirtualDisplay.getDisplay().getDisplayId(); } return INVALID_DISPLAY; } /** * Injects a pair of down/up key events with keycode {@link KeyEvent#KEYCODE_BACK} to the * virtual display. */ public void performBackPress() { if (mVirtualDisplay == null) { return; } final int displayId = mVirtualDisplay.getDisplay().getDisplayId(); final InputManager im = InputManager.getInstance(); im.injectInputEvent(createKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK, displayId), InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); im.injectInputEvent(createKeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK, displayId), InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } private static KeyEvent createKeyEvent(int action, int code, int displayId) { long when = SystemClock.uptimeMillis(); final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); ev.setDisplayId(displayId); return ev; } private void initVirtualDisplay(SurfaceSession surfaceSession) { if (mVirtualDisplay != null) { throw new IllegalStateException("Trying to initialize for the second time."); } final int width = mSurfaceView.getWidth(); final int height = mSurfaceView.getHeight(); final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); mVirtualDisplay = displayManager.createVirtualDisplay( DISPLAY_NAME + "@" + System.identityHashCode(this), width, height, getBaseDisplayDensity(), null, VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL); if (mVirtualDisplay == null) { Log.e(TAG, "Failed to initialize ActivityView"); return; } final int displayId = mVirtualDisplay.getDisplay().getDisplayId(); final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); mRootSurfaceControl = new SurfaceControl.Builder(surfaceSession) .setContainerLayer() .setParent(mSurfaceView.getSurfaceControl()) .setName(DISPLAY_NAME) .build(); try { // TODO: Find a way to consolidate these calls to the server. WindowManagerGlobal.getWindowSession().reparentDisplayContent( getWindow(), mRootSurfaceControl, displayId); wm.dontOverrideDisplayInfo(displayId); if (mSingleTaskInstance) { mActivityTaskManager.setDisplayToSingleTaskInstance(displayId); } wm.setForwardedInsets(displayId, mForwardedInsets); } catch (RemoteException e) { e.rethrowAsRuntimeException(); } mTmpTransaction.show(mRootSurfaceControl).apply(); mTaskStackListener = new TaskStackListenerImpl(); try { mActivityTaskManager.registerTaskStackListener(mTaskStackListener); } catch (RemoteException e) { Log.e(TAG, "Failed to register task stack listener", e); } } private void performRelease() { if (!mOpened) { return; } mSurfaceView.getHolder().removeCallback(mSurfaceCallback); cleanTapExcludeRegion(); if (mTaskStackListener != null) { try { mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); } catch (RemoteException e) { Log.e(TAG, "Failed to unregister task stack listener", e); } mTaskStackListener = null; } final boolean displayReleased; if (mVirtualDisplay != null) { mVirtualDisplay.release(); mVirtualDisplay = null; displayReleased = true; } else { displayReleased = false; } if (displayReleased && mActivityViewCallback != null) { mActivityViewCallback.onActivityViewDestroyed(this); } mGuard.close(); mOpened = false; } /** Report to server that tap exclude region on hosting display should be cleared. */ private void cleanTapExcludeRegion() { if (!isAttachedToWindow() || mTapExcludeRegion.isEmpty()) { return; } // Update tap exclude region with a null region to clean the state on server. try { WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(), null /* region */); mTapExcludeRegion.setEmpty(); } catch (RemoteException e) { e.rethrowAsRuntimeException(); } } /** Get density of the hosting display. */ private int getBaseDisplayDensity() { final WindowManager wm = mContext.getSystemService(WindowManager.class); final DisplayMetrics metrics = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(metrics); return metrics.densityDpi; } @Override protected void finalize() throws Throwable { try { if (mGuard != null) { mGuard.warnIfOpen(); performRelease(); } } finally { super.finalize(); } } /** * Set forwarded insets on the virtual display. * * @see IWindowManager#setForwardedInsets */ public void setForwardedInsets(Insets insets) { mForwardedInsets = insets; if (mVirtualDisplay == null) { return; } try { final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); wm.setForwardedInsets(mVirtualDisplay.getDisplay().getDisplayId(), mForwardedInsets); } catch (RemoteException e) { e.rethrowAsRuntimeException(); } } /** * A task change listener that detects background color change of the topmost stack on our * virtual display and updates the background of the surface view. This background will be shown * when surface view is resized, but the app hasn't drawn its content in new size yet. * It also calls StateCallback.onTaskMovedToFront to notify interested parties that the stack * associated with the {@link ActivityView} has had a Task moved to the front. This is useful * when needing to also bring the host Activity to the foreground at the same time. */ private class TaskStackListenerImpl extends TaskStackListener { @Override public void onTaskDescriptionChanged(ActivityManager.RunningTaskInfo taskInfo) throws RemoteException { if (mVirtualDisplay == null || taskInfo.displayId != mVirtualDisplay.getDisplay().getDisplayId()) { return; } StackInfo stackInfo = getTopMostStackInfo(); if (stackInfo == null) { return; } // Found the topmost stack on target display. Now check if the topmost task's // description changed. if (taskInfo.taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) { mSurfaceView.setResizeBackgroundColor( taskInfo.taskDescription.getBackgroundColor()); } } @Override public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) throws RemoteException { if (mActivityViewCallback == null || mVirtualDisplay == null || taskInfo.displayId != mVirtualDisplay.getDisplay().getDisplayId()) { return; } StackInfo stackInfo = getTopMostStackInfo(); // if StackInfo was null or unrelated to the "move to front" then there's no use // notifying the callback if (stackInfo != null && taskInfo.taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) { mActivityViewCallback.onTaskMovedToFront(taskInfo.taskId); } } @Override public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException { if (mActivityViewCallback == null || mVirtualDisplay == null) { return; } StackInfo stackInfo = getTopMostStackInfo(); // if StackInfo was null or unrelated to the task creation then there's no use // notifying the callback if (stackInfo != null && taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) { mActivityViewCallback.onTaskCreated(taskId, componentName); } } @Override public void onTaskRemovalStarted(ActivityManager.RunningTaskInfo taskInfo) throws RemoteException { if (mActivityViewCallback == null || mVirtualDisplay == null || taskInfo.displayId != mVirtualDisplay.getDisplay().getDisplayId()) { return; } mActivityViewCallback.onTaskRemovalStarted(taskInfo.taskId); } private StackInfo getTopMostStackInfo() throws RemoteException { // Find the topmost task on our virtual display - it will define the background // color of the surface view during resizing. final int displayId = mVirtualDisplay.getDisplay().getDisplayId(); final List stackInfoList = mActivityTaskManager.getAllStackInfos(); // Iterate through stacks from top to bottom. final int stackCount = stackInfoList.size(); for (int i = 0; i < stackCount; i++) { final StackInfo stackInfo = stackInfoList.get(i); // Only look for stacks on our virtual display. if (stackInfo.displayId != displayId) { continue; } // Found the topmost stack on target display. return stackInfo; } return null; } } }