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 static android.app.ActivityManager.StackId.ASSISTANT_STACK_ID; 20 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID; 21 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; 22 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; 23 import static android.app.ActivityManager.StackId.INVALID_STACK_ID; 24 25 import android.annotation.Nullable; 26 import android.app.ActivityManager.StackId; 27 import android.app.ActivityOptions; 28 import android.app.ActivityOptions.OnAnimationStartedListener; 29 import android.content.Context; 30 import android.graphics.Bitmap; 31 import android.graphics.Canvas; 32 import android.graphics.Color; 33 import android.graphics.GraphicBuffer; 34 import android.graphics.Rect; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.IRemoteCallback; 38 import android.os.RemoteException; 39 import android.util.Log; 40 import android.view.AppTransitionAnimationSpec; 41 import android.view.DisplayListCanvas; 42 import android.view.IAppTransitionAnimationSpecsFuture; 43 import android.view.RenderNode; 44 import android.view.ThreadedRenderer; 45 import android.view.View; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.systemui.recents.Recents; 49 import com.android.systemui.recents.RecentsDebugFlags; 50 import com.android.systemui.recents.events.EventBus; 51 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent; 52 import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent; 53 import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent; 54 import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent; 55 import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent; 56 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent; 57 import com.android.systemui.recents.events.component.SetWaitingForTransitionStartEvent; 58 import com.android.systemui.recents.misc.SystemServicesProxy; 59 import com.android.systemui.recents.model.Task; 60 import com.android.systemui.recents.model.TaskStack; 61 import com.android.systemui.statusbar.phone.StatusBar; 62 63 import java.util.ArrayList; 64 import java.util.Collections; 65 import java.util.List; 66 67 /** 68 * A helper class to create transitions to/from Recents 69 */ 70 public class RecentsTransitionHelper { 71 72 private static final String TAG = "RecentsTransitionHelper"; 73 private static final boolean DEBUG = false; 74 75 /** 76 * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently 77 * waiting for the specs to be retrieved. 78 */ 79 private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>(); 80 81 @GuardedBy("this") 82 private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING; 83 84 private Context mContext; 85 private Handler mHandler; 86 private TaskViewTransform mTmpTransform = new TaskViewTransform(); 87 88 private class StartScreenPinningRunnableRunnable implements Runnable { 89 90 private int taskId = -1; 91 92 @Override 93 public void run() { 94 EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext, taskId)); 95 } 96 } 97 private StartScreenPinningRunnableRunnable mStartScreenPinningRunnable 98 = new StartScreenPinningRunnableRunnable(); 99 100 public RecentsTransitionHelper(Context context) { 101 mContext = context; 102 mHandler = new Handler(); 103 } 104 105 /** 106 * Launches the specified {@link Task}. 107 */ 108 public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task, 109 final TaskStackView stackView, final TaskView taskView, 110 final boolean screenPinningRequested, final int destinationStack) { 111 112 final ActivityOptions.OnAnimationStartedListener animStartedListener; 113 final AppTransitionAnimationSpecsFuture transitionFuture; 114 if (taskView != null) { 115 116 // Fetch window rect here already in order not to be blocked on lock contention in WM 117 // when the future calls it. 118 final Rect windowRect = Recents.getSystemServices().getWindowRect(); 119 transitionFuture = getAppTransitionFuture( 120 () -> composeAnimationSpecs(task, stackView, destinationStack, windowRect)); 121 animStartedListener = new OnAnimationStartedListener() { 122 private boolean mHandled; 123 124 @Override 125 public void onAnimationStarted() { 126 if (mHandled) { 127 return; 128 } 129 mHandled = true; 130 131 // If we are launching into another task, cancel the previous task's 132 // window transition 133 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task)); 134 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent()); 135 stackView.cancelAllTaskViewAnimations(); 136 137 if (screenPinningRequested) { 138 // Request screen pinning after the animation runs 139 mStartScreenPinningRunnable.taskId = task.key.id; 140 mHandler.postDelayed(mStartScreenPinningRunnable, 350); 141 } 142 143 if (!Recents.getConfiguration().isLowRamDevice) { 144 // Reset the state where we are waiting for the transition to start 145 EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false)); 146 } 147 } 148 }; 149 } else { 150 // This is only the case if the task is not on screen (scrolled offscreen for example) 151 transitionFuture = null; 152 animStartedListener = new OnAnimationStartedListener() { 153 private boolean mHandled; 154 155 @Override 156 public void onAnimationStarted() { 157 if (mHandled) { 158 return; 159 } 160 mHandled = true; 161 162 // If we are launching into another task, cancel the previous task's 163 // window transition 164 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task)); 165 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent()); 166 stackView.cancelAllTaskViewAnimations(); 167 168 if (!Recents.getConfiguration().isLowRamDevice) { 169 // Reset the state where we are waiting for the transition to start 170 EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false)); 171 } 172 } 173 }; 174 } 175 176 EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(true)); 177 final ActivityOptions opts = ActivityOptions.makeMultiThumbFutureAspectScaleAnimation(mContext, 178 mHandler, transitionFuture != null ? transitionFuture.future : null, 179 animStartedListener, true /* scaleUp */); 180 if (taskView == null) { 181 // If there is no task view, then we do not need to worry about animating out occluding 182 // task views, and we can launch immediately 183 startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack); 184 } else { 185 LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView, 186 screenPinningRequested); 187 if (task.group != null && !task.group.isFrontMostTask(task)) { 188 launchStartedEvent.addPostAnimationCallback(new Runnable() { 189 @Override 190 public void run() { 191 startTaskActivity(stack, task, taskView, opts, transitionFuture, 192 destinationStack); 193 } 194 }); 195 EventBus.getDefault().send(launchStartedEvent); 196 } else { 197 EventBus.getDefault().send(launchStartedEvent); 198 startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack); 199 } 200 } 201 Recents.getSystemServices().sendCloseSystemWindows( 202 StatusBar.SYSTEM_DIALOG_REASON_HOME_KEY); 203 } 204 205 public IRemoteCallback wrapStartedListener(final OnAnimationStartedListener listener) { 206 if (listener == null) { 207 return null; 208 } 209 return new IRemoteCallback.Stub() { 210 @Override 211 public void sendResult(Bundle data) throws RemoteException { 212 mHandler.post(new Runnable() { 213 @Override 214 public void run() { 215 listener.onAnimationStarted(); 216 } 217 }); 218 } 219 }; 220 } 221 222 /** 223 * Starts the activity for the launch task. 224 * 225 * @param taskView this is the {@link TaskView} that we are launching from. This can be null if 226 * we are toggling recents and the launch-to task is now offscreen. 227 * @param destinationStack id of the stack to put the task into. 228 */ 229 private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView, 230 ActivityOptions opts, AppTransitionAnimationSpecsFuture transitionFuture, 231 int destinationStack) { 232 SystemServicesProxy ssp = Recents.getSystemServices(); 233 ssp.startActivityFromRecents(mContext, task.key, task.title, opts, destinationStack, 234 succeeded -> { 235 if (succeeded) { 236 // Keep track of the index of the task launch 237 int taskIndexFromFront = 0; 238 int taskIndex = stack.indexOfStackTask(task); 239 if (taskIndex > -1) { 240 taskIndexFromFront = stack.getTaskCount() - taskIndex - 1; 241 } 242 EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront)); 243 } else { 244 // Dismiss the task if we fail to launch it 245 if (taskView != null) { 246 taskView.dismissTask(); 247 } 248 249 // Keep track of failed launches 250 EventBus.getDefault().send(new LaunchTaskFailedEvent()); 251 } 252 }); 253 if (transitionFuture != null) { 254 mHandler.post(transitionFuture::precacheSpecs); 255 } 256 } 257 258 /** 259 * Creates a future which will later be queried for animation specs for this current transition. 260 * 261 * @param composer The implementation that composes the specs on the UI thread. 262 */ 263 public AppTransitionAnimationSpecsFuture getAppTransitionFuture( 264 final AnimationSpecComposer composer) { 265 synchronized (this) { 266 mAppTransitionAnimationSpecs = SPECS_WAITING; 267 } 268 IAppTransitionAnimationSpecsFuture future = new IAppTransitionAnimationSpecsFuture.Stub() { 269 @Override 270 public AppTransitionAnimationSpec[] get() throws RemoteException { 271 mHandler.post(() -> { 272 synchronized (RecentsTransitionHelper.this) { 273 mAppTransitionAnimationSpecs = composer.composeSpecs(); 274 RecentsTransitionHelper.this.notifyAll(); 275 } 276 }); 277 synchronized (RecentsTransitionHelper.this) { 278 while (mAppTransitionAnimationSpecs == SPECS_WAITING) { 279 try { 280 RecentsTransitionHelper.this.wait(); 281 } catch (InterruptedException e) {} 282 } 283 if (mAppTransitionAnimationSpecs == null) { 284 return null; 285 } 286 AppTransitionAnimationSpec[] specs 287 = new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()]; 288 mAppTransitionAnimationSpecs.toArray(specs); 289 mAppTransitionAnimationSpecs = SPECS_WAITING; 290 return specs; 291 } 292 } 293 }; 294 return new AppTransitionAnimationSpecsFuture(composer, future); 295 } 296 297 /** 298 * Composes the transition spec when docking a task, which includes a full task bitmap. 299 */ 300 public List<AppTransitionAnimationSpec> composeDockAnimationSpec(TaskView taskView, 301 Rect bounds) { 302 mTmpTransform.fillIn(taskView); 303 Task task = taskView.getTask(); 304 GraphicBuffer buffer = RecentsTransitionHelper.composeTaskBitmap(taskView, mTmpTransform); 305 return Collections.singletonList(new AppTransitionAnimationSpec(task.key.id, buffer, 306 bounds)); 307 } 308 309 /** 310 * Composes the animation specs for all the tasks in the target stack. 311 */ 312 private List<AppTransitionAnimationSpec> composeAnimationSpecs(final Task task, 313 final TaskStackView stackView, final int destinationStack, Rect windowRect) { 314 // Ensure we have a valid target stack id 315 final int targetStackId = destinationStack != INVALID_STACK_ID ? 316 destinationStack : task.key.stackId; 317 if (!StackId.useAnimationSpecForAppTransition(targetStackId)) { 318 return null; 319 } 320 321 // Calculate the offscreen task rect (for tasks that are not backed by views) 322 TaskView taskView = stackView.getChildViewForTask(task); 323 TaskStackLayoutAlgorithm stackLayout = stackView.getStackAlgorithm(); 324 Rect offscreenTaskRect = new Rect(); 325 stackLayout.getFrontOfStackTransform().rect.round(offscreenTaskRect); 326 327 // If this is a full screen stack, the transition will be towards the single, full screen 328 // task. We only need the transition spec for this task. 329 List<AppTransitionAnimationSpec> specs = new ArrayList<>(); 330 331 // TODO: Sometimes targetStackId is not initialized after reboot, so we also have to 332 // check for INVALID_STACK_ID 333 if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID || targetStackId == DOCKED_STACK_ID 334 || targetStackId == ASSISTANT_STACK_ID || targetStackId == INVALID_STACK_ID) { 335 if (taskView == null) { 336 specs.add(composeOffscreenAnimationSpec(task, offscreenTaskRect)); 337 } else { 338 mTmpTransform.fillIn(taskView); 339 stackLayout.transformToScreenCoordinates(mTmpTransform, windowRect); 340 AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, taskView, 341 mTmpTransform, true /* addHeaderBitmap */); 342 if (spec != null) { 343 specs.add(spec); 344 } 345 } 346 return specs; 347 } 348 349 // Otherwise, for freeform tasks, create a new animation spec for each task we have to 350 // launch 351 TaskStack stack = stackView.getStack(); 352 ArrayList<Task> tasks = stack.getStackTasks(); 353 int taskCount = tasks.size(); 354 for (int i = taskCount - 1; i >= 0; i--) { 355 Task t = tasks.get(i); 356 if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) { 357 TaskView tv = stackView.getChildViewForTask(t); 358 if (tv == null) { 359 // TODO: Create a different animation task rect for this case (though it should 360 // never happen) 361 specs.add(composeOffscreenAnimationSpec(t, offscreenTaskRect)); 362 } else { 363 mTmpTransform.fillIn(taskView); 364 stackLayout.transformToScreenCoordinates(mTmpTransform, 365 null /* windowOverrideRect */); 366 AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, tv, 367 mTmpTransform, true /* addHeaderBitmap */); 368 if (spec != null) { 369 specs.add(spec); 370 } 371 } 372 } 373 } 374 375 return specs; 376 } 377 378 /** 379 * Composes a single animation spec for the given {@link Task} 380 */ 381 private static AppTransitionAnimationSpec composeOffscreenAnimationSpec(Task task, 382 Rect taskRect) { 383 return new AppTransitionAnimationSpec(task.key.id, null, taskRect); 384 } 385 386 public static GraphicBuffer composeTaskBitmap(TaskView taskView, TaskViewTransform transform) { 387 float scale = transform.scale; 388 int fromWidth = (int) (transform.rect.width() * scale); 389 int fromHeight = (int) (transform.rect.height() * scale); 390 if (fromWidth == 0 || fromHeight == 0) { 391 Log.e(TAG, "Could not compose thumbnail for task: " + taskView.getTask() + 392 " at transform: " + transform); 393 394 return drawViewIntoGraphicBuffer(1, 1, null, 1f, 0x00ffffff); 395 } else { 396 if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) { 397 return drawViewIntoGraphicBuffer(fromWidth, fromHeight, null, 1f, 0xFFff0000); 398 } else { 399 return drawViewIntoGraphicBuffer(fromWidth, fromHeight, taskView, scale, 0); 400 } 401 } 402 } 403 404 private static GraphicBuffer composeHeaderBitmap(TaskView taskView, 405 TaskViewTransform transform) { 406 float scale = transform.scale; 407 int headerWidth = (int) (transform.rect.width()); 408 int headerHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale); 409 if (headerWidth == 0 || headerHeight == 0) { 410 return null; 411 } 412 413 if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) { 414 return drawViewIntoGraphicBuffer(headerWidth, headerHeight, null, 1f, 0xFFff0000); 415 } else { 416 return drawViewIntoGraphicBuffer(headerWidth, headerHeight, taskView.mHeaderView, 417 scale, 0); 418 } 419 } 420 421 public static GraphicBuffer drawViewIntoGraphicBuffer(int bufferWidth, int bufferHeight, 422 View view, float scale, int eraseColor) { 423 RenderNode node = RenderNode.create("RecentsTransition", null); 424 node.setLeftTopRightBottom(0, 0, bufferWidth, bufferHeight); 425 node.setClipToBounds(false); 426 DisplayListCanvas c = node.start(bufferWidth, bufferHeight); 427 c.scale(scale, scale); 428 if (eraseColor != 0) { 429 c.drawColor(eraseColor); 430 } 431 if (view != null) { 432 view.draw(c); 433 } 434 node.end(c); 435 Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, bufferWidth, bufferHeight); 436 return hwBitmap.createGraphicBufferHandle(); 437 } 438 439 /** 440 * Composes a single animation spec for the given {@link TaskView} 441 */ 442 private static AppTransitionAnimationSpec composeAnimationSpec(TaskStackView stackView, 443 TaskView taskView, TaskViewTransform transform, boolean addHeaderBitmap) { 444 GraphicBuffer b = null; 445 if (addHeaderBitmap) { 446 b = composeHeaderBitmap(taskView, transform); 447 if (b == null) { 448 return null; 449 } 450 } 451 452 Rect taskRect = new Rect(); 453 transform.rect.round(taskRect); 454 // Disable in for low ram devices because each task does in Recents does not have fullscreen 455 // height (stackView height) and when transitioning to fullscreen app, the code below would 456 // force the task thumbnail to full stackView height immediately causing the transition 457 // jarring. 458 if (!Recents.getConfiguration().isLowRamDevice && taskView.getTask() != 459 stackView.getStack().getStackFrontMostTask(false /* includeFreeformTasks */)) { 460 taskRect.bottom = taskRect.top + stackView.getMeasuredHeight(); 461 } 462 return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect); 463 } 464 465 public interface AnimationSpecComposer { 466 List<AppTransitionAnimationSpec> composeSpecs(); 467 } 468 469 /** 470 * Class to be returned from {@link #composeAnimationSpec} that gives access to both the future 471 * and the anonymous class used for composing. 472 */ 473 public class AppTransitionAnimationSpecsFuture { 474 475 private final AnimationSpecComposer composer; 476 private final IAppTransitionAnimationSpecsFuture future; 477 478 private AppTransitionAnimationSpecsFuture(AnimationSpecComposer composer, 479 IAppTransitionAnimationSpecsFuture future) { 480 this.composer = composer; 481 this.future = future; 482 } 483 484 public IAppTransitionAnimationSpecsFuture getFuture() { 485 return future; 486 } 487 488 /** 489 * Manually generates and caches the spec such that they are already available when the 490 * future needs. 491 */ 492 public void precacheSpecs() { 493 synchronized (RecentsTransitionHelper.this) { 494 mAppTransitionAnimationSpecs = composer.composeSpecs(); 495 } 496 } 497 } 498 } 499