diff options
author | Robert Carr <racarr@google.com> | 2019-11-11 15:03:15 -0800 |
---|---|---|
committer | Robert Carr <racarr@google.com> | 2020-01-09 13:04:18 -0800 |
commit | 8a2f913ac4f5651cce1ad4c07e42cba918fe8454 (patch) | |
tree | a1a77a566b07208d4afc84e9e7a132488ed0178f | |
parent | b130bfbeb5a83ae6b25782d7e8cf2a8e3a057cd9 (diff) |
Introduce TaskOrganizer
A first take at the TaskOrganizer API for allowing SysUI to control task presentation.
In this CL we introduce the first two primitives:
1. The interface itself for implementation by SysUI
2. Support for organizing a given windowing mode (but atm really only PIP)
We include a sample app that manages the PIP from an APPLICATION_OVERLAY window.
Bug: 139371701
Test: wmtests/TaskOrganizerTests. TaskOrganizerPipTest
Change-Id: I44a8ed311bc5f06285bba2c6ff3b37a7d19a9190
16 files changed, 823 insertions, 9 deletions
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 700b3c1b620e..e5c046c2376c 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -68,6 +68,7 @@ import android.os.StrictMode; import android.os.WorkSource; import android.service.voice.IVoiceInteractionSession; import android.view.IRecentsAnimationRunner; +import android.view.ITaskOrganizer; import android.view.RemoteAnimationDefinition; import android.view.RemoteAnimationAdapter; import android.view.WindowContainerTransaction; @@ -121,6 +122,9 @@ interface IActivityTaskManager { in Intent intent, in String resolvedType, in IBinder resultTo, in String resultWho, int requestCode, int flags, in ProfilerInfo profilerInfo, in Bundle options, IBinder permissionToken, boolean ignoreTargetSecurity, int userId); + + void registerTaskOrganizer(in ITaskOrganizer organizer, int windowingMode); + boolean isActivityStartAllowedOnDisplay(int displayId, in Intent intent, in String resolvedType, int userId); diff --git a/core/java/android/view/ITaskOrganizer.aidl b/core/java/android/view/ITaskOrganizer.aidl new file mode 100644 index 000000000000..e92aafed6f22 --- /dev/null +++ b/core/java/android/view/ITaskOrganizer.aidl @@ -0,0 +1,38 @@ +/* //device/java/android/android/view/ITaskOrganizer.aidl +** +** Copyright 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.view; + +import android.view.IWindowContainer; +import android.view.SurfaceControl; +import android.app.ActivityManager; + +/** + * Interface for ActivityTaskManager/WindowManager to delegate control of tasks. + * {@hide} + */ +oneway interface ITaskOrganizer { + void taskAppeared(in IWindowContainer container, + in ActivityManager.RunningTaskInfo taskInfo); + void taskVanished(in IWindowContainer container); + + /** + * Called upon completion of + * ActivityTaskManagerService#applyTaskOrganizerTransaction + */ + void transactionReady(int id, in SurfaceControl.Transaction t); +}
\ No newline at end of file diff --git a/core/java/android/view/WindowContainerTransaction.java b/core/java/android/view/WindowContainerTransaction.java index 607a87047733..253794f70ef2 100644 --- a/core/java/android/view/WindowContainerTransaction.java +++ b/core/java/android/view/WindowContainerTransaction.java @@ -62,6 +62,18 @@ public class WindowContainerTransaction implements Parcelable { return this; } + /** + * Notify activies within the hiearchy of a container that they have entered picture-in-picture + * mode with the given bounds. + */ + public WindowContainerTransaction scheduleFinishEnterPip(IWindowContainer container, + Rect bounds) { + Change chg = getOrCreateChange(container.asBinder()); + chg.mSchedulePipCallback = true; + chg.mPinnedBounds = new Rect(bounds); + return this; + } + public Map<IBinder, Change> getChanges() { return mChanges; } @@ -104,12 +116,20 @@ public class WindowContainerTransaction implements Parcelable { private @ActivityInfo.Config int mConfigSetMask = 0; private @WindowConfiguration.WindowConfig int mWindowSetMask = 0; + private boolean mSchedulePipCallback = false; + private Rect mPinnedBounds = null; + public Change() {} protected Change(Parcel in) { mConfiguration.readFromParcel(in); mConfigSetMask = in.readInt(); mWindowSetMask = in.readInt(); + mSchedulePipCallback = (in.readInt() != 0); + if (mSchedulePipCallback ) { + mPinnedBounds = new Rect(); + mPinnedBounds.readFromParcel(in); + } } public Configuration getConfiguration() { @@ -126,6 +146,14 @@ public class WindowContainerTransaction implements Parcelable { return mWindowSetMask; } + /** + * Returns the bounds to be used for scheduling the enter pip callback + * or null if no callback is to be scheduled. + */ + public Rect getEnterPipBounds() { + return mPinnedBounds; + } + @Override public String toString() { final boolean changesBounds = @@ -151,6 +179,11 @@ public class WindowContainerTransaction implements Parcelable { mConfiguration.writeToParcel(dest, flags); dest.writeInt(mConfigSetMask); dest.writeInt(mWindowSetMask); + + dest.writeInt(mSchedulePipCallback ? 1 : 0); + if (mSchedulePipCallback ) { + mPinnedBounds.writeToParcel(dest, flags); + } } @Override diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index c95943904d1f..9fd3ea4fc090 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -163,6 +163,7 @@ import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.view.ITaskOrganizer; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -803,6 +804,16 @@ class ActivityStack extends WindowContainer<WindowContainer> implements BoundsAn setWindowingMode(windowingMode, false /* animate */, false /* showRecents */, false /* enteringSplitScreenMode */, false /* deferEnsuringVisibility */, false /* creating */); + + windowingMode = getWindowingMode(); + /* + * Different windowing modes may be managed by different task organizers. If + * getTaskOrganizer returns null, we still call transferToTaskOrganizer to + * make sure we clear it. + */ + final ITaskOrganizer org = + mWmService.mAtmService.mTaskOrganizerController.getTaskOrganizer(windowingMode); + transferToTaskOrganizer(org); } /** @@ -1650,6 +1661,33 @@ class ActivityStack extends WindowContainer<WindowContainer> implements BoundsAn } /** + * Indicate whether the first task in this stack is controlled by a TaskOrganizer. We aren't + * expecting to use the TaskOrganizer in multiple task per stack scenarios so checking + * the first one is ok. + */ + boolean isControlledByTaskOrganizer() { + return getChildCount() > 0 && getTopMostTask().mTaskOrganizer != null; + } + + private static void transferSingleTaskToOrganizer(Task tr, ITaskOrganizer organizer) { + tr.setTaskOrganizer(organizer); + } + + /** + * Transfer control of the leashes and IWindowContainers to the given ITaskOrganizer. + * This will (or shortly there-after) invoke the taskAppeared callbacks. + * If the tasks had a previous TaskOrganizer, setTaskOrganizer will take care of + * emitting the taskVanished callbacks. + */ + void transferToTaskOrganizer(ITaskOrganizer organizer) { + final PooledConsumer c = PooledLambda.obtainConsumer( + ActivityStack::transferSingleTaskToOrganizer, + PooledLambda.__(Task.class), organizer); + forAllTasks(c); + c.recycle(); + } + + /** * Returns true if the stack should be visible. * * @param starting The currently starting activity or null if there is none. @@ -3577,6 +3615,15 @@ class ActivityStack extends WindowContainer<WindowContainer> implements BoundsAn void animateResizePinnedStack(Rect toBounds, Rect sourceHintBounds, int animationDuration, boolean fromFullscreen) { if (!inPinnedWindowingMode()) return; + + /** + * TODO(b/146594635): Remove all PIP animation code from WM once SysUI handles animation. + * If this PIP Task is controlled by a TaskOrganizer, the animation occurs entirely + * on the TaskOrganizer side, so we just hand over the leash without doing any animation. + * We have to be careful to not schedule the enter-pip callback as the TaskOrganizer + * needs to have flexibility to schedule that at an appropriate point in the animation. + */ + if (isControlledByTaskOrganizer()) return; if (toBounds == null /* toFullscreen */) { final Configuration parentConfig = getParent().getConfiguration(); final ActivityRecord top = topRunningNonOverlayTaskActivity(); diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index e8564fc80ac3..2d6eb7537723 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -2574,6 +2574,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { final PooledConsumer c = PooledLambda.obtainConsumer( ActivityRecord::updatePictureInPictureMode, PooledLambda.__(ActivityRecord.class), targetStackBounds, forceUpdate); + task.getStack().setBounds(targetStackBounds); task.forAllActivities(c); c.recycle(); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 76c0e4eeecae..354891840a77 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -226,6 +226,7 @@ import android.util.StatsLog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.view.IRecentsAnimationRunner; +import android.view.ITaskOrganizer; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.WindowContainerTransaction; @@ -662,6 +663,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private FontScaleSettingObserver mFontScaleSettingObserver; + /** + * Stores the registration and state of TaskOrganizers in use. + */ + TaskOrganizerController mTaskOrganizerController = + new TaskOrganizerController(this, mGlobalLock); + private int mDeviceOwnerUid = Process.INVALID_UID; private final class FontScaleSettingObserver extends ContentObserver { @@ -1271,6 +1278,14 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { .execute(); } + @Override + public final void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) { + enforceCallerIsRecentsOrHasPermission( + MANAGE_ACTIVITY_STACKS, "registerTaskOrganizer()"); + synchronized (mGlobalLock) { + mTaskOrganizerController.registerTaskOrganizer(organizer, windowingMode); + } + } @Override public IBinder requestStartActivityPermissionToken(IBinder delegatorToken) { @@ -3319,6 +3334,18 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } + private void applyWindowContainerChange(ConfigurationContainer cc, + WindowContainerTransaction.Change c) { + sanitizeAndApplyConfigChange(cc, c); + + Rect enterPipBounds = c.getEnterPipBounds(); + if (enterPipBounds != null) { + Task tr = (Task) cc; + mStackSupervisor.updatePictureInPictureMode(tr, + enterPipBounds, true); + } + } + @Override public void applyContainerTransaction(WindowContainerTransaction t) { mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "applyContainerTransaction()"); @@ -3335,7 +3362,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { entries.next(); final ConfigurationContainer cc = ConfigurationContainer.RemoteToken.fromBinder( entry.getKey()).getContainer(); - sanitizeAndApplyConfigChange(cc, entry.getValue()); + applyWindowContainerChange(cc, entry.getValue()); } } } finally { @@ -4057,7 +4084,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { throw new IllegalArgumentException("Stack: " + stack + " doesn't support animated resize."); } - if (animate) { + /** + * TODO(b/146594635): Remove all PIP animation code from WM + * once SysUI handles animation. Don't even try to animate TaskOrganized tasks. + */ + if (animate && !stack.isControlledByTaskOrganizer()) { stack.animateResizePinnedStack(null /* destBounds */, null /* sourceHintBounds */, animationDuration, false /* fromFullscreen */); diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index dd3365c900d7..d0310f1a7607 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -666,4 +666,8 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { return sb.toString(); } } + + RemoteToken getRemoteToken() { + return mRemoteToken; + } } diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index c4b67d76607e..091f66c0b19a 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -279,6 +279,19 @@ final class InputMonitor { // we avoid reintroducing this concept by just choosing one of them here. inputWindowHandle.surfaceInset = child.getAttrs().surfaceInsets.left; + /** + * If the window is in a TaskManaged by a TaskOrganizer then most cropping + * will be applied using the SurfaceControl hierarchy from the Organizer. + * This means we need to make sure that these changes in crop are reflected + * in the input windows, and so ensure this flag is set so that + * the input crop always reflects the surface hierarchy. + * we may have some issues with modal-windows, but I guess we can + * cross that bridge when we come to implementing full-screen TaskOrg + */ + if (child.getTask() != null && child.getTask().isControlledByTaskOrganizer()) { + inputWindowHandle.replaceTouchableRegionWithCrop(null /* Use this surfaces crop */); + } + if (child.mGlobalScale != 1) { // If we are scaling the window, input coordinates need // to be inversely scaled to map from what is on screen diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index a7bf6600d7b5..c3e815d10dda 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2168,12 +2168,18 @@ class RootWindowContainer extends WindowContainer<DisplayContent> mService.continueWindowLayout(); } + // TODO(b/146594635): Remove all PIP animation code from WM once SysUI handles animation. // Notify the pinned stack controller to prepare the PiP animation, expect callback - // delivered from SystemUI to WM to start the animation. - final PinnedStackController pinnedStackController = + // delivered from SystemUI to WM to start the animation. Unless we are using + // the TaskOrganizer in which case the animation will be entirely handled + // on that side. + if (mService.mTaskOrganizerController.getTaskOrganizer(WINDOWING_MODE_PINNED) + == null) { + final PinnedStackController pinnedStackController = display.mDisplayContent.getPinnedStackController(); - pinnedStackController.prepareAnimation(sourceHintBounds, aspectRatio, - null /* stackBounds */); + pinnedStackController.prepareAnimation(sourceHintBounds, aspectRatio, + null /* stackBounds */); + } // TODO: revisit the following statement after the animation is moved from WM to SysUI. // Update the visibility of all activities after the they have been reparented to the new diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9a140daad417..5cb7091bbed0 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -117,9 +117,11 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.Debug; import android.os.IBinder; +import android.os.Parcel; import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; @@ -130,6 +132,7 @@ import android.util.DisplayMetrics; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.DisplayInfo; +import android.view.ITaskOrganizer; import android.view.RemoteAnimationTarget; import android.view.Surface; import android.view.SurfaceControl; @@ -425,6 +428,14 @@ class Task extends WindowContainer<WindowContainer> { } /** + * The TaskOrganizer which is delegated presentation of this task. If set the Task will + * emit an IWindowContainer (allowing access to it's SurfaceControl leash) to the organizers + * taskAppeared callback, and emit a taskRemoved callback when the Task is vanished. + */ + ITaskOrganizer mTaskOrganizer; + + + /** * Don't use constructor directly. Use {@link #create(ActivityTaskManagerService, int, * ActivityInfo, Intent, TaskDescription)} instead. */ @@ -445,6 +456,22 @@ class Task extends WindowContainer<WindowContainer> { _voiceSession, _voiceInteractor, stack); } + class TaskToken extends RemoteToken { + TaskToken(ConfigurationContainer container) { + super(container); + } + + @Override + public SurfaceControl getLeash() { + // We need to copy the SurfaceControl instead of returning the original + // because the Parcel FLAGS PARCELABLE_WRITE_RETURN_VALUE cause SurfaceControls + // to release themselves. + SurfaceControl sc = new SurfaceControl(); + sc.copyFrom(getSurfaceControl()); + return sc; + } + } + /** Don't use constructor directly. This is only used by XML parser. */ Task(ActivityTaskManagerService atmService, int _taskId, Intent _intent, Intent _affinityIntent, String _affinity, String _rootAffinity, @@ -469,7 +496,7 @@ class Task extends WindowContainer<WindowContainer> { mTaskDescription = _lastTaskDescription; // Tasks have no set orientation value (including SCREEN_ORIENTATION_UNSPECIFIED). setOrientation(SCREEN_ORIENTATION_UNSET); - mRemoteToken = new RemoteToken(this); + mRemoteToken = new TaskToken(this); affinityIntent = _affinityIntent; affinity = _affinity; rootAffinity = _rootAffinity; @@ -2179,6 +2206,10 @@ class Task extends WindowContainer<WindowContainer> { void removeImmediately() { if (DEBUG_STACK) Slog.i(TAG, "removeTask: removing taskId=" + mTaskId); EventLogTags.writeWmTaskRemoved(mTaskId, "removeTask"); + + // If applicable let the TaskOrganizer know the Task is vanishing. + setTaskOrganizer(null); + super.removeImmediately(); } @@ -2567,6 +2598,12 @@ class Task extends WindowContainer<WindowContainer> { } boolean shouldAnimate() { + /** + * Animations are handled by the TaskOrganizer implementation. + */ + if (isControlledByTaskOrganizer()) { + return false; + } // Don't animate while the task runs recents animation but only if we are in the mode // where we cancel with deferred screenshot, which means that the controller has // transformed the task. @@ -3444,4 +3481,91 @@ class Task extends WindowContainer<WindowContainer> { XmlUtils.skipCurrentTag(in); } } + + boolean isControlledByTaskOrganizer() { + return mTaskOrganizer != null; + } + + @Override + protected void reparentSurfaceControl(SurfaceControl.Transaction t, SurfaceControl newParent) { + /** + * Avoid yanking back control from the TaskOrganizer, which has presumably reparented the + * Surface in to its own hierarchy. + */ + if (isControlledByTaskOrganizer()) { + return; + } + super.reparentSurfaceControl(t, newParent); + } + + private void sendTaskAppeared() { + if (mSurfaceControl != null && mTaskOrganizer != null) { + mAtmService.mTaskOrganizerController.onTaskAppeared(mTaskOrganizer, this); + } + } + + private void sendTaskVanished() { + if (mTaskOrganizer != null) { + mAtmService.mTaskOrganizerController.onTaskVanished(mTaskOrganizer, this); + } + } + + void setTaskOrganizer(ITaskOrganizer organizer) { + // Let the old organizer know it has lost control. + if (mTaskOrganizer != null) { + sendTaskVanished(); + } + mTaskOrganizer = organizer; + sendTaskAppeared(); + } + + // Called on Binder death. + void taskOrganizerDied() { + mTaskOrganizer = null; + } + + @Override + void setSurfaceControl(SurfaceControl sc) { + super.setSurfaceControl(sc); + // If the TaskOrganizer was set before we created the SurfaceControl, we need to + // emit the callbacks now. + sendTaskAppeared(); + } + + @Override + public void updateSurfacePosition() { + // Avoid fighting with the TaskOrganizer over Surface position. + if (isControlledByTaskOrganizer()) { + getPendingTransaction().setPosition(mSurfaceControl, 0, 0); + scheduleAnimation(); + return; + } else { + super.updateSurfacePosition(); + } + } + + @Override + void getRelativeDisplayedPosition(Point outPos) { + // In addition to updateSurfacePosition, we keep other code that sets + // position from fighting with the TaskOrganizer + if (isControlledByTaskOrganizer()) { + outPos.set(0, 0); + return; + } + super.getRelativeDisplayedPosition(outPos); + } + + @Override + public void setWindowingMode(int windowingMode) { + super.setWindowingMode(windowingMode); + windowingMode = getWindowingMode(); + /* + * Different windowing modes may be managed by different task organizers. If + * getTaskOrganizer returns null, we still call transferToTaskOrganizer to + * make sure we clear it. + */ + final ITaskOrganizer org = + mAtmService.mTaskOrganizerController.getTaskOrganizer(windowingMode); + setTaskOrganizer(org); + } } diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java new file mode 100644 index 000000000000..283be4010677 --- /dev/null +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -0,0 +1,167 @@ +/* + * 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 com.android.server.wm; + +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.ITaskOrganizer; +import android.view.SurfaceControl; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Stores the TaskOrganizers associated with a given windowing mode and + * their associated state. + */ +class TaskOrganizerController { + private static final String TAG = "TaskOrganizerController"; + + private WindowManagerGlobalLock mGlobalLock; + + private class DeathRecipient implements IBinder.DeathRecipient { + int mWindowingMode; + ITaskOrganizer mTaskOrganizer; + + DeathRecipient(ITaskOrganizer organizer, int windowingMode) { + mTaskOrganizer = organizer; + mWindowingMode = windowingMode; + } + + @Override + public void binderDied() { + synchronized (mGlobalLock) { + final TaskOrganizerState state = mTaskOrganizerStates.get(mTaskOrganizer); + for (int i = 0; i < state.mOrganizedTasks.size(); i++) { + state.mOrganizedTasks.get(i).taskOrganizerDied(); + } + mTaskOrganizerStates.remove(mTaskOrganizer); + if (mTaskOrganizersForWindowingMode.get(mWindowingMode) == mTaskOrganizer) { + mTaskOrganizersForWindowingMode.remove(mWindowingMode); + } + } + } + }; + + class TaskOrganizerState { + ITaskOrganizer mOrganizer; + DeathRecipient mDeathRecipient; + + ArrayList<Task> mOrganizedTasks = new ArrayList<>(); + + void addTask(Task t) { + mOrganizedTasks.add(t); + } + + void removeTask(Task t) { + mOrganizedTasks.remove(t); + } + + TaskOrganizerState(ITaskOrganizer organizer, DeathRecipient deathRecipient) { + mOrganizer = organizer; + mDeathRecipient = deathRecipient; + } + }; + + + final HashMap<Integer, TaskOrganizerState> mTaskOrganizersForWindowingMode = new HashMap(); + final HashMap<ITaskOrganizer, TaskOrganizerState> mTaskOrganizerStates = new HashMap(); + + final HashMap<Integer, ITaskOrganizer> mTaskOrganizersByPendingSyncId = new HashMap(); + + final ActivityTaskManagerService mService; + + TaskOrganizerController(ActivityTaskManagerService atm, WindowManagerGlobalLock lock) { + mService = atm; + mGlobalLock = lock; + } + + private void clearIfNeeded(int windowingMode) { + final TaskOrganizerState oldState = mTaskOrganizersForWindowingMode.get(windowingMode); + if (oldState != null) { + oldState.mOrganizer.asBinder().unlinkToDeath(oldState.mDeathRecipient, 0); + } + } + + /** + * Register a TaskOrganizer to manage tasks as they enter the given windowing mode. + * If there was already a TaskOrganizer for this windowing mode it will be evicted + * and receive taskVanished callbacks in the process. + */ + void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) { + if (windowingMode != WINDOWING_MODE_PINNED) { + throw new UnsupportedOperationException( + "As of now only Pinned windowing mode is supported for registerTaskOrganizer"); + + } + clearIfNeeded(windowingMode); + DeathRecipient dr = new DeathRecipient(organizer, windowingMode); + try { + organizer.asBinder().linkToDeath(dr, 0); + } catch (RemoteException e) { + Slog.e(TAG, "TaskOrganizer failed to register death recipient"); + } + + final TaskOrganizerState state = new TaskOrganizerState(organizer, dr); + mTaskOrganizersForWindowingMode.put(windowingMode, state); + + mTaskOrganizerStates.put(organizer, state); + } + + ITaskOrganizer getTaskOrganizer(int windowingMode) { + final TaskOrganizerState state = mTaskOrganizersForWindowingMode.get(windowingMode); + if (state == null) { + return null; + } + return state.mOrganizer; + } + + private void sendTaskAppeared(ITaskOrganizer organizer, Task task) { + try { + organizer.taskAppeared(task.getRemoteToken(), task.getTaskInfo()); + } catch (Exception e) { + Slog.e(TAG, "Exception sending taskAppeared callback" + e); + } + } + + private void sendTaskVanished(ITaskOrganizer organizer, Task task) { + try { + organizer.taskVanished(task.getRemoteToken()); + } catch (Exception e) { + Slog.e(TAG, "Exception sending taskVanished callback" + e); + } + } + + void onTaskAppeared(ITaskOrganizer organizer, Task task) { + TaskOrganizerState state = mTaskOrganizerStates.get(organizer); + + state.addTask(task); + sendTaskAppeared(organizer, task); + } + + void onTaskVanished(ITaskOrganizer organizer, Task task) { + final TaskOrganizerState state = mTaskOrganizerStates.get(organizer); + sendTaskVanished(organizer, task); + + // This could trigger TaskAppeared for other tasks in the same stack so make sure + // we do this AFTER sending taskVanished. + state.removeTask(task); + } +} diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index cefef37a1363..79ffd94cc1cb 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -342,7 +342,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (mSurfaceControl == null) { // If we don't yet have a surface, but we now have a parent, we should // build a surface. - mSurfaceControl = makeSurface().build(); + setSurfaceControl(makeSurface().build()); getPendingTransaction().show(mSurfaceControl); updateSurfacePosition(); } else { @@ -496,7 +496,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< mParent.getPendingTransaction().merge(getPendingTransaction()); } - mSurfaceControl = null; + setSurfaceControl(null); mLastSurfacePosition.set(0, 0); scheduleAnimation(); } @@ -2209,4 +2209,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } return mParent.getDimmer(); } + + void setSurfaceControl(SurfaceControl sc) { + mSurfaceControl = sc; + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java new file mode 100644 index 000000000000..8d2da1e6cb5b --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java @@ -0,0 +1,170 @@ +/* + * 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 com.android.server.wm; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.os.Binder; +import android.os.RemoteException; +import android.view.ITaskOrganizer; +import android.view.SurfaceControl; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test class for {@link TaskOrganizer}. + * + * Build/Install/Run: + * atest WmTests:TaskOrganizerTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class TaskOrganizerTests extends WindowTestsBase { + private ITaskOrganizer makeAndRegisterMockOrganizer() { + final ITaskOrganizer organizer = mock(ITaskOrganizer.class); + when(organizer.asBinder()).thenReturn(new Binder()); + + mWm.mAtmService.registerTaskOrganizer(organizer, WINDOWING_MODE_PINNED); + + return organizer; + } + + @Test + public void testAppearVanish() throws RemoteException { + final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); + final Task task = createTaskInStack(stack, 0 /* userId */); + final ITaskOrganizer organizer = makeAndRegisterMockOrganizer(); + + task.setTaskOrganizer(organizer); + verify(organizer).taskAppeared(any(), any()); + assertTrue(task.isControlledByTaskOrganizer()); + + task.removeImmediately(); + verify(organizer).taskVanished(any()); + } + + @Test + public void testSwapOrganizer() throws RemoteException { + final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); + final Task task = createTaskInStack(stack, 0 /* userId */); + final ITaskOrganizer organizer = makeAndRegisterMockOrganizer(); + final ITaskOrganizer organizer2 = makeAndRegisterMockOrganizer(); + + task.setTaskOrganizer(organizer); + verify(organizer).taskAppeared(any(), any()); + task.setTaskOrganizer(organizer2); + verify(organizer).taskVanished(any()); + verify(organizer2).taskAppeared(any(), any()); + } + + @Test + public void testClearOrganizer() throws RemoteException { + final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); + final Task task = createTaskInStack(stack, 0 /* userId */); + final ITaskOrganizer organizer = makeAndRegisterMockOrganizer(); + + task.setTaskOrganizer(organizer); + verify(organizer).taskAppeared(any(), any()); + assertTrue(task.isControlledByTaskOrganizer()); + + task.setTaskOrganizer(null); + verify(organizer).taskVanished(any()); + assertFalse(task.isControlledByTaskOrganizer()); + } + + @Test + public void testTransferStackToOrganizer() throws RemoteException { + final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); + final Task task = createTaskInStack(stack, 0 /* userId */); + final Task task2 = createTaskInStack(stack, 0 /* userId */); + final ITaskOrganizer organizer = makeAndRegisterMockOrganizer(); + + stack.transferToTaskOrganizer(organizer); + + verify(organizer, times(2)).taskAppeared(any(), any()); + assertTrue(task.isControlledByTaskOrganizer()); + assertTrue(task2.isControlledByTaskOrganizer()); + + stack.transferToTaskOrganizer(null); + + verify(organizer, times(2)).taskVanished(any()); + assertFalse(task.isControlledByTaskOrganizer()); + assertFalse(task2.isControlledByTaskOrganizer()); + } + + @Test + public void testRegisterTaskOrganizerTaskWindowingModeChanges() throws RemoteException { + final ITaskOrganizer organizer = makeAndRegisterMockOrganizer(); + + final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); + final Task task = createTaskInStack(stack, 0 /* userId */); + task.setWindowingMode(WINDOWING_MODE_PINNED); + verify(organizer).taskAppeared(any(), any()); + assertTrue(task.isControlledByTaskOrganizer()); + + task.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + verify(organizer).taskVanished(any()); + assertFalse(task.isControlledByTaskOrganizer()); + } + + @Test + public void testRegisterTaskOrganizerStackWindowingModeChanges() throws RemoteException { + final ITaskOrganizer organizer = makeAndRegisterMockOrganizer(); + + final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); + final Task task = createTaskInStack(stack, 0 /* userId */); + final Task task2 = createTaskInStack(stack, 0 /* userId */); + stack.setWindowingMode(WINDOWING_MODE_PINNED); + verify(organizer, times(2)).taskAppeared(any(), any()); + assertTrue(task.isControlledByTaskOrganizer()); + assertTrue(task2.isControlledByTaskOrganizer()); + + stack.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + verify(organizer, times(2)).taskVanished(any()); + assertFalse(task.isControlledByTaskOrganizer()); + assertFalse(task2.isControlledByTaskOrganizer()); + } +} diff --git a/tests/TaskOrganizerTest/Android.bp b/tests/TaskOrganizerTest/Android.bp new file mode 100644 index 000000000000..8a13dbc52c66 --- /dev/null +++ b/tests/TaskOrganizerTest/Android.bp @@ -0,0 +1,22 @@ +// +// 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. +// + +android_test { + name: "TaskOrganizerTest", + srcs: ["**/*.java"], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/TaskOrganizerTest/AndroidManifest.xml b/tests/TaskOrganizerTest/AndroidManifest.xml new file mode 100644 index 000000000000..0cb6c10a7ff5 --- /dev/null +++ b/tests/TaskOrganizerTest/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.taskembed"> + <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" /> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <application> + <service android:name=".TaskOrganizerPipTest" + android:exported="true"> + </service> + </application> +</manifest> diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java new file mode 100644 index 000000000000..6ffa19d4ec98 --- /dev/null +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java @@ -0,0 +1,127 @@ +/* + * 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 com.android.test.taskembed; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.Service; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.ITaskOrganizer; +import android.view.IWindowContainer; +import android.view.WindowContainerTransaction; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +public class TaskOrganizerPipTest extends Service { + static final int PIP_WIDTH = 640; + static final int PIP_HEIGHT = 360; + + class PipOrgView extends SurfaceView implements SurfaceHolder.Callback { + PipOrgView(Context c) { + super(c); + getHolder().addCallback(this); + setZOrderOnTop(true); + } + @Override + public void surfaceCreated(SurfaceHolder holder) { + try { + ActivityTaskManager.getService().registerTaskOrganizer(mOrganizer, + WindowConfiguration.WINDOWING_MODE_PINNED); + } catch (Exception e) { + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + } + + void reparentTask(IWindowContainer wc) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + SurfaceControl leash = null; + try { + leash = wc.getLeash(); + } catch (Exception e) { + // System server died.. oh well + } + t.reparent(leash, getSurfaceControl()) + .setPosition(leash, 0, 0) + .apply(); + } + } + + PipOrgView mPipView; + + class Organizer extends ITaskOrganizer.Stub { + public void taskAppeared(IWindowContainer wc, ActivityManager.RunningTaskInfo ti) { + mPipView.reparentTask(wc); + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.scheduleFinishEnterPip(wc, new Rect(0, 0, PIP_WIDTH, PIP_HEIGHT)); + try { + ActivityTaskManager.getService().applyContainerTransaction(wct); + } catch (Exception e) { + } + } + public void taskVanished(IWindowContainer wc) { + } + public void transactionReady(int id, SurfaceControl.Transaction t) { + } + } + + Organizer mOrganizer = new Organizer(); + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + final WindowManager.LayoutParams wlp = new WindowManager.LayoutParams(); + wlp.setTitle("TaskOrganizerPipTest"); + wlp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + wlp.width = wlp.height = ViewGroup.LayoutParams.WRAP_CONTENT; + + FrameLayout layout = new FrameLayout(this); + ViewGroup.LayoutParams lp = + new ViewGroup.LayoutParams(PIP_WIDTH, PIP_HEIGHT); + mPipView = new PipOrgView(this); + layout.addView(mPipView, lp); + + WindowManager wm = getSystemService(WindowManager.class); + wm.addView(layout, wlp); + } + + @Override + public void onDestroy() { + super.onDestroy(); + } +} |