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.google.android.setupdesign.transition;
18 
19 import android.annotation.TargetApi;
20 import android.app.Activity;
21 import android.app.ActivityOptions;
22 import android.app.Fragment;
23 import android.content.ActivityNotFoundException;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.TypedArray;
27 import android.os.Build;
28 import android.os.Build.VERSION_CODES;
29 import android.os.Bundle;
30 import android.os.Parcelable;
31 import android.util.Log;
32 import android.view.Window;
33 import androidx.annotation.IntDef;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.VisibleForTesting;
36 import com.google.android.material.transition.platform.MaterialSharedAxis;
37 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
38 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
39 import com.google.android.setupcompat.util.BuildCompatUtils;
40 import com.google.android.setupdesign.R;
41 import com.google.android.setupdesign.util.ThemeHelper;
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 
45 /** Helper class for apply the transition to the pages which uses platform version. */
46 public class TransitionHelper {
47 
48   private static final String TAG = "TransitionHelper";
49 
50   /*
51    * In Setup Wizard, all Just-a-sec style screens (i.e. screens that has an indeterminate
52    * progress bar and automatically finishes itself), should do a cross-fade when entering or
53    * exiting the screen. For all other screens, the transition should be a slide-in-from-right
54    * or customized.
55    *
56    * We use two different ways to override the transitions. The first is calling
57    * overridePendingTransition in code, and the second is using windowAnimationStyle in the theme.
58    * They have the following priority when framework is figuring out what transition to use:
59    * 1. overridePendingTransition, entering activity (highest priority)
60    * 2. overridePendingTransition, exiting activity
61    * 3. windowAnimationStyle, entering activity
62    * 4. windowAnimationStyle, exiting activity
63    *
64    * This is why, in general, overridePendingTransition is used to specify the fade animation,
65    * while windowAnimationStyle is used to specify the slide transition. This way fade animation
66    * will take priority over the slide animation.
67    *
68    * Below are types of animation when switching activities. These are return values for
69    * {@link #getTransition()}. Each of these values represents 4 animations: (backward exit,
70    * backward enter, forward exit, forward enter).
71    *
72    * We override the transition in the following flow
73    * +--------------+-------------------------+--------------------------+
74    * |              | going forward           | going backward           |
75    * +--------------+-------------------------+--------------------------+
76    * | old activity | startActivity(OnResult) | onActivityResult         |
77    * +--------------+-------------------------+--------------------------+
78    * | new activity | onStart                 | finish (RESULT_CANCELED) |
79    * +--------------+-------------------------+--------------------------+
80    */
81 
82   /** The constant of transition type. */
83   @Retention(RetentionPolicy.SOURCE)
84   @IntDef({
85     TRANSITION_NONE,
86     TRANSITION_NO_OVERRIDE,
87     TRANSITION_FRAMEWORK_DEFAULT,
88     TRANSITION_SLIDE,
89     TRANSITION_FADE,
90     TRANSITION_FRAMEWORK_DEFAULT_PRE_P,
91     TRANSITION_CAPTIVE,
92   })
93   public @interface TransitionType {}
94 
95   /** No transition, as in overridePendingTransition(0, 0). */
96   public static final int TRANSITION_NONE = -1;
97 
98   /**
99    * No override. If this is specified as the transition, overridePendingTransition will not be
100    * called.
101    */
102   public static final int TRANSITION_NO_OVERRIDE = 0;
103 
104   /**
105    * Override the transition to the framework default. This values are read from {@link
106    * android.R.style#Animation_Activity}.
107    */
108   public static final int TRANSITION_FRAMEWORK_DEFAULT = 1;
109 
110   /** Override the transition to a slide-in-from-right (or from-left for RTL locales). */
111   public static final int TRANSITION_SLIDE = 2;
112 
113   /**
114    * Override the transition to fade in the new activity, while keeping the old activity. Setup
115    * wizard does not use cross fade to avoid the bright-dim-bright effect when transitioning between
116    * two screens that look similar.
117    */
118   public static final int TRANSITION_FADE = 3;
119 
120   /** Override the transition to the old framework default pre P. */
121   public static final int TRANSITION_FRAMEWORK_DEFAULT_PRE_P = 4;
122 
123   /**
124    * Override the transition to the specific transition and the transition type will depends on the
125    * partner resource.
126    */
127   // TODO: Add new partner resource to determine which transition type would be apply.
128   public static final int TRANSITION_CAPTIVE = 5;
129 
130   /**
131    * No override. If this is specified as the transition, the enter/exit transition of the window
132    * will not be set and keep original behavior.
133    */
134   public static final int CONFIG_TRANSITION_NONE = 0;
135 
136   /** Override the transition to the specific type that will depend on the partner resource. */
137   public static final int CONFIG_TRANSITION_SHARED_X_AXIS = 1;
138 
139   /**
140    * Passed in an intent as EXTRA_ACTIVITY_OPTIONS. This is the {@link ActivityOptions} of the
141    * transition used in {@link Activity#startActivity} or {@link Activity#startActivityForResult}.
142    */
143   public static final String EXTRA_ACTIVITY_OPTIONS = "sud:activity_options";
144 
145   /** A flag to avoid the {@link Activity#finish} been called more than once. */
146   @VisibleForTesting static boolean isFinishCalled = false;
147 
148   /** A flag to avoid the {@link Activity#startActivity} called more than once. */
149   @VisibleForTesting static boolean isStartActivity = false;
150 
151   /** A flag to avoid the {@link Activity#startActivityForResult} called more than once. */
152   @VisibleForTesting static boolean isStartActivityForResult = false;
153 
TransitionHelper()154   private TransitionHelper() {}
155 
156   /**
157    * Apply the transition for going forward which is decided by partner resource {@link
158    * PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code setupwizard.transition_type}.
159    * The default transition that will be applied is {@link #TRANSITION_SLIDE}. The timing to apply
160    * the transition is going forward from the previous activity to this, or going forward from this
161    * activity to the next.
162    *
163    * <p>For example, in the flow below, the forward transitions will be applied to all arrows
164    * pointing to the right. Previous screen --> This screen --> Next screen
165    */
166   @TargetApi(VERSION_CODES.LOLLIPOP)
applyForwardTransition(Activity activity)167   public static void applyForwardTransition(Activity activity) {
168     applyForwardTransition(activity, TRANSITION_CAPTIVE);
169   }
170 
171   /**
172    * Apply the transition for going forward which is decided by partner resource {@link
173    * PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code setupwizard.transition_type}.
174    * The default transition that will be applied is {@link #CONFIG_TRANSITION_NONE}. The timing to
175    * apply the transition is going forward from the previous {@link Fragment} to this, or going
176    * forward from this {@link Fragment} to the next.
177    */
178   @TargetApi(VERSION_CODES.M)
applyForwardTransition(Fragment fragment)179   public static void applyForwardTransition(Fragment fragment) {
180     if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
181       if (getConfigTransitionType(fragment.getContext()) == CONFIG_TRANSITION_SHARED_X_AXIS) {
182         MaterialSharedAxis exitTransition =
183             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
184         fragment.setExitTransition(exitTransition);
185 
186         MaterialSharedAxis enterTransition =
187             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
188         fragment.setEnterTransition(enterTransition);
189       } else {
190         Log.w(TAG, "Not apply the forward transition for platform fragment.");
191       }
192     } else {
193       Log.w(
194           TAG,
195           "Not apply the forward transition for platform fragment. The API is supported from"
196               + " Android Sdk "
197               + VERSION_CODES.M);
198     }
199   }
200 
201   /**
202    * Apply the transition for going forward. This is applied when going forward from the previous
203    * activity to this, or going forward from this activity to the next.
204    *
205    * <p>For example, in the flow below, the forward transitions will be applied to all arrows
206    * pointing to the right. Previous screen --> This screen --> Next screen
207    */
208   @TargetApi(VERSION_CODES.LOLLIPOP)
applyForwardTransition(Activity activity, @TransitionType int transitionId)209   public static void applyForwardTransition(Activity activity, @TransitionType int transitionId) {
210     if (transitionId == TRANSITION_SLIDE) {
211       activity.overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
212     } else if (transitionId == TRANSITION_FADE) {
213       activity.overridePendingTransition(android.R.anim.fade_in, R.anim.sud_stay);
214     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT) {
215       TypedArray typedArray =
216           activity.obtainStyledAttributes(
217               android.R.style.Animation_Activity,
218               new int[] {
219                 android.R.attr.activityOpenEnterAnimation, android.R.attr.activityOpenExitAnimation
220               });
221       activity.overridePendingTransition(
222           typedArray.getResourceId(/* index= */ 0, /* defValue= */ 0),
223           typedArray.getResourceId(/* index= */ 1, /* defValue= */ 0));
224       typedArray.recycle();
225     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT_PRE_P) {
226       activity.overridePendingTransition(
227           R.anim.sud_pre_p_activity_open_enter, R.anim.sud_pre_p_activity_open_exit);
228     } else if (transitionId == TRANSITION_NONE) {
229       // For TRANSITION_NONE, turn off the transition
230       activity.overridePendingTransition(/* enterAnim= */ 0, /* exitAnim= */ 0);
231     } else if (transitionId == TRANSITION_CAPTIVE) {
232       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
233         // 1. Do not change the transition behavior by default
234         // 2. If the flag present, apply the transition from transition type
235         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
236           Window window = activity.getWindow();
237           if (window != null) {
238             MaterialSharedAxis exitTransition =
239                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
240             window.setExitTransition(exitTransition);
241 
242             window.setAllowEnterTransitionOverlap(true);
243 
244             MaterialSharedAxis enterTransition =
245                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
246             window.setEnterTransition(enterTransition);
247           } else {
248             Log.w(TAG, "applyForwardTransition: Invalid window=" + window);
249           }
250         }
251       } else {
252         Log.w(TAG, "This API is supported from Android Sdk " + VERSION_CODES.LOLLIPOP);
253       }
254     }
255     // For TRANSITION_NO_OVERRIDE or other values, do not override the transition
256   }
257 
258   /**
259    * Apply the transition for going backward which is decided by partner resource {@link
260    * PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code setupwizard.transition_type}.
261    * The default transition that will be applied is {@link #TRANSITION_SLIDE}. The timing to apply
262    * the transition is going backward from the next activity to this, or going backward from this
263    * activity to the previous.
264    *
265    * <p>For example, in the flow below, the backward transitions will be applied to all arrows
266    * pointing to the left. Previous screen <-- This screen <-- Next screen
267    */
268   @TargetApi(VERSION_CODES.LOLLIPOP)
applyBackwardTransition(Activity activity)269   public static void applyBackwardTransition(Activity activity) {
270     applyBackwardTransition(activity, TRANSITION_CAPTIVE);
271   }
272 
273   /**
274    * Apply the transition for going backward which is decided by partner resource {@link
275    * PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code setupwizard.transition_type}.
276    * The default transition that will be applied is {@link #CONFIG_TRANSITION_NONE}. The timing to
277    * apply the transition is going backward from the next {@link Fragment} to this, or going
278    * backward from this {@link Fragment} to the previous.
279    */
280   @TargetApi(VERSION_CODES.M)
applyBackwardTransition(Fragment fragment)281   public static void applyBackwardTransition(Fragment fragment) {
282     if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
283       if (getConfigTransitionType(fragment.getContext()) == CONFIG_TRANSITION_SHARED_X_AXIS) {
284         MaterialSharedAxis returnTransition =
285             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
286         fragment.setReturnTransition(returnTransition);
287 
288         MaterialSharedAxis reenterTransition =
289             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
290         fragment.setReenterTransition(reenterTransition);
291       } else {
292         Log.w(TAG, "Not apply the backward transition for platform fragment.");
293       }
294     } else {
295       Log.w(
296           TAG,
297           "Not apply the backward transition for platform fragment. The API is supported from"
298               + " Android Sdk "
299               + VERSION_CODES.M);
300     }
301   }
302 
303   /**
304    * Apply the transition for going backward. This is applied when going backward from the next
305    * activity to this, or going backward from this activity to the previous.
306    *
307    * <p>For example, in the flow below, the backward transitions will be applied to all arrows
308    * pointing to the left. Previous screen <-- This screen <-- Next screen
309    */
310   @TargetApi(VERSION_CODES.LOLLIPOP)
applyBackwardTransition(Activity activity, @TransitionType int transitionId)311   public static void applyBackwardTransition(Activity activity, @TransitionType int transitionId) {
312     if (transitionId == TRANSITION_SLIDE) {
313       activity.overridePendingTransition(R.anim.sud_slide_back_in, R.anim.sud_slide_back_out);
314     } else if (transitionId == TRANSITION_FADE) {
315       activity.overridePendingTransition(R.anim.sud_stay, android.R.anim.fade_out);
316     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT) {
317       TypedArray typedArray =
318           activity.obtainStyledAttributes(
319               android.R.style.Animation_Activity,
320               new int[] {
321                 android.R.attr.activityCloseEnterAnimation,
322                 android.R.attr.activityCloseExitAnimation
323               });
324       activity.overridePendingTransition(
325           typedArray.getResourceId(/* index= */ 0, /* defValue= */ 0),
326           typedArray.getResourceId(/* index= */ 1, /* defValue= */ 0));
327       typedArray.recycle();
328     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT_PRE_P) {
329       activity.overridePendingTransition(
330           R.anim.sud_pre_p_activity_close_enter, R.anim.sud_pre_p_activity_close_exit);
331     } else if (transitionId == TRANSITION_NONE) {
332       // For TRANSITION_NONE, turn off the transition
333       activity.overridePendingTransition(/* enterAnim= */ 0, /* exitAnim= */ 0);
334     } else if (transitionId == TRANSITION_CAPTIVE) {
335       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
336         // 1. Do not change the transition behavior by default
337         // 2. If the flag present, apply the transition from transition type
338         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
339           Window window = activity.getWindow();
340           if (window != null) {
341             MaterialSharedAxis reenterTransition =
342                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
343             window.setReenterTransition(reenterTransition);
344 
345             MaterialSharedAxis returnTransition =
346                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
347             window.setReturnTransition(returnTransition);
348           } else {
349             Log.w(TAG, "applyBackwardTransition: Invalid window=" + window);
350           }
351         }
352       } else {
353         Log.w(TAG, "This API is supported from Android Sdk " + VERSION_CODES.LOLLIPOP);
354       }
355     }
356     // For TRANSITION_NO_OVERRIDE or other values, do not override the transition
357   }
358 
359   /**
360    * A wrapper method, create an {@link android.app.ActivityOptions} to transition between
361    * activities as the {@link ActivityOptions} parameter of {@link Activity#startActivity}.
362    *
363    * @throws IllegalArgumentException is thrown when {@code activity} or {@code intent} is null.
364    * @throws android.content.ActivityNotFoundException if there was no {@link Activity} found to run
365    *     the given Intent.
366    */
startActivityWithTransition(Activity activity, Intent intent)367   public static void startActivityWithTransition(Activity activity, Intent intent) {
368     startActivityWithTransition(activity, intent, /* overrideActivityOptions= */ null);
369   }
370 
371   /**
372    * A wrapper method, create an {@link android.app.ActivityOptions} to transition between
373    * activities as the {@link ActivityOptions} parameter of {@link Activity#startActivity}.
374    *
375    * @throws IllegalArgumentException is thrown when {@code activity} or {@code intent} is null.
376    * @throws android.content.ActivityNotFoundException if there was no {@link Activity} found to run
377    *     the given Intent.
378    */
startActivityWithTransition( Activity activity, Intent intent, Bundle overrideActivityOptions)379   public static void startActivityWithTransition(
380       Activity activity, Intent intent, Bundle overrideActivityOptions) {
381     if (activity == null) {
382       throw new IllegalArgumentException("Invalid activity=" + activity);
383     }
384 
385     if (intent == null) {
386       throw new IllegalArgumentException("Invalid intent=" + intent);
387     }
388 
389     if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK) {
390       Log.e(
391           TAG,
392           "The transition won't take effect since the WindowManager does not allow override new"
393               + " task transitions");
394     }
395 
396     if (!isStartActivity) {
397       isStartActivity = true;
398       if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
399         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
400           if (activity.getWindow() != null
401               && !activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
402             Log.w(
403                 TAG,
404                 "The transition won't take effect due to NO FEATURE_ACTIVITY_TRANSITIONS feature");
405           }
406 
407           Bundle bundleActivityOptions;
408           if (overrideActivityOptions != null) {
409             bundleActivityOptions = overrideActivityOptions;
410           } else {
411             bundleActivityOptions = makeActivityOptions(activity, intent);
412           }
413           intent.putExtra(EXTRA_ACTIVITY_OPTIONS, (Parcelable) bundleActivityOptions);
414           activity.startActivity(intent, bundleActivityOptions);
415         } else {
416           Log.w(
417               TAG,
418               "Fallback to using startActivity due to the"
419                   + " ActivityOptions#makeSceneTransitionAnimation is supported from Android Sdk "
420                   + VERSION_CODES.LOLLIPOP);
421           startActivityWithTransitionInternal(activity, intent, overrideActivityOptions);
422         }
423       } else {
424         startActivityWithTransitionInternal(activity, intent, overrideActivityOptions);
425       }
426     }
427     isStartActivity = false;
428   }
429 
startActivityWithTransitionInternal( Activity activity, Intent intent, Bundle overrideActivityOptions)430   private static void startActivityWithTransitionInternal(
431       Activity activity, Intent intent, Bundle overrideActivityOptions) {
432     try {
433       if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
434         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS
435             && overrideActivityOptions != null) {
436           activity.startActivity(intent, overrideActivityOptions);
437         } else {
438           activity.startActivity(intent);
439         }
440       } else {
441         Log.w(
442             TAG,
443             "Fallback to using startActivity(Intent) due to the startActivity(Intent, Bundle) is"
444                 + " supported from Android Sdk "
445                 + VERSION_CODES.JELLY_BEAN);
446         activity.startActivity(intent);
447       }
448     } catch (ActivityNotFoundException e) {
449       Log.w(TAG, "Activity not found when startActivity with transition.");
450       isStartActivity = false;
451       throw e;
452     }
453   }
454 
startActivityForResultWithTransition( Activity activity, Intent intent, int requestCode)455   public static void startActivityForResultWithTransition(
456       Activity activity, Intent intent, int requestCode) {
457     startActivityForResultWithTransition(
458         activity, intent, requestCode, /* overrideActivityOptions= */ null);
459   }
460 
461   /**
462    * A wrapper method, create an {@link android.app.ActivityOptions} to transition between
463    * activities as the {@code activityOptions} parameter of {@link Activity#startActivityForResult}.
464    *
465    * @throws IllegalArgumentException is thrown when {@code activity} or {@code intent} is null.
466    * @throws android.content.ActivityNotFoundException if there was no {@link Activity} found to run
467    *     the given Intent.
468    */
startActivityForResultWithTransition( Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions)469   public static void startActivityForResultWithTransition(
470       Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions) {
471     if (activity == null) {
472       throw new IllegalArgumentException("Invalid activity=" + activity);
473     }
474 
475     if (intent == null) {
476       throw new IllegalArgumentException("Invalid intent=" + intent);
477     }
478 
479     if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK) {
480       Log.e(
481           TAG,
482           "The transition won't take effect since the WindowManager does not allow override new"
483               + " task transitions");
484     }
485 
486     if (!isStartActivityForResult) {
487       isStartActivityForResult = true;
488       if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
489         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
490           if (activity.getWindow() != null
491               && !activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
492             Log.w(
493                 TAG,
494                 "The transition won't take effect due to NO FEATURE_ACTIVITY_TRANSITIONS feature");
495           }
496 
497           Bundle bundleActivityOptions;
498           if (overrideActivityOptions != null) {
499             bundleActivityOptions = overrideActivityOptions;
500           } else {
501             bundleActivityOptions = makeActivityOptions(activity, intent);
502           }
503           intent.putExtra(EXTRA_ACTIVITY_OPTIONS, (Parcelable) bundleActivityOptions);
504           activity.startActivityForResult(intent, requestCode, bundleActivityOptions);
505         } else {
506           Log.w(
507               TAG,
508               "Fallback to using startActivityForResult API due to the"
509                   + " ActivityOptions#makeSceneTransitionAnimation is supported from Android Sdk "
510                   + VERSION_CODES.LOLLIPOP);
511           startActivityForResultWithTransitionInternal(
512               activity, intent, requestCode, overrideActivityOptions);
513         }
514       } else {
515         startActivityForResultWithTransitionInternal(
516             activity, intent, requestCode, overrideActivityOptions);
517       }
518       isStartActivityForResult = false;
519     }
520   }
521 
startActivityForResultWithTransitionInternal( Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions)522   private static void startActivityForResultWithTransitionInternal(
523       Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions) {
524     try {
525       if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
526         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS
527             && overrideActivityOptions != null) {
528           activity.startActivityForResult(intent, requestCode, overrideActivityOptions);
529         } else {
530           activity.startActivityForResult(intent, requestCode);
531         }
532       } else {
533         Log.w(
534             TAG,
535             "Fallback to using startActivityForResult(Intent) due to the"
536                 + " startActivityForResult(Intent,int) is supported from Android Sdk "
537                 + VERSION_CODES.JELLY_BEAN);
538         activity.startActivityForResult(intent, requestCode);
539       }
540     } catch (ActivityNotFoundException e) {
541       Log.w(TAG, "Activity not found when startActivityForResult with transition.");
542       isStartActivityForResult = false;
543       throw e;
544     }
545   }
546 
547   /**
548    * A wrapper method, calling {@link Activity#finishAfterTransition()} to trigger exit transition
549    * when running in Android S and the transition type {link #CONFIG_TRANSITION_SHARED_X_AXIS}.
550    *
551    * @throws IllegalArgumentException is thrown when {@code activity} is null.
552    */
finishActivity(Activity activity)553   public static void finishActivity(Activity activity) {
554     if (activity == null) {
555       throw new IllegalArgumentException("Invalid activity=" + activity);
556     }
557 
558     // Avoids finish been called more than once.
559     if (!isFinishCalled) {
560       isFinishCalled = true;
561       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
562           && getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
563         activity.finishAfterTransition();
564       } else {
565         Log.w(
566             TAG,
567             "Fallback to using Activity#finish() due to the"
568                 + " Activity#finishAfterTransition() is supported from Android Sdk "
569                 + VERSION_CODES.LOLLIPOP);
570         activity.finish();
571       }
572     }
573       isFinishCalled = false;
574   }
575 
576   /**
577    * Returns the transition type from the {@link PartnerConfig#CONFIG_TRANSITION_TYPE} partner
578    * resource on Android S, otherwise returns {@link #CONFIG_TRANSITION_NONE}.
579    */
getConfigTransitionType(Context context)580   public static int getConfigTransitionType(Context context) {
581     return BuildCompatUtils.isAtLeastS() && ThemeHelper.shouldApplyExtendedPartnerConfig(context)
582         ? PartnerConfigHelper.get(context)
583             .getInteger(context, PartnerConfig.CONFIG_TRANSITION_TYPE, CONFIG_TRANSITION_NONE)
584         : CONFIG_TRANSITION_NONE;
585   }
586 
587   /**
588    * A wrapper method, create a {@link Bundle} from {@link ActivityOptions} to transition between
589    * Activities using cross-Activity scene animations. This {@link Bundle} that can be used with
590    * {@link Context#startActivity(Intent, Bundle)} and related methods.
591    *
592    * <p>Example usage:
593    *
594    * <pre>{@code
595    * Intent intent = new Intent("com.example.NEXT_ACTIVITY");
596    * activity.startActivity(intent, TransitionHelper.makeActivityOptions(activity, intent, null);
597    * }</pre>
598    *
599    * <p>Unexpected usage:
600    *
601    * <pre>{@code
602    * Intent intent = new Intent("com.example.NEXT_ACTIVITY");
603    * Intent intent2 = new Intent("com.example.NEXT_ACTIVITY");
604    * activity.startActivity(intent, TransitionHelper.makeActivityOptions(activity, intent2, null);
605    * }</pre>
606    */
607   @Nullable
makeActivityOptions(Activity activity, Intent intent)608   public static Bundle makeActivityOptions(Activity activity, Intent intent) {
609     return makeActivityOptions(activity, intent, false);
610   }
611 
612   /**
613    * A wrapper method, create a {@link Bundle} from {@link ActivityOptions} to transition between
614    * Activities using cross-Activity scene animations. This {@link Bundle} that can be used with
615    * {@link Context#startActivity(Intent, Bundle)} and related methods. When this {@code activity}
616    * is a no UI activity(the activity doesn't inflate any layouts), you will need to pass the bundle
617    * coming from previous UI activity as the {@link ActivityOptions}, otherwise, the transition
618    * won't be take effect. The {@code overrideActivityOptionsFromIntent} is supporting this purpose
619    * to return the {@link ActivityOptions} instead of creating from this no UI activity while the
620    * transition is apply {@link #CONFIG_TRANSITION_SHARED_X_AXIS} config. Moreover, the
621    * startActivity*WithTransition relative methods and {@link #makeActivityOptions} will put {@link
622    * ActivityOptions} to the {@code intent} by default, you can get the {@link ActivityOptions}
623    * which makes from previous activity by accessing {@link #EXTRA_ACTIVITY_OPTIONS} extra from
624    * {@link Activity#getIntent()}.
625    *
626    * <p>Example usage of a no UI activity:
627    *
628    * <pre>{@code
629    * Intent intent = new Intent("com.example.NEXT_ACTIVITY");
630    * activity.startActivity(intent, TransitionHelper.makeActivityOptions(activity, intent, true);
631    * }</pre>
632    */
633   @Nullable
makeActivityOptions( Activity activity, Intent intent, boolean overrideActivityOptionsFromIntent)634   public static Bundle makeActivityOptions(
635       Activity activity, Intent intent, boolean overrideActivityOptionsFromIntent) {
636     Bundle resultBundle = null;
637     if (activity == null || intent == null) {
638       return resultBundle;
639     }
640 
641     if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK) {
642       Log.e(
643           TAG,
644           "The transition won't take effect since the WindowManager does not allow override new"
645               + " task transitions");
646     }
647 
648     if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
649       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
650         if (activity.getWindow() != null
651             && !activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
652           Log.w(
653               TAG,
654               "The transition won't take effect due to NO FEATURE_ACTIVITY_TRANSITIONS feature");
655         }
656 
657         if (overrideActivityOptionsFromIntent && activity.getIntent() != null) {
658           resultBundle = activity.getIntent().getBundleExtra(EXTRA_ACTIVITY_OPTIONS);
659         } else {
660           resultBundle = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle();
661         }
662         intent.putExtra(EXTRA_ACTIVITY_OPTIONS, (Parcelable) resultBundle);
663         return resultBundle;
664       }
665     }
666 
667     return resultBundle;
668   }
669 }
670