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 t, @NonNull Transitions.TransitionFinishCallback finishCallback)206 public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 207 @NonNull SurfaceControl.Transaction t, 208 @NonNull Transitions.TransitionFinishCallback finishCallback) { 209 if (transition != mPendingDismiss && transition != mPendingEnter) { 210 // If we're not in split-mode, just abort 211 if (!mSplitScreen.isDividerVisible()) return false; 212 // Check to see if HOME is involved 213 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 214 final TransitionInfo.Change change = info.getChanges().get(i); 215 if (change.getTaskInfo() == null 216 || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue; 217 if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { 218 mSplitScreen.ensureMinimizedSplit(); 219 } else if (change.getMode() == TRANSIT_CLOSE 220 || change.getMode() == TRANSIT_TO_BACK) { 221 mSplitScreen.ensureNormalSplit(); 222 } 223 } 224 // Use normal animations. 225 return false; 226 } 227 228 mFinishCallback = finishCallback; 229 mFinishTransaction = mTransactionPool.acquire(); 230 mAnimatingTransition = transition; 231 232 // Play fade animations 233 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 234 final TransitionInfo.Change change = info.getChanges().get(i); 235 final SurfaceControl leash = change.getLeash(); 236 final int mode = info.getChanges().get(i).getMode(); 237 238 if (mode == TRANSIT_CHANGE) { 239 if (change.getParent() != null) { 240 // This is probably reparented, so we want the parent to be immediately visible 241 final TransitionInfo.Change parentChange = info.getChange(change.getParent()); 242 t.show(parentChange.getLeash()); 243 t.setAlpha(parentChange.getLeash(), 1.f); 244 // and then animate this layer outside the parent (since, for example, this is 245 // the home task animating from fullscreen to part-screen). 246 t.reparent(leash, info.getRootLeash()); 247 t.setLayer(leash, info.getChanges().size() - i); 248 // build the finish reparent/reposition 249 mFinishTransaction.reparent(leash, parentChange.getLeash()); 250 mFinishTransaction.setPosition(leash, 251 change.getEndRelOffset().x, change.getEndRelOffset().y); 252 } 253 // TODO(shell-transitions): screenshot here 254 final Rect startBounds = new Rect(change.getStartAbsBounds()); 255 final boolean isHome = change.getTaskInfo() != null 256 && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; 257 if (mPendingDismiss == transition && mDismissFromSnap && !isHome) { 258 // Home is special since it doesn't move during fling. Everything else, though, 259 // when dismissing from snap, the top/left is at 0,0. 260 startBounds.offsetTo(0, 0); 261 } 262 final Rect endBounds = new Rect(change.getEndAbsBounds()); 263 startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); 264 endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); 265 startExampleResizeAnimation(leash, startBounds, endBounds); 266 } 267 if (change.getParent() != null) { 268 continue; 269 } 270 271 if (transition == mPendingEnter 272 && mListener.mPrimary.token.equals(change.getContainer()) 273 || mListener.mSecondary.token.equals(change.getContainer())) { 274 t.setWindowCrop(leash, change.getStartAbsBounds().width(), 275 change.getStartAbsBounds().height()); 276 if (mListener.mPrimary.token.equals(change.getContainer())) { 277 // Move layer to top since we want it above the oversized home task during 278 // animation even though home task is on top in hierarchy. 279 t.setLayer(leash, info.getChanges().size() + 1); 280 } 281 } 282 boolean isOpening = Transitions.isOpeningType(info.getType()); 283 if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { 284 // fade in 285 startExampleAnimation(leash, true /* show */); 286 } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { 287 // fade out 288 if (transition == mPendingDismiss && mDismissFromSnap) { 289 // Dismissing via snap-to-top/bottom means that the dismissed task is already 290 // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 291 // and don't animate it so it doesn't pop-in when reparented. 292 t.setAlpha(leash, 0.f); 293 } else { 294 startExampleAnimation(leash, false /* show */); 295 } 296 } 297 } 298 if (transition == mPendingEnter) { 299 // If entering, check if we should enter into minimized or normal split 300 boolean homeIsVisible = false; 301 for (int i = info.getChanges().size() - 1; i >= 0; --i) { 302 final TransitionInfo.Change change = info.getChanges().get(i); 303 if (change.getTaskInfo() == null 304 || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) { 305 continue; 306 } 307 homeIsVisible = change.getMode() == TRANSIT_OPEN 308 || change.getMode() == TRANSIT_TO_FRONT 309 || change.getMode() == TRANSIT_CHANGE; 310 break; 311 } 312 mSplitScreen.finishEnterSplitTransition(homeIsVisible); 313 } 314 t.apply(); 315 onFinish(); 316 return true; 317 } 318 319 @ExternalThread dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize, boolean snapped)320 void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, 321 boolean dismissOrMaximize, boolean snapped) { 322 final WindowContainerTransaction wct = new WindowContainerTransaction(); 323 WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize); 324 mTransitions.getMainExecutor().execute(() -> { 325 mDismissFromSnap = snapped; 326 mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this); 327 }); 328 } 329 onFinish()330 private void onFinish() { 331 if (!mAnimations.isEmpty()) return; 332 mFinishTransaction.apply(); 333 mTransactionPool.release(mFinishTransaction); 334 mFinishTransaction = null; 335 mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); 336 mFinishCallback = null; 337 if (mAnimatingTransition == mPendingEnter) { 338 mPendingEnter = null; 339 } 340 if (mAnimatingTransition == mPendingDismiss) { 341 mSplitScreen.onDismissSplit(); 342 mPendingDismiss = null; 343 } 344 mDismissFromSnap = false; 345 mAnimatingTransition = null; 346 } 347 } 348