1 /* 2 * Copyright (C) 2020 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.wm.shell.legacysplitscreen; 18 19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; 20 import static android.view.WindowManager.TRANSIT_CHANGE; 21 import static android.view.WindowManager.TRANSIT_CLOSE; 22 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; 23 import static android.view.WindowManager.TRANSIT_OPEN; 24 import static android.view.WindowManager.TRANSIT_TO_BACK; 25 import static android.view.WindowManager.TRANSIT_TO_FRONT; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.ValueAnimator; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.app.ActivityManager; 33 import android.app.WindowConfiguration; 34 import android.graphics.Rect; 35 import android.os.IBinder; 36 import android.view.SurfaceControl; 37 import android.view.WindowManager; 38 import android.window.TransitionInfo; 39 import android.window.TransitionRequestInfo; 40 import android.window.WindowContainerTransaction; 41 42 import com.android.wm.shell.common.TransactionPool; 43 import com.android.wm.shell.common.annotations.ExternalThread; 44 import com.android.wm.shell.transition.Transitions; 45 46 import java.util.ArrayList; 47 48 /** Plays transition animations for split-screen */ 49 public class LegacySplitScreenTransitions implements Transitions.TransitionHandler { 50 private static final String TAG = "SplitScreenTransitions"; 51 52 public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 10; 53 54 private final TransactionPool mTransactionPool; 55 private final Transitions mTransitions; 56 private final LegacySplitScreenController mSplitScreen; 57 private final LegacySplitScreenTaskListener mListener; 58 59 private IBinder mPendingDismiss = null; 60 private boolean mDismissFromSnap = false; 61 private IBinder mPendingEnter = null; 62 private IBinder mAnimatingTransition = null; 63 64 /** Keeps track of currently running animations */ 65 private final ArrayList<Animator> mAnimations = new ArrayList<>(); 66 67 private Transitions.TransitionFinishCallback mFinishCallback = null; 68 private SurfaceControl.Transaction mFinishTransaction; 69 LegacySplitScreenTransitions(@onNull TransactionPool pool, @NonNull Transitions transitions, @NonNull LegacySplitScreenController splitScreen, @NonNull LegacySplitScreenTaskListener listener)70 LegacySplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, 71 @NonNull LegacySplitScreenController splitScreen, 72 @NonNull LegacySplitScreenTaskListener listener) { 73 mTransactionPool = pool; 74 mTransitions = transitions; 75 mSplitScreen = splitScreen; 76 mListener = listener; 77 } 78 79 @Override handleRequest(@onNull IBinder transition, @Nullable TransitionRequestInfo request)80 public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 81 @Nullable TransitionRequestInfo request) { 82 WindowContainerTransaction out = null; 83 final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); 84 final @WindowManager.TransitionType int type = request.getType(); 85 if (mSplitScreen.isDividerVisible()) { 86 // try to handle everything while in split-screen 87 out = new WindowContainerTransaction(); 88 if (triggerTask != null) { 89 final boolean shouldDismiss = 90 // if we close the primary-docked task, then leave split-screen since there 91 // is nothing behind it. 92 ((type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK) 93 && triggerTask.parentTaskId == mListener.mPrimary.taskId) 94 // if an activity that is not supported in multi window mode is launched, 95 // we also need to leave split-screen. 96 || ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) 97 && !triggerTask.supportsMultiWindow); 98 // In both cases, dismiss the primary 99 if (shouldDismiss) { 100 WindowManagerProxy.buildDismissSplit(out, mListener, 101 mSplitScreen.getSplitLayout(), true /* dismiss */); 102 if (type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) { 103 out.reorder(triggerTask.token, true /* onTop */); 104 } 105 mPendingDismiss = transition; 106 } 107 } 108 } else if (triggerTask != null) { 109 // Not in split mode, so look for an open with a trigger task. 110 if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) 111 && triggerTask.configuration.windowConfiguration.getWindowingMode() 112 == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { 113 out = new WindowContainerTransaction(); 114 mSplitScreen.prepareEnterSplitTransition(out); 115 mPendingEnter = transition; 116 } 117 } 118 return out; 119 } 120 121 // TODO(shell-transitions): real animations startExampleAnimation(@onNull SurfaceControl leash, boolean show)122 private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { 123 final float end = show ? 1.f : 0.f; 124 final float start = 1.f - end; 125 final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); 126 final ValueAnimator va = ValueAnimator.ofFloat(start, end); 127 va.setDuration(500); 128 va.addUpdateListener(animation -> { 129 float fraction = animation.getAnimatedFraction(); 130 transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); 131 transaction.apply(); 132 }); 133 final Runnable finisher = () -> { 134 transaction.setAlpha(leash, end); 135 transaction.apply(); 136 mTransactionPool.release(transaction); 137 mTransitions.getMainExecutor().execute(() -> { 138 mAnimations.remove(va); 139 onFinish(); 140 }); 141 }; 142 va.addListener(new Animator.AnimatorListener() { 143 @Override 144 public void onAnimationStart(Animator animation) { } 145 146 @Override 147 public void onAnimationEnd(Animator animation) { 148 finisher.run(); 149 } 150 151 @Override 152 public void onAnimationCancel(Animator animation) { 153 finisher.run(); 154 } 155 156 @Override 157 public void onAnimationRepeat(Animator animation) { } 158 }); 159 mAnimations.add(va); 160 mTransitions.getAnimExecutor().execute(va::start); 161 } 162 163 // TODO(shell-transitions): real animations startExampleResizeAnimation(@onNull SurfaceControl leash, @NonNull Rect startBounds, @NonNull Rect endBounds)164 private void startExampleResizeAnimation(@NonNull SurfaceControl leash, 165 @NonNull Rect startBounds, @NonNull Rect endBounds) { 166 final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); 167 final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); 168 va.setDuration(500); 169 va.addUpdateListener(animation -> { 170 float fraction = animation.getAnimatedFraction(); 171 transaction.setWindowCrop(leash, 172 (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), 173 (int) (startBounds.height() * (1.f - fraction) 174 + endBounds.height() * fraction)); 175 transaction.setPosition(leash, 176 startBounds.left * (1.f - fraction) + endBounds.left * fraction, 177 startBounds.top * (1.f - fraction) + endBounds.top * fraction); 178 transaction.apply(); 179 }); 180 final Runnable finisher = () -> { 181 transaction.setWindowCrop(leash, 0, 0); 182 transaction.setPosition(leash, endBounds.left, endBounds.top); 183 transaction.apply(); 184 mTransactionPool.release(transaction); 185 mTransitions.getMainExecutor().execute(() -> { 186 mAnimations.remove(va); 187 onFinish(); 188 }); 189 }; 190 va.addListener(new AnimatorListenerAdapter() { 191 @Override 192 public void onAnimationEnd(Animator animation) { 193 finisher.run(); 194 } 195 196 @Override 197 public void onAnimationCancel(Animator animation) { 198 finisher.run(); 199 } 200 }); 201 mAnimations.add(va); 202 mTransitions.getAnimExecutor().execute(va::start); 203 } 204 205 @Override startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)206 public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 207 @NonNull SurfaceControl.Transaction startTransaction, 208 @NonNull SurfaceControl.Transaction finishTransaction, 209 @NonNull Transitions.TransitionFinishCallback finishCallback) { 210 if (transition != mPendingDismiss && transition != mPendingEnter) { 211 // If we're not in split-mode, just abort 212 if (!mSplitScreen.isDividerVisible()) return false; 213 // Check to see if HOME is involved 214 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 215 final TransitionInfo.Change change = info.getChanges().get(i); 216 if (change.getTaskInfo() == null 217 || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue; 218 if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { 219 mSplitScreen.ensureMinimizedSplit(); 220 } else if (change.getMode() == TRANSIT_CLOSE 221 || change.getMode() == TRANSIT_TO_BACK) { 222 mSplitScreen.ensureNormalSplit(); 223 } 224 } 225 // Use normal animations. 226 return false; 227 } 228 229 mFinishCallback = finishCallback; 230 mFinishTransaction = mTransactionPool.acquire(); 231 mAnimatingTransition = transition; 232 233 // Play fade animations 234 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 235 final TransitionInfo.Change change = info.getChanges().get(i); 236 final SurfaceControl leash = change.getLeash(); 237 final int mode = info.getChanges().get(i).getMode(); 238 239 if (mode == TRANSIT_CHANGE) { 240 if (change.getParent() != null) { 241 // This is probably reparented, so we want the parent to be immediately visible 242 final TransitionInfo.Change parentChange = info.getChange(change.getParent()); 243 startTransaction.show(parentChange.getLeash()); 244 startTransaction.setAlpha(parentChange.getLeash(), 1.f); 245 // and then animate this layer outside the parent (since, for example, this is 246 // the home task animating from fullscreen to part-screen). 247 startTransaction.reparent(leash, info.getRootLeash()); 248 startTransaction.setLayer(leash, info.getChanges().size() - i); 249 // build the finish reparent/reposition 250 mFinishTransaction.reparent(leash, parentChange.getLeash()); 251 mFinishTransaction.setPosition(leash, 252 change.getEndRelOffset().x, change.getEndRelOffset().y); 253 } 254 // TODO(shell-transitions): screenshot here 255 final Rect startBounds = new Rect(change.getStartAbsBounds()); 256 final boolean isHome = change.getTaskInfo() != null 257 && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; 258 if (mPendingDismiss == transition && mDismissFromSnap && !isHome) { 259 // Home is special since it doesn't move during fling. Everything else, though, 260 // when dismissing from snap, the top/left is at 0,0. 261 startBounds.offsetTo(0, 0); 262 } 263 final Rect endBounds = new Rect(change.getEndAbsBounds()); 264 startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); 265 endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); 266 startExampleResizeAnimation(leash, startBounds, endBounds); 267 } 268 if (change.getParent() != null) { 269 continue; 270 } 271 272 if (transition == mPendingEnter 273 && mListener.mPrimary.token.equals(change.getContainer()) 274 || mListener.mSecondary.token.equals(change.getContainer())) { 275 startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(), 276 change.getStartAbsBounds().height()); 277 if (mListener.mPrimary.token.equals(change.getContainer())) { 278 // Move layer to top since we want it above the oversized home task during 279 // animation even though home task is on top in hierarchy. 280 startTransaction.setLayer(leash, info.getChanges().size() + 1); 281 } 282 } 283 boolean isOpening = Transitions.isOpeningType(info.getType()); 284 if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { 285 // fade in 286 startExampleAnimation(leash, true /* show */); 287 } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { 288 // fade out 289 if (transition == mPendingDismiss && mDismissFromSnap) { 290 // Dismissing via snap-to-top/bottom means that the dismissed task is already 291 // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 292 // and don't animate it so it doesn't pop-in when reparented. 293 startTransaction.setAlpha(leash, 0.f); 294 } else { 295 startExampleAnimation(leash, false /* show */); 296 } 297 } 298 } 299 if (transition == mPendingEnter) { 300 // If entering, check if we should enter into minimized or normal split 301 boolean homeIsVisible = false; 302 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 303 final TransitionInfo.Change change = info.getChanges().get(i); 304 if (change.getTaskInfo() == null 305 || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) { 306 continue; 307 } 308 homeIsVisible = change.getMode() == TRANSIT_OPEN 309 || change.getMode() == TRANSIT_TO_FRONT 310 || change.getMode() == TRANSIT_CHANGE; 311 break; 312 } 313 mSplitScreen.finishEnterSplitTransition(homeIsVisible); 314 } 315 startTransaction.apply(); 316 onFinish(); 317 return true; 318 } 319 320 @ExternalThread dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize, boolean snapped)321 void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, 322 boolean dismissOrMaximize, boolean snapped) { 323 final WindowContainerTransaction wct = new WindowContainerTransaction(); 324 WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize); 325 mTransitions.getMainExecutor().execute(() -> { 326 mDismissFromSnap = snapped; 327 mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this); 328 }); 329 } 330 onFinish()331 private void onFinish() { 332 if (!mAnimations.isEmpty()) return; 333 mFinishTransaction.apply(); 334 mTransactionPool.release(mFinishTransaction); 335 mFinishTransaction = null; 336 mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); 337 mFinishCallback = null; 338 if (mAnimatingTransition == mPendingEnter) { 339 mPendingEnter = null; 340 } 341 if (mAnimatingTransition == mPendingDismiss) { 342 mSplitScreen.onDismissSplit(); 343 mPendingDismiss = null; 344 } 345 mDismissFromSnap = false; 346 mAnimatingTransition = null; 347 } 348 } 349