• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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