diff options
author | Winson Chung <winsonc@google.com> | 2020-03-21 23:02:48 -0700 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2020-03-30 23:29:08 +0000 |
commit | a1f869d10e2a0ed1dbaff29eb67b4d0adc694242 (patch) | |
tree | 84222279f4dc36e0a195c1acf672067d6bec1b3d | |
parent | 7873b3875a2d23e233ed9155b7158af81353e194 (diff) |
Add task organizer based task embedder
- Split TaskEmbedder into its current VirtualDisplay implementation
and an implementation that uses task org to create and manage
the task
- Use the task org embedder implementation in separate bubble task view
- Skip task org tasks from triggering task resizing
- Add task org callback for back press on task root if requested
Bug: 148977538
Test: atest CtsWindowManagerDeviceTestCases:ActivityViewTest
Test: atest WmTests:TaskOrganizerTests
Change-Id: Id422bb2547197c617f914ed7cf5085e02a1c3fb5
17 files changed, 1266 insertions, 384 deletions
diff --git a/core/java/android/app/ActivityView.java b/core/java/android/app/ActivityView.java index aaab6b4099e5..073b8d0165a9 100644 --- a/core/java/android/app/ActivityView.java +++ b/core/java/android/app/ActivityView.java @@ -69,6 +69,7 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { // For Host private final Point mWindowPosition = new Point(); private final int[] mTmpArray = new int[2]; + private final Rect mTmpRect = new Rect(); private final Matrix mScreenSurfaceMatrix = new Matrix(); private final Region mTapExcludeRegion = new Region(); @@ -84,10 +85,14 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { this(context, attrs, defStyle, false /*singleTaskInstance*/); } - public ActivityView( - Context context, AttributeSet attrs, int defStyle, boolean singleTaskInstance) { + public ActivityView(Context context, AttributeSet attrs, int defStyle, + boolean singleTaskInstance) { super(context, attrs, defStyle); - mTaskEmbedder = new TaskEmbedder(getContext(), this, singleTaskInstance); + if (useTaskOrganizer()) { + mTaskEmbedder = new TaskOrganizerTaskEmbedder(context, this); + } else { + mTaskEmbedder = new VirtualDisplayTaskEmbedder(context, this, singleTaskInstance); + } mSurfaceView = new SurfaceView(context); // Since ActivityView#getAlpha has been overridden, we should use parent class's alpha // as master to synchronize surface view's alpha value. @@ -129,6 +134,12 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { public void onTaskCreated(int taskId, ComponentName componentName) { } /** + * Called when a task visibility changes. + * @hide + */ + public void onTaskVisibilityChanged(int taskId, boolean visible) { } + + /** * Called when a task is moved to the front of the stack inside the container. * This is a filtered version of {@link TaskStackListener} */ @@ -139,6 +150,12 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { * This is a filtered version of {@link TaskStackListener} */ public void onTaskRemovalStarted(int taskId) { } + + /** + * Called when back is pressed on the root activity of the task. + * @hide + */ + public void onBackPressedOnTaskRoot(int taskId) { } } /** @@ -370,10 +387,8 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { @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. - mTaskEmbedder.notifyBoundsChanged(); - return super.gatherTransparentRegion(region); + return mTaskEmbedder.gatherTransparentRegion(region) + || super.gatherTransparentRegion(region); } private class SurfaceCallback implements SurfaceHolder.Callback { @@ -432,7 +447,6 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { Log.e(TAG, "Failed to initialize ActivityView"); return false; } - mTmpTransaction.show(mTaskEmbedder.getSurfaceControl()).apply(); return true; } @@ -520,6 +534,13 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { /** @hide */ @Override + public Rect getScreenBounds() { + getBoundsOnScreen(mTmpRect); + return mTmpRect; + } + + /** @hide */ + @Override public IWindow getWindow() { return super.getWindow(); } @@ -530,6 +551,15 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { return super.canReceivePointerEvents(); } + /** + * Overridden by instances that require the use of the task organizer implementation instead of + * the virtual display implementation. Not for general use. + * @hide + */ + protected boolean useTaskOrganizer() { + return false; + } + private final class StateCallbackAdapter implements TaskEmbedder.Listener { private final StateCallback mCallback; @@ -553,6 +583,11 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { } @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + mCallback.onTaskVisibilityChanged(taskId, visible); + } + + @Override public void onTaskMovedToFront(int taskId) { mCallback.onTaskMovedToFront(taskId); } @@ -561,5 +596,10 @@ public class ActivityView extends ViewGroup implements TaskEmbedder.Host { public void onTaskRemovalStarted(int taskId) { mCallback.onTaskRemovalStarted(taskId); } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + mCallback.onBackPressedOnTaskRoot(taskId); + } } } diff --git a/core/java/android/app/TaskEmbedder.java b/core/java/android/app/TaskEmbedder.java index b8ad30840173..10c11f2e2cac 100644 --- a/core/java/android/app/TaskEmbedder.java +++ b/core/java/android/app/TaskEmbedder.java @@ -16,11 +16,9 @@ package android.app; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -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.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; @@ -33,38 +31,24 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; 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.DisplayMetrics; import android.util.Log; -import android.view.Display; import android.view.IWindow; import android.view.IWindowManager; -import android.view.IWindowSession; -import android.view.InputDevice; -import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.SurfaceControl; -import android.view.WindowManagerGlobal; -import android.view.inputmethod.InputMethodManager; import dalvik.system.CloseGuard; -import java.util.List; - /** * A component which handles embedded display of tasks within another window. The embedded task can * be presented using the SurfaceControl provided from {@link #getSurfaceControl()}. * * @hide */ -public class TaskEmbedder { +public abstract class TaskEmbedder { private static final String TAG = "TaskEmbedder"; - private static final String DISPLAY_NAME = "TaskVirtualDisplay"; /** * A component which will host the task. @@ -82,6 +66,9 @@ public class TaskEmbedder { /** @return the x/y offset from the origin of the window to the surface */ Point getPositionInWindow(); + /** @return the screen bounds of the host */ + Rect getScreenBounds(); + /** @return whether this surface is able to receive pointer events */ boolean canReceivePointerEvents(); @@ -96,6 +83,11 @@ public class TaskEmbedder { * fill unpainted areas if necessary. */ void onTaskBackgroundColorChanged(TaskEmbedder ts, int bgColor); + + /** + * Posts a runnable to be run on the host's handler. + */ + boolean post(Runnable r); } /** @@ -111,27 +103,29 @@ public class TaskEmbedder { /** Called when a task is created inside the container. */ default void onTaskCreated(int taskId, ComponentName name) {} + /** Called when a task visibility changes. */ + default void onTaskVisibilityChanged(int taskId, boolean visible) {} + /** Called when a task is moved to the front of the stack inside the container. */ default void onTaskMovedToFront(int taskId) {} /** Called when a task is about to be removed from the stack inside the container. */ default void onTaskRemovalStarted(int taskId) {} + + /** Called when a task is created inside the container. */ + default void onBackPressedOnTaskRoot(int taskId) {} } - private IActivityTaskManager mActivityTaskManager = ActivityTaskManager.getService(); - - private final Context mContext; - private TaskEmbedder.Host mHost; - private int mDisplayDensityDpi; - private final boolean mSingleTaskInstance; - private SurfaceControl.Transaction mTransaction; - private SurfaceControl mSurfaceControl; - private VirtualDisplay mVirtualDisplay; - private Insets mForwardedInsets; - private TaskStackListener mTaskStackListener; - private Listener mListener; - private boolean mOpened; // Protected by mGuard. - private DisplayMetrics mTmpDisplayMetrics; + protected IActivityTaskManager mActivityTaskManager = ActivityTaskManager.getService(); + + protected final Context mContext; + protected TaskEmbedder.Host mHost; + + protected SurfaceControl.Transaction mTransaction; + protected SurfaceControl mSurfaceControl; + protected TaskStackListener mTaskStackListener; + protected Listener mListener; + protected boolean mOpened; // Protected by mGuard. private final CloseGuard mGuard = CloseGuard.get(); @@ -141,51 +135,25 @@ public class TaskEmbedder { * * @param context the context * @param host the host for this embedded task - * @param singleTaskInstance whether to apply a single-task constraint to this container */ - public TaskEmbedder(Context context, TaskEmbedder.Host host, boolean singleTaskInstance) { + public TaskEmbedder(Context context, TaskEmbedder.Host host) { mContext = context; mHost = host; - mSingleTaskInstance = singleTaskInstance; } /** - * Whether this container has been initialized. - * - * @return true if initialized - */ - public boolean isInitialized() { - return mVirtualDisplay != null; - } - - /** - * Initialize this container. + * Initialize this container when the ActivityView's SurfaceView is first created. * * @param parent the surface control for the parent surface * @return true if initialized successfully */ public boolean initialize(SurfaceControl parent) { - if (mVirtualDisplay != null) { + if (isInitialized()) { throw new IllegalStateException("Trying to initialize for the second time."); } mTransaction = new SurfaceControl.Transaction(); - - final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - mDisplayDensityDpi = getBaseDisplayDensity(); - - mVirtualDisplay = displayManager.createVirtualDisplay( - DISPLAY_NAME + "@" + System.identityHashCode(this), mHost.getWidth(), - mHost.getHeight(), mDisplayDensityDpi, 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 TaskEmbedder"); - return false; - } - - // Create a container surface to which the DisplayContent will be reparented + // Create a container surface to which the task content will be reparented final String name = "TaskEmbedder - " + Integer.toHexString(System.identityHashCode(this)); mSurfaceControl = new SurfaceControl.Builder() .setContainerLayer() @@ -193,40 +161,96 @@ public class TaskEmbedder { .setName(name) .build(); - final int displayId = getDisplayId(); + if (!onInitialize()) { + return false; + } - final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + mTaskStackListener = createTaskStackListener(); try { - // TODO: Find a way to consolidate these calls to the server. - WindowManagerGlobal.getWindowSession().reparentDisplayContent( - mHost.getWindow(), mSurfaceControl, displayId); - wm.dontOverrideDisplayInfo(displayId); - if (mSingleTaskInstance) { - mContext.getSystemService(ActivityTaskManager.class) - .setDisplayToSingleTaskInstance(displayId); - } - setForwardedInsets(mForwardedInsets); - if (mHost.getWindow() != null) { - updateLocationAndTapExcludeRegion(); - } - mTaskStackListener = new TaskStackListenerImpl(); - try { - mActivityTaskManager.registerTaskStackListener(mTaskStackListener); - } catch (RemoteException e) { - Log.e(TAG, "Failed to register task stack listener", e); - } + mActivityTaskManager.registerTaskStackListener(mTaskStackListener); } catch (RemoteException e) { - e.rethrowAsRuntimeException(); + Log.e(TAG, "Failed to register task stack listener", e); } - if (mListener != null && mVirtualDisplay != null) { + if (mListener != null && isInitialized()) { mListener.onInitialized(); } mOpened = true; mGuard.open("release"); + mTransaction.show(getSurfaceControl()).apply(); return true; } /** + * @return the task stack listener for this embedder + */ + public abstract TaskStackListener createTaskStackListener(); + + /** + * Whether this container has been initialized. + * + * @return true if initialized + */ + public abstract boolean isInitialized(); + + /** + * Called when the task embedder should be initialized. + * @return whether to report whether the embedder was initialized. + */ + public abstract boolean onInitialize(); + + /** + * Called when the task embedder should be released. + * @return whether to report whether the embedder was released. + */ + protected abstract boolean onRelease(); + + /** + * Starts presentation of tasks in this container. + */ + public abstract void start(); + + /** + * Stops presentation of tasks in this container. + */ + public abstract void stop(); + + /** + * This should be called whenever the position or size of the surface changes + * or if touchable areas above the surface are added or removed. + */ + public abstract void notifyBoundsChanged(); + + /** + * Called to update the dimensions whenever the host size changes. + * + * @param width the new width of the surface + * @param height the new height of the surface + */ + public void resizeTask(int width, int height) { + // Do nothing + } + + /** + * Injects a pair of down/up key events with keycode {@link KeyEvent#KEYCODE_BACK} to the + * virtual display. + */ + public abstract void performBackPress(); + + /** + * An opaque unique identifier for this task surface among others being managed by the app. + */ + public abstract int getId(); + + /** + * Calculates and updates the {@param region} with the transparent region for this task + * embedder. + */ + public boolean gatherTransparentRegion(Region region) { + // Do nothing + return false; + } + + /** * Returns the surface control for the task surface. This should be parented to a screen * surface for display/embedding purposes. * @@ -236,34 +260,17 @@ public class TaskEmbedder { return mSurfaceControl; } + public int getDisplayId() { + return INVALID_DISPLAY; + } + /** - * Set forwarded insets on the virtual display. + * Set forwarded insets on the task content. * * @see IWindowManager#setForwardedInsets */ public void setForwardedInsets(Insets insets) { - mForwardedInsets = insets; - if (mVirtualDisplay == null) { - return; - } - try { - final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); - wm.setForwardedInsets(getDisplayId(), mForwardedInsets); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } - } - - /** An opaque unique identifier for this task surface among others being managed by the app. */ - public int getId() { - return getDisplayId(); - } - - int getDisplayId() { - if (mVirtualDisplay != null) { - return mVirtualDisplay.getDisplay().getDisplayId(); - } - return Display.INVALID_DISPLAY; + // Do nothing } /** @@ -372,166 +379,19 @@ public class TaskEmbedder { * Check if container is ready to launch and modify {@param options} to target the virtual * display, creating them if necessary. */ - private ActivityOptions prepareActivityOptions(ActivityOptions options) { - if (mVirtualDisplay == null) { + @CallSuper + protected ActivityOptions prepareActivityOptions(ActivityOptions options) { + if (!isInitialized()) { throw new IllegalStateException( "Trying to start activity before ActivityView is ready."); } if (options == null) { options = ActivityOptions.makeBasic(); } - options.setLaunchDisplayId(getDisplayId()); - options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - options.setTaskAlwaysOnTop(true); return options; } /** - * Stops presentation of tasks in this container. - */ - public void stop() { - if (mVirtualDisplay != null) { - mVirtualDisplay.setDisplayState(false); - clearActivityViewGeometryForIme(); - clearTapExcludeRegion(); - } - } - - /** - * Starts presentation of tasks in this container. - */ - public void start() { - if (mVirtualDisplay != null) { - mVirtualDisplay.setDisplayState(true); - updateLocationAndTapExcludeRegion(); - } - } - - /** - * This should be called whenever the position or size of the surface changes - * or if touchable areas above the surface are added or removed. - */ - public void notifyBoundsChanged() { - updateLocationAndTapExcludeRegion(); - } - - /** - * Updates position and bounds information needed by WM and IME to manage window - * focus and touch events properly. - * <p> - * This should be called whenever the position or size of the surface changes - * or if touchable areas above the surface are added or removed. - */ - private void updateLocationAndTapExcludeRegion() { - if (mVirtualDisplay == null || mHost.getWindow() == null) { - return; - } - reportLocation(mHost.getScreenToTaskMatrix(), mHost.getPositionInWindow()); - applyTapExcludeRegion(mHost.getWindow(), mHost.getTapExcludeRegion()); - } - - /** - * Call to update the position and transform matrix for the embedded surface. - * <p> - * This should not normally be called directly, but through - * {@link #updateLocationAndTapExcludeRegion()}. This method - * is provided as an optimization when managing multiple TaskSurfaces within a view. - * - * @param screenToViewMatrix the matrix/transform from screen space to view space - * @param positionInWindow the window-relative position of the surface - * - * @see InputMethodManager#reportActivityView(int, Matrix) - */ - private void reportLocation(Matrix screenToViewMatrix, Point positionInWindow) { - try { - final int displayId = getDisplayId(); - mContext.getSystemService(InputMethodManager.class) - .reportActivityView(displayId, screenToViewMatrix); - IWindowSession session = WindowManagerGlobal.getWindowSession(); - session.updateDisplayContentLocation(mHost.getWindow(), positionInWindow.x, - positionInWindow.y, displayId); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } - } - - /** - * Call to update the tap exclude region for the window. - * <p> - * This should not normally be called directly, but through - * {@link #updateLocationAndTapExcludeRegion()}. This method - * is provided as an optimization when managing multiple TaskSurfaces within a view. - * - * @see IWindowSession#updateTapExcludeRegion(IWindow, Region) - */ - private void applyTapExcludeRegion(IWindow window, @Nullable Region tapExcludeRegion) { - try { - IWindowSession session = WindowManagerGlobal.getWindowSession(); - session.updateTapExcludeRegion(window, tapExcludeRegion); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } - } - - /** - * @see InputMethodManager#reportActivityView(int, Matrix) - */ - private void clearActivityViewGeometryForIme() { - final int displayId = getDisplayId(); - mContext.getSystemService(InputMethodManager.class).reportActivityView(displayId, null); - } - - /** - * Removes the tap exclude region set by {@link #updateLocationAndTapExcludeRegion()}. - */ - private void clearTapExcludeRegion() { - if (mHost.getWindow() == null) { - Log.w(TAG, "clearTapExcludeRegion: not attached to window!"); - return; - } - applyTapExcludeRegion(mHost.getWindow(), null); - } - - /** - * Called to update the dimensions whenever the host size changes. - * - * @param width the new width of the surface - * @param height the new height of the surface - */ - public void resizeTask(int width, int height) { - mDisplayDensityDpi = getBaseDisplayDensity(); - if (mVirtualDisplay != null) { - mVirtualDisplay.resize(width, height, mDisplayDensityDpi); - } - } - - /** - * 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; - } - - /** * Releases the resources for this TaskEmbedder. Tasks will no longer be launchable * within this container. * @@ -539,9 +399,8 @@ public class TaskEmbedder { * triggered and before {@link Listener#onReleased()}. */ public void release() { - if (mVirtualDisplay == null) { - throw new IllegalStateException( - "Trying to release container that is not initialized."); + if (!isInitialized()) { + throw new IllegalStateException("Trying to release container that is not initialized."); } performRelease(); } @@ -550,14 +409,11 @@ public class TaskEmbedder { if (!mOpened) { return false; } + mTransaction.reparent(mSurfaceControl, null).apply(); mSurfaceControl.release(); - // Clear activity view geometry for IME on this display - clearActivityViewGeometryForIme(); - - // Clear tap-exclude region (if any) for this window. - clearTapExcludeRegion(); + boolean reportReleased = onRelease(); if (mTaskStackListener != null) { try { @@ -568,14 +424,6 @@ public class TaskEmbedder { mTaskStackListener = null; } - boolean reportReleased = false; - if (mVirtualDisplay != null) { - mVirtualDisplay.release(); - mVirtualDisplay = null; - reportReleased = true; - - } - if (mListener != null && reportReleased) { mListener.onReleased(); } @@ -595,98 +443,4 @@ public class TaskEmbedder { super.finalize(); } } - - /** Get density of the hosting display. */ - private int getBaseDisplayDensity() { - if (mTmpDisplayMetrics == null) { - mTmpDisplayMetrics = new DisplayMetrics(); - } - mContext.getDisplayNoVerify().getRealMetrics(mTmpDisplayMetrics); - return mTmpDisplayMetrics.densityDpi; - } - - /** - * 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 (!isInitialized() - || taskInfo.displayId != getDisplayId()) { - return; - } - - ActivityManager.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]) { - mHost.onTaskBackgroundColorChanged(TaskEmbedder.this, - taskInfo.taskDescription.getBackgroundColor()); - } - } - - @Override - public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) - throws RemoteException { - if (!isInitialized() || mListener == null - || taskInfo.displayId != getDisplayId()) { - return; - } - - ActivityManager.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]) { - mListener.onTaskMovedToFront(taskInfo.taskId); - } - } - - @Override - public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException { - if (mListener == null || !isInitialized()) { - return; - } - - ActivityManager.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]) { - mListener.onTaskCreated(taskId, componentName); - } - } - - @Override - public void onTaskRemovalStarted(ActivityManager.RunningTaskInfo taskInfo) - throws RemoteException { - if (mListener == null || !isInitialized() - || taskInfo.displayId != getDisplayId()) { - return; - } - mListener.onTaskRemovalStarted(taskInfo.taskId); - } - - private ActivityManager.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 = getDisplayId(); - final List<ActivityManager.StackInfo> stackInfoList = - mActivityTaskManager.getAllStackInfosOnDisplay(displayId); - if (stackInfoList.isEmpty()) { - return null; - } - return stackInfoList.get(0); - } - } } diff --git a/core/java/android/app/TaskOrganizerTaskEmbedder.java b/core/java/android/app/TaskOrganizerTaskEmbedder.java new file mode 100644 index 000000000000..adc07922154b --- /dev/null +++ b/core/java/android/app/TaskOrganizerTaskEmbedder.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2019 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.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.content.Context; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.Log; +import android.view.KeyEvent; +import android.view.SurfaceControl; +import android.window.ITaskOrganizer; +import android.window.IWindowContainer; +import android.window.WindowContainerTransaction; +import android.window.WindowOrganizer; +import android.window.WindowOrganizer.TaskOrganizer; + +/** + * A component which handles embedded display of tasks within another window. The embedded task can + * be presented using the SurfaceControl provided from {@link #getSurfaceControl()}. + * + * @hide + */ +public class TaskOrganizerTaskEmbedder extends TaskEmbedder { + private static final String TAG = "TaskOrgTaskEmbedder"; + private static final boolean DEBUG = false; + + private ITaskOrganizer.Stub mTaskOrganizer; + private ActivityManager.RunningTaskInfo mTaskInfo; + private IWindowContainer mTaskToken; + private SurfaceControl mTaskLeash; + private boolean mPendingNotifyBoundsChanged; + + /** + * Constructs a new TaskEmbedder. + * + * @param context the context + * @param host the host for this embedded task + */ + public TaskOrganizerTaskEmbedder(Context context, TaskOrganizerTaskEmbedder.Host host) { + super(context, host); + } + + @Override + public TaskStackListener createTaskStackListener() { + return new TaskStackListenerImpl(); + } + + /** + * Whether this container has been initialized. + * + * @return true if initialized + */ + @Override + public boolean isInitialized() { + return mTaskOrganizer != null; + } + + @Override + public boolean onInitialize() { + if (DEBUG) { + log("onInitialize"); + } + // Register the task organizer + mTaskOrganizer = new TaskOrganizerImpl(); + try { + // TODO(wm-shell): This currently prevents other organizers from controlling MULT_WINDOW + // windowing mode tasks. Plan is to migrate this to a wm-shell front-end when that + // infrastructure is ready. + TaskOrganizer.registerOrganizer(mTaskOrganizer, WINDOWING_MODE_MULTI_WINDOW); + TaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskOrganizer, true); + } catch (RemoteException e) { + Log.e(TAG, "Failed to initialize TaskEmbedder", e); + return false; + } + return true; + } + + @Override + protected boolean onRelease() { + if (DEBUG) { + log("onRelease"); + } + if (!isInitialized()) { + return false; + } + try { + TaskOrganizer.unregisterOrganizer(mTaskOrganizer); + } catch (RemoteException e) { + Log.e(TAG, "Failed to remove task"); + } + resetTaskInfo(); + return true; + } + + /** + * Starts presentation of tasks in this container. + */ + @Override + public void start() { + if (DEBUG) { + log("start"); + } + if (!isInitialized()) { + return; + } + if (mTaskToken == null) { + return; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setHidden(mTaskToken, false /* hidden */); + try { + WindowOrganizer.applyTransaction(wct); + } catch (RemoteException e) { + Log.e(TAG, "Failed to unset hidden in transaction"); + } + // TODO(b/151449487): Only call callback once we enable synchronization + if (mListener != null) { + mListener.onTaskVisibilityChanged(getTaskId(), true); + } + } + + /** + * Stops presentation of tasks in this container. + */ + @Override + public void stop() { + if (DEBUG) { + log("stop"); + } + if (!isInitialized()) { + return; + } + if (mTaskToken == null) { + return; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setHidden(mTaskToken, true /* hidden */); + try { + WindowOrganizer.applyTransaction(wct); + } catch (RemoteException e) { + Log.e(TAG, "Failed to set hidden in transaction"); + } + // TODO(b/151449487): Only call callback once we enable synchronization + if (mListener != null) { + mListener.onTaskVisibilityChanged(getTaskId(), false); + } + } + + /** + * This should be called whenever the position or size of the surface changes + * or if touchable areas above the surface are added or removed. + */ + @Override + public void notifyBoundsChanged() { + if (DEBUG) { + log("notifyBoundsChanged: screenBounds=" + mHost.getScreenBounds()); + } + if (mTaskToken == null) { + mPendingNotifyBoundsChanged = true; + return; + } + mPendingNotifyBoundsChanged = false; + + // Update based on the screen bounds + Rect screenBounds = mHost.getScreenBounds(); + if (screenBounds.left < 0 || screenBounds.top < 0) { + screenBounds.offsetTo(0, 0); + } + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mTaskToken, screenBounds); + try { + // TODO(b/151449487): Enable synchronization + WindowOrganizer.applyTransaction(wct); + } catch (RemoteException e) { + Log.e(TAG, "Failed to set bounds in transaction"); + } + } + + /** + * Injects a pair of down/up key events with keycode {@link KeyEvent#KEYCODE_BACK} to the + * virtual display. + */ + @Override + public void performBackPress() { + // Do nothing, the task org task should already have focus if the caller is not focused + return; + } + + /** An opaque unique identifier for this task surface among others being managed by the app. */ + @Override + public int getId() { + return getTaskId(); + } + + /** + * Check if container is ready to launch and create {@link ActivityOptions} to target the + * virtual display. + * @param options The existing options to amend, or null if the caller wants new options to be + * created + */ + @Override + protected ActivityOptions prepareActivityOptions(ActivityOptions options) { + options = super.prepareActivityOptions(options); + options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + return options; + } + + private int getTaskId() { + return mTaskInfo != null + ? mTaskInfo.taskId + : INVALID_TASK_ID; + } + + private void resetTaskInfo() { + if (DEBUG) { + log("resetTaskInfo"); + } + mTaskInfo = null; + mTaskToken = null; + mTaskLeash = null; + } + + private void log(String msg) { + Log.d(TAG, "[" + System.identityHashCode(this) + "] " + msg); + } + + /** + * 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 (!isInitialized()) { + return; + } + if (taskInfo.taskId == mTaskInfo.taskId) { + mTaskInfo.taskDescription = taskInfo.taskDescription; + mHost.onTaskBackgroundColorChanged(TaskOrganizerTaskEmbedder.this, + taskInfo.taskDescription.getBackgroundColor()); + } + } + } + + private class TaskOrganizerImpl extends ITaskOrganizer.Stub { + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) + throws RemoteException { + if (DEBUG) { + log("taskAppeared: " + taskInfo.taskId); + } + + // TODO: Ensure visibility/alpha of the leash in its initial state? + mTaskInfo = taskInfo; + mTaskToken = taskInfo.token; + mTaskLeash = mTaskToken.getLeash(); + mTransaction.reparent(mTaskLeash, mSurfaceControl) + .show(mSurfaceControl).apply(); + if (mPendingNotifyBoundsChanged) { + // TODO: Either defer show or hide and synchronize show with the resize + notifyBoundsChanged(); + } + mHost.post(() -> mHost.onTaskBackgroundColorChanged(TaskOrganizerTaskEmbedder.this, + taskInfo.taskDescription.getBackgroundColor())); + + if (mListener != null) { + mListener.onTaskCreated(taskInfo.taskId, taskInfo.baseActivity); + } + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) + throws RemoteException { + if (DEBUG) { + log("taskVanished: " + taskInfo.taskId); + } + + if (mTaskToken != null && (taskInfo == null + || mTaskToken.asBinder().equals(taskInfo.token.asBinder()))) { + if (mListener != null) { + mListener.onTaskRemovalStarted(taskInfo.taskId); + } + resetTaskInfo(); + } + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) + throws RemoteException { + // Do nothing + } + + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) + throws RemoteException { + if (mListener != null) { + mListener.onBackPressedOnTaskRoot(taskInfo.taskId); + } + } + } +} diff --git a/core/java/android/app/VirtualDisplayTaskEmbedder.java b/core/java/android/app/VirtualDisplayTaskEmbedder.java new file mode 100644 index 000000000000..7ad8f22d346e --- /dev/null +++ b/core/java/android/app/VirtualDisplayTaskEmbedder.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2019 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.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +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.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Insets; +import android.graphics.Matrix; +import android.graphics.Point; +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.util.DisplayMetrics; +import android.util.Log; +import android.view.IWindow; +import android.view.IWindowManager; +import android.view.IWindowSession; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.WindowManagerGlobal; +import android.view.inputmethod.InputMethodManager; + +import java.util.List; + +/** + * A component which handles embedded display of tasks within another window. The embedded task can + * be presented using the SurfaceControl provided from {@link #getSurfaceControl()}. + * + * @hide + */ +public class VirtualDisplayTaskEmbedder extends TaskEmbedder { + private static final String TAG = "VirDispTaskEmbedder"; + private static final String DISPLAY_NAME = "TaskVirtualDisplay"; + + // For Virtual Displays + private int mDisplayDensityDpi; + private final boolean mSingleTaskInstance; + private VirtualDisplay mVirtualDisplay; + private Insets mForwardedInsets; + private DisplayMetrics mTmpDisplayMetrics; + + /** + * Constructs a new TaskEmbedder. + * + * @param context the context + * @param host the host for this embedded task + * @param singleTaskInstance whether to apply a single-task constraint to this container, + * only applicable if virtual displays are used + */ + VirtualDisplayTaskEmbedder(Context context, VirtualDisplayTaskEmbedder.Host host, + boolean singleTaskInstance) { + super(context, host); + mSingleTaskInstance = singleTaskInstance; + } + + @Override + public TaskStackListener createTaskStackListener() { + return new TaskStackListenerImpl(); + } + + /** + * Whether this container has been initialized. + * + * @return true if initialized + */ + @Override + public boolean isInitialized() { + return mVirtualDisplay != null; + } + + @Override + public boolean onInitialize() { + final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + mDisplayDensityDpi = getBaseDisplayDensity(); + mVirtualDisplay = displayManager.createVirtualDisplay( + DISPLAY_NAME + "@" + System.identityHashCode(this), mHost.getWidth(), + mHost.getHeight(), mDisplayDensityDpi, 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 TaskEmbedder"); + return false; + } + + try { + // TODO: Find a way to consolidate these calls to the server. + final int displayId = getDisplayId(); + final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + WindowManagerGlobal.getWindowSession().reparentDisplayContent( + mHost.getWindow(), mSurfaceControl, displayId); + wm.dontOverrideDisplayInfo(displayId); + if (mSingleTaskInstance) { + mContext.getSystemService(ActivityTaskManager.class) + .setDisplayToSingleTaskInstance(displayId); + } + setForwardedInsets(mForwardedInsets); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + + if (mHost.getWindow() != null) { + updateLocationAndTapExcludeRegion(); + } + return true; + } + + @Override + protected boolean onRelease() { + // Clear activity view geometry for IME on this display + clearActivityViewGeometryForIme(); + + // Clear tap-exclude region (if any) for this window. + clearTapExcludeRegion(); + + if (isInitialized()) { + mVirtualDisplay.release(); + mVirtualDisplay = null; + return true; + } + return false; + } + + /** + * Starts presentation of tasks in this container. + */ + @Override + public void start() { + if (isInitialized()) { + mVirtualDisplay.setDisplayState(true); + updateLocationAndTapExcludeRegion(); + } + } + + /** + * Stops presentation of tasks in this container. + */ + @Override + public void stop() { + if (isInitialized()) { + mVirtualDisplay.setDisplayState(false); + clearActivityViewGeometryForIme(); + clearTapExcludeRegion(); + } + } + + /** + * This should be called whenever the position or size of the surface changes + * or if touchable areas above the surface are added or removed. + */ + @Override + public void notifyBoundsChanged() { + updateLocationAndTapExcludeRegion(); + } + + /** + * Called to update the dimensions whenever the host size changes. + * + * @param width the new width of the surface + * @param height the new height of the surface + */ + @Override + public void resizeTask(int width, int height) { + mDisplayDensityDpi = getBaseDisplayDensity(); + if (isInitialized()) { + mVirtualDisplay.resize(width, height, mDisplayDensityDpi); + } + } + + /** + * Injects a pair of down/up key events with keycode {@link KeyEvent#KEYCODE_BACK} to the + * virtual display. + */ + @Override + public void performBackPress() { + if (!isInitialized()) { + 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); + } + + @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. The tap exclude region is only used + // for virtual displays. + notifyBoundsChanged(); + return super.gatherTransparentRegion(region); + } + + /** An opaque unique identifier for this task surface among others being managed by the app. */ + @Override + public int getId() { + return getDisplayId(); + } + + @Override + public int getDisplayId() { + if (isInitialized()) { + return mVirtualDisplay.getDisplay().getDisplayId(); + } + return INVALID_DISPLAY; + } + + /** + * Check if container is ready to launch and create {@link ActivityOptions} to target the + * virtual display. + * @param options The existing options to amend, or null if the caller wants new options to be + * created + */ + @Override + protected ActivityOptions prepareActivityOptions(ActivityOptions options) { + options = super.prepareActivityOptions(options); + options.setLaunchDisplayId(getDisplayId()); + options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + options.setTaskAlwaysOnTop(true); + return options; + } + + /** + * Set forwarded insets on the virtual display. + * + * @see IWindowManager#setForwardedInsets + */ + @Override + public void setForwardedInsets(Insets insets) { + mForwardedInsets = insets; + if (!isInitialized()) { + return; + } + try { + final IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + wm.setForwardedInsets(getDisplayId(), mForwardedInsets); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Updates position and bounds information needed by WM and IME to manage window + * focus and touch events properly. + * <p> + * This should be called whenever the position or size of the surface changes + * or if touchable areas above the surface are added or removed. + */ + private void updateLocationAndTapExcludeRegion() { + if (!isInitialized() || mHost.getWindow() == null) { + return; + } + reportLocation(mHost.getScreenToTaskMatrix(), mHost.getPositionInWindow()); + applyTapExcludeRegion(mHost.getWindow(), mHost.getTapExcludeRegion()); + } + + /** + * Call to update the position and transform matrix for the embedded surface. + * <p> + * This should not normally be called directly, but through + * {@link #updateLocationAndTapExcludeRegion()}. This method + * is provided as an optimization when managing multiple TaskSurfaces within a view. + * + * @param screenToViewMatrix the matrix/transform from screen space to view space + * @param positionInWindow the window-relative position of the surface + * + * @see InputMethodManager#reportActivityView(int, Matrix) + */ + private void reportLocation(Matrix screenToViewMatrix, Point positionInWindow) { + try { + final int displayId = getDisplayId(); + mContext.getSystemService(InputMethodManager.class) + .reportActivityView(displayId, screenToViewMatrix); + IWindowSession session = WindowManagerGlobal.getWindowSession(); + session.updateDisplayContentLocation(mHost.getWindow(), positionInWindow.x, + positionInWindow.y, displayId); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Call to update the tap exclude region for the window. + * <p> + * This should not normally be called directly, but through + * {@link #updateLocationAndTapExcludeRegion()}. This method + * is provided as an optimization when managing multiple TaskSurfaces within a view. + * + * @see IWindowSession#updateTapExcludeRegion(IWindow, Region) + */ + private void applyTapExcludeRegion(IWindow window, @Nullable Region tapExcludeRegion) { + try { + IWindowSession session = WindowManagerGlobal.getWindowSession(); + session.updateTapExcludeRegion(window, tapExcludeRegion); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * @see InputMethodManager#reportActivityView(int, Matrix) + */ + private void clearActivityViewGeometryForIme() { + final int displayId = getDisplayId(); + mContext.getSystemService(InputMethodManager.class).reportActivityView(displayId, null); + } + + /** + * Removes the tap exclude region set by {@link #updateLocationAndTapExcludeRegion()}. + */ + private void clearTapExcludeRegion() { + if (mHost.getWindow() == null) { + Log.w(TAG, "clearTapExcludeRegion: not attached to window!"); + return; + } + applyTapExcludeRegion(mHost.getWindow(), null); + } + + 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; + } + + /** Get density of the hosting display. */ + private int getBaseDisplayDensity() { + if (mTmpDisplayMetrics == null) { + mTmpDisplayMetrics = new DisplayMetrics(); + } + mContext.getDisplayNoVerify().getRealMetrics(mTmpDisplayMetrics); + return mTmpDisplayMetrics.densityDpi; + } + + /** + * 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 (!isInitialized()) { + return; + } + if (taskInfo.displayId != getDisplayId()) { + return; + } + ActivityManager.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]) { + mHost.onTaskBackgroundColorChanged(VirtualDisplayTaskEmbedder.this, + taskInfo.taskDescription.getBackgroundColor()); + } + } + + @Override + public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) + throws RemoteException { + if (!isInitialized() || mListener == null + || taskInfo.displayId != getDisplayId()) { + return; + } + + ActivityManager.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]) { + mListener.onTaskMovedToFront(taskInfo.taskId); + } + } + + @Override + public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException { + if (mListener == null || !isInitialized()) { + return; + } + + ActivityManager.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]) { + mListener.onTaskCreated(taskId, componentName); + } + } + + @Override + public void onTaskRemovalStarted(ActivityManager.RunningTaskInfo taskInfo) + throws RemoteException { + if (mListener == null || !isInitialized() + || taskInfo.displayId != getDisplayId()) { + return; + } + + mListener.onTaskRemovalStarted(taskInfo.taskId); + } + + private ActivityManager.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 = getDisplayId(); + final List<ActivityManager.StackInfo> stackInfoList = + mActivityTaskManager.getAllStackInfosOnDisplay(displayId); + if (stackInfoList.isEmpty()) { + return null; + } + return stackInfoList.get(0); + } + } +} diff --git a/core/java/android/window/ITaskOrganizer.aidl b/core/java/android/window/ITaskOrganizer.aidl index fcf4830fba60..b038b0f0f98d 100644 --- a/core/java/android/window/ITaskOrganizer.aidl +++ b/core/java/android/window/ITaskOrganizer.aidl @@ -41,5 +41,12 @@ oneway interface ITaskOrganizer { * has children. The Divider impl looks at the info and can see that the secondary root task * has adopted an ActivityType of HOME and proceeds to show the minimized dock UX. */ - void onTaskInfoChanged(in ActivityManager.RunningTaskInfo info); + void onTaskInfoChanged(in ActivityManager.RunningTaskInfo taskInfo); + + /** + * Called when the task organizer has requested + * {@link ITaskOrganizerController.setInterceptBackPressedOnTaskRoot} to get notified when the + * user has pressed back on the root activity of a task controlled by the task organizer. + */ + void onBackPressedOnTaskRoot(in ActivityManager.RunningTaskInfo taskInfo); } diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl index a8b6aae80dce..ba659150d99c 100644 --- a/core/java/android/window/ITaskOrganizerController.aidl +++ b/core/java/android/window/ITaskOrganizerController.aidl @@ -57,4 +57,10 @@ interface ITaskOrganizerController { * and thus new tasks just end up directly on the display. */ void setLaunchRoot(int displayId, in IWindowContainer root); + + /** + * Requests that the given task organizer is notified when back is pressed on the root activity + * of one of its controlled tasks. + */ + void setInterceptBackPressedOnTaskRoot(ITaskOrganizer organizer, boolean interceptBackPressed); } diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java index 4bd5b29f98ac..5590e72c989f 100644 --- a/core/java/android/window/WindowOrganizer.java +++ b/core/java/android/window/WindowOrganizer.java @@ -137,6 +137,16 @@ public class WindowOrganizer { getController().setLaunchRoot(displayId, root); } + /** + * Requests that the given task organizer is notified when back is pressed on the root + * activity of one of its controlled tasks. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public static void setInterceptBackPressedOnTaskRoot(ITaskOrganizer organizer, + boolean interceptBackPressed) throws RemoteException { + getController().setInterceptBackPressedOnTaskRoot(organizer, interceptBackPressed); + } + private static ITaskOrganizerController getController() { return ITaskOrganizerControllerSingleton.get(); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTaskView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTaskView.java new file mode 100644 index 000000000000..0d6d137491a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTaskView.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 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.systemui.bubbles; + +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.app.TaskEmbedder; +import android.app.TaskOrganizerTaskEmbedder; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.Region; +import android.view.IWindow; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import dalvik.system.CloseGuard; + + +public class BubbleTaskView extends SurfaceView implements SurfaceHolder.Callback, + TaskEmbedder.Host { + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleTaskView" : TAG_BUBBLES; + + private final CloseGuard mGuard = CloseGuard.get(); + private boolean mOpened; // Protected by mGuard. + + private TaskEmbedder mTaskEmbedder; + private final SurfaceControl.Transaction mTmpTransaction = new SurfaceControl.Transaction(); + private final Rect mTmpRect = new Rect(); + + public BubbleTaskView(Context context) { + super(context); + + mTaskEmbedder = new TaskOrganizerTaskEmbedder(context, this); + setUseAlpha(); + getHolder().addCallback(this); + + mOpened = true; + mGuard.open("release"); + } + + public void setCallback(TaskEmbedder.Listener callback) { + if (callback == null) { + mTaskEmbedder.setListener(null); + return; + } + mTaskEmbedder.setListener(callback); + } + + public void startShortcutActivity(@NonNull ShortcutInfo shortcut, + @NonNull ActivityOptions options, @Nullable Rect sourceBounds) { + mTaskEmbedder.startShortcutActivity(shortcut, options, sourceBounds); + } + + public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, + @NonNull ActivityOptions options) { + mTaskEmbedder.startActivity(pendingIntent, fillInIntent, options); + } + + public void onLocationChanged() { + mTaskEmbedder.notifyBoundsChanged(); + } + + @Override + public Rect getScreenBounds() { + getBoundsOnScreen(mTmpRect); + return mTmpRect; + } + + @Override + public void onTaskBackgroundColorChanged(TaskEmbedder ts, int bgColor) { + setResizeBackgroundColor(bgColor); + } + + @Override + public Region getTapExcludeRegion() { + // Not used + return null; + } + + @Override + public Matrix getScreenToTaskMatrix() { + // Not used + return null; + } + + @Override + public IWindow getWindow() { + // Not used + return null; + } + + @Override + public Point getPositionInWindow() { + // Not used + return null; + } + + @Override + public boolean canReceivePointerEvents() { + // Not used + return false; + } + + public void release() { + if (!mTaskEmbedder.isInitialized()) { + throw new IllegalStateException( + "Trying to release container that is not initialized."); + } + performRelease(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGuard != null) { + mGuard.warnIfOpen(); + performRelease(); + } + } finally { + super.finalize(); + } + } + + private void performRelease() { + if (!mOpened) { + return; + } + getHolder().removeCallback(this); + mTaskEmbedder.release(); + mTaskEmbedder.setListener(null); + + mGuard.close(); + mOpened = false; + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (!mTaskEmbedder.isInitialized()) { + mTaskEmbedder.initialize(getSurfaceControl()); + } else { + mTmpTransaction.reparent(mTaskEmbedder.getSurfaceControl(), + getSurfaceControl()).apply(); + } + mTaskEmbedder.start(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + mTaskEmbedder.resizeTask(width, height); + mTaskEmbedder.notifyBoundsChanged(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mTaskEmbedder.stop(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index da171b27d97c..6a15cb3cffc2 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -318,6 +318,11 @@ public class PipTaskOrganizer extends ITaskOrganizer.Stub { null /* updateBoundsCallback */); } + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + // Do nothing + } + /** * @return {@code true} if the aspect ratio is changed since no other parameters within * {@link PictureInPictureParams} would affect the bounds. diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java index 0a528a63009e..a6f67412fa50 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/SplitScreenTaskOrganizer.java @@ -114,6 +114,10 @@ class SplitScreenTaskOrganizer extends ITaskOrganizer.Stub { mDivider.getHandler().post(() -> handleTaskInfoChanged(taskInfo)); } + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + } + /** * This is effectively a finite state machine which moves between the various split-screen * presentations based on the contents of the split regions. diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 7e999c65c010..bda6da5f0301 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -2420,7 +2420,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return; } ActivityStack stack = r.getRootTask(); - if (stack != null && stack.isSingleTaskInstance()) { + final TaskOrganizerController taskOrgController = + mWindowOrganizerController.mTaskOrganizerController; + if (taskOrgController.handleInterceptBackPressedOnTaskRoot(stack)) { + // This task is handled by a task organizer that has requested the back pressed + // callback + } else if (stack != null && (stack.isSingleTaskInstance())) { // Single-task stacks are used for activities which are presented in floating // windows above full screen activities. Instead of directly finishing the // task, a task change listener is used to notify SystemUI so the action can be diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 4a7edee7beac..459a8d604579 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4131,6 +4131,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo return true; } + if (task.isOrganized()) { + return true; + } + // We need to use the task's dim bounds (which is derived from the visible bounds of // its apps windows) for any touch-related tests. Can't use the task's original // bounds because it might be adjusted to fit the content frame. One example is when diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 8edcd2fb7765..7c47e5024434 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -32,7 +32,6 @@ import android.content.pm.ActivityInfo; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; -import android.os.UserHandle; import android.util.Slog; import android.util.SparseArray; import android.window.ITaskOrganizer; @@ -46,7 +45,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Set; import java.util.WeakHashMap; /** @@ -88,6 +86,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { private final DeathRecipient mDeathRecipient; private final ArrayList<Task> mOrganizedTasks = new ArrayList<>(); private final int mUid; + private boolean mInterceptBackPressedOnTaskRoot; TaskOrganizerState(ITaskOrganizer organizer, int uid) { mOrganizer = organizer; @@ -100,6 +99,10 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { mUid = uid; } + void setInterceptBackPressedOnTaskRoot(boolean interceptBackPressed) { + mInterceptBackPressedOnTaskRoot = interceptBackPressed; + } + void addTask(Task t) { mOrganizedTasks.add(t); try { @@ -473,6 +476,41 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } } + @Override + public void setInterceptBackPressedOnTaskRoot(ITaskOrganizer organizer, + boolean interceptBackPressed) { + enforceStackPermission("setInterceptBackPressedOnTaskRoot()"); + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder()); + if (state != null) { + state.setInterceptBackPressedOnTaskRoot(interceptBackPressed); + } + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + + public boolean handleInterceptBackPressedOnTaskRoot(Task task) { + if (task == null || !task.isOrganized()) { + return false; + } + + final TaskOrganizerState state = mTaskOrganizerStates.get(task.mTaskOrganizer.asBinder()); + if (!state.mInterceptBackPressedOnTaskRoot) { + return false; + } + + try { + state.mOrganizer.onBackPressedOnTaskRoot(task.getTaskInfo()); + } catch (Exception e) { + Slog.e(TAG, "Exception sending interceptBackPressedOnTaskRoot callback" + e); + } + return true; + } + public void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.print(prefix); pw.println("TaskOrganizerController:"); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index ed400ea8e992..bc1f9255ec56 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -1047,5 +1047,8 @@ public class ActivityStarterTests extends ActivityTestsBase { } } } + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + } }; } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java index 4cc84a65fb29..f05acce556ee 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java @@ -50,6 +50,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.StackInfo; +import android.app.IRequestFinishCallback; import android.app.PictureInPictureParams; import android.content.pm.ActivityInfo; import android.content.res.Configuration; @@ -420,6 +421,10 @@ public class TaskOrganizerTests extends WindowTestsBase { @Override public void onTaskInfoChanged(RunningTaskInfo info) throws RemoteException { } + + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + } }; mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(listener, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); @@ -474,6 +479,10 @@ public class TaskOrganizerTests extends WindowTestsBase { lastReportedTiles.add(info); called[0] = true; } + + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + } }; mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(listener, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY); @@ -531,6 +540,10 @@ public class TaskOrganizerTests extends WindowTestsBase { public void onTaskInfoChanged(RunningTaskInfo info) { lastReportedTiles.put(info.token.asBinder(), info); } + + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + } }; mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer( listener, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); @@ -751,6 +764,9 @@ public class TaskOrganizerTests extends WindowTestsBase { @Override public void onTaskInfoChanged(RunningTaskInfo info) { } + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + } }; private ActivityRecord makePipableActivity() { @@ -827,4 +843,31 @@ public class TaskOrganizerTests extends WindowTestsBase { task.removeImmediately(); verify(organizer).onTaskVanished(any()); } + + @Test + public void testInterceptBackPressedOnTaskRoot() throws RemoteException { + final ActivityStack stack = createStack(); + final Task task = createTask(stack); + final ActivityRecord activity = WindowTestUtils.createActivityRecordInTask( + stack.mDisplayContent, task); + final ITaskOrganizer organizer = registerMockOrganizer(WINDOWING_MODE_MULTI_WINDOW); + + // Setup the task to be controlled by the MW mode organizer + stack.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + assertTrue(stack.isOrganized()); + + // Verify a back pressed does not call the organizer + mWm.mAtmService.onBackPressedOnTaskRoot(activity.token, + new IRequestFinishCallback.Default()); + verify(organizer, never()).onBackPressedOnTaskRoot(any()); + + // Enable intercepting back + mWm.mAtmService.mTaskOrganizerController.setInterceptBackPressedOnTaskRoot(organizer, + true); + + // Verify now that the back press does call the organizer + mWm.mAtmService.onBackPressedOnTaskRoot(activity.token, + new IRequestFinishCallback.Default()); + verify(organizer, times(1)).onBackPressedOnTaskRoot(any()); + } } diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java index 5afd39ea9de1..d46807642df1 100644 --- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java @@ -19,8 +19,8 @@ package com.android.test.taskembed; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.window.WindowOrganizer.TaskOrganizer; -import android.app.ActivityManager; import android.app.Activity; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; @@ -35,10 +35,10 @@ import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.window.ITaskOrganizer; import android.window.IWindowContainerTransactionCallback; import android.window.WindowContainerTransaction; -import android.widget.LinearLayout; import android.window.WindowOrganizer; public class TaskOrganizerMultiWindowTest extends Activity { @@ -163,6 +163,9 @@ public class TaskOrganizerMultiWindowTest extends Activity { @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { } + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + } } Organizer mOrganizer = new Organizer(); diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java index 520bc255499b..a589d95880af 100644 --- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java @@ -25,10 +25,10 @@ import android.content.Intent; import android.graphics.Rect; import android.os.IBinder; import android.view.ViewGroup; -import android.window.ITaskOrganizer; -import android.window.WindowContainerTransaction; import android.view.WindowManager; import android.widget.FrameLayout; +import android.window.ITaskOrganizer; +import android.window.WindowContainerTransaction; import android.window.WindowOrganizer; public class TaskOrganizerPipTest extends Service { @@ -52,6 +52,9 @@ public class TaskOrganizerPipTest extends Service { } public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { } + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + } } Organizer mOrganizer = new Organizer(); |