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