• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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.systemui.recents.views;
18 
19 import static android.app.ActivityManager.StackId.ASSISTANT_STACK_ID;
20 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
21 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
22 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
23 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
24 
25 import android.annotation.Nullable;
26 import android.app.ActivityManager.StackId;
27 import android.app.ActivityOptions;
28 import android.app.ActivityOptions.OnAnimationStartedListener;
29 import android.content.Context;
30 import android.graphics.Bitmap;
31 import android.graphics.Canvas;
32 import android.graphics.Color;
33 import android.graphics.GraphicBuffer;
34 import android.graphics.Rect;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.IRemoteCallback;
38 import android.os.RemoteException;
39 import android.util.Log;
40 import android.view.AppTransitionAnimationSpec;
41 import android.view.DisplayListCanvas;
42 import android.view.IAppTransitionAnimationSpecsFuture;
43 import android.view.RenderNode;
44 import android.view.ThreadedRenderer;
45 import android.view.View;
46 
47 import com.android.internal.annotations.GuardedBy;
48 import com.android.systemui.recents.Recents;
49 import com.android.systemui.recents.RecentsDebugFlags;
50 import com.android.systemui.recents.events.EventBus;
51 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
52 import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent;
53 import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
54 import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent;
55 import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
56 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
57 import com.android.systemui.recents.misc.SystemServicesProxy;
58 import com.android.systemui.recents.model.Task;
59 import com.android.systemui.recents.model.TaskStack;
60 import com.android.systemui.statusbar.phone.StatusBar;
61 
62 import java.util.ArrayList;
63 import java.util.Collections;
64 import java.util.List;
65 
66 /**
67  * A helper class to create transitions to/from Recents
68  */
69 public class RecentsTransitionHelper {
70 
71     private static final String TAG = "RecentsTransitionHelper";
72     private static final boolean DEBUG = false;
73 
74     /**
75      * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
76      * waiting for the specs to be retrieved.
77      */
78     private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
79 
80     @GuardedBy("this")
81     private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
82 
83     private Context mContext;
84     private Handler mHandler;
85     private TaskViewTransform mTmpTransform = new TaskViewTransform();
86 
87     private class StartScreenPinningRunnableRunnable implements Runnable {
88 
89         private int taskId = -1;
90 
91         @Override
run()92         public void run() {
93             EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext, taskId));
94         }
95     }
96     private StartScreenPinningRunnableRunnable mStartScreenPinningRunnable
97             = new StartScreenPinningRunnableRunnable();
98 
RecentsTransitionHelper(Context context)99     public RecentsTransitionHelper(Context context) {
100         mContext = context;
101         mHandler = new Handler();
102     }
103 
104     /**
105      * Launches the specified {@link Task}.
106      */
launchTaskFromRecents(final TaskStack stack, @Nullable final Task task, final TaskStackView stackView, final TaskView taskView, final boolean screenPinningRequested, final int destinationStack)107     public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
108             final TaskStackView stackView, final TaskView taskView,
109             final boolean screenPinningRequested, final int destinationStack) {
110 
111         final ActivityOptions.OnAnimationStartedListener animStartedListener;
112         final AppTransitionAnimationSpecsFuture transitionFuture;
113         if (taskView != null) {
114 
115             // Fetch window rect here already in order not to be blocked on lock contention in WM
116             // when the future calls it.
117             final Rect windowRect = Recents.getSystemServices().getWindowRect();
118             transitionFuture = getAppTransitionFuture(
119                     () -> composeAnimationSpecs(task, stackView, destinationStack, windowRect));
120             animStartedListener = () -> {
121                 // If we are launching into another task, cancel the previous task's
122                 // window transition
123                 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
124                 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
125                 stackView.cancelAllTaskViewAnimations();
126 
127                 if (screenPinningRequested) {
128                     // Request screen pinning after the animation runs
129                     mStartScreenPinningRunnable.taskId = task.key.id;
130                     mHandler.postDelayed(mStartScreenPinningRunnable, 350);
131                 }
132             };
133         } else {
134             // This is only the case if the task is not on screen (scrolled offscreen for example)
135             transitionFuture = null;
136             animStartedListener = () -> {
137                 // If we are launching into another task, cancel the previous task's
138                 // window transition
139                 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
140                 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
141                 stackView.cancelAllTaskViewAnimations();
142             };
143         }
144 
145         final ActivityOptions opts = ActivityOptions.makeMultiThumbFutureAspectScaleAnimation(mContext,
146                 mHandler, transitionFuture != null ? transitionFuture.future : null,
147                 animStartedListener, true /* scaleUp */);
148         if (taskView == null) {
149             // If there is no task view, then we do not need to worry about animating out occluding
150             // task views, and we can launch immediately
151             startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack);
152         } else {
153             LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView,
154                     screenPinningRequested);
155             if (task.group != null && !task.group.isFrontMostTask(task)) {
156                 launchStartedEvent.addPostAnimationCallback(new Runnable() {
157                     @Override
158                     public void run() {
159                         startTaskActivity(stack, task, taskView, opts, transitionFuture,
160                                 destinationStack);
161                     }
162                 });
163                 EventBus.getDefault().send(launchStartedEvent);
164             } else {
165                 EventBus.getDefault().send(launchStartedEvent);
166                 startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack);
167             }
168         }
169         Recents.getSystemServices().sendCloseSystemWindows(
170                 StatusBar.SYSTEM_DIALOG_REASON_HOME_KEY);
171     }
172 
wrapStartedListener(final OnAnimationStartedListener listener)173     public IRemoteCallback wrapStartedListener(final OnAnimationStartedListener listener) {
174         if (listener == null) {
175             return null;
176         }
177         return new IRemoteCallback.Stub() {
178             @Override
179             public void sendResult(Bundle data) throws RemoteException {
180                 mHandler.post(new Runnable() {
181                     @Override
182                     public void run() {
183                         listener.onAnimationStarted();
184                     }
185                 });
186             }
187         };
188     }
189 
190     /**
191      * Starts the activity for the launch task.
192      *
193      * @param taskView this is the {@link TaskView} that we are launching from. This can be null if
194      *                 we are toggling recents and the launch-to task is now offscreen.
195      * @param destinationStack id of the stack to put the task into.
196      */
197     private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
198             ActivityOptions opts, AppTransitionAnimationSpecsFuture transitionFuture,
199             int destinationStack) {
200         SystemServicesProxy ssp = Recents.getSystemServices();
201         ssp.startActivityFromRecents(mContext, task.key, task.title, opts, destinationStack,
202                 succeeded -> {
203             if (succeeded) {
204                 // Keep track of the index of the task launch
205                 int taskIndexFromFront = 0;
206                 int taskIndex = stack.indexOfStackTask(task);
207                 if (taskIndex > -1) {
208                     taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
209                 }
210                 EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
211             } else {
212                 // Dismiss the task if we fail to launch it
213                 if (taskView != null) {
214                     taskView.dismissTask();
215                 }
216 
217                 // Keep track of failed launches
218                 EventBus.getDefault().send(new LaunchTaskFailedEvent());
219             }
220         });
221         if (transitionFuture != null) {
222             mHandler.post(transitionFuture::precacheSpecs);
223         }
224     }
225 
226     /**
227      * Creates a future which will later be queried for animation specs for this current transition.
228      *
229      * @param composer The implementation that composes the specs on the UI thread.
230      */
231     public AppTransitionAnimationSpecsFuture getAppTransitionFuture(
232             final AnimationSpecComposer composer) {
233         synchronized (this) {
234             mAppTransitionAnimationSpecs = SPECS_WAITING;
235         }
236         IAppTransitionAnimationSpecsFuture future = new IAppTransitionAnimationSpecsFuture.Stub() {
237             @Override
238             public AppTransitionAnimationSpec[] get() throws RemoteException {
239                 mHandler.post(() -> {
240                     synchronized (RecentsTransitionHelper.this) {
241                         mAppTransitionAnimationSpecs = composer.composeSpecs();
242                         RecentsTransitionHelper.this.notifyAll();
243                     }
244                 });
245                 synchronized (RecentsTransitionHelper.this) {
246                     while (mAppTransitionAnimationSpecs == SPECS_WAITING) {
247                         try {
248                             RecentsTransitionHelper.this.wait();
249                         } catch (InterruptedException e) {}
250                     }
251                     if (mAppTransitionAnimationSpecs == null) {
252                         return null;
253                     }
254                     AppTransitionAnimationSpec[] specs
255                             = new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()];
256                     mAppTransitionAnimationSpecs.toArray(specs);
257                     mAppTransitionAnimationSpecs = SPECS_WAITING;
258                     return specs;
259                 }
260             }
261         };
262         return new AppTransitionAnimationSpecsFuture(composer, future);
263     }
264 
265     /**
266      * Composes the transition spec when docking a task, which includes a full task bitmap.
267      */
268     public List<AppTransitionAnimationSpec> composeDockAnimationSpec(TaskView taskView,
269             Rect bounds) {
270         mTmpTransform.fillIn(taskView);
271         Task task = taskView.getTask();
272         GraphicBuffer buffer = RecentsTransitionHelper.composeTaskBitmap(taskView, mTmpTransform);
273         return Collections.singletonList(new AppTransitionAnimationSpec(task.key.id, buffer,
274                 bounds));
275     }
276 
277     /**
278      * Composes the animation specs for all the tasks in the target stack.
279      */
280     private List<AppTransitionAnimationSpec> composeAnimationSpecs(final Task task,
281             final TaskStackView stackView, final int destinationStack, Rect windowRect) {
282         // Ensure we have a valid target stack id
283         final int targetStackId = destinationStack != INVALID_STACK_ID ?
284                 destinationStack : task.key.stackId;
285         if (!StackId.useAnimationSpecForAppTransition(targetStackId)) {
286             return null;
287         }
288 
289         // Calculate the offscreen task rect (for tasks that are not backed by views)
290         TaskView taskView = stackView.getChildViewForTask(task);
291         TaskStackLayoutAlgorithm stackLayout = stackView.getStackAlgorithm();
292         Rect offscreenTaskRect = new Rect();
293         stackLayout.getFrontOfStackTransform().rect.round(offscreenTaskRect);
294 
295         // If this is a full screen stack, the transition will be towards the single, full screen
296         // task. We only need the transition spec for this task.
297         List<AppTransitionAnimationSpec> specs = new ArrayList<>();
298 
299         // TODO: Sometimes targetStackId is not initialized after reboot, so we also have to
300         // check for INVALID_STACK_ID
301         if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID || targetStackId == DOCKED_STACK_ID
302                 || targetStackId == ASSISTANT_STACK_ID || targetStackId == INVALID_STACK_ID) {
303             if (taskView == null) {
304                 specs.add(composeOffscreenAnimationSpec(task, offscreenTaskRect));
305             } else {
306                 mTmpTransform.fillIn(taskView);
307                 stackLayout.transformToScreenCoordinates(mTmpTransform, windowRect);
308                 AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, taskView,
309                         mTmpTransform, true /* addHeaderBitmap */);
310                 if (spec != null) {
311                     specs.add(spec);
312                 }
313             }
314             return specs;
315         }
316 
317         // Otherwise, for freeform tasks, create a new animation spec for each task we have to
318         // launch
319         TaskStack stack = stackView.getStack();
320         ArrayList<Task> tasks = stack.getStackTasks();
321         int taskCount = tasks.size();
322         for (int i = taskCount - 1; i >= 0; i--) {
323             Task t = tasks.get(i);
324             if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) {
325                 TaskView tv = stackView.getChildViewForTask(t);
326                 if (tv == null) {
327                     // TODO: Create a different animation task rect for this case (though it should
328                     //       never happen)
329                     specs.add(composeOffscreenAnimationSpec(t, offscreenTaskRect));
330                 } else {
331                     mTmpTransform.fillIn(taskView);
332                     stackLayout.transformToScreenCoordinates(mTmpTransform,
333                             null /* windowOverrideRect */);
334                     AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, tv,
335                             mTmpTransform, true /* addHeaderBitmap */);
336                     if (spec != null) {
337                         specs.add(spec);
338                     }
339                 }
340             }
341         }
342 
343         return specs;
344     }
345 
346     /**
347      * Composes a single animation spec for the given {@link Task}
348      */
349     private static AppTransitionAnimationSpec composeOffscreenAnimationSpec(Task task,
350             Rect taskRect) {
351         return new AppTransitionAnimationSpec(task.key.id, null, taskRect);
352     }
353 
354     public static GraphicBuffer composeTaskBitmap(TaskView taskView, TaskViewTransform transform) {
355         float scale = transform.scale;
356         int fromWidth = (int) (transform.rect.width() * scale);
357         int fromHeight = (int) (transform.rect.height() * scale);
358         if (fromWidth == 0 || fromHeight == 0) {
359             Log.e(TAG, "Could not compose thumbnail for task: " + taskView.getTask() +
360                     " at transform: " + transform);
361 
362             return drawViewIntoGraphicBuffer(1, 1, null, 1f, 0x00ffffff);
363         } else {
364             if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
365                 return drawViewIntoGraphicBuffer(fromWidth, fromHeight, null, 1f, 0xFFff0000);
366             } else {
367                 return drawViewIntoGraphicBuffer(fromWidth, fromHeight, taskView, scale, 0);
368             }
369         }
370     }
371 
372     private static GraphicBuffer composeHeaderBitmap(TaskView taskView,
373             TaskViewTransform transform) {
374         float scale = transform.scale;
375         int headerWidth = (int) (transform.rect.width());
376         int headerHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale);
377         if (headerWidth == 0 || headerHeight == 0) {
378             return null;
379         }
380 
381         if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
382             return drawViewIntoGraphicBuffer(headerWidth, headerHeight, null, 1f, 0xFFff0000);
383         } else {
384             return drawViewIntoGraphicBuffer(headerWidth, headerHeight, taskView.mHeaderView,
385                     scale, 0);
386         }
387     }
388 
389     public static GraphicBuffer drawViewIntoGraphicBuffer(int bufferWidth, int bufferHeight,
390             View view, float scale, int eraseColor) {
391         RenderNode node = RenderNode.create("RecentsTransition", null);
392         node.setLeftTopRightBottom(0, 0, bufferWidth, bufferHeight);
393         node.setClipToBounds(false);
394         DisplayListCanvas c = node.start(bufferWidth, bufferHeight);
395         c.scale(scale, scale);
396         if (eraseColor != 0) {
397             c.drawColor(eraseColor);
398         }
399         if (view != null) {
400             view.draw(c);
401         }
402         node.end(c);
403         return ThreadedRenderer.createHardwareBitmap(node, bufferWidth, bufferHeight)
404                 .createGraphicBufferHandle();
405     }
406 
407     /**
408      * Composes a single animation spec for the given {@link TaskView}
409      */
410     private static AppTransitionAnimationSpec composeAnimationSpec(TaskStackView stackView,
411             TaskView taskView, TaskViewTransform transform, boolean addHeaderBitmap) {
412         GraphicBuffer b = null;
413         if (addHeaderBitmap) {
414             b = composeHeaderBitmap(taskView, transform);
415             if (b == null) {
416                 return null;
417             }
418         }
419 
420         Rect taskRect = new Rect();
421         transform.rect.round(taskRect);
422         if (stackView.getStack().getStackFrontMostTask(false /* includeFreeformTasks */) !=
423                 taskView.getTask()) {
424             taskRect.bottom = taskRect.top + stackView.getMeasuredHeight();
425         }
426         return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect);
427     }
428 
429     public interface AnimationSpecComposer {
430         List<AppTransitionAnimationSpec> composeSpecs();
431     }
432 
433     /**
434      * Class to be returned from {@link #composeAnimationSpec} that gives access to both the future
435      * and the anonymous class used for composing.
436      */
437     public class AppTransitionAnimationSpecsFuture {
438 
439         private final AnimationSpecComposer composer;
440         private final IAppTransitionAnimationSpecsFuture future;
441 
442         private AppTransitionAnimationSpecsFuture(AnimationSpecComposer composer,
443                 IAppTransitionAnimationSpecsFuture future) {
444             this.composer = composer;
445             this.future = future;
446         }
447 
448         public IAppTransitionAnimationSpecsFuture getFuture() {
449             return future;
450         }
451 
452         /**
453          * Manually generates and caches the spec such that they are already available when the
454          * future needs.
455          */
456         public void precacheSpecs() {
457             synchronized (RecentsTransitionHelper.this) {
458                 mAppTransitionAnimationSpecs = composer.composeSpecs();
459             }
460         }
461     }
462 }
463