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