• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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