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