1 /* 2 * Copyright (C) 2020 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_UNDEFINED; 22 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; 23 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; 24 import static android.view.Display.DEFAULT_DISPLAY; 25 26 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; 27 28 import android.app.ActivityManager.RunningTaskInfo; 29 import android.graphics.Point; 30 import android.graphics.Rect; 31 import android.util.Log; 32 import android.util.SparseArray; 33 import android.view.SurfaceControl; 34 import android.view.SurfaceSession; 35 import android.window.TaskOrganizer; 36 37 import androidx.annotation.NonNull; 38 39 import com.android.internal.protolog.common.ProtoLog; 40 import com.android.wm.shell.ShellTaskOrganizer; 41 import com.android.wm.shell.common.SurfaceUtils; 42 import com.android.wm.shell.common.SyncTransactionQueue; 43 import com.android.wm.shell.transition.Transitions; 44 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 48 class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener { 49 private static final String TAG = LegacySplitScreenTaskListener.class.getSimpleName(); 50 private static final boolean DEBUG = LegacySplitScreenController.DEBUG; 51 52 private final ShellTaskOrganizer mTaskOrganizer; 53 private final SyncTransactionQueue mSyncQueue; 54 private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>(); 55 56 // TODO(shell-transitions): Remove when switched to shell-transitions. 57 private final SparseArray<Point> mPositionByTaskId = new SparseArray<>(); 58 59 RunningTaskInfo mPrimary; 60 RunningTaskInfo mSecondary; 61 SurfaceControl mPrimarySurface; 62 SurfaceControl mSecondarySurface; 63 SurfaceControl mPrimaryDim; 64 SurfaceControl mSecondaryDim; 65 Rect mHomeBounds = new Rect(); 66 final LegacySplitScreenController mSplitScreenController; 67 private boolean mSplitScreenSupported = false; 68 69 final SurfaceSession mSurfaceSession = new SurfaceSession(); 70 71 private final LegacySplitScreenTransitions mSplitTransitions; 72 LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, SyncTransactionQueue syncQueue)73 LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController, 74 ShellTaskOrganizer shellTaskOrganizer, 75 Transitions transitions, 76 SyncTransactionQueue syncQueue) { 77 mSplitScreenController = splitScreenController; 78 mTaskOrganizer = shellTaskOrganizer; 79 mSplitTransitions = new LegacySplitScreenTransitions(splitScreenController.mTransactionPool, 80 transitions, mSplitScreenController, this); 81 transitions.addHandler(mSplitTransitions); 82 mSyncQueue = syncQueue; 83 } 84 init()85 void init() { 86 synchronized (this) { 87 try { 88 mTaskOrganizer.createRootTask( 89 DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, this); 90 mTaskOrganizer.createRootTask( 91 DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, this); 92 } catch (Exception e) { 93 // teardown to prevent callbacks 94 mTaskOrganizer.removeListener(this); 95 throw e; 96 } 97 } 98 } 99 isSplitScreenSupported()100 boolean isSplitScreenSupported() { 101 return mSplitScreenSupported; 102 } 103 getTransaction()104 SurfaceControl.Transaction getTransaction() { 105 return mSplitScreenController.mTransactionPool.acquire(); 106 } 107 releaseTransaction(SurfaceControl.Transaction t)108 void releaseTransaction(SurfaceControl.Transaction t) { 109 mSplitScreenController.mTransactionPool.release(t); 110 } 111 getTaskOrganizer()112 TaskOrganizer getTaskOrganizer() { 113 return mTaskOrganizer; 114 } 115 getSplitTransitions()116 LegacySplitScreenTransitions getSplitTransitions() { 117 return mSplitTransitions; 118 } 119 120 @Override onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash)121 public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { 122 synchronized (this) { 123 if (taskInfo.hasParentTask()) { 124 handleChildTaskAppeared(taskInfo, leash); 125 return; 126 } 127 128 final int winMode = taskInfo.getWindowingMode(); 129 if (winMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { 130 ProtoLog.v(WM_SHELL_TASK_ORG, 131 "%s onTaskAppeared Primary taskId=%d", TAG, taskInfo.taskId); 132 mPrimary = taskInfo; 133 mPrimarySurface = leash; 134 } else if (winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { 135 ProtoLog.v(WM_SHELL_TASK_ORG, 136 "%s onTaskAppeared Secondary taskId=%d", TAG, taskInfo.taskId); 137 mSecondary = taskInfo; 138 mSecondarySurface = leash; 139 } else { 140 ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared unknown taskId=%d winMode=%d", 141 TAG, taskInfo.taskId, winMode); 142 } 143 144 if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) { 145 mSplitScreenSupported = true; 146 mSplitScreenController.onSplitScreenSupported(); 147 ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared Supported", TAG); 148 149 // Initialize dim surfaces: 150 SurfaceControl.Transaction t = getTransaction(); 151 mPrimaryDim = SurfaceUtils.makeDimLayer( 152 t, mPrimarySurface, "Primary Divider Dim", mSurfaceSession); 153 mSecondaryDim = SurfaceUtils.makeDimLayer( 154 t, mSecondarySurface, "Secondary Divider Dim", mSurfaceSession); 155 t.apply(); 156 releaseTransaction(t); 157 } 158 } 159 } 160 161 @Override onTaskVanished(RunningTaskInfo taskInfo)162 public void onTaskVanished(RunningTaskInfo taskInfo) { 163 synchronized (this) { 164 mPositionByTaskId.remove(taskInfo.taskId); 165 if (taskInfo.hasParentTask()) { 166 mLeashByTaskId.remove(taskInfo.taskId); 167 return; 168 } 169 170 final boolean isPrimaryTask = mPrimary != null 171 && taskInfo.token.equals(mPrimary.token); 172 final boolean isSecondaryTask = mSecondary != null 173 && taskInfo.token.equals(mSecondary.token); 174 175 if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) { 176 mSplitScreenSupported = false; 177 178 SurfaceControl.Transaction t = getTransaction(); 179 t.remove(mPrimaryDim); 180 t.remove(mSecondaryDim); 181 t.remove(mPrimarySurface); 182 t.remove(mSecondarySurface); 183 t.apply(); 184 releaseTransaction(t); 185 186 mSplitScreenController.onTaskVanished(); 187 } 188 } 189 } 190 191 @Override onTaskInfoChanged(RunningTaskInfo taskInfo)192 public void onTaskInfoChanged(RunningTaskInfo taskInfo) { 193 if (taskInfo.displayId != DEFAULT_DISPLAY) { 194 return; 195 } 196 synchronized (this) { 197 if (!taskInfo.supportsMultiWindow) { 198 if (mSplitScreenController.isDividerVisible()) { 199 // Dismiss the split screen if the task no longer supports multi window. 200 if (taskInfo.taskId == mPrimary.taskId 201 || taskInfo.parentTaskId == mPrimary.taskId) { 202 // If the primary is focused, dismiss to primary. 203 mSplitScreenController 204 .startDismissSplit(taskInfo.isFocused /* toPrimaryTask */); 205 } else { 206 // If the secondary is not focused, dismiss to primary. 207 mSplitScreenController 208 .startDismissSplit(!taskInfo.isFocused /* toPrimaryTask */); 209 } 210 } 211 return; 212 } 213 if (taskInfo.hasParentTask()) { 214 // changed messages are noisy since it reports on every ensureVisibility. This 215 // conflicts with legacy app-transitions which "swaps" the position to a 216 // leash. For now, only update when position actually changes to avoid 217 // poorly-timed duplicate calls. 218 if (taskInfo.positionInParent.equals(mPositionByTaskId.get(taskInfo.taskId))) { 219 return; 220 } 221 handleChildTaskChanged(taskInfo); 222 } else { 223 handleTaskInfoChanged(taskInfo); 224 } 225 mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent)); 226 } 227 } 228 handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash)229 private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { 230 mLeashByTaskId.put(taskInfo.taskId, leash); 231 mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent)); 232 if (Transitions.ENABLE_SHELL_TRANSITIONS) return; 233 updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); 234 } 235 handleChildTaskChanged(RunningTaskInfo taskInfo)236 private void handleChildTaskChanged(RunningTaskInfo taskInfo) { 237 if (Transitions.ENABLE_SHELL_TRANSITIONS) return; 238 final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); 239 updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); 240 } 241 updateChildTaskSurface( RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared)242 private void updateChildTaskSurface( 243 RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) { 244 final Point taskPositionInParent = taskInfo.positionInParent; 245 mSyncQueue.runInSync(t -> { 246 t.setWindowCrop(leash, null); 247 t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); 248 if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) { 249 t.setAlpha(leash, 1f); 250 t.setMatrix(leash, 1, 0, 0, 1); 251 t.show(leash); 252 } 253 }); 254 } 255 256 /** 257 * This is effectively a finite state machine which moves between the various split-screen 258 * presentations based on the contents of the split regions. 259 */ handleTaskInfoChanged(RunningTaskInfo info)260 private void handleTaskInfoChanged(RunningTaskInfo info) { 261 if (!mSplitScreenSupported) { 262 // This shouldn't happen; but apparently there is a chance that SysUI crashes without 263 // system server receiving binder-death (or maybe it receives binder-death too late?). 264 // In this situation, when sys-ui restarts, the split root-tasks will still exist so 265 // there is a small window of time during init() where WM might send messages here 266 // before init() fails. So, avoid a cycle of crashes by returning early. 267 Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info); 268 return; 269 } 270 final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME 271 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS 272 && mSplitScreenController.isHomeStackResizable()); 273 final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; 274 final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; 275 if (info.token.asBinder() == mPrimary.token.asBinder()) { 276 mPrimary = info; 277 } else if (info.token.asBinder() == mSecondary.token.asBinder()) { 278 mSecondary = info; 279 } 280 if (DEBUG) { 281 Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary); 282 } 283 if (Transitions.ENABLE_SHELL_TRANSITIONS) return; 284 final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED; 285 final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED; 286 final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME 287 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS 288 && mSplitScreenController.isHomeStackResizable()); 289 if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty 290 && secondaryImpliedMinimize == secondaryImpliesMinimize) { 291 // No relevant changes 292 return; 293 } 294 if (primaryIsEmpty || secondaryIsEmpty) { 295 // At-least one of the splits is empty which means we are currently transitioning 296 // into or out-of split-screen mode. 297 if (DEBUG) { 298 Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType 299 + " " + mSecondary.topActivityType); 300 } 301 if (mSplitScreenController.isDividerVisible()) { 302 // Was in split-mode, which means we are leaving split, so continue that. 303 // This happens when the stack in the primary-split is dismissed. 304 if (DEBUG) { 305 Log.d(TAG, " was in split, so this means leave it " 306 + mPrimary.topActivityType + " " + mSecondary.topActivityType); 307 } 308 mSplitScreenController.startDismissSplit(false /* toPrimaryTask */); 309 } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) { 310 // Wasn't in split-mode (both were empty), but now that the primary split is 311 // populated, we should fully enter split by moving everything else into secondary. 312 // This just tells window-manager to reparent things, the UI will respond 313 // when it gets new task info for the secondary split. 314 if (DEBUG) { 315 Log.d(TAG, " was not in split, but primary is populated, so enter it"); 316 } 317 mSplitScreenController.startEnterSplit(); 318 } 319 } else if (secondaryImpliesMinimize) { 320 // Workaround for b/172686383, we can't rely on the sync bounds change transaction for 321 // the home task to finish before the last updateChildTaskSurface() call even if it's 322 // queued on the sync transaction queue, so ensure that the home task surface is updated 323 // again before we minimize 324 final ArrayList<RunningTaskInfo> tasks = new ArrayList<>(); 325 mSplitScreenController.getWmProxy().getHomeAndRecentsTasks(tasks, 326 mSplitScreenController.getSecondaryRoot()); 327 for (int i = 0; i < tasks.size(); i++) { 328 final RunningTaskInfo taskInfo = tasks.get(i); 329 final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId); 330 if (leash != null) { 331 updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */); 332 } 333 } 334 335 // Both splits are populated but the secondary split has a home/recents stack on top, 336 // so enter minimized mode. 337 mSplitScreenController.ensureMinimizedSplit(); 338 } else { 339 // Both splits are populated by normal activities, so make sure we aren't minimized. 340 mSplitScreenController.ensureNormalSplit(); 341 } 342 } 343 344 @Override attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b)345 public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { 346 if (!mLeashByTaskId.contains(taskId)) { 347 throw new IllegalArgumentException("There is no surface for taskId=" + taskId); 348 } 349 b.setParent(mLeashByTaskId.get(taskId)); 350 } 351 352 @Override dump(@onNull PrintWriter pw, String prefix)353 public void dump(@NonNull PrintWriter pw, String prefix) { 354 final String innerPrefix = prefix + " "; 355 final String childPrefix = innerPrefix + " "; 356 pw.println(prefix + this); 357 pw.println(innerPrefix + "mSplitScreenSupported=" + mSplitScreenSupported); 358 if (mPrimary != null) pw.println(innerPrefix + "mPrimary.taskId=" + mPrimary.taskId); 359 if (mSecondary != null) pw.println(innerPrefix + "mSecondary.taskId=" + mSecondary.taskId); 360 } 361 362 @Override toString()363 public String toString() { 364 return TAG; 365 } 366 } 367