EnterTransitionCoordinator.java revision 5ae0b7abf0d026973d556a8d6685dc2b986bf7b5
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.graphics.drawable.Drawable;
23import android.os.Bundle;
24import android.os.ResultReceiver;
25import android.text.TextUtils;
26import android.transition.Transition;
27import android.transition.TransitionManager;
28import android.util.ArrayMap;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.ViewGroupOverlay;
32import android.view.ViewTreeObserver;
33import android.view.ViewTreeObserver.OnPreDrawListener;
34import android.view.Window;
35import android.view.accessibility.AccessibilityEvent;
36
37import java.util.ArrayList;
38
39/**
40 * This ActivityTransitionCoordinator is created by the Activity to manage
41 * the enter scene and shared element transfer into the Scene, either during
42 * launch of an Activity or returning from a launched Activity.
43 */
44class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
45    private static final String TAG = "EnterTransitionCoordinator";
46
47    private static final int MIN_ANIMATION_FRAMES = 2;
48
49    private boolean mSharedElementTransitionStarted;
50    private Activity mActivity;
51    private boolean mHasStopped;
52    private boolean mIsCanceled;
53    private ObjectAnimator mBackgroundAnimator;
54    private boolean mIsExitTransitionComplete;
55    private boolean mIsReadyForTransition;
56    private Bundle mSharedElementsBundle;
57    private boolean mWasOpaque;
58    private boolean mAreViewsReady;
59    private boolean mIsViewsTransitionStarted;
60    private Transition mEnterViewsTransition;
61    private OnPreDrawListener mViewsReadyListener;
62
63    public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
64            ArrayList<String> sharedElementNames, boolean isReturning) {
65        super(activity.getWindow(), sharedElementNames,
66                getListener(activity, isReturning), isReturning);
67        mActivity = activity;
68        setResultReceiver(resultReceiver);
69        prepareEnter();
70        Bundle resultReceiverBundle = new Bundle();
71        resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
72        mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
73        final View decorView = getDecor();
74        if (decorView != null) {
75            decorView.getViewTreeObserver().addOnPreDrawListener(
76                    new ViewTreeObserver.OnPreDrawListener() {
77                        @Override
78                        public boolean onPreDraw() {
79                            if (mIsReadyForTransition) {
80                                decorView.getViewTreeObserver().removeOnPreDrawListener(this);
81                            }
82                            return mIsReadyForTransition;
83                        }
84                    });
85        }
86    }
87
88    public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
89            ArrayList<View> localViews) {
90        boolean remap = false;
91        for (int i = 0; i < localViews.size(); i++) {
92            View view = localViews.get(i);
93            if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
94                    || !view.isAttachedToWindow()) {
95                remap = true;
96                break;
97            }
98        }
99        if (remap) {
100            triggerViewsReady(mapNamedElements(accepted, localNames));
101        } else {
102            triggerViewsReady(mapSharedElements(accepted, localViews));
103        }
104    }
105
106    public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
107        triggerViewsReady(mapNamedElements(accepted, localNames));
108    }
109
110    public Transition getEnterViewsTransition() {
111        return mEnterViewsTransition;
112    }
113
114    @Override
115    protected void viewsReady(ArrayMap<String, View> sharedElements) {
116        super.viewsReady(sharedElements);
117        mIsReadyForTransition = true;
118        hideViews(mSharedElements);
119        if (getViewsTransition() != null && mTransitioningViews != null) {
120            hideViews(mTransitioningViews);
121        }
122        if (mIsReturning) {
123            sendSharedElementDestination();
124        } else {
125            moveSharedElementsToOverlay();
126        }
127        if (mSharedElementsBundle != null) {
128            onTakeSharedElements();
129        }
130    }
131
132    private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
133        if (mAreViewsReady) {
134            return;
135        }
136        mAreViewsReady = true;
137        final ViewGroup decor = getDecor();
138        // Ensure the views have been laid out before capturing the views -- we need the epicenter.
139        if (decor == null || (decor.isAttachedToWindow() &&
140                (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
141            viewsReady(sharedElements);
142        } else {
143            mViewsReadyListener = new ViewTreeObserver.OnPreDrawListener() {
144                @Override
145                public boolean onPreDraw() {
146                    mViewsReadyListener = null;
147                    decor.getViewTreeObserver().removeOnPreDrawListener(this);
148                    viewsReady(sharedElements);
149                    return true;
150                }
151            };
152            decor.getViewTreeObserver().addOnPreDrawListener(mViewsReadyListener);
153            decor.invalidate();
154        }
155    }
156
157    private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
158            ArrayList<String> localNames) {
159        ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
160        ViewGroup decorView = getDecor();
161        if (decorView != null) {
162            decorView.findNamedViews(sharedElements);
163        }
164        if (accepted != null) {
165            for (int i = 0; i < localNames.size(); i++) {
166                String localName = localNames.get(i);
167                String acceptedName = accepted.get(i);
168                if (localName != null && !localName.equals(acceptedName)) {
169                    View view = sharedElements.remove(localName);
170                    if (view != null) {
171                        sharedElements.put(acceptedName, view);
172                    }
173                }
174            }
175        }
176        return sharedElements;
177    }
178
179    private void sendSharedElementDestination() {
180        boolean allReady;
181        final View decorView = getDecor();
182        if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
183            allReady = false;
184        } else if (decorView == null) {
185            allReady = true;
186        } else {
187            allReady = !decorView.isLayoutRequested();
188            if (allReady) {
189                for (int i = 0; i < mSharedElements.size(); i++) {
190                    if (mSharedElements.get(i).isLayoutRequested()) {
191                        allReady = false;
192                        break;
193                    }
194                }
195            }
196        }
197        if (allReady) {
198            Bundle state = captureSharedElementState();
199            moveSharedElementsToOverlay();
200            mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
201        } else if (decorView != null) {
202            decorView.getViewTreeObserver()
203                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
204                        @Override
205                        public boolean onPreDraw() {
206                            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
207                            if (mResultReceiver != null) {
208                                Bundle state = captureSharedElementState();
209                                moveSharedElementsToOverlay();
210                                mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
211                            }
212                            return true;
213                        }
214                    });
215        }
216        if (allowOverlappingTransitions()) {
217            startEnterTransitionOnly();
218        }
219    }
220
221    private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
222        return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
223    }
224
225    @Override
226    protected void onReceiveResult(int resultCode, Bundle resultData) {
227        switch (resultCode) {
228            case MSG_TAKE_SHARED_ELEMENTS:
229                if (!mIsCanceled) {
230                    mSharedElementsBundle = resultData;
231                    onTakeSharedElements();
232                }
233                break;
234            case MSG_EXIT_TRANSITION_COMPLETE:
235                if (!mIsCanceled) {
236                    mIsExitTransitionComplete = true;
237                    if (mSharedElementTransitionStarted) {
238                        onRemoteExitTransitionComplete();
239                    }
240                }
241                break;
242            case MSG_CANCEL:
243                cancel();
244                break;
245        }
246    }
247
248    public boolean isWaitingForRemoteExit() {
249        return mIsReturning && mResultReceiver != null;
250    }
251
252    /**
253     * This is called onResume. If an Activity is resuming and the transitions
254     * haven't started yet, force the views to appear. This is likely to be
255     * caused by the top Activity finishing before the transitions started.
256     * In that case, we can finish any transition that was started, but we
257     * should cancel any pending transition and just bring those Views visible.
258     */
259    public void forceViewsToAppear() {
260        if (!mIsReturning) {
261            return;
262        }
263        if (!mIsReadyForTransition) {
264            mIsReadyForTransition = true;
265            final ViewGroup decor = getDecor();
266            if (decor != null && mViewsReadyListener != null) {
267                decor.getViewTreeObserver().removeOnPreDrawListener(mViewsReadyListener);
268                mViewsReadyListener = null;
269            }
270            showViews(mTransitioningViews, true);
271            setTransitioningViewsVisiblity(View.VISIBLE, true);
272            mSharedElements.clear();
273            mAllSharedElementNames.clear();
274            mTransitioningViews.clear();
275            mIsReadyForTransition = true;
276            viewsTransitionComplete();
277            sharedElementTransitionComplete();
278        } else {
279            if (!mSharedElementTransitionStarted) {
280                moveSharedElementsFromOverlay();
281                mSharedElementTransitionStarted = true;
282                showViews(mSharedElements, true);
283                mSharedElements.clear();
284                sharedElementTransitionComplete();
285            }
286            if (!mIsViewsTransitionStarted) {
287                mIsViewsTransitionStarted = true;
288                showViews(mTransitioningViews, true);
289                setTransitioningViewsVisiblity(View.VISIBLE, true);
290                mTransitioningViews.clear();
291                viewsTransitionComplete();
292            }
293            cancelPendingTransitions();
294        }
295        mAreViewsReady = true;
296        if (mResultReceiver != null) {
297            mResultReceiver.send(MSG_CANCEL, null);
298            mResultReceiver = null;
299        }
300    }
301
302    private void cancel() {
303        if (!mIsCanceled) {
304            mIsCanceled = true;
305            if (getViewsTransition() == null || mIsViewsTransitionStarted) {
306                showViews(mSharedElements, true);
307            } else if (mTransitioningViews != null) {
308                mTransitioningViews.addAll(mSharedElements);
309            }
310            moveSharedElementsFromOverlay();
311            mSharedElementNames.clear();
312            mSharedElements.clear();
313            mAllSharedElementNames.clear();
314            startSharedElementTransition(null);
315            onRemoteExitTransitionComplete();
316        }
317    }
318
319    public boolean isReturning() {
320        return mIsReturning;
321    }
322
323    protected void prepareEnter() {
324        ViewGroup decorView = getDecor();
325        if (mActivity == null || decorView == null) {
326            return;
327        }
328        mActivity.overridePendingTransition(0, 0);
329        if (!mIsReturning) {
330            mWasOpaque = mActivity.convertToTranslucent(null, null);
331            Drawable background = decorView.getBackground();
332            if (background != null) {
333                getWindow().setBackgroundDrawable(null);
334                background = background.mutate();
335                background.setAlpha(0);
336                getWindow().setBackgroundDrawable(background);
337            }
338        } else {
339            mActivity = null; // all done with it now.
340        }
341    }
342
343    @Override
344    protected Transition getViewsTransition() {
345        Window window = getWindow();
346        if (window == null) {
347            return null;
348        }
349        if (mIsReturning) {
350            return window.getReenterTransition();
351        } else {
352            return window.getEnterTransition();
353        }
354    }
355
356    protected Transition getSharedElementTransition() {
357        Window window = getWindow();
358        if (window == null) {
359            return null;
360        }
361        if (mIsReturning) {
362            return window.getSharedElementReenterTransition();
363        } else {
364            return window.getSharedElementEnterTransition();
365        }
366    }
367
368    private void startSharedElementTransition(Bundle sharedElementState) {
369        ViewGroup decorView = getDecor();
370        if (decorView == null) {
371            return;
372        }
373        // Remove rejected shared elements
374        ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
375        rejectedNames.removeAll(mSharedElementNames);
376        ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
377        if (mListener != null) {
378            mListener.onRejectSharedElements(rejectedSnapshots);
379        }
380        removeNullViews(rejectedSnapshots);
381        startRejectedAnimations(rejectedSnapshots);
382
383        // Now start shared element transition
384        ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
385                mSharedElementNames);
386        showViews(mSharedElements, true);
387        scheduleSetSharedElementEnd(sharedElementSnapshots);
388        ArrayList<SharedElementOriginalState> originalImageViewState =
389                setSharedElementState(sharedElementState, sharedElementSnapshots);
390        requestLayoutForSharedElements();
391
392        boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
393        boolean startSharedElementTransition = true;
394        setGhostVisibility(View.INVISIBLE);
395        scheduleGhostVisibilityChange(View.INVISIBLE);
396        pauseInput();
397        Transition transition = beginTransition(decorView, startEnterTransition,
398                startSharedElementTransition);
399        scheduleGhostVisibilityChange(View.VISIBLE);
400        setGhostVisibility(View.VISIBLE);
401
402        if (startEnterTransition) {
403            startEnterTransition(transition);
404        }
405
406        setOriginalSharedElementState(mSharedElements, originalImageViewState);
407
408        if (mResultReceiver != null) {
409            // We can't trust that the view will disappear on the same frame that the shared
410            // element appears here. Assure that we get at least 2 frames for double-buffering.
411            decorView.postOnAnimation(new Runnable() {
412                int mAnimations;
413
414                @Override
415                public void run() {
416                    if (mAnimations++ < MIN_ANIMATION_FRAMES) {
417                        View decorView = getDecor();
418                        if (decorView != null) {
419                            decorView.postOnAnimation(this);
420                        }
421                    } else if (mResultReceiver != null) {
422                        mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
423                        mResultReceiver = null; // all done sending messages.
424                    }
425                }
426            });
427        }
428    }
429
430    private static void removeNullViews(ArrayList<View> views) {
431        if (views != null) {
432            for (int i = views.size() - 1; i >= 0; i--) {
433                if (views.get(i) == null) {
434                    views.remove(i);
435                }
436            }
437        }
438    }
439
440    private void onTakeSharedElements() {
441        if (!mIsReadyForTransition || mSharedElementsBundle == null) {
442            return;
443        }
444        final Bundle sharedElementState = mSharedElementsBundle;
445        mSharedElementsBundle = null;
446        OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
447            @Override
448            public void onSharedElementsReady() {
449                final View decorView = getDecor();
450                if (decorView != null) {
451                    decorView.getViewTreeObserver()
452                            .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
453                                @Override
454                                public boolean onPreDraw() {
455                                    decorView.getViewTreeObserver().removeOnPreDrawListener(this);
456                                    startTransition(new Runnable() {
457                                        @Override
458                                        public void run() {
459                                            startSharedElementTransition(sharedElementState);
460                                        }
461                                    });
462                                    return false;
463                                }
464                            });
465                    decorView.invalidate();
466                }
467            }
468        };
469        if (mListener == null) {
470            listener.onSharedElementsReady();
471        } else {
472            mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
473        }
474    }
475
476    private void requestLayoutForSharedElements() {
477        int numSharedElements = mSharedElements.size();
478        for (int i = 0; i < numSharedElements; i++) {
479            mSharedElements.get(i).requestLayout();
480        }
481    }
482
483    private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
484            boolean startSharedElementTransition) {
485        Transition sharedElementTransition = null;
486        if (startSharedElementTransition) {
487            if (!mSharedElementNames.isEmpty()) {
488                sharedElementTransition = configureTransition(getSharedElementTransition(), false);
489            }
490            if (sharedElementTransition == null) {
491                sharedElementTransitionStarted();
492                sharedElementTransitionComplete();
493            } else {
494                sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
495                    @Override
496                    public void onTransitionStart(Transition transition) {
497                        sharedElementTransitionStarted();
498                    }
499
500                    @Override
501                    public void onTransitionEnd(Transition transition) {
502                        transition.removeListener(this);
503                        sharedElementTransitionComplete();
504                    }
505                });
506            }
507        }
508        Transition viewsTransition = null;
509        if (startEnterTransition) {
510            mIsViewsTransitionStarted = true;
511            if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
512                viewsTransition = configureTransition(getViewsTransition(), true);
513                if (viewsTransition != null && !mIsReturning) {
514                    stripOffscreenViews();
515                }
516            }
517            if (viewsTransition == null) {
518                viewsTransitionComplete();
519            } else {
520                final ArrayList<View> transitioningViews = mTransitioningViews;
521                viewsTransition.addListener(new ContinueTransitionListener() {
522                    @Override
523                    public void onTransitionStart(Transition transition) {
524                        mEnterViewsTransition = transition;
525                        if (transitioningViews != null) {
526                            showViews(transitioningViews, false);
527                        }
528                        super.onTransitionStart(transition);
529                    }
530
531                    @Override
532                    public void onTransitionEnd(Transition transition) {
533                        mEnterViewsTransition = null;
534                        transition.removeListener(this);
535                        viewsTransitionComplete();
536                        super.onTransitionEnd(transition);
537                    }
538                });
539            }
540        }
541
542        Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
543        if (transition != null) {
544            transition.addListener(new ContinueTransitionListener());
545            if (startEnterTransition) {
546                setTransitioningViewsVisiblity(View.INVISIBLE, false);
547            }
548            TransitionManager.beginDelayedTransition(decorView, transition);
549            if (startEnterTransition) {
550                setTransitioningViewsVisiblity(View.VISIBLE, false);
551            }
552            decorView.invalidate();
553        } else {
554            transitionStarted();
555        }
556        return transition;
557    }
558
559    @Override
560    protected void onTransitionsComplete() {
561        moveSharedElementsFromOverlay();
562        final ViewGroup decorView = getDecor();
563        if (decorView != null) {
564            decorView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
565        }
566    }
567
568    private void sharedElementTransitionStarted() {
569        mSharedElementTransitionStarted = true;
570        if (mIsExitTransitionComplete) {
571            send(MSG_EXIT_TRANSITION_COMPLETE, null);
572        }
573    }
574
575    private void startEnterTransition(Transition transition) {
576        ViewGroup decorView = getDecor();
577        if (!mIsReturning && decorView != null) {
578            Drawable background = decorView.getBackground();
579            if (background != null) {
580                background = background.mutate();
581                getWindow().setBackgroundDrawable(background);
582                mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
583                mBackgroundAnimator.setDuration(getFadeDuration());
584                mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
585                    @Override
586                    public void onAnimationEnd(Animator animation) {
587                        makeOpaque();
588                    }
589                });
590                mBackgroundAnimator.start();
591            } else if (transition != null) {
592                transition.addListener(new Transition.TransitionListenerAdapter() {
593                    @Override
594                    public void onTransitionEnd(Transition transition) {
595                        transition.removeListener(this);
596                        makeOpaque();
597                    }
598                });
599            } else {
600                makeOpaque();
601            }
602        }
603    }
604
605    public void stop() {
606        // Restore the background to its previous state since the
607        // Activity is stopping.
608        if (mBackgroundAnimator != null) {
609            mBackgroundAnimator.end();
610            mBackgroundAnimator = null;
611        } else if (mWasOpaque) {
612            ViewGroup decorView = getDecor();
613            if (decorView != null) {
614                Drawable drawable = decorView.getBackground();
615                if (drawable != null) {
616                    drawable.setAlpha(1);
617                }
618            }
619        }
620        makeOpaque();
621        mIsCanceled = true;
622        mResultReceiver = null;
623        mActivity = null;
624        moveSharedElementsFromOverlay();
625        if (mTransitioningViews != null) {
626            showViews(mTransitioningViews, true);
627            setTransitioningViewsVisiblity(View.VISIBLE, true);
628        }
629        showViews(mSharedElements, true);
630        clearState();
631    }
632
633    /**
634     * Cancels the enter transition.
635     * @return True if the enter transition is still pending capturing the target state. If so,
636     * any transition started on the decor will do nothing.
637     */
638    public boolean cancelEnter() {
639        setGhostVisibility(View.INVISIBLE);
640        mHasStopped = true;
641        mIsCanceled = true;
642        clearState();
643        return super.cancelPendingTransitions();
644    }
645
646    @Override
647    protected void clearState() {
648        mSharedElementsBundle = null;
649        mEnterViewsTransition = null;
650        mResultReceiver = null;
651        if (mBackgroundAnimator != null) {
652            mBackgroundAnimator.cancel();
653            mBackgroundAnimator = null;
654        }
655        super.clearState();
656    }
657
658    private void makeOpaque() {
659        if (!mHasStopped && mActivity != null) {
660            if (mWasOpaque) {
661                mActivity.convertFromTranslucent();
662            }
663            mActivity = null;
664        }
665    }
666
667    private boolean allowOverlappingTransitions() {
668        return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
669                : getWindow().getAllowEnterTransitionOverlap();
670    }
671
672    private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
673        if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
674            return;
675        }
676        final ViewGroup decorView = getDecor();
677        if (decorView != null) {
678            ViewGroupOverlay overlay = decorView.getOverlay();
679            ObjectAnimator animator = null;
680            int numRejected = rejectedSnapshots.size();
681            for (int i = 0; i < numRejected; i++) {
682                View snapshot = rejectedSnapshots.get(i);
683                overlay.add(snapshot);
684                animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
685                animator.start();
686            }
687            animator.addListener(new AnimatorListenerAdapter() {
688                @Override
689                public void onAnimationEnd(Animator animation) {
690                    ViewGroupOverlay overlay = decorView.getOverlay();
691                    int numRejected = rejectedSnapshots.size();
692                    for (int i = 0; i < numRejected; i++) {
693                        overlay.remove(rejectedSnapshots.get(i));
694                    }
695                }
696            });
697        }
698    }
699
700    protected void onRemoteExitTransitionComplete() {
701        if (!allowOverlappingTransitions()) {
702            startEnterTransitionOnly();
703        }
704    }
705
706    private void startEnterTransitionOnly() {
707        startTransition(new Runnable() {
708            @Override
709            public void run() {
710                boolean startEnterTransition = true;
711                boolean startSharedElementTransition = false;
712                ViewGroup decorView = getDecor();
713                if (decorView != null) {
714                    Transition transition = beginTransition(decorView, startEnterTransition,
715                            startSharedElementTransition);
716                    startEnterTransition(transition);
717                }
718            }
719        });
720    }
721}
722