1 /*
2  * Copyright (C) 2020 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  */
16 
17 package com.android.keyguard;
18 
19 import static androidx.constraintlayout.widget.ConstraintSet.END;
20 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
21 
22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
23 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
24 
25 import android.animation.Animator;
26 import android.animation.ValueAnimator;
27 import android.annotation.Nullable;
28 import android.content.res.Configuration;
29 import android.graphics.Rect;
30 import android.transition.ChangeBounds;
31 import android.transition.Transition;
32 import android.transition.TransitionListenerAdapter;
33 import android.transition.TransitionManager;
34 import android.transition.TransitionSet;
35 import android.transition.TransitionValues;
36 import android.util.Slog;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.FrameLayout;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.constraintlayout.widget.ConstraintLayout;
44 import androidx.constraintlayout.widget.ConstraintSet;
45 import androidx.viewpager.widget.ViewPager;
46 
47 import com.android.app.animation.Interpolators;
48 import com.android.internal.jank.InteractionJankMonitor;
49 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
50 import com.android.keyguard.logging.KeyguardLogger;
51 import com.android.systemui.Dumpable;
52 import com.android.systemui.animation.ViewHierarchyAnimator;
53 import com.android.systemui.dump.DumpManager;
54 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
55 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
56 import com.android.systemui.plugins.clocks.ClockController;
57 import com.android.systemui.power.domain.interactor.PowerInteractor;
58 import com.android.systemui.power.shared.model.ScreenPowerState;
59 import com.android.systemui.res.R;
60 import com.android.systemui.statusbar.notification.AnimatableProperty;
61 import com.android.systemui.statusbar.notification.PropertyAnimator;
62 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
63 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
64 import com.android.systemui.statusbar.phone.DozeParameters;
65 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
66 import com.android.systemui.statusbar.policy.ConfigurationController;
67 import com.android.systemui.statusbar.policy.KeyguardStateController;
68 import com.android.systemui.util.ViewController;
69 
70 import kotlin.coroutines.CoroutineContext;
71 import kotlin.coroutines.EmptyCoroutineContext;
72 
73 import java.io.PrintWriter;
74 
75 import javax.inject.Inject;
76 
77 /**
78  * Injectable controller for {@link KeyguardStatusView}.
79  */
80 public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> implements
81         Dumpable {
82     private static final boolean DEBUG = KeyguardConstants.DEBUG;
83     @VisibleForTesting static final String TAG = "KeyguardStatusViewController";
84     private static final long STATUS_AREA_HEIGHT_ANIMATION_MILLIS = 133;
85 
86     /**
87      * Duration to use for the animator when the keyguard status view alignment changes, and a
88      * custom clock animation is in use.
89      */
90     private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000;
91 
92     public static final AnimationProperties CLOCK_ANIMATION_PROPERTIES =
93             new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
94 
95     private final KeyguardSliceViewController mKeyguardSliceViewController;
96     private final KeyguardClockSwitchController mKeyguardClockSwitchController;
97     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
98     private final ConfigurationController mConfigurationController;
99     private final KeyguardVisibilityHelper mKeyguardVisibilityHelper;
100     private final InteractionJankMonitor mInteractionJankMonitor;
101     private final Rect mClipBounds = new Rect();
102     private final KeyguardInteractor mKeyguardInteractor;
103     private final PowerInteractor mPowerInteractor;
104     private final DozeParameters mDozeParameters;
105 
106     private View mStatusArea = null;
107     private ValueAnimator mStatusAreaHeightAnimator = null;
108 
109     private Boolean mSplitShadeEnabled = false;
110     private Boolean mStatusViewCentered = true;
111     private DumpManager mDumpManager;
112 
113     private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener =
114             new TransitionListenerAdapter() {
115                 @Override
116                 public void onTransitionCancel(Transition transition) {
117                     mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
118                 }
119 
120                 @Override
121                 public void onTransitionEnd(Transition transition) {
122                     mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
123                 }
124             };
125 
126     private final View.OnLayoutChangeListener mStatusAreaLayoutChangeListener =
127             new View.OnLayoutChangeListener() {
128                 @Override
129                 public void onLayoutChange(View v,
130                         int left, int top, int right, int bottom,
131                         int oldLeft, int oldTop, int oldRight, int oldBottom
132                 ) {
133                     if (!mDozeParameters.getAlwaysOn()) {
134                         return;
135                     }
136 
137                     int oldHeight = oldBottom - oldTop;
138                     int diff = v.getHeight() - oldHeight;
139                     if (diff == 0) {
140                         return;
141                     }
142 
143                     int startValue = -1 * diff;
144                     long duration = STATUS_AREA_HEIGHT_ANIMATION_MILLIS;
145                     if (mStatusAreaHeightAnimator != null
146                             && mStatusAreaHeightAnimator.isRunning()) {
147                         duration += mStatusAreaHeightAnimator.getDuration()
148                                 - mStatusAreaHeightAnimator.getCurrentPlayTime();
149                         startValue += (int) mStatusAreaHeightAnimator.getAnimatedValue();
150                         mStatusAreaHeightAnimator.cancel();
151                         mStatusAreaHeightAnimator = null;
152                     }
153 
154                     mStatusAreaHeightAnimator = ValueAnimator.ofInt(startValue, 0);
155                     mStatusAreaHeightAnimator.setDuration(duration);
156                     final View nic = mKeyguardClockSwitchController.getAodNotifIconContainer();
157                     if (nic != null) {
158                         mStatusAreaHeightAnimator.addUpdateListener(anim -> {
159                             nic.setTranslationY((int) anim.getAnimatedValue());
160                         });
161                     }
162                     mStatusAreaHeightAnimator.start();
163                 }
164             };
165 
166     @Inject
KeyguardStatusViewController( KeyguardStatusView keyguardStatusView, KeyguardSliceViewController keyguardSliceViewController, KeyguardClockSwitchController keyguardClockSwitchController, KeyguardStateController keyguardStateController, KeyguardUpdateMonitor keyguardUpdateMonitor, ConfigurationController configurationController, DozeParameters dozeParameters, ScreenOffAnimationController screenOffAnimationController, KeyguardLogger logger, InteractionJankMonitor interactionJankMonitor, KeyguardInteractor keyguardInteractor, DumpManager dumpManager, PowerInteractor powerInteractor)167     public KeyguardStatusViewController(
168             KeyguardStatusView keyguardStatusView,
169             KeyguardSliceViewController keyguardSliceViewController,
170             KeyguardClockSwitchController keyguardClockSwitchController,
171             KeyguardStateController keyguardStateController,
172             KeyguardUpdateMonitor keyguardUpdateMonitor,
173             ConfigurationController configurationController,
174             DozeParameters dozeParameters,
175             ScreenOffAnimationController screenOffAnimationController,
176             KeyguardLogger logger,
177             InteractionJankMonitor interactionJankMonitor,
178             KeyguardInteractor keyguardInteractor,
179             DumpManager dumpManager,
180             PowerInteractor powerInteractor) {
181         super(keyguardStatusView);
182         mKeyguardSliceViewController = keyguardSliceViewController;
183         mKeyguardClockSwitchController = keyguardClockSwitchController;
184         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
185         mConfigurationController = configurationController;
186         mDozeParameters = dozeParameters;
187         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
188                 dozeParameters, screenOffAnimationController, /* animateYPos= */ true,
189                 logger.getBuffer());
190         mInteractionJankMonitor = interactionJankMonitor;
191         mDumpManager = dumpManager;
192         mKeyguardInteractor = keyguardInteractor;
193         mPowerInteractor = powerInteractor;
194     }
195 
196     @Override
onInit()197     public void onInit() {
198         mKeyguardClockSwitchController.init();
199         final View mediaHostContainer = mView.findViewById(R.id.status_view_media_container);
200         if (mediaHostContainer != null) {
201             mKeyguardClockSwitchController.getView().addOnLayoutChangeListener(
202                     (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
203                         if (!mSplitShadeEnabled
204                                 || mKeyguardClockSwitchController.getView().getSplitShadeCentered()
205                                 // Note: isKeyguardVisible() returns false after Launcher -> AOD.
206                                 || !mKeyguardUpdateMonitor.isKeyguardVisible()) {
207                             return;
208                         }
209 
210                         int oldHeight = oldBottom - oldTop;
211                         if (v.getHeight() == oldHeight) return;
212 
213                         if (mediaHostContainer.getVisibility() != View.VISIBLE
214                                 // If the media is appearing, also don't do the transition.
215                                 || mediaHostContainer.getHeight() == 0) {
216                             return;
217                         }
218 
219                         ViewHierarchyAnimator.Companion.animateNextUpdate(mediaHostContainer,
220                                 Interpolators.STANDARD, /* duration= */ 500L,
221                                 /* animateChildren= */ false);
222                     });
223         }
224 
225         mDumpManager.registerDumpable(getInstanceName(), this);
226         if (MigrateClocksToBlueprint.isEnabled()) {
227             startCoroutines(EmptyCoroutineContext.INSTANCE);
228             mView.setVisibility(View.GONE);
229         }
230     }
231 
startCoroutines(CoroutineContext context)232     void startCoroutines(CoroutineContext context) {
233         collectFlow(mView, mKeyguardInteractor.getDozeTimeTick(),
234                 (Long millis) -> {
235                         dozeTimeTick();
236                 }, context);
237 
238         collectFlow(mView, mPowerInteractor.getScreenPowerState(),
239                 (ScreenPowerState powerState) -> {
240                     if (powerState == ScreenPowerState.SCREEN_TURNING_ON) {
241                         dozeTimeTick();
242                     }
243                 }, context);
244     }
245 
getView()246     public KeyguardStatusView getView() {
247         return mView;
248     }
249 
250     @Override
onViewAttached()251     protected void onViewAttached() {
252         mStatusArea = mView.findViewById(R.id.keyguard_status_area);
253         if (MigrateClocksToBlueprint.isEnabled()) {
254             return;
255         }
256 
257         mStatusArea.addOnLayoutChangeListener(mStatusAreaLayoutChangeListener);
258         mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
259         mConfigurationController.addCallback(mConfigurationListener);
260     }
261 
262     @Override
onViewDetached()263     protected void onViewDetached() {
264         if (MigrateClocksToBlueprint.isEnabled()) {
265             return;
266         }
267 
268         mStatusArea.removeOnLayoutChangeListener(mStatusAreaLayoutChangeListener);
269         mKeyguardUpdateMonitor.removeCallback(mInfoCallback);
270         mConfigurationController.removeCallback(mConfigurationListener);
271     }
272 
273     /** Sets the StatusView as shown on an external display. */
setDisplayedOnSecondaryDisplay()274     public void setDisplayedOnSecondaryDisplay() {
275         mKeyguardClockSwitchController.setShownOnSecondaryDisplay(true);
276     }
277 
278     /**
279      * Called in notificationPanelViewController to avoid leak
280      */
onDestroy()281     public void onDestroy() {
282         mDumpManager.unregisterDumpable(getInstanceName());
283     }
284 
285     /**
286      * Updates views on doze time tick.
287      */
dozeTimeTick()288     public void dozeTimeTick() {
289         refreshTime();
290         mKeyguardSliceViewController.refresh();
291     }
292 
293     /**
294      * Set which clock should be displayed on the keyguard. The other one will be automatically
295      * hidden.
296      */
displayClock(@lockSize int clockSize, boolean animate)297     public void displayClock(@ClockSize int clockSize, boolean animate) {
298         mKeyguardClockSwitchController.displayClock(clockSize, animate);
299     }
300 
301     /**
302      * Performs fold to aod animation of the clocks (changes font weight from bold to thin).
303      * This animation is played when AOD is enabled and foldable device is fully folded, it is
304      * displayed on the outer screen
305      * @param foldFraction current fraction of fold animation complete
306      */
animateFoldToAod(float foldFraction)307     public void animateFoldToAod(float foldFraction) {
308         mKeyguardClockSwitchController.animateFoldToAod(foldFraction);
309     }
310 
311     /**
312      * Sets a translationY on the views on the keyguard, except on the media view.
313      */
setTranslationY(float translationY, boolean excludeMedia)314     public void setTranslationY(float translationY, boolean excludeMedia) {
315         mView.setChildrenTranslationY(translationY, excludeMedia);
316     }
317 
318     /**
319      * Set keyguard status view alpha.
320      */
setAlpha(float alpha)321     public void setAlpha(float alpha) {
322         if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) {
323             mView.setAlpha(alpha);
324         }
325     }
326 
327     /**
328      * Update the pivot position based on the parent view
329      */
updatePivot(float parentWidth, float parentHeight)330     public void updatePivot(float parentWidth, float parentHeight) {
331         mView.setPivotX(parentWidth / 2f);
332         mView.setPivotY(mKeyguardClockSwitchController.getClockHeight() / 2f);
333     }
334 
335     /**
336      * Get the height of the keyguard status view without the notification icon area, as that's
337      * only visible on AOD.
338      *
339      * We internally animate height changes to the status area to prevent discontinuities in the
340      * doze animation introduced by the height suddenly changing due to smartpace.
341      */
getLockscreenHeight()342     public int getLockscreenHeight() {
343         int heightAnimValue = mStatusAreaHeightAnimator == null ? 0 :
344                 (int) mStatusAreaHeightAnimator.getAnimatedValue();
345         return mView.getHeight() + heightAnimValue
346                 - mKeyguardClockSwitchController.getNotificationIconAreaHeight();
347     }
348 
349     /**
350      * Get y-bottom position of the currently visible clock.
351      */
getClockBottom(int statusBarHeaderHeight)352     public int getClockBottom(int statusBarHeaderHeight) {
353         return mKeyguardClockSwitchController.getClockBottom(statusBarHeaderHeight);
354     }
355 
356     /**
357      * @return true if the currently displayed clock is top aligned (as opposed to center aligned)
358      */
isClockTopAligned()359     public boolean isClockTopAligned() {
360         return mKeyguardClockSwitchController.isClockTopAligned();
361     }
362 
363     /**
364      * Pass top margin from ClockPositionAlgorithm in NotificationPanelViewController
365      * Use for clock view in LS to compensate for top margin to align to the screen
366      * Regardless of translation from AOD and unlock gestures
367      */
setLockscreenClockY(int clockY)368     public void setLockscreenClockY(int clockY) {
369         mKeyguardClockSwitchController.setLockscreenClockY(clockY);
370     }
371 
372     /**
373      * Set whether the view accessibility importance mode.
374      */
setStatusAccessibilityImportance(int mode)375     public void setStatusAccessibilityImportance(int mode) {
376         mView.setImportantForAccessibility(mode);
377     }
378 
379     @VisibleForTesting
setProperty(AnimatableProperty property, float value, boolean animate)380     void setProperty(AnimatableProperty property, float value, boolean animate) {
381         PropertyAnimator.setProperty(mView, property, value, CLOCK_ANIMATION_PROPERTIES, animate);
382     }
383 
384     /**
385      * Update position of the view with an optional animation
386      */
updatePosition(int x, int y, float scale, boolean animate)387     public void updatePosition(int x, int y, float scale, boolean animate) {
388         setProperty(AnimatableProperty.Y, y, animate);
389 
390         ClockController clock = mKeyguardClockSwitchController.getClock();
391         if (clock != null && clock.getConfig().getUseAlternateSmartspaceAODTransition()) {
392             // If requested, scale the entire view instead of just the clock view
393             mKeyguardClockSwitchController.updatePosition(x, 1f /* scale */,
394                     CLOCK_ANIMATION_PROPERTIES, animate);
395             setProperty(AnimatableProperty.SCALE_X, scale, animate);
396             setProperty(AnimatableProperty.SCALE_Y, scale, animate);
397         } else {
398             mKeyguardClockSwitchController.updatePosition(x, scale,
399                     CLOCK_ANIMATION_PROPERTIES, animate);
400             setProperty(AnimatableProperty.SCALE_X, 1f, animate);
401             setProperty(AnimatableProperty.SCALE_Y, 1f, animate);
402         }
403     }
404 
405     /**
406      * Set the visibility of the keyguard status view based on some new state.
407      */
setKeyguardStatusViewVisibility( int statusBarState, boolean keyguardFadingAway, boolean goingToFullShade, int oldStatusBarState)408     public void setKeyguardStatusViewVisibility(
409             int statusBarState,
410             boolean keyguardFadingAway,
411             boolean goingToFullShade,
412             int oldStatusBarState) {
413         mKeyguardVisibilityHelper.setViewVisibility(
414                 statusBarState, keyguardFadingAway, goingToFullShade, oldStatusBarState);
415     }
416 
refreshTime()417     private void refreshTime() {
418         mKeyguardClockSwitchController.refresh();
419     }
420 
421     private final ConfigurationController.ConfigurationListener mConfigurationListener =
422             new ConfigurationController.ConfigurationListener() {
423         @Override
424         public void onLocaleListChanged() {
425             refreshTime();
426             mKeyguardClockSwitchController.onLocaleListChanged();
427         }
428 
429         @Override
430         public void onConfigChanged(Configuration newConfig) {
431             mKeyguardClockSwitchController.onConfigChanged();
432         }
433     };
434 
435     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
436         @Override
437         public void onTimeChanged() {
438             Slog.v(TAG, "onTimeChanged");
439             refreshTime();
440         }
441 
442         @Override
443         public void onKeyguardVisibilityChanged(boolean visible) {
444             if (visible) {
445                 if (DEBUG) Slog.v(TAG, "refresh statusview visible:true");
446                 refreshTime();
447             }
448         }
449     };
450 
451     /**
452      * Rect that specifies how KSV should be clipped, on its parent's coordinates.
453      */
setClipBounds(Rect clipBounds)454     public void setClipBounds(Rect clipBounds) {
455         if (clipBounds != null) {
456             mClipBounds.set(clipBounds.left, (int) (clipBounds.top - mView.getY()),
457                     clipBounds.right, (int) (clipBounds.bottom - mView.getY()));
458             mView.setClipBounds(mClipBounds);
459         } else {
460             mView.setClipBounds(null);
461         }
462     }
463 
464     /**
465      * Returns true if the large clock will block the notification shelf in AOD
466      */
isLargeClockBlockingNotificationShelf()467     public boolean isLargeClockBlockingNotificationShelf() {
468         ClockController clock = mKeyguardClockSwitchController.getClock();
469         return clock != null && clock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay();
470     }
471 
472     /**
473      * Set if the split shade is enabled
474      */
setSplitShadeEnabled(boolean enabled)475     public void setSplitShadeEnabled(boolean enabled) {
476         mKeyguardClockSwitchController.setSplitShadeEnabled(enabled);
477         mSplitShadeEnabled = enabled;
478     }
479 
480     /**
481      * Updates the alignment of the KeyguardStatusView and animates the transition if requested.
482      */
updateAlignment( ConstraintLayout layout, boolean splitShadeEnabled, boolean shouldBeCentered, boolean animate)483     public void updateAlignment(
484             ConstraintLayout layout,
485             boolean splitShadeEnabled,
486             boolean shouldBeCentered,
487             boolean animate) {
488         if (MigrateClocksToBlueprint.isEnabled()) {
489             mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
490         } else {
491             mKeyguardClockSwitchController.setSplitShadeCentered(
492                     splitShadeEnabled && shouldBeCentered);
493         }
494         if (mStatusViewCentered == shouldBeCentered) {
495             return;
496         }
497 
498         mStatusViewCentered = shouldBeCentered;
499         if (layout == null) {
500             return;
501         }
502 
503         ConstraintSet constraintSet = new ConstraintSet();
504         constraintSet.clone(layout);
505         int guideline;
506         if (MigrateClocksToBlueprint.isEnabled()) {
507             guideline = R.id.split_shade_guideline;
508         } else {
509             guideline = R.id.qs_edge_guideline;
510         }
511 
512         int statusConstraint = shouldBeCentered ? PARENT_ID : guideline;
513         constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END);
514         if (!animate) {
515             constraintSet.applyTo(layout);
516             return;
517         }
518 
519         mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
520         /* This transition blocks any layout changes while running. For that reason
521         * special logic with setting visibility was added to {@link BcSmartspaceView#setDozing}
522         * for split shade to avoid jump of the media object. */
523         ChangeBounds transition = new ChangeBounds();
524         if (splitShadeEnabled) {
525             // Excluding media from the transition on split-shade, as it doesn't transition
526             // horizontally properly.
527             transition.excludeTarget(R.id.status_view_media_container, true);
528 
529             // Exclude smartspace viewpager and its children from the transition.
530             //     - Each step of the transition causes the ViewPager to invoke resize,
531             //     which invokes scrolling to the recalculated position. The scrolling
532             //     actions are congested, resulting in kinky translation, and
533             //     delay in settling to the final position. (http://b/281620564#comment1)
534             //     - Also, the scrolling is unnecessary in the transition. We just want
535             //     the viewpager to stay on the same page.
536             //     - Exclude by Class type instead of resource id, since the resource id
537             //     isn't available for all devices, and probably better to exclude all
538             //     ViewPagers any way.
539             transition.excludeTarget(ViewPager.class, true);
540             transition.excludeChildren(ViewPager.class, true);
541         }
542 
543         transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
544         transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
545 
546         ClockController clock = mKeyguardClockSwitchController.getClock();
547         boolean customClockAnimation = clock != null
548                 && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation();
549         // When migrateClocksToBlueprint is on, customized clock animation is conducted in
550         // KeyguardClockViewBinder
551         if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) {
552             // Find the clock, so we can exclude it from this transition.
553             FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large);
554 
555             // The clock container can sometimes be null. If it is, just fall back to the
556             // old animation rather than setting up the custom animations.
557             if (clockContainerView == null || clockContainerView.getChildCount() == 0) {
558                 transition.addListener(mKeyguardStatusAlignmentTransitionListener);
559                 TransitionManager.beginDelayedTransition(layout, transition);
560             } else {
561                 View clockView = clockContainerView.getChildAt(0);
562 
563                 TransitionSet set = new TransitionSet();
564                 set.addTransition(transition);
565 
566                 SplitShadeTransitionAdapter adapter =
567                         new SplitShadeTransitionAdapter(mKeyguardClockSwitchController);
568 
569                 // Use linear here, so the actual clock can pick its own interpolator.
570                 adapter.setInterpolator(Interpolators.LINEAR);
571                 adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION);
572                 adapter.addTarget(clockView);
573                 set.addTransition(adapter);
574 
575                 if (splitShadeEnabled) {
576                     // Exclude smartspace viewpager and its children from the transition set.
577                     //     - This is necessary in addition to excluding them from the
578                     //     ChangeBounds child transition.
579                     //     - Without this, the viewpager is scrolled to the new position
580                     //     (corresponding to its end size) before the size change is realized.
581                     //     Note that the size change is realized at the end of the ChangeBounds
582                     //     transition. With the "prescrolling", the viewpager ends up in a weird
583                     //     position, then recovers smoothly during the transition, and ends at
584                     //     the position for the current page.
585                     //     - Exclude by Class type instead of resource id, since the resource id
586                     //     isn't available for all devices, and probably better to exclude all
587                     //     ViewPagers any way.
588                     set.excludeTarget(ViewPager.class, true);
589                     set.excludeChildren(ViewPager.class, true);
590                 }
591 
592                 set.addListener(mKeyguardStatusAlignmentTransitionListener);
593                 TransitionManager.beginDelayedTransition(layout, set);
594             }
595         } else {
596             transition.addListener(mKeyguardStatusAlignmentTransitionListener);
597             TransitionManager.beginDelayedTransition(layout, transition);
598         }
599 
600         constraintSet.applyTo(layout);
601     }
602 
getClockController()603     public ClockController getClockController() {
604         return mKeyguardClockSwitchController.getClock();
605     }
606 
607     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)608     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
609         mView.dump(pw, args);
610     }
611 
getInstanceName()612     String getInstanceName() {
613         return TAG + "#" + hashCode();
614     }
615 
616     @VisibleForTesting
617     static class SplitShadeTransitionAdapter extends Transition {
618         private static final String PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft";
619         private static final String PROP_BOUNDS_RIGHT = "splitShadeTransitionAdapter:boundsRight";
620         private static final String PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow";
621         private static final String[] TRANSITION_PROPERTIES = {
622                 PROP_BOUNDS_LEFT, PROP_BOUNDS_RIGHT, PROP_X_IN_WINDOW};
623 
624         private final KeyguardClockSwitchController mController;
625 
626         @VisibleForTesting
SplitShadeTransitionAdapter(KeyguardClockSwitchController controller)627         SplitShadeTransitionAdapter(KeyguardClockSwitchController controller) {
628             mController = controller;
629         }
630 
captureValues(TransitionValues transitionValues)631         private void captureValues(TransitionValues transitionValues) {
632             transitionValues.values.put(PROP_BOUNDS_LEFT, transitionValues.view.getLeft());
633             transitionValues.values.put(PROP_BOUNDS_RIGHT, transitionValues.view.getRight());
634             int[] locationInWindowTmp = new int[2];
635             transitionValues.view.getLocationInWindow(locationInWindowTmp);
636             transitionValues.values.put(PROP_X_IN_WINDOW, locationInWindowTmp[0]);
637         }
638 
639         @Override
captureEndValues(TransitionValues transitionValues)640         public void captureEndValues(TransitionValues transitionValues) {
641             captureValues(transitionValues);
642         }
643 
644         @Override
captureStartValues(TransitionValues transitionValues)645         public void captureStartValues(TransitionValues transitionValues) {
646             captureValues(transitionValues);
647         }
648 
649         @Nullable
650         @Override
createAnimator(@onNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)651         public Animator createAnimator(@NonNull ViewGroup sceneRoot,
652                 @Nullable TransitionValues startValues,
653                 @Nullable TransitionValues endValues) {
654             if (startValues == null || endValues == null) {
655                 return null;
656             }
657             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
658 
659             int fromLeft = (int) startValues.values.get(PROP_BOUNDS_LEFT);
660             int fromWindowX = (int) startValues.values.get(PROP_X_IN_WINDOW);
661             int toWindowX = (int) endValues.values.get(PROP_X_IN_WINDOW);
662             // Using windowX, to determine direction, instead of left, as in RTL the difference of
663             // toLeft - fromLeft is always positive, even when moving left.
664             int direction = toWindowX - fromWindowX > 0 ? 1 : -1;
665 
666             anim.addUpdateListener(animation -> {
667                 ClockController clock = mController.getClock();
668                 if (clock == null) {
669                     return;
670                 }
671 
672                 clock.getLargeClock().getAnimations()
673                         .onPositionUpdated(fromLeft, direction, animation.getAnimatedFraction());
674             });
675 
676             return anim;
677         }
678 
679         @Override
getTransitionProperties()680         public String[] getTransitionProperties() {
681             return TRANSITION_PROPERTIES;
682         }
683     }
684 }
685