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