blob: 6e6f7a538494eb00208a2a7db7fae736ed49d0de [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;
21import android.app.ActivityManager.StackId;
Winson83c1b072015-11-09 10:48:04 -080022import android.app.ActivityOptions;
23import android.content.Context;
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Rect;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.IRemoteCallback;
30import android.os.RemoteException;
31import android.util.Log;
Winson83c1b072015-11-09 10:48:04 -080032import android.view.AppTransitionAnimationSpec;
33import android.view.IAppTransitionAnimationSpecsFuture;
34import android.view.WindowManagerGlobal;
35import com.android.internal.annotations.GuardedBy;
36import com.android.systemui.recents.Constants;
Filip Gruszczynski1a4dfe52015-11-15 10:58:57 -080037import com.android.systemui.recents.ExitRecentsWindowFirstAnimationFrameEvent;
Winson83c1b072015-11-09 10:48:04 -080038import com.android.systemui.recents.Recents;
Winsonc742f972015-11-12 11:32:21 -080039import com.android.systemui.recents.RecentsDebugFlags;
Winson83c1b072015-11-09 10:48:04 -080040import com.android.systemui.recents.events.EventBus;
41import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
42import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
43import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
44import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
45import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
46import com.android.systemui.recents.misc.SystemServicesProxy;
47import com.android.systemui.recents.model.Task;
48import com.android.systemui.recents.model.TaskStack;
49
50import java.util.ArrayList;
51import java.util.List;
52
Filip Gruszczynski96daf322015-11-18 18:01:27 -080053import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
Winson83c1b072015-11-09 10:48:04 -080054import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
55import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
56import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
57
58/**
59 * A helper class to create transitions to/from Recents
60 */
61public class RecentsTransitionHelper {
62
63 private static final String TAG = "RecentsTransitionHelper";
64 private static final boolean DEBUG = false;
65
66 /**
67 * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
68 * waiting for the specs to be retrieved.
69 */
70 private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
71
72 @GuardedBy("this")
73 private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
74
75 private Context mContext;
76 private Handler mHandler;
77 private TaskViewTransform mTmpTransform = new TaskViewTransform();
78
79 private Runnable mStartScreenPinningRunnable = new Runnable() {
80 @Override
81 public void run() {
82 EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext));
83 }
84 };
85
86 public RecentsTransitionHelper(Context context, Handler handler) {
87 mContext = context;
88 mHandler = handler;
89 }
90
91 /**
92 * Launches the specified {@link Task}.
93 */
94 public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
95 final TaskStackView stackView, final TaskView taskView,
96 final boolean lockToTask, final Rect bounds, int destinationStack) {
97 final ActivityOptions opts = ActivityOptions.makeBasic();
98 if (bounds != null) {
99 opts.setBounds(bounds.isEmpty() ? null : bounds);
100 }
101
102 final ActivityOptions.OnAnimationStartedListener animStartedListener;
103 final IAppTransitionAnimationSpecsFuture transitionFuture;
104 if (task.thumbnail != null && task.thumbnail.getWidth() > 0 &&
105 task.thumbnail.getHeight() > 0) {
106 transitionFuture = getAppTransitionFuture(task, stackView, destinationStack);
107 animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
108 @Override
109 public void onAnimationStarted() {
110 // If we are launching into another task, cancel the previous task's
111 // window transition
112 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
Filip Gruszczynski1a4dfe52015-11-15 10:58:57 -0800113 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
Winson83c1b072015-11-09 10:48:04 -0800114
115 if (lockToTask) {
116 // Request screen pinning after the animation runs
117 mHandler.postDelayed(mStartScreenPinningRunnable, 350);
118 }
119 }
120 };
121 } else {
122 // This is only the case if the task is not on screen (scrolled offscreen for example)
123 transitionFuture = null;
Filip Gruszczynski1a4dfe52015-11-15 10:58:57 -0800124 animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
125 @Override
126 public void onAnimationStarted() {
127 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
128 }
129 };
Winson83c1b072015-11-09 10:48:04 -0800130 }
131
132 if (taskView == null) {
133 // If there is no task view, then we do not need to worry about animating out occluding
134 // task views, and we can launch immediately
135 startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener);
136 } else {
137 if (task.group != null && !task.group.isFrontMostTask(task)) {
138 stackView.startLaunchTaskAnimation(taskView, new Runnable() {
139 @Override
140 public void run() {
141 startTaskActivity(stack, task, taskView, opts, transitionFuture,
142 animStartedListener);
143 }
144 }, lockToTask);
145 } else {
146 stackView.startLaunchTaskAnimation(taskView, null, lockToTask);
147 startTaskActivity(stack, task, taskView, opts, transitionFuture,
148 animStartedListener);
149 }
150 }
151 }
152
153 /**
154 * Starts the activity for the launch task.
155 *
156 * @param taskView this is the {@link TaskView} that we are launching from. This can be null if
157 * we are toggling recents and the launch-to task is now offscreen.
158 */
159 private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
160 ActivityOptions opts, IAppTransitionAnimationSpecsFuture transitionFuture,
161 final ActivityOptions.OnAnimationStartedListener animStartedListener) {
162 SystemServicesProxy ssp = Recents.getSystemServices();
163 if (ssp.startActivityFromRecents(mContext, task.key.id, task.activityLabel, opts)) {
164 // Keep track of the index of the task launch
165 int taskIndexFromFront = 0;
166 int taskIndex = stack.indexOfTask(task);
167 if (taskIndex > -1) {
168 taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
169 }
170 EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
171 } else {
172 // Dismiss the task if we fail to launch it
173 EventBus.getDefault().send(new DismissTaskViewEvent(task, taskView));
174
175 // Keep track of failed launches
176 EventBus.getDefault().send(new LaunchTaskFailedEvent());
177 }
178 if (transitionFuture != null) {
179 IRemoteCallback.Stub callback = null;
180 if (animStartedListener != null) {
181 callback = new IRemoteCallback.Stub() {
182 @Override
183 public void sendResult(Bundle data) throws RemoteException {
184 mHandler.post(new Runnable() {
185 @Override
186 public void run() {
187 if (animStartedListener != null) {
188 animStartedListener.onAnimationStarted();
189 }
190 }
191 });
192 }
193 };
194 }
195 try {
196 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,
260 layoutAlgorithm.mCurrentStackRect.bottom);
261
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();
278 ArrayList<Task> tasks = stack.getTasks();
279 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) {
311 // Disable any focused state before we draw the header
312 // Upfront the processing of the thumbnail
313 if (taskView.isFocusedTask()) {
314 taskView.setFocusedState(false, false /* animated */, false /* requestViewFocus */);
315 }
316
317 Bitmap b = null;
318 if (addHeaderBitmap) {
319 float scale = transform.scale;
320 int fromHeaderWidth = (int) (taskView.mHeaderView.getMeasuredWidth() * scale);
321 int fromHeaderHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale);
322 b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight,
323 Bitmap.Config.ARGB_8888);
324
Winsonc742f972015-11-12 11:32:21 -0800325 if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
Winson83c1b072015-11-09 10:48:04 -0800326 b.eraseColor(0xFFff0000);
327 } else {
328 Canvas c = new Canvas(b);
329 c.scale(scale, scale);
330 taskView.mHeaderView.draw(c);
331 c.setBitmap(null);
332 }
333 b = b.createAshmemBitmap();
334 }
335
336 Rect taskRect = new Rect();
337 transform.rect.round(taskRect);
338 return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect);
339 }
340}