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