1 /*
2  * Copyright (C) 2014 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 package android.app;
17 
18 import android.content.Intent;
19 import android.os.Bundle;
20 import android.os.ResultReceiver;
21 import android.transition.Transition;
22 import android.util.SparseArray;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.view.Window;
26 
27 import com.android.internal.view.OneShotPreDrawListener;
28 
29 import java.lang.ref.WeakReference;
30 import java.util.ArrayList;
31 
32 /**
33  * This class contains all persistence-related functionality for Activity Transitions.
34  * Activities start exit and enter Activity Transitions through this class.
35  */
36 class ActivityTransitionState {
37 
38     private static final String PENDING_EXIT_SHARED_ELEMENTS = "android:pendingExitSharedElements";
39 
40     private static final String EXITING_MAPPED_FROM = "android:exitingMappedFrom";
41 
42     private static final String EXITING_MAPPED_TO = "android:exitingMappedTo";
43 
44     /**
45      * The shared elements that the calling Activity has said that they transferred to this
46      * Activity and will be transferred back during exit animation.
47      */
48     private ArrayList<String> mPendingExitNames;
49 
50     /**
51      * The names of shared elements that were shared to the called Activity.
52      */
53     private ArrayList<String> mExitingFrom;
54 
55     /**
56      * The names of local Views that were shared out, mapped to those elements in mExitingFrom.
57      */
58     private ArrayList<String> mExitingTo;
59 
60     /**
61      * The local Views that were shared out, mapped to those elements in mExitingFrom.
62      */
63     private ArrayList<View> mExitingToView;
64 
65     /**
66      * The ExitTransitionCoordinator used to start an Activity. Used to make the elements restore
67      * Visibility of exited Views.
68      */
69     private ExitTransitionCoordinator mCalledExitCoordinator;
70 
71     /**
72      * The ExitTransitionCoordinator used to return to a previous Activity when called with
73      * {@link android.app.Activity#finishAfterTransition()}.
74      */
75     private ExitTransitionCoordinator mReturnExitCoordinator;
76 
77     /**
78      * We must be able to cancel entering transitions to stop changing the Window to
79      * opaque when we exit before making the Window opaque.
80      */
81     private EnterTransitionCoordinator mEnterTransitionCoordinator;
82 
83     /**
84      * ActivityOptions used on entering this Activity.
85      */
86     private ActivityOptions mEnterActivityOptions;
87 
88     /**
89      * Has an exit transition been started? If so, we don't want to double-exit.
90      */
91     private boolean mHasExited;
92 
93     /**
94      * Postpone painting and starting the enter transition until this is false.
95      */
96     private boolean mIsEnterPostponed;
97 
98     /**
99      * Potential exit transition coordinators.
100      */
101     private SparseArray<WeakReference<ExitTransitionCoordinator>> mExitTransitionCoordinators;
102 
103     /**
104      * Next key for mExitTransitionCoordinator.
105      */
106     private int mExitTransitionCoordinatorsKey = 1;
107 
108     private boolean mIsEnterTriggered;
109 
ActivityTransitionState()110     public ActivityTransitionState() {
111     }
112 
addExitTransitionCoordinator(ExitTransitionCoordinator exitTransitionCoordinator)113     public int addExitTransitionCoordinator(ExitTransitionCoordinator exitTransitionCoordinator) {
114         if (mExitTransitionCoordinators == null) {
115             mExitTransitionCoordinators = new SparseArray<>();
116         }
117         WeakReference<ExitTransitionCoordinator> ref = new WeakReference(exitTransitionCoordinator);
118         // clean up old references:
119         for (int i = mExitTransitionCoordinators.size() - 1; i >= 0; i--) {
120             WeakReference<ExitTransitionCoordinator> oldRef
121                     = mExitTransitionCoordinators.valueAt(i);
122             if (oldRef.get() == null) {
123                 mExitTransitionCoordinators.removeAt(i);
124             }
125         }
126         int newKey = mExitTransitionCoordinatorsKey++;
127         mExitTransitionCoordinators.append(newKey, ref);
128         return newKey;
129     }
130 
readState(Bundle bundle)131     public void readState(Bundle bundle) {
132         if (bundle != null) {
133             if (mEnterTransitionCoordinator == null || mEnterTransitionCoordinator.isReturning()) {
134                 mPendingExitNames = bundle.getStringArrayList(PENDING_EXIT_SHARED_ELEMENTS);
135             }
136             if (mEnterTransitionCoordinator == null) {
137                 mExitingFrom = bundle.getStringArrayList(EXITING_MAPPED_FROM);
138                 mExitingTo = bundle.getStringArrayList(EXITING_MAPPED_TO);
139             }
140         }
141     }
142 
143     /**
144      * Returns the element names to be used for exit animation. It caches the list internally so
145      * that it is preserved through activty destroy and restore.
146      */
getPendingExitNames()147     private ArrayList<String> getPendingExitNames() {
148         if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {
149             mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();
150         }
151         return mPendingExitNames;
152     }
153 
saveState(Bundle bundle)154     public void saveState(Bundle bundle) {
155         ArrayList<String> pendingExitNames = getPendingExitNames();
156         if (pendingExitNames != null) {
157             bundle.putStringArrayList(PENDING_EXIT_SHARED_ELEMENTS, pendingExitNames);
158         }
159         if (mExitingFrom != null) {
160             bundle.putStringArrayList(EXITING_MAPPED_FROM, mExitingFrom);
161             bundle.putStringArrayList(EXITING_MAPPED_TO, mExitingTo);
162         }
163     }
164 
setEnterActivityOptions(Activity activity, ActivityOptions options)165     public void setEnterActivityOptions(Activity activity, ActivityOptions options) {
166         final Window window = activity.getWindow();
167         if (window == null) {
168             return;
169         }
170         // ensure Decor View has been created so that the window features are activated
171         window.getDecorView();
172         if (window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
173                 && options != null && mEnterActivityOptions == null
174                 && mEnterTransitionCoordinator == null
175                 && options.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
176             mEnterActivityOptions = options;
177             mIsEnterTriggered = false;
178             if (mEnterActivityOptions.isReturning()) {
179                 restoreExitedViews();
180                 int result = mEnterActivityOptions.getResultCode();
181                 if (result != 0) {
182                     Intent intent = mEnterActivityOptions.getResultData();
183                     if (intent != null) {
184                         intent.setExtrasClassLoader(activity.getClassLoader());
185                     }
186                     activity.onActivityReenter(result, intent);
187                 }
188             }
189         }
190     }
191 
enterReady(Activity activity)192     public void enterReady(Activity activity) {
193         if (mEnterActivityOptions == null || mIsEnterTriggered) {
194             return;
195         }
196         mIsEnterTriggered = true;
197         mHasExited = false;
198         ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames();
199         ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver();
200         if (mEnterActivityOptions.isReturning()) {
201             restoreExitedViews();
202             activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
203         }
204         mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
205                 resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
206                 mEnterActivityOptions.isCrossTask());
207         if (mEnterActivityOptions.isCrossTask()) {
208             mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
209             mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
210         }
211 
212         if (!mIsEnterPostponed) {
213             startEnter();
214         }
215     }
216 
postponeEnterTransition()217     public void postponeEnterTransition() {
218         mIsEnterPostponed = true;
219     }
220 
startPostponedEnterTransition()221     public void startPostponedEnterTransition() {
222         if (mIsEnterPostponed) {
223             mIsEnterPostponed = false;
224             if (mEnterTransitionCoordinator != null) {
225                 startEnter();
226             }
227         }
228     }
229 
startEnter()230     private void startEnter() {
231         if (mEnterTransitionCoordinator.isReturning()) {
232             if (mExitingToView != null) {
233                 mEnterTransitionCoordinator.viewInstancesReady(mExitingFrom, mExitingTo,
234                         mExitingToView);
235             } else {
236                 mEnterTransitionCoordinator.namedViewsReady(mExitingFrom, mExitingTo);
237             }
238         } else {
239             mEnterTransitionCoordinator.namedViewsReady(null, null);
240             mPendingExitNames = null;
241         }
242 
243         mExitingFrom = null;
244         mExitingTo = null;
245         mExitingToView = null;
246         mEnterActivityOptions = null;
247     }
248 
onStop()249     public void onStop() {
250         restoreExitedViews();
251         if (mEnterTransitionCoordinator != null) {
252             mEnterTransitionCoordinator.stop();
253             mEnterTransitionCoordinator = null;
254         }
255         if (mReturnExitCoordinator != null) {
256             mReturnExitCoordinator.stop();
257             mReturnExitCoordinator = null;
258         }
259     }
260 
onResume(Activity activity)261     public void onResume(Activity activity) {
262         // After orientation change, the onResume can come in before the top Activity has
263         // left, so if the Activity is not top, wait a second for the top Activity to exit.
264         if (mEnterTransitionCoordinator == null || activity.isTopOfTask()) {
265             restoreExitedViews();
266             restoreReenteringViews();
267         } else {
268             activity.mHandler.postDelayed(new Runnable() {
269                 @Override
270                 public void run() {
271                     if (mEnterTransitionCoordinator == null ||
272                             mEnterTransitionCoordinator.isWaitingForRemoteExit()) {
273                         restoreExitedViews();
274                         restoreReenteringViews();
275                     }
276                 }
277             }, 1000);
278         }
279     }
280 
clear()281     public void clear() {
282         mPendingExitNames = null;
283         mExitingFrom = null;
284         mExitingTo = null;
285         mExitingToView = null;
286         mCalledExitCoordinator = null;
287         mEnterTransitionCoordinator = null;
288         mEnterActivityOptions = null;
289         mExitTransitionCoordinators = null;
290     }
291 
restoreExitedViews()292     private void restoreExitedViews() {
293         if (mCalledExitCoordinator != null) {
294             mCalledExitCoordinator.resetViews();
295             mCalledExitCoordinator = null;
296         }
297     }
298 
restoreReenteringViews()299     private void restoreReenteringViews() {
300         if (mEnterTransitionCoordinator != null && mEnterTransitionCoordinator.isReturning() &&
301                 !mEnterTransitionCoordinator.isCrossTask()) {
302             mEnterTransitionCoordinator.forceViewsToAppear();
303             mExitingFrom = null;
304             mExitingTo = null;
305             mExitingToView = null;
306         }
307     }
308 
startExitBackTransition(final Activity activity)309     public boolean startExitBackTransition(final Activity activity) {
310         ArrayList<String> pendingExitNames = getPendingExitNames();
311         if (pendingExitNames == null || mCalledExitCoordinator != null) {
312             return false;
313         } else {
314             if (!mHasExited) {
315                 mHasExited = true;
316                 Transition enterViewsTransition = null;
317                 ViewGroup decor = null;
318                 boolean delayExitBack = false;
319                 if (mEnterTransitionCoordinator != null) {
320                     enterViewsTransition = mEnterTransitionCoordinator.getEnterViewsTransition();
321                     decor = mEnterTransitionCoordinator.getDecor();
322                     delayExitBack = mEnterTransitionCoordinator.cancelEnter();
323                     mEnterTransitionCoordinator = null;
324                     if (enterViewsTransition != null && decor != null) {
325                         enterViewsTransition.pause(decor);
326                     }
327                 }
328 
329                 mReturnExitCoordinator = new ExitTransitionCoordinator(activity,
330                         activity.getWindow(), activity.mEnterTransitionListener, pendingExitNames,
331                         null, null, true);
332                 if (enterViewsTransition != null && decor != null) {
333                     enterViewsTransition.resume(decor);
334                 }
335                 if (delayExitBack && decor != null) {
336                     final ViewGroup finalDecor = decor;
337                     OneShotPreDrawListener.add(decor, () -> {
338                         if (mReturnExitCoordinator != null) {
339                             mReturnExitCoordinator.startExit(activity.mResultCode,
340                                     activity.mResultData);
341                         }
342                     });
343                 } else {
344                     mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData);
345                 }
346             }
347             return true;
348         }
349     }
350 
isTransitionRunning()351     public boolean isTransitionRunning() {
352         // Note that *only* enter *or* exit will be running at any given time
353         if (mEnterTransitionCoordinator != null) {
354             if (mEnterTransitionCoordinator.isTransitionRunning()) {
355                 return true;
356             }
357         }
358         if (mCalledExitCoordinator != null) {
359             if (mCalledExitCoordinator.isTransitionRunning()) {
360                 return true;
361             }
362         }
363         if (mReturnExitCoordinator != null) {
364             if (mReturnExitCoordinator.isTransitionRunning()) {
365                 return true;
366             }
367         }
368         return false;
369     }
370 
startExitOutTransition(Activity activity, Bundle options)371     public void startExitOutTransition(Activity activity, Bundle options) {
372         mEnterTransitionCoordinator = null;
373         if (!activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) ||
374                 mExitTransitionCoordinators == null) {
375             return;
376         }
377         ActivityOptions activityOptions = new ActivityOptions(options);
378         if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
379             int key = activityOptions.getExitCoordinatorKey();
380             int index = mExitTransitionCoordinators.indexOfKey(key);
381             if (index >= 0) {
382                 mCalledExitCoordinator = mExitTransitionCoordinators.valueAt(index).get();
383                 mExitTransitionCoordinators.removeAt(index);
384                 if (mCalledExitCoordinator != null) {
385                     mExitingFrom = mCalledExitCoordinator.getAcceptedNames();
386                     mExitingTo = mCalledExitCoordinator.getMappedNames();
387                     mExitingToView = mCalledExitCoordinator.copyMappedViews();
388                     mCalledExitCoordinator.startExit();
389                 }
390             }
391         }
392     }
393 }
394