1 /* 2 * Copyright (C) 2022 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.activityembedding; 18 19 import static android.app.ActivityOptions.ANIM_CUSTOM; 20 import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; 21 import static android.window.TransitionInfo.FLAG_FILLS_TASK; 22 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; 23 24 import static com.android.wm.shell.transition.DefaultTransitionHandler.isSupportedOverrideAnimation; 25 import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE; 26 27 import static java.util.Objects.requireNonNull; 28 29 import android.content.Context; 30 import android.graphics.Rect; 31 import android.os.IBinder; 32 import android.util.ArrayMap; 33 import android.view.SurfaceControl; 34 import android.window.TransitionInfo; 35 import android.window.TransitionInfo.AnimationOptions; 36 import android.window.TransitionRequestInfo; 37 import android.window.WindowContainerTransaction; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.wm.shell.shared.TransitionUtil; 44 import com.android.wm.shell.sysui.ShellInit; 45 import com.android.wm.shell.transition.Transitions; 46 47 import java.util.List; 48 49 /** 50 * Responsible for handling ActivityEmbedding related transitions. 51 */ 52 public class ActivityEmbeddingController implements Transitions.TransitionHandler { 53 54 private final Context mContext; 55 @VisibleForTesting 56 final Transitions mTransitions; 57 @VisibleForTesting 58 final ActivityEmbeddingAnimationRunner mAnimationRunner; 59 60 /** 61 * Keeps track of the currently-running transition callback associated with each transition 62 * token. 63 */ 64 private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks = 65 new ArrayMap<>(); 66 ActivityEmbeddingController(@onNull Context context, @NonNull ShellInit shellInit, @NonNull Transitions transitions)67 private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit, 68 @NonNull Transitions transitions) { 69 mContext = requireNonNull(context); 70 mTransitions = requireNonNull(transitions); 71 mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this); 72 73 shellInit.addInitCallback(this::onInit, this); 74 } 75 76 /** 77 * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not 78 * supported. 79 */ 80 @Nullable create(@onNull Context context, @NonNull ShellInit shellInit, @NonNull Transitions transitions)81 public static ActivityEmbeddingController create(@NonNull Context context, 82 @NonNull ShellInit shellInit, @NonNull Transitions transitions) { 83 return Transitions.ENABLE_SHELL_TRANSITIONS 84 ? new ActivityEmbeddingController(context, shellInit, transitions) 85 : null; 86 } 87 88 /** Registers to handle transitions. */ onInit()89 public void onInit() { 90 mTransitions.addHandler(this); 91 } 92 93 /** Whether ActivityEmbeddingController should animate this transition. */ shouldAnimate(@onNull TransitionInfo info)94 public boolean shouldAnimate(@NonNull TransitionInfo info) { 95 if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) { 96 // The TRANSIT_TASK_FRAGMENT_DRAG_RESIZE type happens when the user drags the 97 // interactive divider to resize the split containers. The content is veiled, so we will 98 // handle the transition with a jump cut. 99 return true; 100 } 101 boolean containsEmbeddingChange = false; 102 for (TransitionInfo.Change change : info.getChanges()) { 103 if (!change.hasFlags(FLAG_FILLS_TASK) && change.hasFlags( 104 FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { 105 // Whether the Task contains any ActivityEmbedding split before or after the 106 // transition. 107 containsEmbeddingChange = true; 108 } 109 } 110 if (!containsEmbeddingChange) { 111 // Let the system to play the default animation if there is no ActivityEmbedding split 112 // window. This allows to play the app customized animation when there is no embedding, 113 // such as the device is in a folded state. 114 return false; 115 } 116 117 if (containsNonEmbeddedChange(info) && !handleNonEmbeddedChanges(info.getChanges())) { 118 return false; 119 } 120 121 return shouldAnimateAnimationOptions(info); 122 } 123 shouldAnimateAnimationOptions(@onNull TransitionInfo info)124 private boolean shouldAnimateAnimationOptions(@NonNull TransitionInfo info) { 125 for (TransitionInfo.Change change : info.getChanges()) { 126 if (!shouldAnimateAnimationOptions(change.getAnimationOptions())) { 127 // If any of override animation is not supported, don't animate the transition. 128 return false; 129 } 130 } 131 return true; 132 } 133 shouldAnimateAnimationOptions(@ullable AnimationOptions options)134 private boolean shouldAnimateAnimationOptions(@Nullable AnimationOptions options) { 135 if (options == null) { 136 return true; 137 } 138 // Scene-transition should be handled by app side. 139 if (options.getType() == ANIM_SCENE_TRANSITION) { 140 return false; 141 } 142 // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition, 143 // and Activity#overrideActivityTransition are supported. 144 if (options.getType() == ANIM_CUSTOM) { 145 return true; 146 } 147 // Use default transition handler to animate other override animation. 148 return !isSupportedOverrideAnimation(options); 149 } 150 151 @Override startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)152 public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 153 @NonNull SurfaceControl.Transaction startTransaction, 154 @NonNull SurfaceControl.Transaction finishTransaction, 155 @NonNull Transitions.TransitionFinishCallback finishCallback) { 156 157 if (!shouldAnimate(info)) return false; 158 159 // Start ActivityEmbedding animation. 160 mTransitionCallbacks.put(transition, finishCallback); 161 mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction); 162 return true; 163 } 164 165 @Override mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback)166 public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 167 @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, 168 @NonNull IBinder mergeTarget, 169 @NonNull Transitions.TransitionFinishCallback finishCallback) { 170 mAnimationRunner.cancelAnimationFromMerge(); 171 } 172 173 /** Whether TransitionInfo contains non-ActivityEmbedding embedded window. */ containsNonEmbeddedChange(@onNull TransitionInfo info)174 private boolean containsNonEmbeddedChange(@NonNull TransitionInfo info) { 175 for (TransitionInfo.Change change : info.getChanges()) { 176 if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { 177 return true; 178 } 179 } 180 return false; 181 } 182 handleNonEmbeddedChanges(List<TransitionInfo.Change> changes)183 private boolean handleNonEmbeddedChanges(List<TransitionInfo.Change> changes) { 184 final Rect nonClosingEmbeddedArea = new Rect(); 185 for (int i = changes.size() - 1; i >= 0; i--) { 186 final TransitionInfo.Change change = changes.get(i); 187 if (!TransitionUtil.isClosingType(change.getMode())) { 188 if (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { 189 nonClosingEmbeddedArea.union(change.getEndAbsBounds()); 190 continue; 191 } 192 // Not able to handle non-embedded container if it is not closing. 193 return false; 194 } 195 } 196 for (int i = changes.size() - 1; i >= 0; i--) { 197 final TransitionInfo.Change change = changes.get(i); 198 if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY) 199 && !nonClosingEmbeddedArea.contains(change.getEndAbsBounds())) { 200 // Unknown to animate containers outside the area of embedded activities. 201 return false; 202 } 203 } 204 // Drop the non-embedded closing change because it is occluded by embedded activities. 205 for (int i = changes.size() - 1; i >= 0; i--) { 206 final TransitionInfo.Change change = changes.get(i); 207 if (!change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { 208 changes.remove(i); 209 } 210 } 211 return true; 212 } 213 214 @Nullable 215 @Override handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)216 public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 217 @NonNull TransitionRequestInfo request) { 218 return null; 219 } 220 221 @Override setAnimScaleSetting(float scale)222 public void setAnimScaleSetting(float scale) { 223 mAnimationRunner.setAnimScaleSetting(scale); 224 } 225 226 /** Called when the animation is finished. */ onAnimationFinished(@onNull IBinder transition)227 void onAnimationFinished(@NonNull IBinder transition) { 228 final Transitions.TransitionFinishCallback callback = 229 mTransitionCallbacks.remove(transition); 230 if (callback == null) { 231 throw new IllegalStateException("No finish callback found"); 232 } 233 callback.onTransitionFinished(null /* wct */); 234 } 235 } 236