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 /** Called when the container is ready for launching activities. */ onInitialized()57 default void onInitialized() {} 58 59 /** Called when the container can no longer launch activities. */ onReleased()60 default void onReleased() {} 61 62 /** Called when a task is created inside the container. */ onTaskCreated(int taskId, ComponentName name)63 default void onTaskCreated(int taskId, ComponentName name) {} 64 65 /** Called when a task visibility changes. */ onTaskVisibilityChanged(int taskId, boolean visible)66 default void onTaskVisibilityChanged(int taskId, boolean visible) {} 67 68 /** Called when a task is about to be removed from the stack inside the container. */ onTaskRemovalStarted(int taskId)69 default void onTaskRemovalStarted(int taskId) {} 70 71 /** Called when a task is created inside the container. */ onBackPressedOnTaskRoot(int taskId)72 default void onBackPressedOnTaskRoot(int taskId) {} 73 } 74 75 private final CloseGuard mGuard = new CloseGuard(); 76 77 private final ShellTaskOrganizer mTaskOrganizer; 78 private final Executor mShellExecutor; 79 private final SyncTransactionQueue mSyncQueue; 80 81 private ActivityManager.RunningTaskInfo mTaskInfo; 82 private WindowContainerToken mTaskToken; 83 private SurfaceControl mTaskLeash; 84 private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); 85 private boolean mSurfaceCreated; 86 private boolean mIsInitialized; 87 private Listener mListener; 88 private Executor mListenerExecutor; 89 private Rect mObscuredTouchRect; 90 91 private final Rect mTmpRect = new Rect(); 92 private final Rect mTmpRootRect = new Rect(); 93 private final int[] mTmpLocation = new int[2]; 94 TaskView(Context context, ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue)95 public TaskView(Context context, ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) { 96 super(context, null, 0, 0, true /* disableBackgroundLayer */); 97 98 mTaskOrganizer = organizer; 99 mShellExecutor = organizer.getExecutor(); 100 mSyncQueue = syncQueue; 101 setUseAlpha(); 102 getHolder().addCallback(this); 103 mGuard.open("release"); 104 } 105 106 /** 107 * Only one listener may be set on the view, throws an exception otherwise. 108 */ setListener(@onNull Executor executor, Listener listener)109 public void setListener(@NonNull Executor executor, Listener listener) { 110 if (mListener != null) { 111 throw new IllegalStateException( 112 "Trying to set a listener when one has already been set"); 113 } 114 mListener = listener; 115 mListenerExecutor = executor; 116 } 117 118 /** 119 * Launch an activity represented by {@link ShortcutInfo}. 120 * <p>The owner of this container must be allowed to access the shortcut information, 121 * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. 122 * 123 * @param shortcut the shortcut used to launch the activity. 124 * @param options options for the activity. 125 * @param launchBounds the bounds (window size and position) that the activity should be 126 * launched in, in pixels and in screen coordinates. 127 */ startShortcutActivity(@onNull ShortcutInfo shortcut, @NonNull ActivityOptions options, @Nullable Rect launchBounds)128 public void startShortcutActivity(@NonNull ShortcutInfo shortcut, 129 @NonNull ActivityOptions options, @Nullable Rect launchBounds) { 130 prepareActivityOptions(options, launchBounds); 131 LauncherApps service = mContext.getSystemService(LauncherApps.class); 132 try { 133 service.startShortcut(shortcut, null /* sourceBounds */, options.toBundle()); 134 } catch (Exception e) { 135 throw new RuntimeException(e); 136 } 137 } 138 139 /** 140 * Launch a new activity. 141 * 142 * @param pendingIntent Intent used to launch an activity. 143 * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} 144 * @param options options for the activity. 145 * @param launchBounds the bounds (window size and position) that the activity should be 146 * launched in, in pixels and in screen coordinates. 147 */ startActivity(@onNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, @NonNull ActivityOptions options, @Nullable Rect launchBounds)148 public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, 149 @NonNull ActivityOptions options, @Nullable Rect launchBounds) { 150 prepareActivityOptions(options, launchBounds); 151 try { 152 pendingIntent.send(mContext, 0 /* code */, fillInIntent, 153 null /* onFinished */, null /* handler */, null /* requiredPermission */, 154 options.toBundle()); 155 } catch (Exception e) { 156 throw new RuntimeException(e); 157 } 158 } 159 prepareActivityOptions(ActivityOptions options, Rect launchBounds)160 private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) { 161 final Binder launchCookie = new Binder(); 162 mShellExecutor.execute(() -> { 163 mTaskOrganizer.setPendingLaunchCookieListener(launchCookie, this); 164 }); 165 options.setLaunchBounds(launchBounds); 166 options.setLaunchCookie(launchCookie); 167 options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); 168 options.setRemoveWithTaskOrganizer(true); 169 } 170 171 /** 172 * Indicates a region of the view that is not touchable. 173 * 174 * @param obscuredRect the obscured region of the view. 175 */ setObscuredTouchRect(Rect obscuredRect)176 public void setObscuredTouchRect(Rect obscuredRect) { 177 mObscuredTouchRect = obscuredRect; 178 } 179 180 /** 181 * Call when view position or size has changed. Do not call when animating. 182 */ onLocationChanged()183 public void onLocationChanged() { 184 if (mTaskToken == null) { 185 return; 186 } 187 // Update based on the screen bounds 188 getBoundsOnScreen(mTmpRect); 189 getRootView().getBoundsOnScreen(mTmpRootRect); 190 if (!mTmpRootRect.contains(mTmpRect)) { 191 mTmpRect.offsetTo(0, 0); 192 } 193 194 WindowContainerTransaction wct = new WindowContainerTransaction(); 195 wct.setBounds(mTaskToken, mTmpRect); 196 mSyncQueue.queue(wct); 197 } 198 199 /** 200 * Release this container if it is initialized. 201 */ release()202 public void release() { 203 performRelease(); 204 } 205 206 @Override finalize()207 protected void finalize() throws Throwable { 208 try { 209 if (mGuard != null) { 210 mGuard.warnIfOpen(); 211 performRelease(); 212 } 213 } finally { 214 super.finalize(); 215 } 216 } 217 performRelease()218 private void performRelease() { 219 getHolder().removeCallback(this); 220 mShellExecutor.execute(() -> { 221 mTaskOrganizer.removeListener(this); 222 resetTaskInfo(); 223 }); 224 mGuard.close(); 225 if (mListener != null && mIsInitialized) { 226 mListenerExecutor.execute(() -> { 227 mListener.onReleased(); 228 }); 229 mIsInitialized = false; 230 } 231 } 232 resetTaskInfo()233 private void resetTaskInfo() { 234 mTaskInfo = null; 235 mTaskToken = null; 236 mTaskLeash = null; 237 } 238 updateTaskVisibility()239 private void updateTaskVisibility() { 240 WindowContainerTransaction wct = new WindowContainerTransaction(); 241 wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); 242 mSyncQueue.queue(wct); 243 if (mListener == null) { 244 return; 245 } 246 int taskId = mTaskInfo.taskId; 247 mSyncQueue.runInSync((t) -> { 248 mListenerExecutor.execute(() -> { 249 mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated); 250 }); 251 }); 252 } 253 254 @Override onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)255 public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, 256 SurfaceControl leash) { 257 mTaskInfo = taskInfo; 258 mTaskToken = taskInfo.token; 259 mTaskLeash = leash; 260 261 if (mSurfaceCreated) { 262 // Surface is ready, so just reparent the task to this surface control 263 mTransaction.reparent(mTaskLeash, getSurfaceControl()) 264 .show(mTaskLeash) 265 .apply(); 266 } else { 267 // The surface has already been destroyed before the task has appeared, 268 // so go ahead and hide the task entirely 269 updateTaskVisibility(); 270 } 271 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true); 272 onLocationChanged(); 273 if (taskInfo.taskDescription != null) { 274 int backgroundColor = taskInfo.taskDescription.getBackgroundColor(); 275 mSyncQueue.runInSync((t) -> { 276 setResizeBackgroundColor(t, backgroundColor); 277 }); 278 } 279 280 if (mListener != null) { 281 final int taskId = taskInfo.taskId; 282 final ComponentName baseActivity = taskInfo.baseActivity; 283 mListenerExecutor.execute(() -> { 284 mListener.onTaskCreated(taskId, baseActivity); 285 }); 286 } 287 } 288 289 @Override onTaskVanished(ActivityManager.RunningTaskInfo taskInfo)290 public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { 291 if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; 292 293 if (mListener != null) { 294 final int taskId = taskInfo.taskId; 295 mListenerExecutor.execute(() -> { 296 mListener.onTaskRemovalStarted(taskId); 297 }); 298 } 299 mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); 300 301 // Unparent the task when this surface is destroyed 302 mTransaction.reparent(mTaskLeash, null).apply(); 303 resetTaskInfo(); 304 } 305 306 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)307 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 308 if (taskInfo.taskDescription != null) { 309 setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); 310 } 311 } 312 313 @Override onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo)314 public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { 315 if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return; 316 if (mListener != null) { 317 final int taskId = taskInfo.taskId; 318 mListenerExecutor.execute(() -> { 319 mListener.onBackPressedOnTaskRoot(taskId); 320 }); 321 } 322 } 323 324 @Override attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b)325 public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { 326 if (mTaskInfo.taskId != taskId) { 327 throw new IllegalArgumentException("There is no surface for taskId=" + taskId); 328 } 329 b.setParent(mTaskLeash); 330 } 331 332 @Override dump(@ndroidx.annotation.NonNull PrintWriter pw, String prefix)333 public void dump(@androidx.annotation.NonNull PrintWriter pw, String prefix) { 334 final String innerPrefix = prefix + " "; 335 final String childPrefix = innerPrefix + " "; 336 pw.println(prefix + this); 337 } 338 339 @Override toString()340 public String toString() { 341 return "TaskView" + ":" + (mTaskInfo != null ? mTaskInfo.taskId : "null"); 342 } 343 344 @Override surfaceCreated(SurfaceHolder holder)345 public void surfaceCreated(SurfaceHolder holder) { 346 mSurfaceCreated = true; 347 if (mListener != null && !mIsInitialized) { 348 mIsInitialized = true; 349 mListenerExecutor.execute(() -> { 350 mListener.onInitialized(); 351 }); 352 } 353 mShellExecutor.execute(() -> { 354 if (mTaskToken == null) { 355 // Nothing to update, task is not yet available 356 return; 357 } 358 // Reparent the task when this surface is created 359 mTransaction.reparent(mTaskLeash, getSurfaceControl()) 360 .show(mTaskLeash) 361 .apply(); 362 updateTaskVisibility(); 363 }); 364 } 365 366 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)367 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 368 if (mTaskToken == null) { 369 return; 370 } 371 onLocationChanged(); 372 } 373 374 @Override surfaceDestroyed(SurfaceHolder holder)375 public void surfaceDestroyed(SurfaceHolder holder) { 376 mSurfaceCreated = false; 377 mShellExecutor.execute(() -> { 378 if (mTaskToken == null) { 379 // Nothing to update, task is not yet available 380 return; 381 } 382 383 // Unparent the task when this surface is destroyed 384 mTransaction.reparent(mTaskLeash, null).apply(); 385 updateTaskVisibility(); 386 }); 387 } 388 389 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)390 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 391 // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this 392 // is dependent on the order of listener. 393 // If there are multiple TaskViews, we'll set the touchable area as the root-view, then 394 // subtract each TaskView from it. 395 if (inoutInfo.touchableRegion.isEmpty()) { 396 inoutInfo.setTouchableInsets( 397 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 398 View root = getRootView(); 399 root.getLocationInWindow(mTmpLocation); 400 mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight()); 401 inoutInfo.touchableRegion.set(mTmpRootRect); 402 } 403 getLocationInWindow(mTmpLocation); 404 mTmpRect.set(mTmpLocation[0], mTmpLocation[1], 405 mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); 406 inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); 407 408 if (mObscuredTouchRect != null) { 409 inoutInfo.touchableRegion.union(mObscuredTouchRect); 410 } 411 } 412 413 @Override onAttachedToWindow()414 protected void onAttachedToWindow() { 415 super.onAttachedToWindow(); 416 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 417 } 418 419 @Override onDetachedFromWindow()420 protected void onDetachedFromWindow() { 421 super.onDetachedFromWindow(); 422 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 423 } 424 } 425