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 */
16package android.app;
17
18import android.content.Intent;
19import android.os.Bundle;
20import android.os.ResultReceiver;
21import android.transition.Transition;
22import android.util.SparseArray;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.Window;
26
27import com.android.internal.view.OneShotPreDrawListener;
28
29import java.lang.ref.WeakReference;
30import 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 */
36class ActivityTransitionState {
37
38    private static final String ENTERING_SHARED_ELEMENTS = "android:enteringSharedElements";
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.
47     */
48    private ArrayList<String> mEnteringNames;
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
110    public ActivityTransitionState() {
111    }
112
113    public int addExitTransitionCoordinator(ExitTransitionCoordinator exitTransitionCoordinator) {
114        if (mExitTransitionCoordinators == null) {
115            mExitTransitionCoordinators =
116                    new SparseArray<WeakReference<ExitTransitionCoordinator>>();
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.get() == null) {
124                mExitTransitionCoordinators.removeAt(i);
125            }
126        }
127        int newKey = mExitTransitionCoordinatorsKey++;
128        mExitTransitionCoordinators.append(newKey, ref);
129        return newKey;
130    }
131
132    public void readState(Bundle bundle) {
133        if (bundle != null) {
134            if (mEnterTransitionCoordinator == null || mEnterTransitionCoordinator.isReturning()) {
135                mEnteringNames = bundle.getStringArrayList(ENTERING_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    public void saveState(Bundle bundle) {
145        if (mEnteringNames != null) {
146            bundle.putStringArrayList(ENTERING_SHARED_ELEMENTS, mEnteringNames);
147        }
148        if (mExitingFrom != null) {
149            bundle.putStringArrayList(EXITING_MAPPED_FROM, mExitingFrom);
150            bundle.putStringArrayList(EXITING_MAPPED_TO, mExitingTo);
151        }
152    }
153
154    public void setEnterActivityOptions(Activity activity, ActivityOptions options) {
155        final Window window = activity.getWindow();
156        if (window == null) {
157            return;
158        }
159        // ensure Decor View has been created so that the window features are activated
160        window.getDecorView();
161        if (window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
162                && options != null && mEnterActivityOptions == null
163                && mEnterTransitionCoordinator == null
164                && options.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
165            mEnterActivityOptions = options;
166            mIsEnterTriggered = false;
167            if (mEnterActivityOptions.isReturning()) {
168                restoreExitedViews();
169                int result = mEnterActivityOptions.getResultCode();
170                if (result != 0) {
171                    Intent intent = mEnterActivityOptions.getResultData();
172                    if (intent != null) {
173                        intent.setExtrasClassLoader(activity.getClassLoader());
174                    }
175                    activity.onActivityReenter(result, intent);
176                }
177            }
178        }
179    }
180
181    public void enterReady(Activity activity) {
182        if (mEnterActivityOptions == null || mIsEnterTriggered) {
183            return;
184        }
185        mIsEnterTriggered = true;
186        mHasExited = false;
187        ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames();
188        ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver();
189        if (mEnterActivityOptions.isReturning()) {
190            restoreExitedViews();
191            activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
192        }
193        mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
194                resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
195                mEnterActivityOptions.isCrossTask());
196        if (mEnterActivityOptions.isCrossTask()) {
197            mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
198            mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
199        }
200
201        if (!mIsEnterPostponed) {
202            startEnter();
203        }
204    }
205
206    public void postponeEnterTransition() {
207        mIsEnterPostponed = true;
208    }
209
210    public void startPostponedEnterTransition() {
211        if (mIsEnterPostponed) {
212            mIsEnterPostponed = false;
213            if (mEnterTransitionCoordinator != null) {
214                startEnter();
215            }
216        }
217    }
218
219    private void startEnter() {
220        if (mEnterTransitionCoordinator.isReturning()) {
221            if (mExitingToView != null) {
222                mEnterTransitionCoordinator.viewInstancesReady(mExitingFrom, mExitingTo,
223                        mExitingToView);
224            } else {
225                mEnterTransitionCoordinator.namedViewsReady(mExitingFrom, mExitingTo);
226            }
227        } else {
228            mEnterTransitionCoordinator.namedViewsReady(null, null);
229            mEnteringNames = mEnterTransitionCoordinator.getAllSharedElementNames();
230        }
231
232        mExitingFrom = null;
233        mExitingTo = null;
234        mExitingToView = null;
235        mEnterActivityOptions = null;
236    }
237
238    public void onStop() {
239        restoreExitedViews();
240        if (mEnterTransitionCoordinator != null) {
241            mEnterTransitionCoordinator.stop();
242            mEnterTransitionCoordinator = null;
243        }
244        if (mReturnExitCoordinator != null) {
245            mReturnExitCoordinator.stop();
246            mReturnExitCoordinator = null;
247        }
248    }
249
250    public void onResume(Activity activity, boolean isTopOfTask) {
251        // After orientation change, the onResume can come in before the top Activity has
252        // left, so if the Activity is not top, wait a second for the top Activity to exit.
253        if (isTopOfTask || mEnterTransitionCoordinator == null) {
254            restoreExitedViews();
255            restoreReenteringViews();
256        } else {
257            activity.mHandler.postDelayed(new Runnable() {
258                @Override
259                public void run() {
260                    if (mEnterTransitionCoordinator == null ||
261                            mEnterTransitionCoordinator.isWaitingForRemoteExit()) {
262                        restoreExitedViews();
263                        restoreReenteringViews();
264                    }
265                }
266            }, 1000);
267        }
268    }
269
270    public void clear() {
271        mEnteringNames = null;
272        mExitingFrom = null;
273        mExitingTo = null;
274        mExitingToView = null;
275        mCalledExitCoordinator = null;
276        mEnterTransitionCoordinator = null;
277        mEnterActivityOptions = null;
278        mExitTransitionCoordinators = null;
279    }
280
281    private void restoreExitedViews() {
282        if (mCalledExitCoordinator != null) {
283            mCalledExitCoordinator.resetViews();
284            mCalledExitCoordinator = null;
285        }
286    }
287
288    private void restoreReenteringViews() {
289        if (mEnterTransitionCoordinator != null && mEnterTransitionCoordinator.isReturning() &&
290                !mEnterTransitionCoordinator.isCrossTask()) {
291            mEnterTransitionCoordinator.forceViewsToAppear();
292            mExitingFrom = null;
293            mExitingTo = null;
294            mExitingToView = null;
295        }
296    }
297
298    public boolean startExitBackTransition(final Activity activity) {
299        if (mEnteringNames == null || mCalledExitCoordinator != null) {
300            return false;
301        } else {
302            if (!mHasExited) {
303                mHasExited = true;
304                Transition enterViewsTransition = null;
305                ViewGroup decor = null;
306                boolean delayExitBack = false;
307                if (mEnterTransitionCoordinator != null) {
308                    enterViewsTransition = mEnterTransitionCoordinator.getEnterViewsTransition();
309                    decor = mEnterTransitionCoordinator.getDecor();
310                    delayExitBack = mEnterTransitionCoordinator.cancelEnter();
311                    mEnterTransitionCoordinator = null;
312                    if (enterViewsTransition != null && decor != null) {
313                        enterViewsTransition.pause(decor);
314                    }
315                }
316
317                mReturnExitCoordinator = new ExitTransitionCoordinator(activity,
318                        activity.getWindow(), activity.mEnterTransitionListener, mEnteringNames,
319                        null, null, true);
320                if (enterViewsTransition != null && decor != null) {
321                    enterViewsTransition.resume(decor);
322                }
323                if (delayExitBack && decor != null) {
324                    final ViewGroup finalDecor = decor;
325                    OneShotPreDrawListener.add(decor, () -> {
326                        if (mReturnExitCoordinator != null) {
327                            mReturnExitCoordinator.startExit(activity.mResultCode,
328                                    activity.mResultData);
329                        }
330                    });
331                } else {
332                    mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData);
333                }
334            }
335            return true;
336        }
337    }
338
339    public boolean isTransitionRunning() {
340        // Note that *only* enter *or* exit will be running at any given time
341        if (mEnterTransitionCoordinator != null) {
342            if (mEnterTransitionCoordinator.isTransitionRunning()) {
343                return true;
344            }
345        }
346        if (mCalledExitCoordinator != null) {
347            if (mCalledExitCoordinator.isTransitionRunning()) {
348                return true;
349            }
350        }
351        if (mReturnExitCoordinator != null) {
352            if (mReturnExitCoordinator.isTransitionRunning()) {
353                return true;
354            }
355        }
356        return false;
357    }
358
359    public void startExitOutTransition(Activity activity, Bundle options) {
360        mEnterTransitionCoordinator = null;
361        if (!activity.getWindow().hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) ||
362                mExitTransitionCoordinators == null) {
363            return;
364        }
365        ActivityOptions activityOptions = new ActivityOptions(options);
366        if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
367            int key = activityOptions.getExitCoordinatorKey();
368            int index = mExitTransitionCoordinators.indexOfKey(key);
369            if (index >= 0) {
370                mCalledExitCoordinator = mExitTransitionCoordinators.valueAt(index).get();
371                mExitTransitionCoordinators.removeAt(index);
372                if (mCalledExitCoordinator != null) {
373                    mExitingFrom = mCalledExitCoordinator.getAcceptedNames();
374                    mExitingTo = mCalledExitCoordinator.getMappedNames();
375                    mExitingToView = mCalledExitCoordinator.copyMappedViews();
376                    mCalledExitCoordinator.startExit();
377                }
378            }
379        }
380    }
381}
382