• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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   /** Override the transition to a fade-through-from-right (or from-left for RTL locales). */
131   public static final int TRANSITION_FADE_THROUGH = 6;
132 
133   /**
134    * No override. If this is specified as the transition, the enter/exit transition of the window
135    * will not be set and keep original behavior.
136    */
137   public static final int CONFIG_TRANSITION_NONE = 0;
138 
139   /** Override the transition to the specific type that will depend on the partner resource. */
140   public static final int CONFIG_TRANSITION_SHARED_X_AXIS = 1;
141 
142   /**
143    * Passed in an intent as EXTRA_ACTIVITY_OPTIONS. This is the {@link ActivityOptions} of the
144    * transition used in {@link Activity#startActivity} or {@link Activity#startActivityForResult}.
145    */
146   public static final String EXTRA_ACTIVITY_OPTIONS = "sud:activity_options";
147 
148   /** A flag to avoid the {@link Activity#finish} been called more than once. */
149   @VisibleForTesting static boolean isFinishCalled = false;
150 
151   /** A flag to avoid the {@link Activity#startActivity} called more than once. */
152   @VisibleForTesting static boolean isStartActivity = false;
153 
154   /** A flag to avoid the {@link Activity#startActivityForResult} called more than once. */
155   @VisibleForTesting static boolean isStartActivityForResult = false;
156 
TransitionHelper()157   private TransitionHelper() {}
158 
159   /**
160    * Apply the transition for going forward which is decided by {@code Animation.SudWindowAnimation}
161    * theme if the API level is equal or higher than {@link android.os.Build.VERSION_CODES#U}.
162    *
163    * <p>Otherwise, apply the transition for going forward which is decided by partner resource
164    * {@link PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code
165    * setupwizard.transition_type} if the API level is equal or lower than {@link
166    * android.os.Build.VERSION_CODES#T}. The default transition that will be applied is {@link
167    * #TRANSITION_SLIDE}.
168    *
169    * <p>The timing to apply the transition is going forward from the previous activity to this, or
170    * going forward from this activity to the next.
171    *
172    * <p>For example, in the flow below, the forward transitions will be applied to all arrows
173    * pointing to the right. Previous screen --> This screen --> Next screen
174    */
175   @TargetApi(VERSION_CODES.LOLLIPOP)
applyForwardTransition(Activity activity)176   public static void applyForwardTransition(Activity activity) {
177     applyForwardTransition(activity, TRANSITION_CAPTIVE);
178   }
179 
180   /**
181    * Apply the transition for going forward which is decided by partner resource {@link
182    * PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code setupwizard.transition_type}.
183    * The default transition that will be applied is {@link #CONFIG_TRANSITION_NONE}. The timing to
184    * apply the transition is going forward from the previous {@link Fragment} to this, or going
185    * forward from this {@link Fragment} to the next.
186    */
187   @TargetApi(VERSION_CODES.M)
applyForwardTransition(Fragment fragment)188   public static void applyForwardTransition(Fragment fragment) {
189     if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
190       if (getConfigTransitionType(fragment.getContext()) == CONFIG_TRANSITION_SHARED_X_AXIS) {
191         MaterialSharedAxis exitTransition =
192             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
193         fragment.setExitTransition(exitTransition);
194 
195         MaterialSharedAxis enterTransition =
196             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
197         fragment.setEnterTransition(enterTransition);
198       } else {
199         Log.w(TAG, "Not apply the forward transition for platform fragment.");
200       }
201     } else {
202       Log.w(
203           TAG,
204           "Not apply the forward transition for platform fragment. The API is supported from"
205               + " Android Sdk "
206               + VERSION_CODES.M);
207     }
208   }
209 
210   /**
211    * Apply the transition for going forward which is decided by {@code Animation.SudWindowAnimation}
212    * theme if the API level is equal or higher than {@link android.os.Build.VERSION_CODES#U}.
213    *
214    * <p>Otherwise, apply the transition for going forward which is decided by the argument {@code
215    * transitionId} if the API level is equal or lower than {@link android.os.Build.VERSION_CODES#T}.
216    *
217    * <p>The timing to apply the transition is going forward from the previous activity to this, or
218    * going forward from this activity to the next.
219    */
220   @TargetApi(VERSION_CODES.LOLLIPOP)
applyForwardTransition(Activity activity, @TransitionType int transitionId)221   public static void applyForwardTransition(Activity activity, @TransitionType int transitionId) {
222     applyForwardTransition(activity, transitionId, /* useClientTransitionSettings= */ false);
223   }
224 
225   /**
226    * Apply the transition for going forward which is decided by {@code Animation.SudWindowAnimation}
227    * theme if the API level is equal or higher than {@link android.os.Build.VERSION_CODES#U}, and
228    * argument {@code useClientTransitionSettings} is false, and System property {@code
229    * suw_apply_glif_theme_controlled_transition} is true, and {@code TRANSITION_FADE_THOUGH}
230    * transition is not specified.
231    *
232    * <p>Otherwise, apply the transition for going forward which is decided by the argument {@code
233    * transitionId}, {@code shared_x_axis_activity} transition is used only when {@code
234    * TRANSITION_FADE_TROUGH} transition is specified, and System property {@code *
235    * suw_apply_glif_theme_controlled_transition} is true, and the API level is equal or more than
236    * {@link android.os.Build.VERSION_CODES#U}, other {@code transitionId} can be specified if the
237    * API level is equal or lower than {@link android.os.Build.VERSION_CODES#T}, or argument {@code
238    * useClientTransitionSettings} is true, or System property {@code
239    * suw_apply_glif_theme_controlled_transition} is false. The default transition that will be
240    * applied is {@link #TRANSITION_SLIDE}.
241    *
242    * <p>The timing to apply the transition is going forward from the previous activity to this, or
243    * going forward from this activity to the next.
244    *
245    * <p>For example, in the flow below, the forward transitions will be applied to all arrows
246    * pointing to the right. Previous screen --> This screen --> Next screen
247    */
248   @TargetApi(VERSION_CODES.LOLLIPOP)
applyForwardTransition( Activity activity, @TransitionType int transitionId, boolean useClientTransitionSettings)249   public static void applyForwardTransition(
250       Activity activity, @TransitionType int transitionId, boolean useClientTransitionSettings) {
251     if (BuildCompatUtils.isAtLeastU()
252         && !useClientTransitionSettings
253         && PartnerConfigHelper.isGlifThemeControlledTransitionApplied(activity)
254         && transitionId != TRANSITION_FADE_THROUGH) {
255       // Do nothing
256     } else if (BuildCompatUtils.isAtLeastU() && transitionId == TRANSITION_FADE_THROUGH) {
257       int openEnterTransition = R.anim.shared_x_axis_activity_open_enter;
258       if (PartnerConfigHelper.isGlifThemeControlledTransitionApplied(activity)) {
259         if (ThemeHelper.shouldApplyDynamicColor(activity)) {
260           openEnterTransition = R.anim.shared_x_axis_activity_open_enter_dynamic_color;
261         }
262         activity.overridePendingTransition(
263             openEnterTransition, R.anim.shared_x_axis_activity_open_exit);
264       } else {
265         activity.overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
266       }
267     } else if (transitionId == TRANSITION_SLIDE) {
268       activity.overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
269     } else if (transitionId == TRANSITION_FADE) {
270       activity.overridePendingTransition(android.R.anim.fade_in, R.anim.sud_stay);
271     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT) {
272       TypedArray typedArray =
273           activity.obtainStyledAttributes(
274               android.R.style.Animation_Activity,
275               new int[] {
276                 android.R.attr.activityOpenEnterAnimation, android.R.attr.activityOpenExitAnimation
277               });
278       activity.overridePendingTransition(
279           typedArray.getResourceId(/* index= */ 0, /* defValue= */ 0),
280           typedArray.getResourceId(/* index= */ 1, /* defValue= */ 0));
281       typedArray.recycle();
282     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT_PRE_P) {
283       activity.overridePendingTransition(
284           R.anim.sud_pre_p_activity_open_enter, R.anim.sud_pre_p_activity_open_exit);
285     } else if (transitionId == TRANSITION_NONE) {
286       // For TRANSITION_NONE, turn off the transition
287       activity.overridePendingTransition(/* enterAnim= */ 0, /* exitAnim= */ 0);
288     } else if (transitionId == TRANSITION_CAPTIVE) {
289       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
290         // 1. Do not change the transition behavior by default
291         // 2. If the flag present, apply the transition from transition type
292         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
293           Window window = activity.getWindow();
294           if (window != null) {
295             MaterialSharedAxis exitTransition =
296                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
297             window.setExitTransition(exitTransition);
298 
299             window.setAllowEnterTransitionOverlap(true);
300 
301             MaterialSharedAxis enterTransition =
302                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true);
303             window.setEnterTransition(enterTransition);
304           } else {
305             Log.w(TAG, "applyForwardTransition: Invalid window=" + window);
306           }
307         }
308       } else {
309         Log.w(TAG, "This API is supported from Android Sdk " + VERSION_CODES.LOLLIPOP);
310       }
311     }
312     // For TRANSITION_NO_OVERRIDE or other values, do not override the transition
313   }
314 
315   /**
316    * Apply the transition for going backward which is decided by {@code
317    * Animation.SudWindowAnimation} theme if the API level is equal or higher than {@link
318    * android.os.Build.VERSION_CODES#U}.
319    *
320    * <p>Otherwise, apply the transition for going backward which is decided by partner resource
321    * {@link PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code
322    * setupwizard.transition_type} if the API level is equal or lower than {@link
323    * android.os.Build.VERSION_CODES#T}. The default transition that will be applied is {@link
324    * #TRANSITION_SLIDE}.
325    *
326    * <p>The timing to apply the transition is going backward from the next activity to this, or
327    * going backward from this activity to the previous.
328    *
329    * <p>For example, in the flow below, the backward transitions will be applied to all arrows
330    * pointing to the left. Previous screen <-- This screen <-- Next screen.
331    */
332   @TargetApi(VERSION_CODES.LOLLIPOP)
applyBackwardTransition(Activity activity)333   public static void applyBackwardTransition(Activity activity) {
334     applyBackwardTransition(activity, TRANSITION_CAPTIVE);
335   }
336 
337   /**
338    * Apply the transition for going backward which is decided by partner resource {@link
339    * PartnerConfig#CONFIG_TRANSITION_TYPE} and system property {@code setupwizard.transition_type}.
340    * The default transition that will be applied is {@link #CONFIG_TRANSITION_NONE}. The timing to
341    * apply the transition is going backward from the next {@link Fragment} to this, or going
342    * backward from this {@link Fragment} to the previous.
343    */
344   @TargetApi(VERSION_CODES.M)
applyBackwardTransition(Fragment fragment)345   public static void applyBackwardTransition(Fragment fragment) {
346     if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
347       if (getConfigTransitionType(fragment.getContext()) == CONFIG_TRANSITION_SHARED_X_AXIS) {
348         MaterialSharedAxis returnTransition =
349             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
350         fragment.setReturnTransition(returnTransition);
351 
352         MaterialSharedAxis reenterTransition =
353             new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
354         fragment.setReenterTransition(reenterTransition);
355       } else {
356         Log.w(TAG, "Not apply the backward transition for platform fragment.");
357       }
358     } else {
359       Log.w(
360           TAG,
361           "Not apply the backward transition for platform fragment. The API is supported from"
362               + " Android Sdk "
363               + VERSION_CODES.M);
364     }
365   }
366 
367   /**
368    * Apply the transition for going backward which is decided by {@code
369    * Animation.SudWindowAnimation} theme if the API level is equal or higher than {@link
370    * android.os.Build.VERSION_CODES#U}.
371    *
372    * <p>Otherwise, apply the transition for going backward which is decided by the argument {@code
373    * transitionId} if the API level is equal or lower than {@link android.os.Build.VERSION_CODES#T}.
374    *
375    * <p>The timing to apply the transition is going backward from the next activity to this, or
376    * going backward from this activity to the previous.
377    */
378   @TargetApi(VERSION_CODES.LOLLIPOP)
applyBackwardTransition(Activity activity, @TransitionType int transitionId)379   public static void applyBackwardTransition(Activity activity, @TransitionType int transitionId) {
380     applyBackwardTransition(activity, transitionId, /* useClientTransitionSettings= */ false);
381   }
382 
383   /**
384    * Apply the transition for going backward which is decided by {@code
385    * Animation.SudWindowAnimation} theme if the API level is equal or higher than {@link
386    * android.os.Build.VERSION_CODES#U}, and argument {@code useClientTransitionSettings} is false,
387    * and System property {@code suw_apply_glif_theme_controlled_transition} is true, and {@code
388    * TRANSITION_FADE_THOUGH} transition is not specified.
389    *
390    * <p>Otherwise, apply the transition for going backward which is decided by the argument {@code
391    * transitionId}, {@code shared_x_axis_activity} transition is used only when {@code
392    * TRANSITION_FADE_TROUGH} transition is specified, and System property {@code *
393    * suw_apply_glif_theme_controlled_transition} is true, and the API level is equal or more than
394    * {@link android.os.Build.VERSION_CODES#U}, other {@code transitionId} can be specified if the
395    * API level is equal or lower than {@link android.os.Build.VERSION_CODES#T}, or argument {@code
396    * useClientTransitionSettings} is true, or System property {@code
397    * suw_apply_glif_theme_controlled_transition} is false. The default transition that will be
398    * applied is {@link #TRANSITION_SLIDE}.
399    *
400    * <p>The timing to apply the transition is going backward from the next activity to this, or
401    * going backward from this activity to the previous.
402    *
403    * <p>For example, in the flow below, the backward transitions will be applied to all arrows
404    * pointing to the left. Previous screen <-- This screen <-- Next screen
405    */
406   @TargetApi(VERSION_CODES.LOLLIPOP)
applyBackwardTransition( Activity activity, @TransitionType int transitionId, boolean useClientTransitionSettings)407   public static void applyBackwardTransition(
408       Activity activity, @TransitionType int transitionId, boolean useClientTransitionSettings) {
409     if (BuildCompatUtils.isAtLeastU()
410         && !useClientTransitionSettings
411         && PartnerConfigHelper.isGlifThemeControlledTransitionApplied(activity)
412         && transitionId != TRANSITION_FADE_THROUGH) {
413       // Do nothing
414     } else if (BuildCompatUtils.isAtLeastU() && transitionId == TRANSITION_FADE_THROUGH) {
415       if (PartnerConfigHelper.isGlifThemeControlledTransitionApplied(activity)) {
416         int closeEnterTransition = R.anim.shared_x_axis_activity_close_enter;
417         if (ThemeHelper.shouldApplyDynamicColor(activity)) {
418           closeEnterTransition = R.anim.shared_x_axis_activity_close_enter_dynamic_color;
419         }
420         activity.overridePendingTransition(
421             closeEnterTransition, R.anim.shared_x_axis_activity_close_exit);
422       } else {
423         activity.overridePendingTransition(R.anim.sud_slide_back_in, R.anim.sud_slide_back_out);
424       }
425     } else if (transitionId == TRANSITION_SLIDE) {
426       activity.overridePendingTransition(R.anim.sud_slide_back_in, R.anim.sud_slide_back_out);
427     } else if (transitionId == TRANSITION_FADE) {
428       activity.overridePendingTransition(R.anim.sud_stay, android.R.anim.fade_out);
429     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT) {
430       TypedArray typedArray =
431           activity.obtainStyledAttributes(
432               android.R.style.Animation_Activity,
433               new int[] {
434                 android.R.attr.activityCloseEnterAnimation,
435                 android.R.attr.activityCloseExitAnimation
436               });
437       activity.overridePendingTransition(
438           typedArray.getResourceId(/* index= */ 0, /* defValue= */ 0),
439           typedArray.getResourceId(/* index= */ 1, /* defValue= */ 0));
440       typedArray.recycle();
441     } else if (transitionId == TRANSITION_FRAMEWORK_DEFAULT_PRE_P) {
442       activity.overridePendingTransition(
443           R.anim.sud_pre_p_activity_close_enter, R.anim.sud_pre_p_activity_close_exit);
444     } else if (transitionId == TRANSITION_NONE) {
445       // For TRANSITION_NONE, turn off the transition
446       activity.overridePendingTransition(/* enterAnim= */ 0, /* exitAnim= */ 0);
447     } else if (transitionId == TRANSITION_CAPTIVE) {
448       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
449         // 1. Do not change the transition behavior by default
450         // 2. If the flag present, apply the transition from transition type
451         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
452           Window window = activity.getWindow();
453           if (window != null) {
454             MaterialSharedAxis reenterTransition =
455                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
456             window.setReenterTransition(reenterTransition);
457 
458             MaterialSharedAxis returnTransition =
459                 new MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false);
460             window.setReturnTransition(returnTransition);
461           } else {
462             Log.w(TAG, "applyBackwardTransition: Invalid window=" + window);
463           }
464         }
465       } else {
466         Log.w(TAG, "This API is supported from Android Sdk " + VERSION_CODES.LOLLIPOP);
467       }
468       // For TRANSITION_NO_OVERRIDE or other values, do not override the transition
469     }
470   }
471 
472   /**
473    * A wrapper method, create an {@link android.app.ActivityOptions} to transition between
474    * activities as the {@link ActivityOptions} parameter of {@link Activity#startActivity}.
475    *
476    * @throws IllegalArgumentException is thrown when {@code activity} or {@code intent} is null.
477    * @throws android.content.ActivityNotFoundException if there was no {@link Activity} found to run
478    *     the given Intent.
479    */
startActivityWithTransition(Activity activity, Intent intent)480   public static void startActivityWithTransition(Activity activity, Intent intent) {
481     startActivityWithTransition(activity, intent, /* overrideActivityOptions= */ null);
482   }
483 
484   /**
485    * A wrapper method, create an {@link android.app.ActivityOptions} to transition between
486    * activities as the {@link ActivityOptions} parameter of {@link Activity#startActivity}.
487    *
488    * @throws IllegalArgumentException is thrown when {@code activity} or {@code intent} is null.
489    * @throws android.content.ActivityNotFoundException if there was no {@link Activity} found to run
490    *     the given Intent.
491    */
startActivityWithTransition( Activity activity, Intent intent, Bundle overrideActivityOptions)492   public static void startActivityWithTransition(
493       Activity activity, Intent intent, Bundle overrideActivityOptions) {
494     if (activity == null) {
495       throw new IllegalArgumentException("Invalid activity=" + activity);
496     }
497 
498     if (intent == null) {
499       throw new IllegalArgumentException("Invalid intent=" + intent);
500     }
501 
502     if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK) {
503       Log.e(
504           TAG,
505           "The transition won't take effect since the WindowManager does not allow override new"
506               + " task transitions");
507     }
508 
509     if (!isStartActivity) {
510       isStartActivity = true;
511       if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
512         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
513           if (activity.getWindow() != null
514               && !activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
515             Log.w(
516                 TAG,
517                 "The transition won't take effect due to NO FEATURE_ACTIVITY_TRANSITIONS feature");
518           }
519 
520           Bundle bundleActivityOptions;
521           if (overrideActivityOptions != null) {
522             bundleActivityOptions = overrideActivityOptions;
523           } else {
524             bundleActivityOptions = makeActivityOptions(activity, intent);
525           }
526           intent.putExtra(EXTRA_ACTIVITY_OPTIONS, (Parcelable) bundleActivityOptions);
527           activity.startActivity(intent, bundleActivityOptions);
528         } else {
529           Log.w(
530               TAG,
531               "Fallback to using startActivity due to the"
532                   + " ActivityOptions#makeSceneTransitionAnimation is supported from Android Sdk "
533                   + VERSION_CODES.LOLLIPOP);
534           startActivityWithTransitionInternal(activity, intent, overrideActivityOptions);
535         }
536       } else {
537         startActivityWithTransitionInternal(activity, intent, overrideActivityOptions);
538       }
539     }
540     isStartActivity = false;
541   }
542 
startActivityWithTransitionInternal( Activity activity, Intent intent, Bundle overrideActivityOptions)543   private static void startActivityWithTransitionInternal(
544       Activity activity, Intent intent, Bundle overrideActivityOptions) {
545     try {
546       if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
547         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS
548             && overrideActivityOptions != null) {
549           activity.startActivity(intent, overrideActivityOptions);
550         } else {
551           activity.startActivity(intent);
552         }
553       } else {
554         Log.w(
555             TAG,
556             "Fallback to using startActivity(Intent) due to the startActivity(Intent, Bundle) is"
557                 + " supported from Android Sdk "
558                 + VERSION_CODES.JELLY_BEAN);
559         activity.startActivity(intent);
560       }
561     } catch (ActivityNotFoundException e) {
562       Log.w(TAG, "Activity not found when startActivity with transition.");
563       isStartActivity = false;
564       throw e;
565     }
566   }
567 
startActivityForResultWithTransition( Activity activity, Intent intent, int requestCode)568   public static void startActivityForResultWithTransition(
569       Activity activity, Intent intent, int requestCode) {
570     startActivityForResultWithTransition(
571         activity, intent, requestCode, /* overrideActivityOptions= */ null);
572   }
573 
574   /**
575    * A wrapper method, create an {@link android.app.ActivityOptions} to transition between
576    * activities as the {@code activityOptions} parameter of {@link Activity#startActivityForResult}.
577    *
578    * @throws IllegalArgumentException is thrown when {@code activity} or {@code intent} is null.
579    * @throws android.content.ActivityNotFoundException if there was no {@link Activity} found to run
580    *     the given Intent.
581    */
startActivityForResultWithTransition( Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions)582   public static void startActivityForResultWithTransition(
583       Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions) {
584     if (activity == null) {
585       throw new IllegalArgumentException("Invalid activity=" + activity);
586     }
587 
588     if (intent == null) {
589       throw new IllegalArgumentException("Invalid intent=" + intent);
590     }
591 
592     if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK) {
593       Log.e(
594           TAG,
595           "The transition won't take effect since the WindowManager does not allow override new"
596               + " task transitions");
597     }
598 
599     if (!isStartActivityForResult) {
600       isStartActivityForResult = true;
601       if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
602         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
603           if (activity.getWindow() != null
604               && !activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
605             Log.w(
606                 TAG,
607                 "The transition won't take effect due to NO FEATURE_ACTIVITY_TRANSITIONS feature");
608           }
609 
610           Bundle bundleActivityOptions;
611           if (overrideActivityOptions != null) {
612             bundleActivityOptions = overrideActivityOptions;
613           } else {
614             bundleActivityOptions = makeActivityOptions(activity, intent);
615           }
616           intent.putExtra(EXTRA_ACTIVITY_OPTIONS, (Parcelable) bundleActivityOptions);
617           activity.startActivityForResult(intent, requestCode, bundleActivityOptions);
618         } else {
619           Log.w(
620               TAG,
621               "Fallback to using startActivityForResult API due to the"
622                   + " ActivityOptions#makeSceneTransitionAnimation is supported from Android Sdk "
623                   + VERSION_CODES.LOLLIPOP);
624           startActivityForResultWithTransitionInternal(
625               activity, intent, requestCode, overrideActivityOptions);
626         }
627       } else {
628         startActivityForResultWithTransitionInternal(
629             activity, intent, requestCode, overrideActivityOptions);
630       }
631       isStartActivityForResult = false;
632     }
633   }
634 
startActivityForResultWithTransitionInternal( Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions)635   private static void startActivityForResultWithTransitionInternal(
636       Activity activity, Intent intent, int requestCode, Bundle overrideActivityOptions) {
637     try {
638       if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
639         if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS
640             && overrideActivityOptions != null) {
641           activity.startActivityForResult(intent, requestCode, overrideActivityOptions);
642         } else {
643           activity.startActivityForResult(intent, requestCode);
644         }
645       } else {
646         Log.w(
647             TAG,
648             "Fallback to using startActivityForResult(Intent) due to the"
649                 + " startActivityForResult(Intent,int) is supported from Android Sdk "
650                 + VERSION_CODES.JELLY_BEAN);
651         activity.startActivityForResult(intent, requestCode);
652       }
653     } catch (ActivityNotFoundException e) {
654       Log.w(TAG, "Activity not found when startActivityForResult with transition.");
655       isStartActivityForResult = false;
656       throw e;
657     }
658   }
659 
660   /**
661    * A wrapper method, calling {@link Activity#finishAfterTransition()} to trigger exit transition
662    * when running in Android S and the transition type {link #CONFIG_TRANSITION_SHARED_X_AXIS}.
663    *
664    * @throws IllegalArgumentException is thrown when {@code activity} is null.
665    */
finishActivity(Activity activity)666   public static void finishActivity(Activity activity) {
667     if (activity == null) {
668       throw new IllegalArgumentException("Invalid activity=" + activity);
669     }
670 
671     // Avoids finish been called more than once.
672     if (!isFinishCalled) {
673       isFinishCalled = true;
674       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP
675           && getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
676         activity.finishAfterTransition();
677       } else {
678         Log.w(
679             TAG,
680             "Fallback to using Activity#finish() due to the"
681                 + " Activity#finishAfterTransition() is supported from Android Sdk "
682                 + VERSION_CODES.LOLLIPOP);
683         activity.finish();
684       }
685     }
686       isFinishCalled = false;
687   }
688 
689   /**
690    * Returns the transition type from the {@link PartnerConfig#CONFIG_TRANSITION_TYPE} partner
691    * resource on Android S, otherwise returns {@link #CONFIG_TRANSITION_NONE}.
692    */
getConfigTransitionType(Context context)693   public static int getConfigTransitionType(Context context) {
694     return BuildCompatUtils.isAtLeastS() && ThemeHelper.shouldApplyExtendedPartnerConfig(context)
695         ? PartnerConfigHelper.get(context)
696             .getInteger(context, PartnerConfig.CONFIG_TRANSITION_TYPE, CONFIG_TRANSITION_NONE)
697         : CONFIG_TRANSITION_NONE;
698   }
699 
700   /**
701    * A wrapper method, create a {@link Bundle} from {@link ActivityOptions} to transition between
702    * Activities using cross-Activity scene animations. This {@link Bundle} that can be used with
703    * {@link Context#startActivity(Intent, Bundle)} and related methods.
704    *
705    * <p>Example usage:
706    *
707    * <pre>{@code
708    * Intent intent = new Intent("com.example.NEXT_ACTIVITY");
709    * activity.startActivity(intent, TransitionHelper.makeActivityOptions(activity, intent, null);
710    * }</pre>
711    *
712    * <p>Unexpected usage:
713    *
714    * <pre>{@code
715    * Intent intent = new Intent("com.example.NEXT_ACTIVITY");
716    * Intent intent2 = new Intent("com.example.NEXT_ACTIVITY");
717    * activity.startActivity(intent, TransitionHelper.makeActivityOptions(activity, intent2, null);
718    * }</pre>
719    */
720   @Nullable
makeActivityOptions(Activity activity, Intent intent)721   public static Bundle makeActivityOptions(Activity activity, Intent intent) {
722     return makeActivityOptions(activity, intent, false);
723   }
724 
725   /**
726    * A wrapper method, create a {@link Bundle} from {@link ActivityOptions} to transition between
727    * Activities using cross-Activity scene animations. This {@link Bundle} that can be used with
728    * {@link Context#startActivity(Intent, Bundle)} and related methods. When this {@code activity}
729    * is a no UI activity(the activity doesn't inflate any layouts), you will need to pass the bundle
730    * coming from previous UI activity as the {@link ActivityOptions}, otherwise, the transition
731    * won't be take effect. The {@code overrideActivityOptionsFromIntent} is supporting this purpose
732    * to return the {@link ActivityOptions} instead of creating from this no UI activity while the
733    * transition is apply {@link #CONFIG_TRANSITION_SHARED_X_AXIS} config. Moreover, the
734    * startActivity*WithTransition relative methods and {@link #makeActivityOptions} will put {@link
735    * ActivityOptions} to the {@code intent} by default, you can get the {@link ActivityOptions}
736    * which makes from previous activity by accessing {@link #EXTRA_ACTIVITY_OPTIONS} extra from
737    * {@link Activity#getIntent()}.
738    *
739    * <p>Example usage of a no UI activity:
740    *
741    * <pre>{@code
742    * Intent intent = new Intent("com.example.NEXT_ACTIVITY");
743    * activity.startActivity(intent, TransitionHelper.makeActivityOptions(activity, intent, true);
744    * }</pre>
745    */
746   @Nullable
makeActivityOptions( Activity activity, Intent intent, boolean overrideActivityOptionsFromIntent)747   public static Bundle makeActivityOptions(
748       Activity activity, Intent intent, boolean overrideActivityOptionsFromIntent) {
749     Bundle resultBundle = null;
750     if (activity == null || intent == null) {
751       return resultBundle;
752     }
753 
754     if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == Intent.FLAG_ACTIVITY_NEW_TASK) {
755       Log.e(
756           TAG,
757           "The transition won't take effect since the WindowManager does not allow override new"
758               + " task transitions");
759     }
760 
761     if (getConfigTransitionType(activity) == CONFIG_TRANSITION_SHARED_X_AXIS) {
762       if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
763         if (activity.getWindow() != null
764             && !activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) {
765           Log.w(
766               TAG,
767               "The transition won't take effect due to NO FEATURE_ACTIVITY_TRANSITIONS feature");
768         }
769 
770         if (overrideActivityOptionsFromIntent && activity.getIntent() != null) {
771           resultBundle = activity.getIntent().getBundleExtra(EXTRA_ACTIVITY_OPTIONS);
772         } else {
773           resultBundle = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle();
774         }
775         intent.putExtra(EXTRA_ACTIVITY_OPTIONS, (Parcelable) resultBundle);
776         return resultBundle;
777       }
778     }
779 
780     return resultBundle;
781   }
782 }
783