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