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