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.taskview; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.ActivityManager; 22 import android.app.ActivityOptions; 23 import android.app.PendingIntent; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.LauncherApps; 28 import android.content.pm.ShortcutInfo; 29 import android.graphics.Insets; 30 import android.graphics.Rect; 31 import android.graphics.Region; 32 import android.os.Handler; 33 import android.view.SurfaceControl; 34 import android.view.SurfaceHolder; 35 import android.view.SurfaceView; 36 import android.view.View; 37 import android.view.ViewTreeObserver; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 41 import java.util.concurrent.Executor; 42 43 /** 44 * A {@link SurfaceView} that can display a task. This is a concrete implementation for 45 * {@link TaskViewBase} which interacts {@link TaskViewTaskController}. 46 */ 47 public class TaskView extends SurfaceView implements SurfaceHolder.Callback, 48 ViewTreeObserver.OnComputeInternalInsetsListener, TaskViewBase { 49 /** Callback for listening task state. */ 50 public interface Listener { 51 /** 52 * Only called once when the surface has been created & the container is ready for 53 * launching activities. 54 */ onInitialized()55 default void onInitialized() {} 56 57 /** Called when the container can no longer launch activities. */ onReleased()58 default void onReleased() {} 59 60 /** Called when a task is created inside the container. */ onTaskCreated(int taskId, ComponentName name)61 default void onTaskCreated(int taskId, ComponentName name) {} 62 63 /** Called when a task visibility changes. */ onTaskVisibilityChanged(int taskId, boolean visible)64 default void onTaskVisibilityChanged(int taskId, boolean visible) {} 65 66 /** Called when a task is about to be removed from the stack inside the container. */ onTaskRemovalStarted(int taskId)67 default void onTaskRemovalStarted(int taskId) {} 68 69 /** Called when a task is created inside the container. */ onBackPressedOnTaskRoot(int taskId)70 default void onBackPressedOnTaskRoot(int taskId) {} 71 } 72 73 private final Rect mTmpRect = new Rect(); 74 private final Rect mTmpRootRect = new Rect(); 75 private final int[] mTmpLocation = new int[2]; 76 private final Rect mBoundsOnScreen = new Rect(); 77 private final TaskViewController mTaskViewController; 78 private final TaskViewTaskController mTaskViewTaskController; 79 private Region mObscuredTouchRegion; 80 private Insets mCaptionInsets; 81 private Handler mHandler; 82 TaskView(Context context, TaskViewController taskViewController, TaskViewTaskController taskViewTaskController)83 public TaskView(Context context, TaskViewController taskViewController, 84 TaskViewTaskController taskViewTaskController) { 85 super(context, null, 0, 0, true /* disableBackgroundLayer */); 86 mTaskViewController = taskViewController; 87 mTaskViewTaskController = taskViewTaskController; 88 // TODO(b/266736992): Think about a better way to set the TaskViewBase on the 89 // TaskViewTaskController and vice-versa 90 mTaskViewTaskController.setTaskViewBase(this); 91 mHandler = Handler.getMain(); 92 getHolder().addCallback(this); 93 } 94 getController()95 public TaskViewTaskController getController() { 96 return mTaskViewTaskController; 97 } 98 99 /** 100 * Launch a new activity. 101 * 102 * @param pendingIntent Intent used to launch an activity. 103 * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} 104 * @param options options for the activity. 105 * @param launchBounds the bounds (window size and position) that the activity should be 106 * launched in, in pixels and in screen coordinates. 107 */ startActivity(@onNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, @NonNull ActivityOptions options, @Nullable Rect launchBounds)108 public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, 109 @NonNull ActivityOptions options, @Nullable Rect launchBounds) { 110 mTaskViewController.startActivity(mTaskViewTaskController, pendingIntent, fillInIntent, 111 options, launchBounds); 112 } 113 114 /** 115 * Launch an activity represented by {@link ShortcutInfo}. 116 * <p>The owner of this container must be allowed to access the shortcut information, 117 * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. 118 * 119 * @param shortcut the shortcut used to launch the activity. 120 * @param options options for the activity. 121 * @param launchBounds the bounds (window size and position) that the activity should be 122 * launched in, in pixels and in screen coordinates. 123 */ startShortcutActivity(@onNull ShortcutInfo shortcut, @NonNull ActivityOptions options, @Nullable Rect launchBounds)124 public void startShortcutActivity(@NonNull ShortcutInfo shortcut, 125 @NonNull ActivityOptions options, @Nullable Rect launchBounds) { 126 mTaskViewController.startShortcutActivity(mTaskViewTaskController, shortcut, options, 127 launchBounds); 128 } 129 130 /** 131 * Moves the current task in taskview out of the view and back to fullscreen. 132 */ moveToFullscreen()133 public void moveToFullscreen() { 134 mTaskViewController.moveTaskViewToFullscreen(mTaskViewTaskController); 135 } 136 137 @Override onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)138 public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { 139 if (mTaskViewController.isUsingShellTransitions()) { 140 // No need for additional work as it is already taken care of during 141 // prepareOpenAnimation(). 142 return; 143 } 144 onLocationChanged(); 145 if (taskInfo.taskDescription != null) { 146 final int bgColor = taskInfo.taskDescription.getBackgroundColor(); 147 runOnViewThread(() -> setResizeBackgroundColor(bgColor)); 148 } 149 } 150 151 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)152 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 153 if (taskInfo.taskDescription != null) { 154 final int bgColor = taskInfo.taskDescription.getBackgroundColor(); 155 runOnViewThread(() -> setResizeBackgroundColor(bgColor)); 156 } 157 } 158 159 /** 160 * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise. 161 */ isInitialized()162 public boolean isInitialized() { 163 return mTaskViewTaskController.isInitialized(); 164 } 165 166 @Override getCurrentBoundsOnScreen()167 public Rect getCurrentBoundsOnScreen() { 168 getBoundsOnScreen(mTmpRect); 169 return mTmpRect; 170 } 171 172 @Override setResizeBgColor(SurfaceControl.Transaction t, int bgColor)173 public void setResizeBgColor(SurfaceControl.Transaction t, int bgColor) { 174 if (mHandler.getLooper().isCurrentThread()) { 175 // We can only use the transaction if it can updated synchronously, otherwise the tx 176 // will be applied immediately after but also used/updated on the view thread which 177 // will lead to a race and/or crash 178 runOnViewThread(() -> setResizeBackgroundColor(t, bgColor)); 179 } else { 180 runOnViewThread(() -> setResizeBackgroundColor(bgColor)); 181 } 182 } 183 184 /** 185 * Only one listener may be set on the view, throws an exception otherwise. 186 */ setListener(@onNull Executor executor, TaskView.Listener listener)187 public void setListener(@NonNull Executor executor, TaskView.Listener listener) { 188 mTaskViewTaskController.setListener(executor, listener); 189 } 190 191 /** 192 * Indicates a region of the view that is not touchable. 193 * 194 * @param obscuredRect the obscured region of the view. 195 */ setObscuredTouchRect(Rect obscuredRect)196 public void setObscuredTouchRect(Rect obscuredRect) { 197 mObscuredTouchRegion = obscuredRect != null ? new Region(obscuredRect) : null; 198 invalidate(); 199 } 200 201 /** 202 * Indicates a region of the view that is not touchable. 203 * 204 * @param obscuredRegion the obscured region of the view. 205 */ setObscuredTouchRegion(Region obscuredRegion)206 public void setObscuredTouchRegion(Region obscuredRegion) { 207 mObscuredTouchRegion = obscuredRegion; 208 } 209 210 /** 211 * Sets a region of the task to inset to allow for a caption bar. Currently only top insets 212 * are supported. 213 * <p> 214 * This region will be factored in as an area of taskview that is not touchable activity 215 * content (i.e. you don't need to additionally set {@link #setObscuredTouchRect(Rect)} for 216 * the caption area). 217 * 218 * @param captionInsets the insets to apply to task view. 219 */ setCaptionInsets(Insets captionInsets)220 public void setCaptionInsets(Insets captionInsets) { 221 mCaptionInsets = captionInsets; 222 if (captionInsets == null) { 223 // If captions are null we can set them now; otherwise they'll get set in 224 // onComputeInternalInsets. 225 mTaskViewTaskController.setCaptionInsets(null); 226 } 227 } 228 229 /** 230 * Call when view position or size has changed. Do not call when animating. 231 */ onLocationChanged()232 public void onLocationChanged() { 233 getBoundsOnScreen(mTmpRect); 234 mTaskViewController.setTaskBounds(mTaskViewTaskController, mTmpRect); 235 } 236 237 /** 238 * Call to remove the task from window manager. This task will not appear in recents. 239 */ removeTask()240 public void removeTask() { 241 mTaskViewController.removeTaskView(mTaskViewTaskController, null /* token */); 242 } 243 244 /** 245 * Release this container if it is initialized. 246 */ release()247 public void release() { 248 getHolder().removeCallback(this); 249 mTaskViewTaskController.release(); 250 } 251 252 @Override toString()253 public String toString() { 254 return mTaskViewTaskController.toString(); 255 } 256 257 @Override surfaceCreated(SurfaceHolder holder)258 public void surfaceCreated(SurfaceHolder holder) { 259 mTaskViewTaskController.surfaceCreated(getSurfaceControl()); 260 } 261 262 @Override surfaceChanged(@ndroidx.annotation.NonNull SurfaceHolder holder, int format, int width, int height)263 public void surfaceChanged(@androidx.annotation.NonNull SurfaceHolder holder, int format, 264 int width, int height) { 265 getBoundsOnScreen(mTmpRect); 266 mTaskViewController.setTaskBounds(mTaskViewTaskController, mTmpRect); 267 } 268 269 @Override surfaceDestroyed(SurfaceHolder holder)270 public void surfaceDestroyed(SurfaceHolder holder) { 271 mTaskViewTaskController.surfaceDestroyed(); 272 } 273 274 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)275 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 276 // TODO(b/176854108): Consider to move the logic into gatherTransparentRegions since this 277 // is dependent on the order of listener. 278 // If there are multiple TaskViews, we'll set the touchable area as the root-view, then 279 // subtract each TaskView from it. 280 if (inoutInfo.touchableRegion.isEmpty()) { 281 inoutInfo.setTouchableInsets( 282 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 283 View root = getRootView(); 284 root.getLocationInWindow(mTmpLocation); 285 mTmpRootRect.set(mTmpLocation[0], mTmpLocation[1], root.getWidth(), root.getHeight()); 286 inoutInfo.touchableRegion.set(mTmpRootRect); 287 } 288 getLocationInWindow(mTmpLocation); 289 mTmpRect.set(mTmpLocation[0], mTmpLocation[1], 290 mTmpLocation[0] + getWidth(), mTmpLocation[1] + getHeight()); 291 if (mCaptionInsets != null) { 292 mTmpRect.inset(mCaptionInsets); 293 getBoundsOnScreen(mBoundsOnScreen); 294 mTaskViewTaskController.setCaptionInsets(new Rect( 295 mBoundsOnScreen.left, 296 mBoundsOnScreen.top, 297 mBoundsOnScreen.right + getWidth(), 298 mBoundsOnScreen.top + mCaptionInsets.top)); 299 } 300 inoutInfo.touchableRegion.op(mTmpRect, Region.Op.DIFFERENCE); 301 302 if (mObscuredTouchRegion != null) { 303 inoutInfo.touchableRegion.op(mObscuredTouchRegion, Region.Op.UNION); 304 } 305 } 306 307 @Override onAttachedToWindow()308 protected void onAttachedToWindow() { 309 super.onAttachedToWindow(); 310 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 311 mHandler = getHandler(); 312 } 313 314 @Override onDetachedFromWindow()315 protected void onDetachedFromWindow() { 316 super.onDetachedFromWindow(); 317 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 318 mHandler = Handler.getMain(); 319 } 320 321 /** Returns the task info for the task in the TaskView. */ 322 @Nullable getTaskInfo()323 public ActivityManager.RunningTaskInfo getTaskInfo() { 324 return mTaskViewTaskController.getTaskInfo(); 325 } 326 327 /** 328 * Sets the handler, only for testing. 329 */ 330 @VisibleForTesting setHandler(Handler viewHandler)331 void setHandler(Handler viewHandler) { 332 mHandler = viewHandler; 333 } 334 335 /** 336 * Ensures that the given runnable runs on the view's thread. 337 */ runOnViewThread(Runnable r)338 private void runOnViewThread(Runnable r) { 339 if (mHandler.getLooper().isCurrentThread()) { 340 r.run(); 341 } else { 342 // If this call is not from the same thread as the view, then post it 343 mHandler.post(r); 344 } 345 } 346 } 347