Home | History | Annotate | Download | only in views
      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.misc.SystemServicesProxy;
     58 import com.android.systemui.recents.model.Task;
     59 import com.android.systemui.recents.model.TaskStack;
     60 import com.android.systemui.statusbar.phone.StatusBar;
     61 
     62 import java.util.ArrayList;
     63 import java.util.Collections;
     64 import java.util.List;
     65 
     66 /**
     67  * A helper class to create transitions to/from Recents
     68  */
     69 public 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