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; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.ActivityManager; 24 import android.app.ActivityOptions; 25 import android.app.PendingIntent; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.LauncherApps; 30 import android.content.pm.ShortcutInfo; 31 import android.graphics.Rect; 32 import android.graphics.Region; 33 import android.os.Binder; 34 import android.util.CloseGuard; 35 import android.view.SurfaceControl; 36 import android.view.SurfaceHolder; 37 import android.view.SurfaceView; 38 import android.view.View; 39 import android.view.ViewTreeObserver; 40 import android.window.WindowContainerToken; 41 import android.window.WindowContainerTransaction; 42 43 import com.android.wm.shell.common.SyncTransactionQueue; 44 45 import java.io.PrintWriter; 46 import java.util.concurrent.Executor; 47 48 /** 49 * View that can display a task. 50 */ 51 public class TaskView extends SurfaceView implements SurfaceHolder.Callback, 52 ShellTaskOrganizer.TaskListener, ViewTreeObserver.OnComputeInternalInsetsListener { 53 54 /** Callback for listening task state. */ 55 public interface Listener { 56 /** 57 * Only called once when the surface has been created & the container is ready for 58 * launching activities. 59 */ onInitialized()60 default void onInitialized() {} 61 62 /** Called when the container can no longer launch activities. */ onReleased()63 default void onReleased() {} 64 65 /** Called when a task is created inside the container. */ onTaskCreated(int taskId, ComponentName name)66 default void onTaskCreated(int taskId, ComponentName name) {} 67 68 /** Called when a task visibility changes. */ onTaskVisibilityChanged(int taskId, boolean visible)69 default void onTaskVisibilityChanged(int taskId, boolean visible) {} 70 71 /** Called when a task is about to be removed from the stack inside the container. */ onTaskRemovalStarted(int taskId)72 default void onTaskRemovalStarted(int taskId) {} 73 74 /** Called when a task is created inside the container. */ onBackPressedOnTaskRoot(int taskId)75 default void onBackPressedOnTaskRoot(int taskId) {} 76 } 77 78 private final CloseGuard mGuard = new CloseGuard(); 79 80 private final ShellTaskOrganizer mTaskOrganizer; 81 private final Executor mShellExecutor; 82 private final SyncTransactionQueue mSyncQueue; 83 private final TaskViewTransitions mTaskViewTransitions; 84 85 protected ActivityManager.RunningTaskInfo mTaskInfo; 86 private WindowContainerToken mTaskToken; 87 private SurfaceControl mTaskLeash; 88 private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); 89 private boolean mSurfaceCreated; 90 private boolean mIsInitialized; 91 private boolean mNotifiedForInitialized; 92 private Listener mListener; 93 private Executor mListenerExecutor; 94 private Region mObscuredTouchRegion; 95 96 private final Rect mTmpRect = new Rect(); 97 private final Rect mTmpRootRect = new Rect(); 98 private final int[] mTmpLocation = new int[2]; 99 TaskView(Context context, ShellTaskOrganizer organizer, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue)100 public TaskView(Context context, ShellTaskOrganizer organizer, 101 TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue) { 102 super(context, null, 0, 0, true /* disableBackgroundLayer */); 103 104 mTaskOrganizer = organizer; 105 mShellExecutor = organizer.getExecutor(); 106 mSyncQueue = syncQueue; 107 mTaskViewTransitions = taskViewTransitions; 108 if (mTaskViewTransitions != null) { 109 mTaskViewTransitions.addTaskView(this); 110 } 111 setUseAlpha(); 112 getHolder().addCallback(this); 113 mGuard.open("release"); 114 } 115 116 /** 117 * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise. 118 */ isInitialized()119 public boolean isInitialized() { 120 return mIsInitialized; 121 } 122 123 /** Until all users are converted, we may have mixed-use (eg. Car). */ isUsingShellTransitions()124 private boolean isUsingShellTransitions() { 125 return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled(); 126 } 127 128 /** 129 * Only one listener may be set on the view, throws an exception otherwise. 130 */ setListener(@onNull Executor executor, Listener listener)131 public void setListener(@NonNull Executor executor, Listener listener) { 132 if (mListener != null) { 133 throw new IllegalStateException( 134 "Trying to set a listener when one has already been set"); 135 } 136 mListener = listener; 137 mListenerExecutor = executor; 138 } 139 140 /** 141 * Launch an activity represented by {@link ShortcutInfo}. 142 * <p>The owner of this container must be allowed to access the shortcut information, 143 * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. 144 * 145 * @param shortcut the shortcut used to launch the activity. 146 * @param options options for the activity. 147 * @param launchBounds the bounds (window size and position) that the activity should be 148 * launched in, in pixels and in screen coordinates. 149 */ startShortcutActivity(@onNull ShortcutInfo shortcut, @NonNull ActivityOptions options, @Nullable Rect launchBounds)150 public void startShortcutActivity(@NonNull ShortcutInfo shortcut, 151 @NonNull ActivityOptions options, @Nullable Rect launchBounds) { 152 prepareActivityOptions(options, launchBounds); 153 LauncherApps service = mContext.getSystemService(LauncherApps.class); 154 if (isUsingShellTransitions()) { 155 mShellExecutor.execute(() -> { 156 final WindowContainerTransaction wct = new WindowContainerTransaction(); 157 wct.startShortcut(mContext.getPackageName(), shortcut, options.toBundle()); 158 mTaskViewTransitions.startTaskView(wct, this); 159 }); 160 return; 161 } 162 try { 163 service.startShortcut(shortcut, null /* sourceBounds */, options.toBundle()); 164 } catch (Exception e) { 165 throw new RuntimeException(e); 166 } 167 } 168 169 /** 170 * Launch a new activity. 171 * 172 * @param pendingIntent Intent used to launch an activity. 173 * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} 174 * @param options options for the activity. 175 * @param launchBounds the bounds (window size and position) that the activity should be 176 * launched in, in pixels and in screen coordinates. 177 */ startActivity(@onNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, @NonNull ActivityOptions options, @Nullable Rect launchBounds)178 public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, 179 @NonNull ActivityOptions options, @Nullable Rect launchBounds) { 180 prepareActivityOptions(options, launchBounds); 181 if (isUsingShellTransitions()) { 182 mShellExecutor.execute(() -> { 183 WindowContainerTransaction wct = new WindowContainerTransaction(); 184 wct.sendPendingIntent(pendingIntent, fillInIntent, options.toBundle()); 185 mTaskViewTransitions.startTaskView(wct, this); 186 }); 187 return; 188 } 189 try { 190 pendingIntent.send(mContext, 0 /* code */, fillInIntent, 191 null /* onFinished */, null /* handler */, null /* requiredPermission */, 192 options.toBundle()); 193 } catch (Exception e) { 194 throw new RuntimeException(e); 195 } 196 } 197 prepareActivityOptions(ActivityOptions options, Rect launchBounds)198 private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) { 199 final Binder launchCookie = new Binder(); 200 mShellExecutor.execute(() -> { 201 mTaskOrganizer.setPendingLaunchCookieListener(launchCookie, this); 202 }); 203 options.setLaunchBounds(launchBounds); 204 options.setLaunchCookie(launchCookie); 205 options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); 206 options.setRemoveWithTaskOrganizer(true); 207 } 208 209 /** 210 * Indicates a region of the view that is not touchable. 211 * 212 * @param obscuredRect the obscured region of the view. 213 */ setObscuredTouchRect(Rect obscuredRect)214 public void setObscuredTouchRect(Rect obscuredRect) { 215 mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null; 216 } 217 218 /** 219 * Indicates a region of the view that is not touchable. 220 * 221 * @param obscuredRegion the obscured region of the view. 222 */ setObscuredTouchRegion(Region obscuredRegion)223 public void setObscuredTouchRegion(Region obscuredRegion) { 224 mObscuredTouchRegion = obscuredRegion; 225 } 226 227 /** 228 * Call when view position or size has changed. Do not call when animating. 229 */ onLocationChanged()230 public void onLocationChanged() { 231 if (mTaskToken == null) { 232 return; 233 } 234 // Sync Transactions can't operate simultaneously with shell transition collection. 235 // The transition animation (upon showing) will sync the location itself. 236 if (isUsingShellTransitions() && mTaskViewTransitions.hasPending()) return; 237 238 WindowContainerTransaction wct = new WindowContainerTransaction(); 239 updateWindowBounds(wct); 240 mSyncQueue.queue(wct); 241 } 242 updateWindowBounds(WindowContainerTransaction wct)243 private void updateWindowBounds(WindowContainerTransaction wct) { 244 getBoundsOnScreen(mTmpRect); 245 wct.setBounds(mTaskToken, mTmpRect); 246 } 247 248 /** 249 * Release this container if it is initialized. 250 */ release()251 public void release() { 252 performRelease(); 253 } 254 255 @Override finalize()256 protected void finalize() throws Throwable { 257 try { 258 if (mGuard != null) { 259 mGuard.warnIfOpen(); 260 performRelease(); 261 } 262 } finally { 263 super.finalize(); 264 } 265 } 266 performRelease()267 private void performRelease() { 268 getHolder().removeCallback(this); 269 if (mTaskViewTransitions != null) { 270 mTaskViewTransitions.removeTaskView(this); 271 } 272 mShellExecutor.execute(() -> { 273 mTaskOrganizer.removeListener(this); 274 resetTaskInfo(); 275 }); 276 mGuard.close(); 277 mIsInitialized = false; 278 notifyReleased(); 279 } 280 281 /** Called when the {@link TaskView} has been released. */ notifyReleased()282 protected void notifyReleased() { 283 if (mListener != null && mNotifiedForInitialized) { 284 mListenerExecutor.execute(() -> { 285 mListener.onReleased(); 286 }); 287 mNotifiedForInitialized = false; 288 } 289 } 290 resetTaskInfo()291 private void resetTaskInfo() { 292 mTaskInfo = null; 293 mTaskToken = null; 294 mTaskLeash = null; 295 } 296 updateTaskVisibility()297 private void updateTaskVisibility() { 298 WindowContainerTransaction wct = new WindowContainerTransaction(); 299 wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); 300 mSyncQueue.queue(wct); 301 if (mListener == null) { 302 return; 303 } 304 int taskId = mTaskInfo.taskId; 305 mSyncQueue.runInSync((t) -> { 306 mListenerExecutor.execute(() -> { 307 mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated); 308 }); 309 }); 310 } 311 312 @Override onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)313 public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, 314 SurfaceControl leash) { 315 if (isUsingShellTransitions()) { 316 // Everything else handled by enter transition. 317 return; 318 } 319 mTaskInfo = taskInfo; 320 mTaskToken = taskInfo.token; 321 mTaskLeash = leash; 322 323 if (mSurfaceCreated) { 324 // Surface is ready, so just reparent the task to this surface control 325 mTransaction.reparent(mTaskLeash, getSurfaceControl()) 326 .show(mTaskLeash) 327 .apply(); 328 } else { 329 // The surface has already been destroyed before the task has appeared, 330 // so go ahead and hide the task entirely 331 updateTaskVisibility(); 332 } 333 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true); 334 onLocationChanged(); 335 if (taskInfo.taskDescription != null) { 336 int backgroundColor = taskInfo.taskDescription.getBackgroundColor(); 337 mSyncQueue.runInSync((t) -> { 338 setResizeBackgroundColor(t, backgroundColor); 339 }); 340 } 341 342 if (mListener != null) { 343 final int taskId = taskInfo.taskId; 344 final ComponentName baseActivity = taskInfo.baseActivity; 345 mListenerExecutor.execute(() -> { 346 mListener.onTaskCreated(taskId, baseActivity); 347 }); 348 } 349 } 350 351 @Override onTaskVanished(ActivityManager.RunningTaskInfo taskInfo)352 public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { 353 // Unlike Appeared, we can't yet guarantee that vanish will happen within a transition that 354 // we know about -- so leave clean-up here even if shell transitions are enabled. 355 if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; 356 357 if (mListener != null) { 358 final int taskId = taskInfo.taskId; 359 mListenerExecutor.execute(() -> { 360 mListener.onTaskRemovalStarted(taskId); 361 }); 362 } 363 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); 364 365 // Unparent the task when this surface is destroyed 366 mTransaction.reparent(mTaskLeash, null).apply(); 367 resetTaskInfo(); 368 } 369 370 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)371 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 372 if (taskInfo.taskDescription != null) { 373 setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); 374 } 375 } 376 377 @Override onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo)378 public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { 379 if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; 380 if (mListener != null) { 381 final int taskId = taskInfo.taskId; 382 mListenerExecutor.execute(() -> { 383 mListener.onBackPressedOnTaskRoot(taskId); 384 }); 385 } 386 } 387 388 @Override attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b)389 public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { 390 b.setParent(findTaskSurface(taskId)); 391 } 392 393 @Override reparentChildSurfaceToTask(int taskId, SurfaceControl sc, SurfaceControl.Transaction t)394 public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, 395 SurfaceControl.Transaction t) { 396 t.reparent(sc, findTaskSurface(taskId)); 397 } 398 findTaskSurface(int taskId)399 private SurfaceControl findTaskSurface(int taskId) { 400 if (mTaskInfo == null || mTaskLeash == null || mTaskInfo.taskId != taskId) { 401 throw new IllegalArgumentException("There is no surface for taskId=" + taskId); 402 } 403 return mTaskLeash; 404 } 405 406 @Override dump(@ndroidx.annotation.NonNull PrintWriter pw, String prefix)407 public void dump(@androidx.annotation.NonNull PrintWriter pw, String prefix) { 408 final String innerPrefix = prefix + " "; 409 final String childPrefix = innerPrefix + " "; 410 pw.println(prefix + this); 411 } 412 413 @Override toString()414 public String toString() { 415 return "TaskView" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null"); 416 } 417 418 @Override surfaceCreated(SurfaceHolder holder)419 public void surfaceCreated(SurfaceHolder holder) { 420 mSurfaceCreated = true; 421 mIsInitialized = true; 422 notifyInitialized(); 423 mShellExecutor.execute(() -> { 424 if (mTaskToken == null) { 425 // Nothing to update, task is not yet available 426 return; 427 } 428 if (isUsingShellTransitions()) { 429 mTaskViewTransitions.setTaskViewVisible(this, true /* visible */); 430 return; 431 } 432 // Reparent the task when this surface is created 433 mTransaction.reparent(mTaskLeash, getSurfaceControl()) 434 .show(mTaskLeash) 435 .apply(); 436 updateTaskVisibility(); 437 }); 438 } 439 440 /** Called when the {@link TaskView} is initialized. */ notifyInitialized()441 protected void notifyInitialized() { 442 if (mListener != null && !mNotifiedForInitialized) { 443 mNotifiedForInitialized = true; 444 mListenerExecutor.execute(() -> { 445 mListener.onInitialized(); 446 }); 447 } 448 } 449 450 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)451 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 452 if (mTaskToken == null) { 453 return; 454 } 455 onLocationChanged(); 456 } 457 458 @Override surfaceDestroyed(SurfaceHolder holder)459 public void surfaceDestroyed(SurfaceHolder holder) { 460 mSurfaceCreated = false; 461 mShellExecutor.execute(() -> { 462 if (mTaskToken == null) { 463 // Nothing to update, task is not yet available 464 return; 465 } 466 467 if (isUsingShellTransitions()) { 468 mTaskViewTransitions.setTaskViewVisible(this, false /* visible */); 469 return; 470 } 471 472 // Unparent the task when this surface is destroyed 473 mTransaction.reparent(mTaskLeash, null).apply(); 474 updateTaskVisibility(); 475 }); 476 } 477 478 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)479 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 480 // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this 481 // is dependent on the order of listener. 482 // If there are multiple TaskViews, we'll set the touchable area as the root-view, then 483 // subtract each TaskView from it. 484 if (inoutInfo.touchableRegion.isEmpty()) { 485 inoutInfo.setTouchableInsets( 486 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 487 View root = getRootView(); 488 root.getLocationInWindow(mTmpLocation); 489 mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight()); 490 inoutInfo.touchableRegion.set(mTmpRootRect); 491 } 492 getLocationInWindow(mTmpLocation); 493 mTmpRect.set(mTmpLocation[0], mTmpLocation[1], 494 mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); 495 inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); 496 497 if (mObscuredTouchRegion != null) { 498 inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION); 499 } 500 } 501 502 @Override onAttachedToWindow()503 protected void onAttachedToWindow() { 504 super.onAttachedToWindow(); 505 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 506 } 507 508 @Override onDetachedFromWindow()509 protected void onDetachedFromWindow() { 510 super.onDetachedFromWindow(); 511 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 512 } 513 514 /** Returns the task info for the task in the TaskView. */ 515 @Nullable getTaskInfo()516 public ActivityManager.RunningTaskInfo getTaskInfo() { 517 return mTaskInfo; 518 } 519 prepareHideAnimation(@onNull SurfaceControl.Transaction finishTransaction)520 void prepareHideAnimation(@NonNull SurfaceControl.Transaction finishTransaction) { 521 if (mTaskToken == null) { 522 // Nothing to update, task is not yet available 523 return; 524 } 525 526 finishTransaction.reparent(mTaskLeash, null).apply(); 527 528 if (mListener != null) { 529 final int taskId = mTaskInfo.taskId; 530 mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated /* visible */); 531 } 532 } 533 534 /** 535 * Called when the associated Task closes. If the TaskView is just being hidden, prepareHide 536 * is used instead. 537 */ prepareCloseAnimation()538 void prepareCloseAnimation() { 539 if (mTaskToken != null) { 540 if (mListener != null) { 541 final int taskId = mTaskInfo.taskId; 542 mListenerExecutor.execute(() -> { 543 mListener.onTaskRemovalStarted(taskId); 544 }); 545 } 546 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); 547 } 548 resetTaskInfo(); 549 } 550 prepareOpenAnimation(final boolean newTask, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, WindowContainerTransaction wct)551 void prepareOpenAnimation(final boolean newTask, 552 @NonNull SurfaceControl.Transaction startTransaction, 553 @NonNull SurfaceControl.Transaction finishTransaction, 554 ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, 555 WindowContainerTransaction wct) { 556 mTaskInfo = taskInfo; 557 mTaskToken = mTaskInfo.token; 558 mTaskLeash = leash; 559 if (mSurfaceCreated) { 560 // Surface is ready, so just reparent the task to this surface control 561 startTransaction.reparent(mTaskLeash, getSurfaceControl()) 562 .show(mTaskLeash) 563 .apply(); 564 // Also reparent on finishTransaction since the finishTransaction will reparent back 565 // to its "original" parent by default. 566 finishTransaction.reparent(mTaskLeash, getSurfaceControl()) 567 .setPosition(mTaskLeash, 0, 0) 568 .apply(); 569 570 // TODO: determine if this is really necessary or not 571 updateWindowBounds(wct); 572 } else { 573 // The surface has already been destroyed before the task has appeared, 574 // so go ahead and hide the task entirely 575 wct.setHidden(mTaskToken, true /* hidden */); 576 // listener callback is below 577 } 578 if (newTask) { 579 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true /* intercept */); 580 } 581 582 if (mTaskInfo.taskDescription != null) { 583 int backgroundColor = mTaskInfo.taskDescription.getBackgroundColor(); 584 setResizeBackgroundColor(startTransaction, backgroundColor); 585 } 586 587 if (mListener != null) { 588 final int taskId = mTaskInfo.taskId; 589 final ComponentName baseActivity = mTaskInfo.baseActivity; 590 591 mListenerExecutor.execute(() -> { 592 if (newTask) { 593 mListener.onTaskCreated(taskId, baseActivity); 594 } 595 // Even if newTask, send a visibilityChange if the surface was destroyed. 596 if (!newTask || !mSurfaceCreated) { 597 mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated /* visible */); 598 } 599 }); 600 } 601 } 602 } 603