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