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.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.animation.ObjectAnimator;
21import android.app.SharedElementCallback.OnSharedElementsReadyListener;
22import android.content.Intent;
23import android.graphics.Color;
24import android.graphics.Matrix;
25import android.graphics.RectF;
26import android.graphics.drawable.ColorDrawable;
27import android.graphics.drawable.Drawable;
28import android.os.Build.VERSION_CODES;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Message;
32import android.os.ResultReceiver;
33import android.transition.Transition;
34import android.transition.TransitionManager;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.ViewTreeObserver;
38
39import java.util.ArrayList;
40
41/**
42 * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation
43 * to govern the exit of the Scene and the shared elements when calling an Activity as well as
44 * the reentry of the Scene when coming back from the called Activity.
45 */
46class ExitTransitionCoordinator extends ActivityTransitionCoordinator {
47    private static final String TAG = "ExitTransitionCoordinator";
48    private static final long MAX_WAIT_MS = 1000;
49
50    private Bundle mSharedElementBundle;
51    private boolean mExitNotified;
52    private boolean mSharedElementNotified;
53    private Activity mActivity;
54    private boolean mIsBackgroundReady;
55    private boolean mIsCanceled;
56    private Handler mHandler;
57    private ObjectAnimator mBackgroundAnimator;
58    private boolean mIsHidden;
59    private Bundle mExitSharedElementBundle;
60    private boolean mIsExitStarted;
61    private boolean mSharedElementsHidden;
62
63    public ExitTransitionCoordinator(Activity activity, ArrayList<String> names,
64            ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) {
65        super(activity.getWindow(), names, getListener(activity, isReturning), isReturning);
66        viewsReady(mapSharedElements(accepted, mapped));
67        stripOffscreenViews();
68        mIsBackgroundReady = !isReturning;
69        mActivity = activity;
70    }
71
72    private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
73        return isReturning ? activity.mEnterTransitionListener : activity.mExitTransitionListener;
74    }
75
76    @Override
77    protected void onReceiveResult(int resultCode, Bundle resultData) {
78        switch (resultCode) {
79            case MSG_SET_REMOTE_RECEIVER:
80                stopCancel();
81                mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER);
82                if (mIsCanceled) {
83                    mResultReceiver.send(MSG_CANCEL, null);
84                    mResultReceiver = null;
85                } else {
86                    notifyComplete();
87                }
88                break;
89            case MSG_HIDE_SHARED_ELEMENTS:
90                stopCancel();
91                if (!mIsCanceled) {
92                    hideSharedElements();
93                }
94                break;
95            case MSG_START_EXIT_TRANSITION:
96                mHandler.removeMessages(MSG_CANCEL);
97                startExit();
98                break;
99            case MSG_SHARED_ELEMENT_DESTINATION:
100                mExitSharedElementBundle = resultData;
101                sharedElementExitBack();
102                break;
103            case MSG_CANCEL:
104                mIsCanceled = true;
105                finish();
106                break;
107        }
108    }
109
110    private void stopCancel() {
111        if (mHandler != null) {
112            mHandler.removeMessages(MSG_CANCEL);
113        }
114    }
115
116    private void delayCancel() {
117        if (mHandler != null) {
118            mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS);
119        }
120    }
121
122    public void resetViews() {
123        if (mTransitioningViews != null) {
124            showViews(mTransitioningViews, true);
125            setTransitioningViewsVisiblity(View.VISIBLE, true);
126        }
127        showViews(mSharedElements, true);
128        mIsHidden = true;
129        ViewGroup decorView = getDecor();
130        if (!mIsReturning && decorView != null) {
131            decorView.suppressLayout(false);
132        }
133        moveSharedElementsFromOverlay();
134        clearState();
135    }
136
137    private void sharedElementExitBack() {
138        final ViewGroup decorView = getDecor();
139        if (decorView != null) {
140            decorView.suppressLayout(true);
141        }
142        if (decorView != null && mExitSharedElementBundle != null &&
143                !mExitSharedElementBundle.isEmpty() &&
144                !mSharedElements.isEmpty() && getSharedElementTransition() != null) {
145            startTransition(new Runnable() {
146                public void run() {
147                    startSharedElementExit(decorView);
148                }
149            });
150        } else {
151            sharedElementTransitionComplete();
152        }
153    }
154
155    private void startSharedElementExit(final ViewGroup decorView) {
156        Transition transition = getSharedElementExitTransition();
157        transition.addListener(new Transition.TransitionListenerAdapter() {
158            @Override
159            public void onTransitionEnd(Transition transition) {
160                transition.removeListener(this);
161                if (isViewsTransitionComplete()) {
162                    delayCancel();
163                }
164            }
165        });
166        final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle,
167                mSharedElementNames);
168        decorView.getViewTreeObserver()
169                .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
170                    @Override
171                    public boolean onPreDraw() {
172                        decorView.getViewTreeObserver().removeOnPreDrawListener(this);
173                        setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots);
174                        return true;
175                    }
176                });
177        setGhostVisibility(View.INVISIBLE);
178        scheduleGhostVisibilityChange(View.INVISIBLE);
179        if (mListener != null) {
180            mListener.onSharedElementEnd(mSharedElementNames, mSharedElements,
181                    sharedElementSnapshots);
182        }
183        TransitionManager.beginDelayedTransition(decorView, transition);
184        scheduleGhostVisibilityChange(View.VISIBLE);
185        setGhostVisibility(View.VISIBLE);
186        decorView.invalidate();
187    }
188
189    private void hideSharedElements() {
190        moveSharedElementsFromOverlay();
191        if (!mIsHidden) {
192            hideViews(mSharedElements);
193        }
194        mSharedElementsHidden = true;
195        finishIfNecessary();
196    }
197
198    public void startExit() {
199        if (!mIsExitStarted) {
200            mIsExitStarted = true;
201            pauseInput();
202            ViewGroup decorView = getDecor();
203            if (decorView != null) {
204                decorView.suppressLayout(true);
205            }
206            moveSharedElementsToOverlay();
207            startTransition(new Runnable() {
208                @Override
209                public void run() {
210                    beginTransitions();
211                }
212            });
213        }
214    }
215
216    public void startExit(int resultCode, Intent data) {
217        if (!mIsExitStarted) {
218            mIsExitStarted = true;
219            pauseInput();
220            ViewGroup decorView = getDecor();
221            if (decorView != null) {
222                decorView.suppressLayout(true);
223            }
224            mHandler = new Handler() {
225                @Override
226                public void handleMessage(Message msg) {
227                    mIsCanceled = true;
228                    finish();
229                }
230            };
231            delayCancel();
232            moveSharedElementsToOverlay();
233            if (decorView != null && decorView.getBackground() == null) {
234                getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
235            }
236            final boolean targetsM = decorView == null || decorView.getContext()
237                    .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M;
238            ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames :
239                    mAllSharedElementNames;
240            ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this,
241                    sharedElementNames, resultCode, data);
242            mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() {
243                @Override
244                public void onTranslucentConversionComplete(boolean drawComplete) {
245                    if (!mIsCanceled) {
246                        fadeOutBackground();
247                    }
248                }
249            }, options);
250            startTransition(new Runnable() {
251                @Override
252                public void run() {
253                    startExitTransition();
254                }
255            });
256        }
257    }
258
259    public void stop() {
260        if (mIsReturning && mActivity != null) {
261            // Override the previous ActivityOptions. We don't want the
262            // activity to have options since we're essentially canceling the
263            // transition and finishing right now.
264            mActivity.convertToTranslucent(null, null);
265            finish();
266        }
267    }
268
269    private void startExitTransition() {
270        Transition transition = getExitTransition();
271        ViewGroup decorView = getDecor();
272        if (transition != null && decorView != null && mTransitioningViews != null) {
273            setTransitioningViewsVisiblity(View.VISIBLE, false);
274            TransitionManager.beginDelayedTransition(decorView, transition);
275            setTransitioningViewsVisiblity(View.INVISIBLE, false);
276            decorView.invalidate();
277        } else {
278            transitionStarted();
279        }
280    }
281
282    private void fadeOutBackground() {
283        if (mBackgroundAnimator == null) {
284            ViewGroup decor = getDecor();
285            Drawable background;
286            if (decor != null && (background = decor.getBackground()) != null) {
287                background = background.mutate();
288                getWindow().setBackgroundDrawable(background);
289                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0);
290                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
291                    @Override
292                    public void onAnimationEnd(Animator animation) {
293                        mBackgroundAnimator = null;
294                        if (!mIsCanceled) {
295                            mIsBackgroundReady = true;
296                            notifyComplete();
297                        }
298                    }
299                });
300                mBackgroundAnimator.setDuration(getFadeDuration());
301                mBackgroundAnimator.start();
302            } else {
303                mIsBackgroundReady = true;
304            }
305        }
306    }
307
308    private Transition getExitTransition() {
309        Transition viewsTransition = null;
310        if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
311            viewsTransition = configureTransition(getViewsTransition(), true);
312        }
313        if (viewsTransition == null) {
314            viewsTransitionComplete();
315        } else {
316            final ArrayList<View> transitioningViews = mTransitioningViews;
317            viewsTransition.addListener(new ContinueTransitionListener() {
318                @Override
319                public void onTransitionEnd(Transition transition) {
320                    transition.removeListener(this);
321                    viewsTransitionComplete();
322                    if (mIsHidden && transitioningViews != null) {
323                        showViews(transitioningViews, true);
324                        setTransitioningViewsVisiblity(View.VISIBLE, true);
325                    }
326                    if (mSharedElementBundle != null) {
327                        delayCancel();
328                    }
329                    super.onTransitionEnd(transition);
330                }
331            });
332        }
333        return viewsTransition;
334    }
335
336    private Transition getSharedElementExitTransition() {
337        Transition sharedElementTransition = null;
338        if (!mSharedElements.isEmpty()) {
339            sharedElementTransition = configureTransition(getSharedElementTransition(), false);
340        }
341        if (sharedElementTransition == null) {
342            sharedElementTransitionComplete();
343        } else {
344            sharedElementTransition.addListener(new ContinueTransitionListener() {
345                @Override
346                public void onTransitionEnd(Transition transition) {
347                    transition.removeListener(this);
348                    sharedElementTransitionComplete();
349                    if (mIsHidden) {
350                        showViews(mSharedElements, true);
351                    }
352                }
353            });
354            mSharedElements.get(0).invalidate();
355        }
356        return sharedElementTransition;
357    }
358
359    private void beginTransitions() {
360        Transition sharedElementTransition = getSharedElementExitTransition();
361        Transition viewsTransition = getExitTransition();
362
363        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
364        ViewGroup decorView = getDecor();
365        if (transition != null && decorView != null) {
366            setGhostVisibility(View.INVISIBLE);
367            scheduleGhostVisibilityChange(View.INVISIBLE);
368            if (viewsTransition != null) {
369                setTransitioningViewsVisiblity(View.VISIBLE, false);
370            }
371            TransitionManager.beginDelayedTransition(decorView, transition);
372            scheduleGhostVisibilityChange(View.VISIBLE);
373            setGhostVisibility(View.VISIBLE);
374            if (viewsTransition != null) {
375                setTransitioningViewsVisiblity(View.INVISIBLE, false);
376            }
377            decorView.invalidate();
378        } else {
379            transitionStarted();
380        }
381    }
382
383    protected boolean isReadyToNotify() {
384        return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady;
385    }
386
387    @Override
388    protected void sharedElementTransitionComplete() {
389        mSharedElementBundle = mExitSharedElementBundle == null
390                ? captureSharedElementState() : captureExitSharedElementsState();
391        super.sharedElementTransitionComplete();
392    }
393
394    private Bundle captureExitSharedElementsState() {
395        Bundle bundle = new Bundle();
396        RectF bounds = new RectF();
397        Matrix matrix = new Matrix();
398        for (int i = 0; i < mSharedElements.size(); i++) {
399            String name = mSharedElementNames.get(i);
400            Bundle sharedElementState = mExitSharedElementBundle.getBundle(name);
401            if (sharedElementState != null) {
402                bundle.putBundle(name, sharedElementState);
403            } else {
404                View view = mSharedElements.get(i);
405                captureSharedElementState(view, name, bundle, matrix, bounds);
406            }
407        }
408        return bundle;
409    }
410
411    @Override
412    protected void onTransitionsComplete() {
413        notifyComplete();
414    }
415
416    protected void notifyComplete() {
417        if (isReadyToNotify()) {
418            if (!mSharedElementNotified) {
419                mSharedElementNotified = true;
420                delayCancel();
421                if (mListener == null) {
422                    mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
423                    notifyExitComplete();
424                } else {
425                    final ResultReceiver resultReceiver = mResultReceiver;
426                    final Bundle sharedElementBundle = mSharedElementBundle;
427                    mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
428                            new OnSharedElementsReadyListener() {
429                                @Override
430                                public void onSharedElementsReady() {
431                                    resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
432                                            sharedElementBundle);
433                                    notifyExitComplete();
434                                }
435                            });
436                }
437            } else {
438                notifyExitComplete();
439            }
440        }
441    }
442
443    private void notifyExitComplete() {
444        if (!mExitNotified && isViewsTransitionComplete()) {
445            mExitNotified = true;
446            mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null);
447            mResultReceiver = null; // done talking
448            ViewGroup decorView = getDecor();
449            if (!mIsReturning && decorView != null) {
450                decorView.suppressLayout(false);
451            }
452            finishIfNecessary();
453        }
454    }
455
456    private void finishIfNecessary() {
457        if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() ||
458                mSharedElementsHidden)) {
459            finish();
460        }
461        if (!mIsReturning && mExitNotified) {
462            mActivity = null; // don't need it anymore
463        }
464    }
465
466    private void finish() {
467        stopCancel();
468        if (mActivity != null) {
469            mActivity.mActivityTransitionState.clear();
470            mActivity.finish();
471            mActivity.overridePendingTransition(0, 0);
472            mActivity = null;
473        }
474        // Clear the state so that we can't hold any references accidentally and leak memory.
475        clearState();
476    }
477
478    @Override
479    protected void clearState() {
480        mHandler = null;
481        mSharedElementBundle = null;
482        if (mBackgroundAnimator != null) {
483            mBackgroundAnimator.cancel();
484            mBackgroundAnimator = null;
485        }
486        mExitSharedElementBundle = null;
487        super.clearState();
488    }
489
490    @Override
491    protected boolean moveSharedElementWithParent() {
492        return !mIsReturning;
493    }
494
495    @Override
496    protected Transition getViewsTransition() {
497        if (mIsReturning) {
498            return getWindow().getReturnTransition();
499        } else {
500            return getWindow().getExitTransition();
501        }
502    }
503
504    protected Transition getSharedElementTransition() {
505        if (mIsReturning) {
506            return getWindow().getSharedElementReturnTransition();
507        } else {
508            return getWindow().getSharedElementExitTransition();
509        }
510    }
511}
512