| /* |
| * 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.server.wm; |
| |
| import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS; |
| import static android.Manifest.permission.READ_FRAME_BUFFER; |
| |
| import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS; |
| import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED; |
| import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; |
| import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; |
| import static com.android.server.wm.WindowContainer.POSITION_TOP; |
| |
| import android.app.WindowConfiguration; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Configuration; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| import android.view.Surface; |
| import android.view.SurfaceControl; |
| import android.window.IDisplayAreaOrganizerController; |
| import android.window.ITaskOrganizerController; |
| import android.window.IWindowContainerTransactionCallback; |
| import android.window.IWindowOrganizerController; |
| import android.window.WindowContainerToken; |
| import android.window.WindowContainerTransaction; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.function.pooled.PooledConsumer; |
| import com.android.internal.util.function.pooled.PooledLambda; |
| |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Server side implementation for the interface for organizing windows |
| * @see android.window.WindowOrganizer |
| */ |
| class WindowOrganizerController extends IWindowOrganizerController.Stub |
| implements BLASTSyncEngine.TransactionReadyListener { |
| |
| private static final String TAG = "WindowOrganizerController"; |
| |
| /** Flag indicating that an applied transaction may have effected lifecycle */ |
| private static final int TRANSACT_EFFECTS_CLIENT_CONFIG = 1; |
| private static final int TRANSACT_EFFECTS_LIFECYCLE = 1 << 1; |
| |
| /** |
| * Masks specifying which configurations task-organizers can control. Incoming transactions |
| * will be filtered to only include these. |
| */ |
| static final int CONTROLLABLE_CONFIGS = ActivityInfo.CONFIG_WINDOW_CONFIGURATION |
| | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE | ActivityInfo.CONFIG_SCREEN_SIZE; |
| static final int CONTROLLABLE_WINDOW_CONFIGS = WindowConfiguration.WINDOW_CONFIG_BOUNDS |
| | WindowConfiguration.WINDOW_CONFIG_APP_BOUNDS; |
| |
| private final ActivityTaskManagerService mService; |
| private final WindowManagerGlobalLock mGlobalLock; |
| |
| private final BLASTSyncEngine mBLASTSyncEngine = new BLASTSyncEngine(); |
| private final HashMap<Integer, IWindowContainerTransactionCallback> |
| mTransactionCallbacksByPendingSyncId = new HashMap(); |
| |
| final TaskOrganizerController mTaskOrganizerController; |
| final DisplayAreaOrganizerController mDisplayAreaOrganizerController; |
| |
| WindowOrganizerController(ActivityTaskManagerService atm) { |
| mService = atm; |
| mGlobalLock = atm.mGlobalLock; |
| mTaskOrganizerController = new TaskOrganizerController(mService); |
| mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService); |
| } |
| |
| @Override |
| public void applyTransaction(WindowContainerTransaction t) { |
| applySyncTransaction(t, null /*callback*/); |
| } |
| |
| @Override |
| public int applySyncTransaction(WindowContainerTransaction t, |
| IWindowContainerTransactionCallback callback) { |
| enforceStackPermission("applySyncTransaction()"); |
| int syncId = -1; |
| if (t == null) { |
| throw new IllegalArgumentException( |
| "Null transaction passed to applySyncTransaction"); |
| } |
| long ident = Binder.clearCallingIdentity(); |
| try { |
| synchronized (mGlobalLock) { |
| int effects = 0; |
| |
| /** |
| * If callback is non-null we are looking to synchronize this transaction by |
| * collecting all the results in to a SurfaceFlinger transaction and then delivering |
| * that to the given transaction ready callback. See {@link BLASTSyncEngine} for the |
| * details of the operation. But at a high level we create a sync operation with a |
| * given ID and an associated callback. Then we notify each WindowContainer in this |
| * WindowContainer transaction that it is participating in a sync operation with |
| * that ID. Once everything is notified we tell the BLASTSyncEngine "setSyncReady" |
| * which means that we have added everything to the set. At any point after this, |
| * all the WindowContainers will eventually finish applying their changes and notify |
| * the BLASTSyncEngine which will deliver the Transaction to the callback. |
| */ |
| if (callback != null) { |
| syncId = startSyncWithOrganizer(callback); |
| } |
| mService.deferWindowLayout(); |
| try { |
| ArraySet<WindowContainer> haveConfigChanges = new ArraySet<>(); |
| Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries = |
| t.getChanges().entrySet().iterator(); |
| while (entries.hasNext()) { |
| final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = |
| entries.next(); |
| final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); |
| if (!wc.isAttached()) { |
| Slog.e(TAG, "Attempt to operate on detached container: " + wc); |
| continue; |
| } |
| // Make sure we add to the syncSet before performing |
| // operations so we don't end up splitting effects between the WM |
| // pending transaction and the BLASTSync transaction. |
| if (syncId >= 0) { |
| addToSyncSet(syncId, wc); |
| } |
| |
| int containerEffect = applyWindowContainerChange(wc, entry.getValue()); |
| effects |= containerEffect; |
| |
| // Lifecycle changes will trigger ensureConfig for everything. |
| if ((effects & TRANSACT_EFFECTS_LIFECYCLE) == 0 |
| && (containerEffect & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) { |
| haveConfigChanges.add(wc); |
| } |
| } |
| // Hierarchy changes |
| final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps(); |
| for (int i = 0, n = hops.size(); i < n; ++i) { |
| final WindowContainerTransaction.HierarchyOp hop = hops.get(i); |
| final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer()); |
| if (!wc.isAttached()) { |
| Slog.e(TAG, "Attempt to operate on detached container: " + wc); |
| continue; |
| } |
| if (syncId >= 0) { |
| addToSyncSet(syncId, wc); |
| } |
| effects |= sanitizeAndApplyHierarchyOp(wc, hop); |
| } |
| // Queue-up bounds-change transactions for tasks which are now organized. Do |
| // this after hierarchy ops so we have the final organized state. |
| entries = t.getChanges().entrySet().iterator(); |
| while (entries.hasNext()) { |
| final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = |
| entries.next(); |
| final Task task = WindowContainer.fromBinder(entry.getKey()).asTask(); |
| final Rect surfaceBounds = entry.getValue().getBoundsChangeSurfaceBounds(); |
| if (task == null || !task.isAttached() || surfaceBounds == null) { |
| continue; |
| } |
| if (!task.isOrganized()) { |
| final Task parent = |
| task.getParent() != null ? task.getParent().asTask() : null; |
| // Also allow direct children of created-by-organizer tasks to be |
| // controlled. In the future, these will become organized anyways. |
| if (parent == null || !parent.mCreatedByOrganizer) { |
| throw new IllegalArgumentException( |
| "Can't manipulate non-organized task surface " + task); |
| } |
| } |
| final SurfaceControl.Transaction sft = new SurfaceControl.Transaction(); |
| final SurfaceControl sc = task.getSurfaceControl(); |
| sft.setPosition(sc, surfaceBounds.left, surfaceBounds.top); |
| if (surfaceBounds.isEmpty()) { |
| sft.setWindowCrop(sc, null); |
| } else { |
| sft.setWindowCrop(sc, surfaceBounds.width(), surfaceBounds.height()); |
| } |
| task.setMainWindowSizeChangeTransaction(sft); |
| } |
| if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) { |
| // Already calls ensureActivityConfig |
| mService.mRootWindowContainer.ensureActivitiesVisible( |
| null, 0, PRESERVE_WINDOWS); |
| } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) { |
| final PooledConsumer f = PooledLambda.obtainConsumer( |
| ActivityRecord::ensureActivityConfiguration, |
| PooledLambda.__(ActivityRecord.class), 0, |
| true /* preserveWindow */); |
| try { |
| for (int i = haveConfigChanges.size() - 1; i >= 0; --i) { |
| haveConfigChanges.valueAt(i).forAllActivities(f); |
| } |
| } finally { |
| f.recycle(); |
| } |
| } |
| |
| if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) == 0) { |
| mService.addWindowLayoutReasons(LAYOUT_REASON_CONFIG_CHANGED); |
| } |
| } finally { |
| mService.continueWindowLayout(); |
| if (syncId >= 0) { |
| setSyncReady(syncId); |
| } |
| } |
| } |
| } finally { |
| Binder.restoreCallingIdentity(ident); |
| } |
| return syncId; |
| } |
| |
| private int applyChanges(WindowContainer container, WindowContainerTransaction.Change change) { |
| // The "client"-facing API should prevent bad changes; however, just in case, sanitize |
| // masks here. |
| final int configMask = change.getConfigSetMask() & CONTROLLABLE_CONFIGS; |
| final int windowMask = change.getWindowSetMask() & CONTROLLABLE_WINDOW_CONFIGS; |
| int effects = 0; |
| if (configMask != 0) { |
| Configuration c = new Configuration(container.getRequestedOverrideConfiguration()); |
| c.setTo(change.getConfiguration(), configMask, windowMask); |
| container.onRequestedOverrideConfigurationChanged(c); |
| // TODO(b/145675353): remove the following once we could apply new bounds to the |
| // pinned stack together with its children. |
| resizePinnedStackIfNeeded(container, configMask, windowMask, c); |
| effects |= TRANSACT_EFFECTS_CLIENT_CONFIG; |
| } |
| if ((change.getChangeMask() & WindowContainerTransaction.Change.CHANGE_FOCUSABLE) != 0) { |
| if (container.setFocusable(change.getFocusable())) { |
| effects |= TRANSACT_EFFECTS_LIFECYCLE; |
| } |
| } |
| |
| final int windowingMode = change.getWindowingMode(); |
| if (windowingMode > -1) { |
| container.setWindowingMode(windowingMode); |
| } |
| return effects; |
| } |
| |
| private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) { |
| int effects = 0; |
| final SurfaceControl.Transaction t = c.getBoundsChangeTransaction(); |
| |
| if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_HIDDEN) != 0) { |
| if (tr.setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, c.getHidden())) { |
| effects = TRANSACT_EFFECTS_LIFECYCLE; |
| } |
| } |
| |
| final int childWindowingMode = c.getActivityWindowingMode(); |
| if (childWindowingMode > -1) { |
| tr.setActivityWindowingMode(childWindowingMode); |
| } |
| |
| if (t != null) { |
| tr.setMainWindowSizeChangeTransaction(t); |
| } |
| |
| Rect enterPipBounds = c.getEnterPipBounds(); |
| if (enterPipBounds != null) { |
| mService.mStackSupervisor.updatePictureInPictureMode(tr, enterPipBounds, true); |
| } |
| |
| return effects; |
| } |
| |
| private int sanitizeAndApplyHierarchyOp(WindowContainer container, |
| WindowContainerTransaction.HierarchyOp hop) { |
| final Task task = container.asTask(); |
| if (task == null) { |
| throw new IllegalArgumentException("Invalid container in hierarchy op"); |
| } |
| final DisplayContent dc = task.getDisplayContent(); |
| if (dc == null) { |
| Slog.w(TAG, "Container is no longer attached: " + task); |
| return 0; |
| } |
| final ActivityStack as = (ActivityStack) task; |
| |
| if (hop.isReparent()) { |
| final boolean isNonOrganizedRootableTask = |
| (task.isRootTask() && !task.mCreatedByOrganizer) |
| || task.getParent().asTask().mCreatedByOrganizer; |
| if (isNonOrganizedRootableTask) { |
| Task newParent = hop.getNewParent() == null ? null |
| : WindowContainer.fromBinder(hop.getNewParent()).asTask(); |
| if (task.getParent() != newParent) { |
| if (newParent == null) { |
| // Re-parent task to display as a root task. |
| as.reparent(dc.getDefaultTaskDisplayArea(), hop.getToTop()); |
| } else if (newParent.inMultiWindowMode() && !task.isResizeable() |
| && task.isLeafTask()) { |
| Slog.w(TAG, "Can't support task that doesn't support multi-window mode in" |
| + " multi-window mode... newParent=" + newParent + " task=" + task); |
| return 0; |
| } else { |
| task.reparent((ActivityStack) newParent, |
| hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM, |
| false /*moveParents*/, "sanitizeAndApplyHierarchyOp"); |
| } |
| } else { |
| final ActivityStack rootTask = |
| (ActivityStack) (newParent != null ? newParent : task.getRootTask()); |
| if (hop.getToTop()) { |
| as.getDisplayArea().positionStackAtTop(rootTask, |
| false /* includingParents */); |
| } else { |
| as.getDisplayArea().positionStackAtBottom(rootTask); |
| } |
| } |
| } else { |
| throw new RuntimeException("Reparenting leaf Tasks is not supported now."); |
| } |
| } else { |
| // Ugh, of course ActivityStack has its own special reorder logic... |
| if (task.isRootTask()) { |
| if (hop.getToTop()) { |
| as.getDisplayArea().positionStackAtTop(as, false /* includingParents */); |
| } else { |
| as.getDisplayArea().positionStackAtBottom(as); |
| } |
| } else { |
| task.getParent().positionChildAt( |
| hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM, |
| task, false /* includingParents */); |
| } |
| } |
| return TRANSACT_EFFECTS_LIFECYCLE; |
| } |
| |
| private void sanitizeWindowContainer(WindowContainer wc) { |
| if (!(wc instanceof Task) && !(wc instanceof DisplayArea)) { |
| throw new RuntimeException("Invalid token in task or displayArea transaction"); |
| } |
| } |
| |
| private int applyWindowContainerChange(WindowContainer wc, |
| WindowContainerTransaction.Change c) { |
| sanitizeWindowContainer(wc); |
| |
| int effects = applyChanges(wc, c); |
| |
| if (wc instanceof Task) { |
| effects |= applyTaskChanges(wc.asTask(), c); |
| } |
| |
| return effects; |
| } |
| |
| private void resizePinnedStackIfNeeded(ConfigurationContainer container, int configMask, |
| int windowMask, Configuration config) { |
| if ((container instanceof ActivityStack) |
| && ((configMask & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0) |
| && ((windowMask & WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)) { |
| final ActivityStack stack = (ActivityStack) container; |
| if (stack.inPinnedWindowingMode()) { |
| stack.resize(config.windowConfiguration.getBounds(), |
| PRESERVE_WINDOWS, true /* deferResume */); |
| } |
| } |
| } |
| |
| @Override |
| public ITaskOrganizerController getTaskOrganizerController() { |
| enforceStackPermission("getTaskOrganizerController()"); |
| return mTaskOrganizerController; |
| } |
| |
| @Override |
| public IDisplayAreaOrganizerController getDisplayAreaOrganizerController() { |
| enforceStackPermission("getDisplayAreaOrganizerController()"); |
| return mDisplayAreaOrganizerController; |
| } |
| |
| @VisibleForTesting |
| int startSyncWithOrganizer(IWindowContainerTransactionCallback callback) { |
| int id = mBLASTSyncEngine.startSyncSet(this); |
| mTransactionCallbacksByPendingSyncId.put(id, callback); |
| return id; |
| } |
| |
| @VisibleForTesting |
| void setSyncReady(int id) { |
| mBLASTSyncEngine.setReady(id); |
| } |
| |
| @VisibleForTesting |
| void addToSyncSet(int syncId, WindowContainer wc) { |
| mBLASTSyncEngine.addToSyncSet(syncId, wc); |
| } |
| |
| @Override |
| public void onTransactionReady(int mSyncId, Set<WindowContainer> windowContainersReady) { |
| final IWindowContainerTransactionCallback callback = |
| mTransactionCallbacksByPendingSyncId.get(mSyncId); |
| |
| SurfaceControl.Transaction mergedTransaction = new SurfaceControl.Transaction(); |
| for (WindowContainer container : windowContainersReady) { |
| container.mergeBlastSyncTransaction(mergedTransaction); |
| } |
| |
| try { |
| callback.onTransactionReady(mSyncId, mergedTransaction); |
| } catch (RemoteException e) { |
| // If there's an exception when trying to send the mergedTransaction to the client, we |
| // should immediately apply it here so the transactions aren't lost. |
| mergedTransaction.apply(); |
| } |
| |
| mTransactionCallbacksByPendingSyncId.remove(mSyncId); |
| } |
| |
| @Override |
| public boolean takeScreenshot(WindowContainerToken token, SurfaceControl outSurfaceControl) { |
| mService.mAmInternal.enforceCallingPermission(READ_FRAME_BUFFER, "takeScreenshot()"); |
| final WindowContainer wc = WindowContainer.fromBinder(token.asBinder()); |
| if (wc == null) { |
| throw new RuntimeException("Invalid token in screenshot transaction"); |
| } |
| |
| final Rect bounds = new Rect(); |
| wc.getBounds(bounds); |
| bounds.offsetTo(0, 0); |
| SurfaceControl.ScreenshotGraphicBuffer buffer = SurfaceControl.captureLayers( |
| wc.getSurfaceControl(), bounds, 1); |
| |
| if (buffer == null || buffer.getGraphicBuffer() == null) { |
| return false; |
| } |
| |
| SurfaceControl screenshot = mService.mWindowManager.mSurfaceControlFactory.apply(null) |
| .setName(wc.getName() + " - Organizer Screenshot") |
| .setBufferSize(bounds.width(), bounds.height()) |
| .setFormat(PixelFormat.TRANSLUCENT) |
| .setParent(wc.getParentSurfaceControl()) |
| .setCallsite("WindowOrganizerController.takeScreenshot") |
| .build(); |
| |
| Surface surface = new Surface(); |
| surface.copyFrom(screenshot); |
| surface.attachAndQueueBufferWithColorSpace(buffer.getGraphicBuffer(), null); |
| surface.release(); |
| |
| outSurfaceControl.copyFrom(screenshot, "WindowOrganizerController.takeScreenshot"); |
| return true; |
| } |
| |
| private void enforceStackPermission(String func) { |
| mService.mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, func); |
| } |
| } |