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