package com.android.keyguard; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; import androidx.core.content.res.ResourcesCompat; import com.android.app.animation.Interpolators; import com.android.keyguard.dagger.KeyguardStatusViewScope; import com.android.systemui.R; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.plugins.ClockController; import com.android.systemui.shared.clocks.DefaultClockController; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @KeyguardStatusViewScope public class KeyguardClockSwitch extends RelativeLayout { private static final String TAG = "KeyguardClockSwitch"; public static final String MISSING_CLOCK_ID = "CLOCK_MISSING"; private static final long CLOCK_OUT_MILLIS = 133; private static final long CLOCK_IN_MILLIS = 167; public static final long CLOCK_IN_START_DELAY_MILLIS = 133; private static final long STATUS_AREA_START_DELAY_MILLIS = 0; private static final long STATUS_AREA_MOVE_UP_MILLIS = 967; private static final long STATUS_AREA_MOVE_DOWN_MILLIS = 467; private static final float SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER = 1.4f; private static final float SMARTSPACE_TOP_PADDING_MULTIPLIER = 2.625f; @IntDef({LARGE, SMALL}) @Retention(RetentionPolicy.SOURCE) public @interface ClockSize { } public static final int LARGE = 0; public static final int SMALL = 1; // compensate for translation of parents subject to device screen // In this case, the translation comes from KeyguardStatusView public int screenOffsetYPadding = 0; /** Returns a region for the large clock to position itself, based on the given parent. */ public static Rect getLargeClockRegion(ViewGroup parent) { int largeClockTopMargin = parent.getResources() .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin); int targetHeight = parent.getResources() .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2; int top = parent.getHeight() / 2 - targetHeight / 2 + largeClockTopMargin / 2; return new Rect( parent.getLeft(), top, parent.getRight(), top + targetHeight); } /** Returns a region for the small clock to position itself, based on the given parent. */ public static Rect getSmallClockRegion(ViewGroup parent) { int targetHeight = parent.getResources() .getDimensionPixelSize(R.dimen.small_clock_text_size); return new Rect( parent.getLeft(), parent.getTop(), parent.getRight(), parent.getTop() + targetHeight); } /** * Frame for small/large clocks */ private KeyguardClockFrame mSmallClockFrame; private KeyguardClockFrame mLargeClockFrame; private ClockController mClock; // It's bc_smartspace_view, assigned by KeyguardClockSwitchController // to get the top padding for translating smartspace for weather clock private View mSmartspace; // Smartspace in weather clock is translated by this value // to compensate for the position invisible dateWeatherView private int mSmartspaceTop = -1; private KeyguardStatusAreaView mStatusArea; private int mSmartspaceTopOffset; private float mWeatherClockSmartspaceScaling = 1f; private int mWeatherClockSmartspaceTranslateX = 0; private int mWeatherClockSmartspaceTranslateY = 0; private int mDrawAlpha = 255; private int mStatusBarHeight = 0; /** * Maintain state so that a newly connected plugin can be initialized. */ private float mDarkAmount; private boolean mSplitShadeCentered = false; /** * Indicates which clock is currently displayed - should be one of {@link ClockSize}. * Use null to signify it is uninitialized. */ @ClockSize private Integer mDisplayedClockSize = null; @VisibleForTesting AnimatorSet mClockInAnim = null; @VisibleForTesting AnimatorSet mClockOutAnim = null; @VisibleForTesting AnimatorSet mStatusAreaAnim = null; private int mClockSwitchYAmount; @VisibleForTesting boolean mChildrenAreLaidOut = false; @VisibleForTesting boolean mAnimateOnLayout = true; private LogBuffer mLogBuffer = null; public KeyguardClockSwitch(Context context, AttributeSet attrs) { super(context, attrs); } /** * Apply dp changes on configuration change */ public void onConfigChanged() { mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_clock_switch_y_shift); mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize( R.dimen.keyguard_smartspace_top_offset) * mContext.getResources().getConfiguration().fontScale / mContext.getResources().getDisplayMetrics().density * SMARTSPACE_TOP_PADDING_MULTIPLIER); mWeatherClockSmartspaceScaling = ResourcesCompat.getFloat( mContext.getResources(), R.dimen.weather_clock_smartspace_scale); mWeatherClockSmartspaceTranslateX = mContext.getResources().getDimensionPixelSize( R.dimen.weather_clock_smartspace_translateX); mWeatherClockSmartspaceTranslateY = mContext.getResources().getDimensionPixelSize( R.dimen.weather_clock_smartspace_translateY); mStatusBarHeight = mContext.getResources().getDimensionPixelSize( R.dimen.status_bar_height); updateStatusArea(/* animate= */false); } /** Get bc_smartspace_view from KeyguardClockSwitchController * Use its top to decide the translation value */ public void setSmartspace(View smartspace) { mSmartspace = smartspace; } /** Sets whether the large clock is being shown on a connected display. */ public void setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay) { if (mClock != null) { mClock.getLargeClock().getEvents().onSecondaryDisplayChanged(onSecondaryDisplay); } } /** * Enable or disable split shade specific positioning */ public void setSplitShadeCentered(boolean splitShadeCentered) { if (mSplitShadeCentered != splitShadeCentered) { mSplitShadeCentered = splitShadeCentered; updateStatusArea(/* animate= */true); } } @Override protected void onFinishInflate() { super.onFinishInflate(); mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); mStatusArea = findViewById(R.id.keyguard_status_area); onConfigChanged(); } @Override protected boolean onSetAlpha(int alpha) { mDrawAlpha = alpha; return true; } @Override protected void dispatchDraw(Canvas canvas) { KeyguardClockFrame.saveCanvasAlpha( this, canvas, mDrawAlpha, c -> { super.dispatchDraw(c); return kotlin.Unit.INSTANCE; }); } public void setLogBuffer(LogBuffer logBuffer) { mLogBuffer = logBuffer; } public LogBuffer getLogBuffer() { return mLogBuffer; } /** Returns the id of the currently rendering clock */ public String getClockId() { if (mClock == null) { return MISSING_CLOCK_ID; } return mClock.getConfig().getId(); } void setClock(ClockController clock, int statusBarState) { mClock = clock; // Disconnect from existing plugin. mSmallClockFrame.removeAllViews(); mLargeClockFrame.removeAllViews(); if (clock == null) { if (mLogBuffer != null) { mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown"); } return; } // Attach small and big clock views to hierarchy. if (mLogBuffer != null) { mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch"); } mSmallClockFrame.addView(clock.getSmallClock().getView()); mLargeClockFrame.addView(clock.getLargeClock().getView()); updateClockTargetRegions(); updateStatusArea(/* animate= */false); } private void updateStatusArea(boolean animate) { if (mDisplayedClockSize != null && mChildrenAreLaidOut) { updateClockViews(mDisplayedClockSize == LARGE, animate); } } void updateClockTargetRegions() { if (mClock != null) { if (mSmallClockFrame.isLaidOut()) { Rect targetRegion = getSmallClockRegion(mSmallClockFrame); mClock.getSmallClock().getEvents().onTargetRegionChanged(targetRegion); } if (mLargeClockFrame.isLaidOut()) { Rect targetRegion = getLargeClockRegion(mLargeClockFrame); if (mClock instanceof DefaultClockController) { mClock.getLargeClock().getEvents().onTargetRegionChanged( targetRegion); } else { mClock.getLargeClock().getEvents().onTargetRegionChanged( new Rect( targetRegion.left, targetRegion.top - screenOffsetYPadding, targetRegion.right, targetRegion.bottom - screenOffsetYPadding)); } } } } private void updateClockViews(boolean useLargeClock, boolean animate) { if (mLogBuffer != null) { mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> { msg.setBool1(useLargeClock); msg.setBool2(animate); msg.setBool3(mChildrenAreLaidOut); return kotlin.Unit.INSTANCE; }, (msg) -> "updateClockViews" + "; useLargeClock=" + msg.getBool1() + "; animate=" + msg.getBool2() + "; mChildrenAreLaidOut=" + msg.getBool3()); } if (mClockInAnim != null) mClockInAnim.cancel(); if (mClockOutAnim != null) mClockOutAnim.cancel(); if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); mClockInAnim = null; mClockOutAnim = null; mStatusAreaAnim = null; View in, out; // statusAreaYTranslation uses for the translation for both mStatusArea and mSmallClockFrame // statusAreaClockTranslateY only uses for mStatusArea float statusAreaYTranslation, statusAreaClockScale = 1f; float statusAreaClockTranslateX = 0f, statusAreaClockTranslateY = 0f; float clockInYTranslation, clockOutYTranslation; if (useLargeClock) { out = mSmallClockFrame; in = mLargeClockFrame; if (indexOfChild(in) == -1) addView(in, 0); statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop() + mSmartspaceTopOffset; // TODO: Load from clock config when less risky if (mClock != null && mClock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay()) { statusAreaClockScale = mWeatherClockSmartspaceScaling; statusAreaClockTranslateX = mWeatherClockSmartspaceTranslateX; if (mSplitShadeCentered) { statusAreaClockTranslateX *= SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER; } // On large weather clock, // top padding for time is status bar height from top of the screen. // On small one, // it's screenOffsetYPadding (translationY for KeyguardStatusView), // Cause smartspace is positioned according to the smallClockFrame // we need to translate the difference between bottom of large clock and small clock // Also, we need to counter offset the empty date weather view, mSmartspaceTop // mWeatherClockSmartspaceTranslateY is only for Felix statusAreaClockTranslateY = mStatusBarHeight - 0.6F * mSmallClockFrame.getHeight() - mSmartspaceTop - screenOffsetYPadding - statusAreaYTranslation + mWeatherClockSmartspaceTranslateY; } clockInYTranslation = 0; clockOutYTranslation = 0; // Small clock translation is handled with statusArea } else { in = mSmallClockFrame; out = mLargeClockFrame; statusAreaYTranslation = 0f; clockInYTranslation = 0f; clockOutYTranslation = mClockSwitchYAmount * -1f; // Must remove in order for notifications to appear in the proper place, ideally this // would happen after the out animation runs, but we can't guarantee that the // nofications won't enter only after the out animation runs. removeView(out); } if (!animate) { out.setAlpha(0f); out.setTranslationY(clockOutYTranslation); out.setVisibility(INVISIBLE); in.setAlpha(1f); in.setTranslationY(clockInYTranslation); in.setVisibility(VISIBLE); mStatusArea.setScaleX(statusAreaClockScale); mStatusArea.setScaleY(statusAreaClockScale); mStatusArea.setTranslateXFromClockDesign(statusAreaClockTranslateX); mStatusArea.setTranslateYFromClockDesign(statusAreaClockTranslateY); mStatusArea.setTranslateYFromClockSize(statusAreaYTranslation); mSmallClockFrame.setTranslationY(statusAreaYTranslation); return; } mClockOutAnim = new AnimatorSet(); mClockOutAnim.setDuration(CLOCK_OUT_MILLIS); mClockOutAnim.setInterpolator(Interpolators.LINEAR); mClockOutAnim.playTogether( ObjectAnimator.ofFloat(out, ALPHA, 0f), ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation)); mClockOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { if (mClockOutAnim == animation) { out.setVisibility(INVISIBLE); mClockOutAnim = null; } } }); in.setVisibility(View.VISIBLE); mClockInAnim = new AnimatorSet(); mClockInAnim.setDuration(CLOCK_IN_MILLIS); mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); mClockInAnim.playTogether( ObjectAnimator.ofFloat(in, ALPHA, 1f), ObjectAnimator.ofFloat(in, TRANSLATION_Y, clockInYTranslation)); mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS); mClockInAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { if (mClockInAnim == animation) { mClockInAnim = null; } } }); mStatusAreaAnim = new AnimatorSet(); mStatusAreaAnim.setStartDelay(STATUS_AREA_START_DELAY_MILLIS); mStatusAreaAnim.setDuration( useLargeClock ? STATUS_AREA_MOVE_UP_MILLIS : STATUS_AREA_MOVE_DOWN_MILLIS); mStatusAreaAnim.setInterpolator(Interpolators.EMPHASIZED); mStatusAreaAnim.playTogether( ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_SIZE.getProperty(), statusAreaYTranslation), ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation), ObjectAnimator.ofFloat(mStatusArea, SCALE_X, statusAreaClockScale), ObjectAnimator.ofFloat(mStatusArea, SCALE_Y, statusAreaClockScale), ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_X_CLOCK_DESIGN.getProperty(), statusAreaClockTranslateX), ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_DESIGN.getProperty(), statusAreaClockTranslateY)); mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { if (mStatusAreaAnim == animation) { mStatusAreaAnim = null; } } }); mClockInAnim.start(); mClockOutAnim.start(); mStatusAreaAnim.start(); } /** * Display the desired clock and hide the other one * * @return true if desired clock appeared and false if it was already visible */ boolean switchToClock(@ClockSize int clockSize, boolean animate) { if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) { return false; } // let's make sure clock is changed only after all views were laid out so we can // translate them properly if (mChildrenAreLaidOut) { updateClockViews(clockSize == LARGE, animate); } mDisplayedClockSize = clockSize; return true; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (changed) { post(() -> updateClockTargetRegions()); } if (mSmartspace != null && mSmartspaceTop != mSmartspace.getTop()) { mSmartspaceTop = mSmartspace.getTop(); post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); } if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); } mChildrenAreLaidOut = true; } public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); pw.println(" mSmallClockFrame = " + mSmallClockFrame); pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha()); pw.println(" mLargeClockFrame = " + mLargeClockFrame); pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha()); pw.println(" mStatusArea = " + mStatusArea); pw.println(" mDisplayedClockSize = " + mDisplayedClockSize); } }