Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 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 android.annotation.Nullable; |
Filip Gruszczynski | 96daf32 | 2015-11-18 18:01:27 -0800 | [diff] [blame^] | 20 | import android.app.ActivityManager; |
| 21 | import android.app.ActivityManager.StackId; |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 22 | import android.app.ActivityOptions; |
| 23 | import android.content.Context; |
| 24 | import android.graphics.Bitmap; |
| 25 | import android.graphics.Canvas; |
| 26 | import android.graphics.Rect; |
| 27 | import android.os.Bundle; |
| 28 | import android.os.Handler; |
| 29 | import android.os.IRemoteCallback; |
| 30 | import android.os.RemoteException; |
| 31 | import android.util.Log; |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 32 | import android.view.AppTransitionAnimationSpec; |
| 33 | import android.view.IAppTransitionAnimationSpecsFuture; |
| 34 | import android.view.WindowManagerGlobal; |
| 35 | import com.android.internal.annotations.GuardedBy; |
| 36 | import com.android.systemui.recents.Constants; |
Filip Gruszczynski | 1a4dfe5 | 2015-11-15 10:58:57 -0800 | [diff] [blame] | 37 | import com.android.systemui.recents.ExitRecentsWindowFirstAnimationFrameEvent; |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 38 | import com.android.systemui.recents.Recents; |
Winson | c742f97 | 2015-11-12 11:32:21 -0800 | [diff] [blame] | 39 | import com.android.systemui.recents.RecentsDebugFlags; |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 40 | import com.android.systemui.recents.events.EventBus; |
| 41 | import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent; |
| 42 | import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent; |
| 43 | import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent; |
| 44 | import com.android.systemui.recents.events.component.ScreenPinningRequestEvent; |
| 45 | import com.android.systemui.recents.events.ui.DismissTaskViewEvent; |
| 46 | import com.android.systemui.recents.misc.SystemServicesProxy; |
| 47 | import com.android.systemui.recents.model.Task; |
| 48 | import com.android.systemui.recents.model.TaskStack; |
| 49 | |
| 50 | import java.util.ArrayList; |
| 51 | import java.util.List; |
| 52 | |
Filip Gruszczynski | 96daf32 | 2015-11-18 18:01:27 -0800 | [diff] [blame^] | 53 | import static android.app.ActivityManager.StackId.DOCKED_STACK_ID; |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 54 | import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; |
| 55 | import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; |
| 56 | import static android.app.ActivityManager.StackId.INVALID_STACK_ID; |
| 57 | |
| 58 | /** |
| 59 | * A helper class to create transitions to/from Recents |
| 60 | */ |
| 61 | public 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 Gruszczynski | 1a4dfe5 | 2015-11-15 10:58:57 -0800 | [diff] [blame] | 113 | EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent()); |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 114 | |
| 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 Gruszczynski | 1a4dfe5 | 2015-11-15 10:58:57 -0800 | [diff] [blame] | 124 | animStartedListener = new ActivityOptions.OnAnimationStartedListener() { |
| 125 | @Override |
| 126 | public void onAnimationStarted() { |
| 127 | EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent()); |
| 128 | } |
| 129 | }; |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 130 | } |
| 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 Gruszczynski | 96daf32 | 2015-11-18 18:01:27 -0800 | [diff] [blame^] | 250 | if (!StackId.useAnimationSpecForAppTransition(targetStackId)) { |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 251 | 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 Gruszczynski | fe9823f | 2015-11-12 14:09:26 -0800 | [diff] [blame] | 282 | if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) { |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 283 | 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 | |
Winson | c742f97 | 2015-11-12 11:32:21 -0800 | [diff] [blame] | 325 | if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) { |
Winson | 83c1b07 | 2015-11-09 10:48:04 -0800 | [diff] [blame] | 326 | 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 | } |