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
17package com.android.systemui.recents.views;
18
19import static android.app.ActivityManager.StackId.ASSISTANT_STACK_ID;
20import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
21import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
22import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
23import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
24
25import android.annotation.Nullable;
26import android.app.ActivityManager.StackId;
27import android.app.ActivityOptions;
28import android.app.ActivityOptions.OnAnimationStartedListener;
29import android.content.Context;
30import android.graphics.Bitmap;
31import android.graphics.Canvas;
32import android.graphics.Color;
33import android.graphics.GraphicBuffer;
34import android.graphics.Rect;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.IRemoteCallback;
38import android.os.RemoteException;
39import android.util.Log;
40import android.view.AppTransitionAnimationSpec;
41import android.view.DisplayListCanvas;
42import android.view.IAppTransitionAnimationSpecsFuture;
43import android.view.RenderNode;
44import android.view.ThreadedRenderer;
45import android.view.View;
46
47import com.android.internal.annotations.GuardedBy;
48import com.android.systemui.recents.Recents;
49import com.android.systemui.recents.RecentsDebugFlags;
50import com.android.systemui.recents.events.EventBus;
51import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
52import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent;
53import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
54import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent;
55import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
56import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
57import com.android.systemui.recents.misc.SystemServicesProxy;
58import com.android.systemui.recents.model.Task;
59import com.android.systemui.recents.model.TaskStack;
60import com.android.systemui.statusbar.phone.StatusBar;
61
62import java.util.ArrayList;
63import java.util.Collections;
64import java.util.List;
65
66/**
67 * A helper class to create transitions to/from Recents
68 */
69public 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
92        public void run() {
93            EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext, taskId));
94        }
95    }
96    private StartScreenPinningRunnableRunnable mStartScreenPinningRunnable
97            = new StartScreenPinningRunnableRunnable();
98
99    public RecentsTransitionHelper(Context context) {
100        mContext = context;
101        mHandler = new Handler();
102    }
103
104    /**
105     * Launches the specified {@link Task}.
106     */
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
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