1 /* 2 * Copyright (C) 2024 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.animation; 18 19 import android.annotation.IntDef; 20 import android.annotation.Nullable; 21 import android.app.ActivityOptions; 22 import android.app.ActivityOptions.LaunchCookie; 23 import android.app.PendingIntent; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.os.Build; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.RemoteException; 30 import android.util.Log; 31 import android.window.IRemoteTransition; 32 import android.window.RemoteTransition; 33 34 import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer; 35 import com.android.systemui.animation.shared.IOriginTransitions; 36 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.util.concurrent.atomic.AtomicInteger; 40 import java.util.function.Predicate; 41 import java.util.function.Supplier; 42 43 /** 44 * A session object that holds origin transition states for starting an activity from an on-screen 45 * UI component and smoothly transitioning back from the activity to the same UI component. 46 * 47 * @hide 48 */ 49 public class OriginTransitionSession { 50 private static final String TAG = "OriginTransitionSession"; 51 static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG); 52 53 @Retention(RetentionPolicy.SOURCE) 54 @IntDef(value = {NOT_STARTED, STARTED, CANCELLED}) 55 private @interface State {} 56 57 @State private static final int NOT_STARTED = 0; 58 @State private static final int STARTED = 1; 59 @State private static final int CANCELLED = 5; 60 61 private final String mName; 62 @Nullable private final IOriginTransitions mOriginTransitions; 63 private final Predicate<RemoteTransition> mIntentStarter; 64 @Nullable private final IRemoteTransition mEntryTransition; 65 @Nullable private final IRemoteTransition mExitTransition; 66 private final AtomicInteger mState = new AtomicInteger(NOT_STARTED); 67 68 @Nullable private RemoteTransition mOriginTransition; 69 OriginTransitionSession( String name, @Nullable IOriginTransitions originTransitions, Predicate<RemoteTransition> intentStarter, @Nullable IRemoteTransition entryTransition, @Nullable IRemoteTransition exitTransition)70 private OriginTransitionSession( 71 String name, 72 @Nullable IOriginTransitions originTransitions, 73 Predicate<RemoteTransition> intentStarter, 74 @Nullable IRemoteTransition entryTransition, 75 @Nullable IRemoteTransition exitTransition) { 76 mName = name; 77 mOriginTransitions = originTransitions; 78 mIntentStarter = intentStarter; 79 mEntryTransition = entryTransition; 80 mExitTransition = exitTransition; 81 if (hasExitTransition() && !hasEntryTransition()) { 82 throw new IllegalArgumentException( 83 "Entry transition must be supplied if you want to play an exit transition!"); 84 } 85 } 86 87 /** 88 * Launch the target intent with the supplied entry transition. After this method, the entry 89 * transition is expected to receive callbacks. The exit transition will be registered and 90 * triggered when the system detects a return from the launched activity to the launching 91 * activity. 92 */ start()93 public boolean start() { 94 if (!mState.compareAndSet(NOT_STARTED, STARTED)) { 95 logE("start: illegal state - " + stateToString(mState.get())); 96 return false; 97 } 98 99 RemoteTransition remoteTransition = null; 100 if (hasEntryTransition() && hasExitTransition()) { 101 logD("start: starting with entry and exit transition."); 102 try { 103 remoteTransition = 104 mOriginTransition = 105 mOriginTransitions.makeOriginTransition( 106 new RemoteTransition(mEntryTransition, mName + "-entry"), 107 new RemoteTransition(mExitTransition, mName + "-exit")); 108 } catch (RemoteException e) { 109 logE("Unable to create origin transition!", e); 110 } 111 } else if (hasEntryTransition()) { 112 logD("start: starting with entry transition."); 113 remoteTransition = new RemoteTransition(mEntryTransition, mName + "-entry"); 114 115 } else { 116 logD("start: starting without transition."); 117 } 118 if (mIntentStarter.test(remoteTransition)) { 119 return true; 120 } else { 121 // Animation is cancelled by intent starter. 122 logD("start: cancelled by intent starter!"); 123 cancel(); 124 return false; 125 } 126 } 127 128 /** 129 * Cancel the current transition and the registered exit transition if it exists. After this 130 * method, this session object can no longer be used. Clients need to create a new session 131 * object if they want to launch another intent with origin transition. 132 */ cancel()133 public void cancel() { 134 final int lastState = mState.getAndSet(CANCELLED); 135 if (lastState == CANCELLED || lastState == NOT_STARTED) { 136 return; 137 } 138 logD("cancel: cancelled transition. Last state: " + stateToString(lastState)); 139 if (mOriginTransition != null) { 140 try { 141 mOriginTransitions.cancelOriginTransition(mOriginTransition); 142 mOriginTransition = null; 143 } catch (RemoteException e) { 144 logE("Unable to cancel origin transition!", e); 145 } 146 } 147 if (mEntryTransition instanceof OriginRemoteTransition) { 148 ((OriginRemoteTransition) mEntryTransition).cancel(); 149 } 150 if (mExitTransition instanceof OriginRemoteTransition) { 151 ((OriginRemoteTransition) mExitTransition).cancel(); 152 } 153 } 154 hasEntryTransition()155 private boolean hasEntryTransition() { 156 return mEntryTransition != null; 157 } 158 hasExitTransition()159 private boolean hasExitTransition() { 160 return mOriginTransitions != null && mExitTransition != null; 161 } 162 logD(String msg)163 private void logD(String msg) { 164 if (DEBUG) { 165 Log.d(TAG, "Session[" + mName + "] - " + msg); 166 } 167 } 168 logE(String msg)169 private void logE(String msg) { 170 Log.e(TAG, "Session[" + mName + "] - " + msg); 171 } 172 logE(String msg, Throwable e)173 private void logE(String msg, Throwable e) { 174 Log.e(TAG, "Session[" + mName + "] - " + msg, e); 175 } 176 stateToString(@tate int state)177 private static String stateToString(@State int state) { 178 switch (state) { 179 case NOT_STARTED: 180 return "NOT_STARTED"; 181 case STARTED: 182 return "STARTED"; 183 case CANCELLED: 184 return "CANCELLED"; 185 default: 186 return "UNKNOWN(" + state + ")"; 187 } 188 } 189 190 /** 191 * A builder to build a {@link OriginTransitionSession}. 192 * 193 * @hide 194 */ 195 public static class Builder { 196 private final Context mContext; 197 @Nullable private final IOriginTransitions mOriginTransitions; 198 @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier; 199 @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier; 200 private Handler mHandler = new Handler(Looper.getMainLooper()); 201 private String mName; 202 @Nullable private Predicate<RemoteTransition> mIntentStarter; 203 204 /** Create a builder that only supports entry transition. */ Builder(Context context)205 public Builder(Context context) { 206 this(context, /* originTransitions= */ null); 207 } 208 209 /** Create a builder that supports both entry and return transition. */ Builder(Context context, @Nullable IOriginTransitions originTransitions)210 public Builder(Context context, @Nullable IOriginTransitions originTransitions) { 211 mContext = context; 212 mOriginTransitions = originTransitions; 213 mName = context.getPackageName(); 214 } 215 216 /** Specify a name that is used in logging. */ withName(String name)217 public Builder withName(String name) { 218 mName = name; 219 return this; 220 } 221 222 /** Specify an intent that will be launched when the session started. */ withIntent(Intent intent)223 public Builder withIntent(Intent intent) { 224 return withIntentStarter( 225 transition -> { 226 mContext.startActivity( 227 intent, createDefaultActivityOptions(transition).toBundle()); 228 return true; 229 }); 230 } 231 232 /** Specify a pending intent that will be launched when the session started. */ withPendingIntent(PendingIntent pendingIntent)233 public Builder withPendingIntent(PendingIntent pendingIntent) { 234 return withIntentStarter( 235 transition -> { 236 try { 237 pendingIntent.send(createDefaultActivityOptions(transition).toBundle()); 238 return true; 239 } catch (PendingIntent.CanceledException e) { 240 Log.e(TAG, "Failed to launch pending intent!", e); 241 return false; 242 } 243 }); 244 } 245 246 private static ActivityOptions createDefaultActivityOptions( 247 @Nullable RemoteTransition transition) { 248 ActivityOptions options = 249 transition == null 250 ? ActivityOptions.makeBasic() 251 : ActivityOptions.makeRemoteTransition(transition); 252 LaunchCookie cookie = new LaunchCookie(); 253 options.setLaunchCookie(cookie); 254 return options; 255 } 256 257 /** 258 * Specify an intent starter function that will be called to start an activity. The function 259 * accepts an optional {@link RemoteTransition} object which can be used to create an {@link 260 * ActivityOptions} for the activity launch. The function can also return a {@code false} 261 * result to cancel the session. 262 * 263 * <p>Note: it's encouraged to use {@link #withIntent(Intent)} or {@link 264 * #withPendingIntent(PendingIntent)} instead of this method. Use it only if the default 265 * activity launch code doesn't satisfy your requirement. 266 */ 267 public Builder withIntentStarter(Predicate<RemoteTransition> intentStarter) { 268 mIntentStarter = intentStarter; 269 return this; 270 } 271 272 /** Add an entry transition to the builder. */ 273 public Builder withEntryTransition(IRemoteTransition transition) { 274 mEntryTransitionSupplier = () -> transition; 275 return this; 276 } 277 278 /** Add an origin entry transition to the builder. */ 279 public Builder withEntryTransition( 280 UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) { 281 mEntryTransitionSupplier = 282 () -> 283 new OriginRemoteTransition( 284 mContext, 285 /* isEntry= */ true, 286 entryOrigin, 287 entryPlayer, 288 entryDuration, 289 mHandler); 290 return this; 291 } 292 293 /** Add an exit transition to the builder. */ 294 public Builder withExitTransition(IRemoteTransition transition) { 295 mExitTransitionSupplier = () -> transition; 296 return this; 297 } 298 299 /** Add an origin exit transition to the builder. */ 300 public Builder withExitTransition( 301 UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) { 302 mExitTransitionSupplier = 303 () -> 304 new OriginRemoteTransition( 305 mContext, 306 /* isEntry= */ false, 307 exitTarget, 308 exitPlayer, 309 exitDuration, 310 mHandler); 311 return this; 312 } 313 314 /** Supply a handler where transition callbacks will run. */ 315 public Builder withHandler(Handler handler) { 316 mHandler = handler; 317 return this; 318 } 319 320 /** Build an {@link OriginTransitionSession}. */ 321 public OriginTransitionSession build() { 322 if (mIntentStarter == null) { 323 throw new IllegalArgumentException("No intent, pending intent, or intent starter!"); 324 } 325 return new OriginTransitionSession( 326 mName, 327 mOriginTransitions, 328 mIntentStarter, 329 mEntryTransitionSupplier == null ? null : mEntryTransitionSupplier.get(), 330 mExitTransitionSupplier == null ? null : mExitTransitionSupplier.get()); 331 } 332 } 333 } 334