1 /* 2 * Copyright (C) 2021 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.taskview; 18 19 import static android.view.WindowManager.TRANSIT_CHANGE; 20 import static android.view.WindowManager.TRANSIT_CLOSE; 21 import static android.view.WindowManager.TRANSIT_OPEN; 22 import static android.view.WindowManager.TRANSIT_TO_BACK; 23 import static android.view.WindowManager.TRANSIT_TO_FRONT; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.app.ActivityManager; 28 import android.graphics.Rect; 29 import android.os.IBinder; 30 import android.util.ArrayMap; 31 import android.util.Slog; 32 import android.view.SurfaceControl; 33 import android.view.WindowManager; 34 import android.window.TransitionInfo; 35 import android.window.TransitionRequestInfo; 36 import android.window.WindowContainerTransaction; 37 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.wm.shell.transition.Transitions; 41 import com.android.wm.shell.util.TransitionUtil; 42 43 import java.util.ArrayList; 44 import java.util.Objects; 45 46 /** 47 * Handles Shell Transitions that involve TaskView tasks. 48 */ 49 public class TaskViewTransitions implements Transitions.TransitionHandler { 50 static final String TAG = "TaskViewTransitions"; 51 52 private final ArrayMap<TaskViewTaskController, TaskViewRequestedState> mTaskViews = 53 new ArrayMap<>(); 54 private final ArrayList<PendingTransition> mPending = new ArrayList<>(); 55 private final Transitions mTransitions; 56 private final boolean[] mRegistered = new boolean[]{ false }; 57 58 /** 59 * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be 60 * in-flight (collecting) at a time (because otherwise, the operations could get merged into 61 * a single transition). So, keep a queue here until we add a queue in server-side. 62 */ 63 @VisibleForTesting 64 static class PendingTransition { 65 final @WindowManager.TransitionType int mType; 66 final WindowContainerTransaction mWct; 67 final @NonNull TaskViewTaskController mTaskView; 68 IBinder mClaimed; 69 70 /** 71 * This is needed because arbitrary activity launches can still "intrude" into any 72 * transition since `startActivity` is a synchronous call. Once that is solved, we can 73 * remove this. 74 */ 75 final IBinder mLaunchCookie; 76 PendingTransition(@indowManager.TransitionType int type, @Nullable WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView, @Nullable IBinder launchCookie)77 PendingTransition(@WindowManager.TransitionType int type, 78 @Nullable WindowContainerTransaction wct, 79 @NonNull TaskViewTaskController taskView, 80 @Nullable IBinder launchCookie) { 81 mType = type; 82 mWct = wct; 83 mTaskView = taskView; 84 mLaunchCookie = launchCookie; 85 } 86 } 87 88 /** 89 * Visibility and bounds state that has been requested for a {@link TaskViewTaskController}. 90 */ 91 private static class TaskViewRequestedState { 92 boolean mVisible; 93 Rect mBounds = new Rect(); 94 } 95 TaskViewTransitions(Transitions transitions)96 public TaskViewTransitions(Transitions transitions) { 97 mTransitions = transitions; 98 // Defer registration until the first TaskView because we want this to be the "first" in 99 // priority when handling requests. 100 // TODO(210041388): register here once we have an explicit ordering mechanism. 101 } 102 addTaskView(TaskViewTaskController tv)103 void addTaskView(TaskViewTaskController tv) { 104 synchronized (mRegistered) { 105 if (!mRegistered[0]) { 106 mRegistered[0] = true; 107 mTransitions.addHandler(this); 108 } 109 } 110 mTaskViews.put(tv, new TaskViewRequestedState()); 111 } 112 removeTaskView(TaskViewTaskController tv)113 void removeTaskView(TaskViewTaskController tv) { 114 mTaskViews.remove(tv); 115 // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell 116 } 117 isEnabled()118 boolean isEnabled() { 119 return mTransitions.isRegistered(); 120 } 121 122 /** 123 * Looks through the pending transitions for a closing transaction that matches the provided 124 * `taskView`. 125 * @param taskView the pending transition should be for this. 126 */ findPendingCloseTransition(TaskViewTaskController taskView)127 private PendingTransition findPendingCloseTransition(TaskViewTaskController taskView) { 128 for (int i = mPending.size() - 1; i >= 0; --i) { 129 if (mPending.get(i).mTaskView != taskView) continue; 130 if (TransitionUtil.isClosingType(mPending.get(i).mType)) { 131 return mPending.get(i); 132 } 133 } 134 return null; 135 } 136 137 /** 138 * Looks through the pending transitions for a opening transaction that matches the provided 139 * `taskView`. 140 * @param taskView the pending transition should be for this. 141 */ 142 @VisibleForTesting findPendingOpeningTransition(TaskViewTaskController taskView)143 PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) { 144 for (int i = mPending.size() - 1; i >= 0; --i) { 145 if (mPending.get(i).mTaskView != taskView) continue; 146 if (TransitionUtil.isOpeningType(mPending.get(i).mType)) { 147 return mPending.get(i); 148 } 149 } 150 return null; 151 } 152 153 /** 154 * Looks through the pending transitions for one matching `taskView`. 155 * @param taskView the pending transition should be for this. 156 * @param type the type of transition it's looking for 157 */ findPending(TaskViewTaskController taskView, int type)158 PendingTransition findPending(TaskViewTaskController taskView, int type) { 159 for (int i = mPending.size() - 1; i >= 0; --i) { 160 if (mPending.get(i).mTaskView != taskView) continue; 161 if (mPending.get(i).mType == type) { 162 return mPending.get(i); 163 } 164 } 165 return null; 166 } 167 168 /** 169 * Returns all the pending transitions for a given `taskView`. 170 * @param taskView the pending transition should be for this. 171 */ findAllPending(TaskViewTaskController taskView)172 ArrayList<PendingTransition> findAllPending(TaskViewTaskController taskView) { 173 ArrayList<PendingTransition> list = new ArrayList<>(); 174 for (int i = mPending.size() - 1; i >= 0; --i) { 175 if (mPending.get(i).mTaskView != taskView) continue; 176 list.add(mPending.get(i)); 177 } 178 return list; 179 } 180 findPending(IBinder claimed)181 private PendingTransition findPending(IBinder claimed) { 182 for (int i = 0; i < mPending.size(); ++i) { 183 if (mPending.get(i).mClaimed != claimed) continue; 184 return mPending.get(i); 185 } 186 return null; 187 } 188 189 /** @return whether there are pending transitions on TaskViews. */ hasPending()190 public boolean hasPending() { 191 return !mPending.isEmpty(); 192 } 193 194 @Override handleRequest(@onNull IBinder transition, @Nullable TransitionRequestInfo request)195 public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 196 @Nullable TransitionRequestInfo request) { 197 final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); 198 if (triggerTask == null) { 199 return null; 200 } 201 final TaskViewTaskController taskView = findTaskView(triggerTask); 202 if (taskView == null) return null; 203 // Opening types should all be initiated by shell 204 if (!TransitionUtil.isClosingType(request.getType())) return null; 205 PendingTransition pending = new PendingTransition(request.getType(), null, 206 taskView, null /* cookie */); 207 pending.mClaimed = transition; 208 mPending.add(pending); 209 return new WindowContainerTransaction(); 210 } 211 findTaskView(ActivityManager.RunningTaskInfo taskInfo)212 private TaskViewTaskController findTaskView(ActivityManager.RunningTaskInfo taskInfo) { 213 for (int i = 0; i < mTaskViews.size(); ++i) { 214 if (mTaskViews.keyAt(i).getTaskInfo() == null) continue; 215 if (taskInfo.token.equals(mTaskViews.keyAt(i).getTaskInfo().token)) { 216 return mTaskViews.keyAt(i); 217 } 218 } 219 return null; 220 } 221 startTaskView(@onNull WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie)222 void startTaskView(@NonNull WindowContainerTransaction wct, 223 @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie) { 224 updateVisibilityState(taskView, true /* visible */); 225 mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView, launchCookie)); 226 startNextTransition(); 227 } 228 closeTaskView(@onNull WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView)229 void closeTaskView(@NonNull WindowContainerTransaction wct, 230 @NonNull TaskViewTaskController taskView) { 231 updateVisibilityState(taskView, false /* visible */); 232 mPending.add(new PendingTransition(TRANSIT_CLOSE, wct, taskView, null /* cookie */)); 233 startNextTransition(); 234 } 235 setTaskViewVisible(TaskViewTaskController taskView, boolean visible)236 void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { 237 if (mTaskViews.get(taskView) == null) return; 238 if (mTaskViews.get(taskView).mVisible == visible) return; 239 if (taskView.getTaskInfo() == null) { 240 // Nothing to update, task is not yet available 241 return; 242 } 243 mTaskViews.get(taskView).mVisible = visible; 244 final WindowContainerTransaction wct = new WindowContainerTransaction(); 245 wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */); 246 wct.setBounds(taskView.getTaskInfo().token, mTaskViews.get(taskView).mBounds); 247 PendingTransition pending = new PendingTransition( 248 visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */); 249 mPending.add(pending); 250 startNextTransition(); 251 // visibility is reported in transition. 252 } 253 updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen)254 void updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen) { 255 TaskViewRequestedState state = mTaskViews.get(taskView); 256 if (state == null) return; 257 state.mBounds.set(boundsOnScreen); 258 } 259 updateVisibilityState(TaskViewTaskController taskView, boolean visible)260 void updateVisibilityState(TaskViewTaskController taskView, boolean visible) { 261 TaskViewRequestedState state = mTaskViews.get(taskView); 262 if (state == null) return; 263 state.mVisible = visible; 264 } 265 setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen)266 void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) { 267 TaskViewRequestedState state = mTaskViews.get(taskView); 268 if (state == null || Objects.equals(boundsOnScreen, state.mBounds)) { 269 return; 270 } 271 state.mBounds.set(boundsOnScreen); 272 if (!state.mVisible) { 273 // Task view isn't visible, the bounds will next visibility update. 274 return; 275 } 276 PendingTransition pendingOpen = findPendingOpeningTransition(taskView); 277 if (pendingOpen != null) { 278 // There is already an opening transition in-flight, the window bounds will be 279 // set in prepareOpenAnimation (via the window crop) if needed. 280 return; 281 } 282 WindowContainerTransaction wct = new WindowContainerTransaction(); 283 wct.setBounds(taskView.getTaskInfo().token, boundsOnScreen); 284 mPending.add(new PendingTransition(TRANSIT_CHANGE, wct, taskView, null /* cookie */)); 285 startNextTransition(); 286 } 287 startNextTransition()288 private void startNextTransition() { 289 if (mPending.isEmpty()) return; 290 final PendingTransition pending = mPending.get(0); 291 if (pending.mClaimed != null) { 292 // Wait for this to start animating. 293 return; 294 } 295 pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this); 296 } 297 298 @Override onTransitionConsumed(@onNull IBinder transition, boolean aborted, @NonNull SurfaceControl.Transaction finishTransaction)299 public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, 300 @NonNull SurfaceControl.Transaction finishTransaction) { 301 if (!aborted) return; 302 final PendingTransition pending = findPending(transition); 303 if (pending == null) return; 304 mPending.remove(pending); 305 startNextTransition(); 306 } 307 308 @Override startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)309 public boolean startAnimation(@NonNull IBinder transition, 310 @NonNull TransitionInfo info, 311 @NonNull SurfaceControl.Transaction startTransaction, 312 @NonNull SurfaceControl.Transaction finishTransaction, 313 @NonNull Transitions.TransitionFinishCallback finishCallback) { 314 PendingTransition pending = findPending(transition); 315 if (pending != null) { 316 mPending.remove(pending); 317 } 318 if (mTaskViews.isEmpty()) { 319 if (pending != null) { 320 Slog.e(TAG, "Pending taskview transition but no task-views"); 321 } 322 return false; 323 } 324 boolean stillNeedsMatchingLaunch = pending != null && pending.mLaunchCookie != null; 325 int changesHandled = 0; 326 WindowContainerTransaction wct = null; 327 for (int i = 0; i < info.getChanges().size(); ++i) { 328 final TransitionInfo.Change chg = info.getChanges().get(i); 329 if (chg.getTaskInfo() == null) continue; 330 if (TransitionUtil.isClosingType(chg.getMode())) { 331 final boolean isHide = chg.getMode() == TRANSIT_TO_BACK; 332 TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); 333 if (tv == null && !isHide) { 334 // TaskView can be null when closing 335 changesHandled++; 336 continue; 337 } 338 if (tv == null) { 339 if (pending != null) { 340 Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " 341 + "shouldn't happen, so there may be a visual artifact: " 342 + chg.getTaskInfo().taskId); 343 } 344 continue; 345 } 346 if (isHide) { 347 tv.prepareHideAnimation(finishTransaction); 348 } else { 349 tv.prepareCloseAnimation(); 350 } 351 changesHandled++; 352 } else if (TransitionUtil.isOpeningType(chg.getMode())) { 353 final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN; 354 final TaskViewTaskController tv; 355 if (taskIsNew) { 356 if (pending == null 357 || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) { 358 Slog.e(TAG, "Found a launching TaskView in the wrong transition. All " 359 + "TaskView launches should be initiated by shell and in their " 360 + "own transition: " + chg.getTaskInfo().taskId); 361 continue; 362 } 363 stillNeedsMatchingLaunch = false; 364 tv = pending.mTaskView; 365 } else { 366 tv = findTaskView(chg.getTaskInfo()); 367 if (tv == null) { 368 if (pending != null) { 369 Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " 370 + "shouldn't happen, so there may be a visual artifact: " 371 + chg.getTaskInfo().taskId); 372 } 373 continue; 374 } 375 } 376 if (wct == null) wct = new WindowContainerTransaction(); 377 tv.prepareOpenAnimation(taskIsNew, startTransaction, finishTransaction, 378 chg.getTaskInfo(), chg.getLeash(), wct); 379 changesHandled++; 380 } else if (chg.getMode() == TRANSIT_CHANGE) { 381 TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); 382 if (tv == null) { 383 if (pending != null) { 384 Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " 385 + "shouldn't happen, so there may be a visual artifact: " 386 + chg.getTaskInfo().taskId); 387 } 388 continue; 389 } 390 startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); 391 finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) 392 .setPosition(chg.getLeash(), 0, 0); 393 changesHandled++; 394 } 395 } 396 if (stillNeedsMatchingLaunch) { 397 Slog.w(TAG, "Expected a TaskView launch in this transition but didn't get one, " 398 + "cleaning up the task view"); 399 // Didn't find a task so the task must have never launched 400 pending.mTaskView.setTaskNotFound(); 401 } else if (wct == null && pending == null && changesHandled != info.getChanges().size()) { 402 // Just some house-keeping, let another handler animate. 403 return false; 404 } 405 // No animation, just show it immediately. 406 startTransaction.apply(); 407 finishCallback.onTransitionFinished(wct); 408 startNextTransition(); 409 return true; 410 } 411 } 412