blob: 0af7c1e9379e69b677a41cd84bb7b6e2860a8021 [file] [log] [blame]
Winson83c1b072015-11-09 10:48:04 -08001/*
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 android.annotation.Nullable;
Filip Gruszczynski96daf322015-11-18 18:01:27 -080020import android.app.ActivityManager.StackId;
Winson83c1b072015-11-09 10:48:04 -080021import android.app.ActivityOptions;
22import android.content.Context;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.IRemoteCallback;
29import android.os.RemoteException;
30import android.util.Log;
Winson83c1b072015-11-09 10:48:04 -080031import android.view.AppTransitionAnimationSpec;
32import android.view.IAppTransitionAnimationSpecsFuture;
33import android.view.WindowManagerGlobal;
34import com.android.internal.annotations.GuardedBy;
Filip Gruszczynski1a4dfe52015-11-15 10:58:57 -080035import com.android.systemui.recents.ExitRecentsWindowFirstAnimationFrameEvent;
Winson83c1b072015-11-09 10:48:04 -080036import com.android.systemui.recents.Recents;
Winsonc742f972015-11-12 11:32:21 -080037import com.android.systemui.recents.RecentsDebugFlags;
Winson83c1b072015-11-09 10:48:04 -080038import com.android.systemui.recents.events.EventBus;
39import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
40import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
41import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
42import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
43import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
44import com.android.systemui.recents.misc.SystemServicesProxy;
45import com.android.systemui.recents.model.Task;
46import com.android.systemui.recents.model.TaskStack;
47
48import java.util.ArrayList;
49import java.util.List;
50
51import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
52import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
53import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
54
55/**
56 * A helper class to create transitions to/from Recents
57 */
58public class RecentsTransitionHelper {
59
60 private static final String TAG = "RecentsTransitionHelper";
61 private static final boolean DEBUG = false;
62
63 /**
64 * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
65 * waiting for the specs to be retrieved.
66 */
67 private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
68
69 @GuardedBy("this")
70 private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
71
72 private Context mContext;
73 private Handler mHandler;
74 private TaskViewTransform mTmpTransform = new TaskViewTransform();
75
76 private Runnable mStartScreenPinningRunnable = new Runnable() {
77 @Override
78 public void run() {
79 EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext));
80 }
81 };
82
83 public RecentsTransitionHelper(Context context, Handler handler) {
84 mContext = context;
85 mHandler = handler;
86 }
87
88 /**
89 * Launches the specified {@link Task}.
90 */
91 public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
92 final TaskStackView stackView, final TaskView taskView,
93 final boolean lockToTask, final Rect bounds, int destinationStack) {
94 final ActivityOptions opts = ActivityOptions.makeBasic();
95 if (bounds != null) {
Wale Ogunwale7a8fa602015-11-18 15:56:57 -080096 opts.setLaunchBounds(bounds.isEmpty() ? null : bounds);
Winson83c1b072015-11-09 10:48:04 -080097 }
98
99 final ActivityOptions.OnAnimationStartedListener animStartedListener;
100 final IAppTransitionAnimationSpecsFuture transitionFuture;
101 if (task.thumbnail != null && task.thumbnail.getWidth() > 0 &&
102 task.thumbnail.getHeight() > 0) {
103 transitionFuture = getAppTransitionFuture(task, stackView, destinationStack);
104 animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
105 @Override
106 public void onAnimationStarted() {
107 // If we are launching into another task, cancel the previous task's
108 // window transition
109 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
Filip Gruszczynski1a4dfe52015-11-15 10:58:57 -0800110 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
Winson83c1b072015-11-09 10:48:04 -0800111
112 if (lockToTask) {
113 // Request screen pinning after the animation runs
114 mHandler.postDelayed(mStartScreenPinningRunnable, 350);
115 }
116 }
117 };
118 } else {
119 // This is only the case if the task is not on screen (scrolled offscreen for example)
120 transitionFuture = null;
Filip Gruszczynski1a4dfe52015-11-15 10:58:57 -0800121 animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
122 @Override
123 public void onAnimationStarted() {
124 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
125 }
126 };
Winson83c1b072015-11-09 10:48:04 -0800127 }
128
129 if (taskView == null) {
130 // If there is no task view, then we do not need to worry about animating out occluding
131 // task views, and we can launch immediately
132 startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener);
133 } else {
134 if (task.group != null && !task.group.isFrontMostTask(task)) {
135 stackView.startLaunchTaskAnimation(taskView, new Runnable() {
136 @Override
137 public void run() {
138 startTaskActivity(stack, task, taskView, opts, transitionFuture,
139 animStartedListener);
140 }
141 }, lockToTask);
142 } else {
143 stackView.startLaunchTaskAnimation(taskView, null, lockToTask);
144 startTaskActivity(stack, task, taskView, opts, transitionFuture,
145 animStartedListener);
146 }
147 }
148 }
149
150 /**
151 * Starts the activity for the launch task.
152 *
153 * @param taskView this is the {@link TaskView} that we are launching from. This can be null if
154 * we are toggling recents and the launch-to task is now offscreen.
155 */
156 private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
157 ActivityOptions opts, IAppTransitionAnimationSpecsFuture transitionFuture,
158 final ActivityOptions.OnAnimationStartedListener animStartedListener) {
159 SystemServicesProxy ssp = Recents.getSystemServices();
Winson Chung296278a2015-12-17 12:09:02 -0500160 if (ssp.startActivityFromRecents(mContext, task.key.id, task.title, opts)) {
Winson83c1b072015-11-09 10:48:04 -0800161 // Keep track of the index of the task launch
162 int taskIndexFromFront = 0;
Winson250608a2015-11-24 15:00:31 -0800163 int taskIndex = stack.indexOfStackTask(task);
Winson83c1b072015-11-09 10:48:04 -0800164 if (taskIndex > -1) {
Winson250608a2015-11-24 15:00:31 -0800165 taskIndexFromFront = stack.getStackTaskCount() - taskIndex - 1;
Winson83c1b072015-11-09 10:48:04 -0800166 }
167 EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
168 } else {
169 // Dismiss the task if we fail to launch it
170 EventBus.getDefault().send(new DismissTaskViewEvent(task, taskView));
171
172 // Keep track of failed launches
173 EventBus.getDefault().send(new LaunchTaskFailedEvent());
174 }
175 if (transitionFuture != null) {
176 IRemoteCallback.Stub callback = null;
177 if (animStartedListener != null) {
178 callback = new IRemoteCallback.Stub() {
179 @Override
180 public void sendResult(Bundle data) throws RemoteException {
181 mHandler.post(new Runnable() {
182 @Override
183 public void run() {
184 if (animStartedListener != null) {
185 animStartedListener.onAnimationStarted();
186 }
187 }
188 });
189 }
190 };
191 }
192 try {
Jorim Jaggic4d49cd2015-11-24 13:48:26 -0800193 synchronized (this) {
194 mAppTransitionAnimationSpecs = SPECS_WAITING;
195 }
Winson83c1b072015-11-09 10:48:04 -0800196 WindowManagerGlobal.getWindowManagerService()
197 .overridePendingAppTransitionMultiThumbFuture(transitionFuture,
198 callback, true /* scaleUp */);
199 } catch (RemoteException e) {
200 Log.w(TAG, "Failed to override transition: " + e);
201 }
202 }
203 }
204
205 /**
206 * Creates a future which will later be queried for animation specs for this current transition.
207 */
208 private IAppTransitionAnimationSpecsFuture getAppTransitionFuture(final Task task,
209 final TaskStackView stackView, final int destinationStack) {
210 return new IAppTransitionAnimationSpecsFuture.Stub() {
211 @Override
212 public AppTransitionAnimationSpec[] get() throws RemoteException {
213 mHandler.post(new Runnable() {
214 @Override
215 public void run() {
216 synchronized (RecentsTransitionHelper.this) {
217 mAppTransitionAnimationSpecs = composeAnimationSpecs(task, stackView,
218 destinationStack);
219 RecentsTransitionHelper.this.notifyAll();
220 }
221 }
222 });
223 synchronized (RecentsTransitionHelper.this) {
224 while (mAppTransitionAnimationSpecs == SPECS_WAITING) {
225 try {
226 RecentsTransitionHelper.this.wait();
227 } catch (InterruptedException e) {}
228 }
229 if (mAppTransitionAnimationSpecs == null) {
230 return null;
231 }
232 AppTransitionAnimationSpec[] specs
233 = new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()];
234 mAppTransitionAnimationSpecs.toArray(specs);
235 mAppTransitionAnimationSpecs = SPECS_WAITING;
236 return specs;
237 }
238 }
239 };
240 }
241
242 /**
243 * Composes the animation specs for all the tasks in the target stack.
244 */
245 private List<AppTransitionAnimationSpec> composeAnimationSpecs(final Task task,
246 final TaskStackView stackView, final int destinationStack) {
247 // Ensure we have a valid target stack id
248 final int targetStackId = destinationStack != INVALID_STACK_ID ?
249 destinationStack : task.key.stackId;
Filip Gruszczynski96daf322015-11-18 18:01:27 -0800250 if (!StackId.useAnimationSpecForAppTransition(targetStackId)) {
Winson83c1b072015-11-09 10:48:04 -0800251 return null;
252 }
253
254 // Calculate the offscreen task rect (for tasks that are not backed by views)
255 float stackScroll = stackView.getScroller().getStackScroll();
256 TaskView taskView = stackView.getChildViewForTask(task);
257 TaskStackLayoutAlgorithm layoutAlgorithm = stackView.getStackAlgorithm();
258 Rect offscreenTaskRect = new Rect(layoutAlgorithm.mTaskRect);
259 offscreenTaskRect.offsetTo(offscreenTaskRect.left,
Winsonf0d1c442015-12-01 11:04:45 -0800260 layoutAlgorithm.mStackRect.bottom);
Winson83c1b072015-11-09 10:48:04 -0800261
262 // If this is a full screen stack, the transition will be towards the single, full screen
263 // task. We only need the transition spec for this task.
264 List<AppTransitionAnimationSpec> specs = new ArrayList<>();
265 if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID) {
266 if (taskView == null) {
267 specs.add(composeOffscreenAnimationSpec(task, offscreenTaskRect));
268 } else {
269 layoutAlgorithm.getStackTransform(task, stackScroll, mTmpTransform, null);
270 specs.add(composeAnimationSpec(taskView, mTmpTransform, true /* addHeaderBitmap */));
271 }
272 return specs;
273 }
274
275 // Otherwise, for freeform tasks, create a new animation spec for each task we have to
276 // launch
277 TaskStack stack = stackView.getStack();
Winson250608a2015-11-24 15:00:31 -0800278 ArrayList<Task> tasks = stack.getStackTasks();
Winson83c1b072015-11-09 10:48:04 -0800279 int taskCount = tasks.size();
280 for (int i = taskCount - 1; i >= 0; i--) {
281 Task t = tasks.get(i);
Filip Gruszczynskife9823f2015-11-12 14:09:26 -0800282 if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) {
Winson83c1b072015-11-09 10:48:04 -0800283 TaskView tv = stackView.getChildViewForTask(t);
284 if (tv == null) {
285 // TODO: Create a different animation task rect for this case (though it should
286 // never happen)
287 specs.add(composeOffscreenAnimationSpec(t, offscreenTaskRect));
288 } else {
289 layoutAlgorithm.getStackTransform(task, stackScroll, mTmpTransform, null);
290 specs.add(composeAnimationSpec(tv, mTmpTransform, true /* addHeaderBitmap */));
291 }
292 }
293 }
294
295 return specs;
296 }
297
298 /**
299 * Composes a single animation spec for the given {@link Task}
300 */
301 private static AppTransitionAnimationSpec composeOffscreenAnimationSpec(Task task,
302 Rect taskRect) {
303 return new AppTransitionAnimationSpec(task.key.id, null, taskRect);
304 }
305
306 /**
307 * Composes a single animation spec for the given {@link TaskView}
308 */
309 private static AppTransitionAnimationSpec composeAnimationSpec(TaskView taskView,
310 TaskViewTransform transform, boolean addHeaderBitmap) {
Winson83c1b072015-11-09 10:48:04 -0800311 Bitmap b = null;
312 if (addHeaderBitmap) {
313 float scale = transform.scale;
Winson Chung509d0d02015-12-16 15:43:12 -0500314 int fromHeaderWidth = (int) (transform.rect.width());
Winson83c1b072015-11-09 10:48:04 -0800315 int fromHeaderHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale);
316 b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight,
317 Bitmap.Config.ARGB_8888);
318
Winsonc742f972015-11-12 11:32:21 -0800319 if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
Winson83c1b072015-11-09 10:48:04 -0800320 b.eraseColor(0xFFff0000);
321 } else {
322 Canvas c = new Canvas(b);
323 c.scale(scale, scale);
324 taskView.mHeaderView.draw(c);
325 c.setBitmap(null);
326 }
327 b = b.createAshmemBitmap();
328 }
329
330 Rect taskRect = new Rect();
331 transform.rect.round(taskRect);
332 return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect);
333 }
334}