1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.legacysplitscreen; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; 21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 23 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 24 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; 25 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 26 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 27 import static android.content.res.Configuration.ORIENTATION_UNDEFINED; 28 import static android.view.Display.DEFAULT_DISPLAY; 29 30 import android.annotation.NonNull; 31 import android.app.ActivityManager; 32 import android.app.ActivityTaskManager; 33 import android.graphics.Rect; 34 import android.os.RemoteException; 35 import android.util.Log; 36 import android.view.Display; 37 import android.view.SurfaceControl; 38 import android.view.WindowManagerGlobal; 39 import android.window.TaskOrganizer; 40 import android.window.WindowContainerToken; 41 import android.window.WindowContainerTransaction; 42 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.util.ArrayUtils; 45 import com.android.wm.shell.common.SyncTransactionQueue; 46 import com.android.wm.shell.transition.Transitions; 47 48 import java.util.ArrayList; 49 import java.util.List; 50 51 /** 52 * Proxy to simplify calls into window manager/activity manager 53 */ 54 class WindowManagerProxy { 55 56 private static final String TAG = "WindowManagerProxy"; 57 private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS}; 58 private static final int[] CONTROLLED_ACTIVITY_TYPES = { 59 ACTIVITY_TYPE_STANDARD, 60 ACTIVITY_TYPE_HOME, 61 ACTIVITY_TYPE_RECENTS, 62 ACTIVITY_TYPE_UNDEFINED 63 }; 64 private static final int[] CONTROLLED_WINDOWING_MODES = { 65 WINDOWING_MODE_FULLSCREEN, 66 WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, 67 WINDOWING_MODE_UNDEFINED 68 }; 69 70 @GuardedBy("mDockedRect") 71 private final Rect mDockedRect = new Rect(); 72 73 private final Rect mTmpRect1 = new Rect(); 74 75 @GuardedBy("mDockedRect") 76 private final Rect mTouchableRegion = new Rect(); 77 78 private final SyncTransactionQueue mSyncTransactionQueue; 79 private final TaskOrganizer mTaskOrganizer; 80 WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer)81 WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) { 82 mSyncTransactionQueue = syncQueue; 83 mTaskOrganizer = taskOrganizer; 84 } 85 dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, final boolean dismissOrMaximize)86 void dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles, 87 LegacySplitDisplayLayout layout, final boolean dismissOrMaximize) { 88 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 89 tiles.mSplitScreenController.startDismissSplit(!dismissOrMaximize, true /* snapped */); 90 } else { 91 applyDismissSplit(tiles, layout, dismissOrMaximize); 92 } 93 } 94 setResizing(final boolean resizing)95 public void setResizing(final boolean resizing) { 96 try { 97 ActivityTaskManager.getService().setSplitScreenResizing(resizing); 98 } catch (RemoteException e) { 99 Log.w(TAG, "Error calling setDockedStackResizing: " + e); 100 } 101 } 102 103 /** Sets a touch region */ setTouchRegion(Rect region)104 public void setTouchRegion(Rect region) { 105 try { 106 synchronized (mDockedRect) { 107 mTouchableRegion.set(region); 108 } 109 WindowManagerGlobal.getWindowManagerService().setDockedTaskDividerTouchRegion( 110 mTouchableRegion); 111 } catch (RemoteException e) { 112 Log.w(TAG, "Failed to set touchable region: " + e); 113 } 114 } 115 applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout)116 void applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout) { 117 WindowContainerTransaction t = new WindowContainerTransaction(); 118 splitLayout.resizeSplits(position, t); 119 applySyncTransaction(t); 120 } 121 getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out, WindowContainerToken parent)122 boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out, 123 WindowContainerToken parent) { 124 boolean resizable = false; 125 List<ActivityManager.RunningTaskInfo> rootTasks = parent == null 126 ? mTaskOrganizer.getRootTasks(Display.DEFAULT_DISPLAY, HOME_AND_RECENTS) 127 : mTaskOrganizer.getChildTasks(parent, HOME_AND_RECENTS); 128 for (int i = 0, n = rootTasks.size(); i < n; ++i) { 129 final ActivityManager.RunningTaskInfo ti = rootTasks.get(i); 130 out.add(ti); 131 if (ti.topActivityType == ACTIVITY_TYPE_HOME) { 132 resizable = ti.isResizeable; 133 } 134 } 135 return resizable; 136 } 137 138 /** 139 * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary 140 * split is minimized. This actually "sticks out" of the secondary split area, but when in 141 * minimized mode, the secondary split gets a 'negative' crop to expose it. 142 */ applyHomeTasksMinimized(LegacySplitDisplayLayout layout, WindowContainerToken parent, @NonNull WindowContainerTransaction wct)143 boolean applyHomeTasksMinimized(LegacySplitDisplayLayout layout, WindowContainerToken parent, 144 @NonNull WindowContainerTransaction wct) { 145 // Resize the home/recents stacks to the larger minimized-state size 146 final Rect homeBounds; 147 final ArrayList<ActivityManager.RunningTaskInfo> homeStacks = new ArrayList<>(); 148 boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent); 149 if (isHomeResizable) { 150 homeBounds = layout.calcResizableMinimizedHomeStackBounds(); 151 } else { 152 // home is not resizable, so lock it to its inherent orientation size. 153 homeBounds = new Rect(0, 0, 0, 0); 154 for (int i = homeStacks.size() - 1; i >= 0; --i) { 155 if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_HOME) { 156 final int orient = homeStacks.get(i).configuration.orientation; 157 final boolean displayLandscape = layout.mDisplayLayout.isLandscape(); 158 final boolean isLandscape = orient == ORIENTATION_LANDSCAPE 159 || (orient == ORIENTATION_UNDEFINED && displayLandscape); 160 homeBounds.right = isLandscape == displayLandscape 161 ? layout.mDisplayLayout.width() : layout.mDisplayLayout.height(); 162 homeBounds.bottom = isLandscape == displayLandscape 163 ? layout.mDisplayLayout.height() : layout.mDisplayLayout.width(); 164 break; 165 } 166 } 167 } 168 for (int i = homeStacks.size() - 1; i >= 0; --i) { 169 // For non-resizable homes, the minimized size is actually the fullscreen-size. As a 170 // result, we don't minimize for recents since it only shows half-size screenshots. 171 if (!isHomeResizable) { 172 if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_RECENTS) { 173 continue; 174 } 175 wct.setWindowingMode(homeStacks.get(i).token, WINDOWING_MODE_FULLSCREEN); 176 } 177 wct.setBounds(homeStacks.get(i).token, homeBounds); 178 } 179 layout.mTiles.mHomeBounds.set(homeBounds); 180 return isHomeResizable; 181 } 182 183 /** @see #buildEnterSplit */ applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout)184 boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) { 185 // Set launchtile first so that any stack created after 186 // getAllRootTaskInfos and before reparent (even if unlikely) are placed 187 // correctly. 188 WindowContainerTransaction wct = new WindowContainerTransaction(); 189 wct.setLaunchRoot(tiles.mSecondary.token, CONTROLLED_WINDOWING_MODES, 190 CONTROLLED_ACTIVITY_TYPES); 191 final boolean isHomeResizable = buildEnterSplit(wct, tiles, layout); 192 applySyncTransaction(wct); 193 return isHomeResizable; 194 } 195 196 /** 197 * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split. 198 * This assumes there is already something in the primary split since that is usually what 199 * triggers a call to this. In the same transaction, this overrides the home task bounds via 200 * {@link #applyHomeTasksMinimized}. 201 * 202 * @return whether the home stack is resizable 203 */ buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout)204 boolean buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, 205 LegacySplitDisplayLayout layout) { 206 List<ActivityManager.RunningTaskInfo> rootTasks = 207 mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */); 208 if (rootTasks.isEmpty()) { 209 return false; 210 } 211 ActivityManager.RunningTaskInfo topHomeTask = null; 212 for (int i = rootTasks.size() - 1; i >= 0; --i) { 213 final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i); 214 // Check whether the task can be moved to split secondary. 215 if (!rootTask.supportsMultiWindow && rootTask.topActivityType != ACTIVITY_TYPE_HOME) { 216 continue; 217 } 218 // Only move split controlling tasks to split secondary. 219 final int windowingMode = rootTask.getWindowingMode(); 220 if (!ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, windowingMode) 221 || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, rootTask.getActivityType()) 222 // Excludes split screen secondary due to it's the root we're reparenting to. 223 || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { 224 continue; 225 } 226 // Since this iterates from bottom to top, update topHomeTask for every fullscreen task 227 // so it will be left with the status of the top one. 228 topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null; 229 outWct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); 230 } 231 // Move the secondary split-forward. 232 outWct.reorder(tiles.mSecondary.token, true /* onTop */); 233 boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, 234 outWct); 235 if (topHomeTask != null && !Transitions.ENABLE_SHELL_TRANSITIONS) { 236 // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST 237 // is enabled, this temporarily syncs the home surface position with offset until 238 // sync transaction finishes. 239 outWct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds); 240 } 241 return isHomeResizable; 242 } 243 isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti)244 static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) { 245 final int atype = ti.getActivityType(); 246 return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS; 247 } 248 249 /** @see #buildDismissSplit */ applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize)250 void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, 251 boolean dismissOrMaximize) { 252 // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished 253 // plus specific APIs to clean this up. 254 final WindowContainerTransaction wct = new WindowContainerTransaction(); 255 // Set launch root first so that any task created after getChildContainers and 256 // before reparent (pretty unlikely) are put into fullscreen. 257 wct.setLaunchRoot(tiles.mSecondary.token, null, null); 258 buildDismissSplit(wct, tiles, layout, dismissOrMaximize); 259 applySyncTransaction(wct); 260 } 261 262 /** 263 * Reparents all tile members back to their display and resets home task override bounds. 264 * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary 265 * split (thus resulting in the top of the secondary split becoming 266 * fullscreen. {@code false} resolves the other way. 267 */ buildDismissSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize)268 static void buildDismissSplit(WindowContainerTransaction outWct, 269 LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, 270 boolean dismissOrMaximize) { 271 // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished 272 // plus specific APIs to clean this up. 273 final TaskOrganizer taskOrg = tiles.getTaskOrganizer(); 274 List<ActivityManager.RunningTaskInfo> primaryChildren = 275 taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */); 276 List<ActivityManager.RunningTaskInfo> secondaryChildren = 277 taskOrg.getChildTasks(tiles.mSecondary.token, null /* activityTypes */); 278 // In some cases (eg. non-resizable is launched), system-server will leave split-screen. 279 // as a result, the above will not capture any tasks; yet, we need to clean-up the 280 // home task bounds. 281 List<ActivityManager.RunningTaskInfo> freeHomeAndRecents = 282 taskOrg.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS); 283 // Filter out the root split tasks 284 freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token) 285 || p.token.equals(tiles.mPrimary.token)); 286 287 if (primaryChildren.isEmpty() && secondaryChildren.isEmpty() 288 && freeHomeAndRecents.isEmpty()) { 289 return; 290 } 291 if (dismissOrMaximize) { 292 // Dismissing, so move all primary split tasks first 293 for (int i = primaryChildren.size() - 1; i >= 0; --i) { 294 outWct.reparent(primaryChildren.get(i).token, null /* parent */, 295 true /* onTop */); 296 } 297 boolean homeOnTop = false; 298 // Don't need to worry about home tasks because they are already in the "proper" 299 // order within the secondary split. 300 for (int i = secondaryChildren.size() - 1; i >= 0; --i) { 301 final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); 302 outWct.reparent(ti.token, null /* parent */, true /* onTop */); 303 if (isHomeOrRecentTask(ti)) { 304 outWct.setBounds(ti.token, null); 305 outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); 306 if (i == 0) { 307 homeOnTop = true; 308 } 309 } 310 } 311 if (homeOnTop && !Transitions.ENABLE_SHELL_TRANSITIONS) { 312 // Translate/update-crop of secondary out-of-band with sync transaction -- instead 313 // play this in sync with new home-app frame because until BALST is enabled this 314 // shows up on screen before the syncTransaction returns. 315 // We only have access to the secondary root surface, though, so in order to 316 // position things properly, we have to take into account the existing negative 317 // offset/crop of the minimized-home task. 318 final boolean landscape = layout.mDisplayLayout.isLandscape(); 319 final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left 320 : layout.mSecondary.left; 321 final int posY = landscape ? layout.mSecondary.top 322 : layout.mSecondary.top - tiles.mHomeBounds.top; 323 final SurfaceControl.Transaction sft = new SurfaceControl.Transaction(); 324 sft.setPosition(tiles.mSecondarySurface, posX, posY); 325 final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(), 326 layout.mDisplayLayout.height()); 327 crop.offset(-posX, -posY); 328 sft.setWindowCrop(tiles.mSecondarySurface, crop); 329 outWct.setBoundsChangeTransaction(tiles.mSecondary.token, sft); 330 } 331 } else { 332 // Maximize, so move non-home secondary split first 333 for (int i = secondaryChildren.size() - 1; i >= 0; --i) { 334 if (isHomeOrRecentTask(secondaryChildren.get(i))) { 335 continue; 336 } 337 outWct.reparent(secondaryChildren.get(i).token, null /* parent */, 338 true /* onTop */); 339 } 340 // Find and place home tasks in-between. This simulates the fact that there was 341 // nothing behind the primary split's tasks. 342 for (int i = secondaryChildren.size() - 1; i >= 0; --i) { 343 final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i); 344 if (isHomeOrRecentTask(ti)) { 345 outWct.reparent(ti.token, null /* parent */, true /* onTop */); 346 // reset bounds and mode too 347 outWct.setBounds(ti.token, null); 348 outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED); 349 } 350 } 351 for (int i = primaryChildren.size() - 1; i >= 0; --i) { 352 outWct.reparent(primaryChildren.get(i).token, null /* parent */, 353 true /* onTop */); 354 } 355 } 356 for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) { 357 outWct.setBounds(freeHomeAndRecents.get(i).token, null); 358 outWct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED); 359 } 360 // Reset focusable to true 361 outWct.setFocusable(tiles.mPrimary.token, true /* focusable */); 362 } 363 364 /** 365 * Utility to apply a sync transaction serially with other sync transactions. 366 * 367 * @see SyncTransactionQueue#queue 368 */ applySyncTransaction(WindowContainerTransaction wct)369 void applySyncTransaction(WindowContainerTransaction wct) { 370 mSyncTransactionQueue.queue(wct); 371 } 372 373 /** 374 * @see SyncTransactionQueue#queueIfWaiting 375 */ queueSyncTransactionIfWaiting(WindowContainerTransaction wct)376 boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) { 377 return mSyncTransactionQueue.queueIfWaiting(wct); 378 } 379 380 /** 381 * @see SyncTransactionQueue#runInSync 382 */ runInSync(SyncTransactionQueue.TransactionRunnable runnable)383 void runInSync(SyncTransactionQueue.TransactionRunnable runnable) { 384 mSyncTransactionQueue.runInSync(runnable); 385 } 386 } 387