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