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