1 package com.android.keyguard; 2 3 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; 4 5 import android.animation.Animator; 6 import android.animation.AnimatorListenerAdapter; 7 import android.animation.ValueAnimator; 8 import android.app.WallpaperManager; 9 import android.content.Context; 10 import android.graphics.Paint; 11 import android.graphics.Paint.Style; 12 import android.os.Build; 13 import android.transition.Transition; 14 import android.transition.TransitionListenerAdapter; 15 import android.transition.TransitionManager; 16 import android.transition.TransitionSet; 17 import android.transition.TransitionValues; 18 import android.util.AttributeSet; 19 import android.util.Log; 20 import android.util.MathUtils; 21 import android.view.View; 22 import android.view.ViewGroup; 23 import android.widget.FrameLayout; 24 import android.widget.RelativeLayout; 25 import android.widget.TextClock; 26 27 import androidx.annotation.VisibleForTesting; 28 29 import com.android.internal.colorextraction.ColorExtractor; 30 import com.android.internal.colorextraction.ColorExtractor.OnColorsChangedListener; 31 import com.android.keyguard.clock.ClockManager; 32 import com.android.systemui.Interpolators; 33 import com.android.systemui.colorextraction.SysuiColorExtractor; 34 import com.android.systemui.plugins.ClockPlugin; 35 import com.android.systemui.plugins.statusbar.StatusBarStateController; 36 import com.android.systemui.statusbar.StatusBarState; 37 38 import java.io.FileDescriptor; 39 import java.io.PrintWriter; 40 import java.util.Arrays; 41 import java.util.TimeZone; 42 43 import javax.inject.Inject; 44 import javax.inject.Named; 45 46 /** 47 * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. 48 */ 49 public class KeyguardClockSwitch extends RelativeLayout { 50 51 private static final String TAG = "KeyguardClockSwitch"; 52 private static final boolean CUSTOM_CLOCKS_ENABLED = false; 53 54 /** 55 * Animation fraction when text is transitioned to/from bold. 56 */ 57 private static final float TO_BOLD_TRANSITION_FRACTION = 0.7f; 58 59 /** 60 * Controller used to track StatusBar state to know when to show the big_clock_container. 61 */ 62 private final StatusBarStateController mStatusBarStateController; 63 64 /** 65 * Color extractor used to apply colors from wallpaper to custom clock faces. 66 */ 67 private final SysuiColorExtractor mSysuiColorExtractor; 68 69 /** 70 * Manager used to know when to show a custom clock face. 71 */ 72 private final ClockManager mClockManager; 73 74 /** 75 * Layout transition that scales the default clock face. 76 */ 77 private final Transition mTransition; 78 79 private final ClockVisibilityTransition mClockTransition; 80 private final ClockVisibilityTransition mBoldClockTransition; 81 82 /** 83 * Optional/alternative clock injected via plugin. 84 */ 85 private ClockPlugin mClockPlugin; 86 87 /** 88 * Default clock. 89 */ 90 private TextClock mClockView; 91 92 /** 93 * Default clock, bold version. 94 * Used to transition to bold when shrinking the default clock. 95 */ 96 private TextClock mClockViewBold; 97 98 /** 99 * Frame for default and custom clock. 100 */ 101 private FrameLayout mSmallClockFrame; 102 103 /** 104 * Container for big custom clock. 105 */ 106 private ViewGroup mBigClockContainer; 107 108 /** 109 * Status area (date and other stuff) shown below the clock. Plugin can decide whether or not to 110 * show it below the alternate clock. 111 */ 112 private View mKeyguardStatusArea; 113 114 /** 115 * Maintain state so that a newly connected plugin can be initialized. 116 */ 117 private float mDarkAmount; 118 119 /** 120 * If the Keyguard Slice has a header (big center-aligned text.) 121 */ 122 private boolean mShowingHeader; 123 private boolean mSupportsDarkText; 124 private int[] mColorPalette; 125 126 /** 127 * Track the state of the status bar to know when to hide the big_clock_container. 128 */ 129 private int mStatusBarState; 130 131 private final StatusBarStateController.StateListener mStateListener = 132 new StatusBarStateController.StateListener() { 133 @Override 134 public void onStateChanged(int newState) { 135 mStatusBarState = newState; 136 updateBigClockVisibility(); 137 } 138 }; 139 140 private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin; 141 142 /** 143 * Listener for changes to the color palette. 144 * 145 * The color palette changes when the wallpaper is changed. 146 */ 147 private final OnColorsChangedListener mColorsListener = (extractor, which) -> { 148 if ((which & WallpaperManager.FLAG_LOCK) != 0) { 149 updateColors(); 150 } 151 }; 152 153 @Inject KeyguardClockSwitch(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, ClockManager clockManager)154 public KeyguardClockSwitch(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, 155 StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, 156 ClockManager clockManager) { 157 super(context, attrs); 158 mStatusBarStateController = statusBarStateController; 159 mStatusBarState = mStatusBarStateController.getState(); 160 mSysuiColorExtractor = colorExtractor; 161 mClockManager = clockManager; 162 163 mClockTransition = new ClockVisibilityTransition().setCutoff( 164 1 - TO_BOLD_TRANSITION_FRACTION); 165 mClockTransition.addTarget(R.id.default_clock_view); 166 mBoldClockTransition = new ClockVisibilityTransition().setCutoff( 167 TO_BOLD_TRANSITION_FRACTION); 168 mBoldClockTransition.addTarget(R.id.default_clock_view_bold); 169 mTransition = new TransitionSet() 170 .setOrdering(TransitionSet.ORDERING_TOGETHER) 171 .addTransition(mClockTransition) 172 .addTransition(mBoldClockTransition) 173 .setDuration(KeyguardSliceView.DEFAULT_ANIM_DURATION / 2) 174 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 175 } 176 177 /** 178 * Returns if this view is presenting a custom clock, or the default implementation. 179 */ hasCustomClock()180 public boolean hasCustomClock() { 181 return mClockPlugin != null; 182 } 183 184 @Override onFinishInflate()185 protected void onFinishInflate() { 186 super.onFinishInflate(); 187 mClockView = findViewById(R.id.default_clock_view); 188 mClockViewBold = findViewById(R.id.default_clock_view_bold); 189 mSmallClockFrame = findViewById(R.id.clock_view); 190 mKeyguardStatusArea = findViewById(R.id.keyguard_status_area); 191 } 192 193 @Override onAttachedToWindow()194 protected void onAttachedToWindow() { 195 super.onAttachedToWindow(); 196 if (CUSTOM_CLOCKS_ENABLED) { 197 mClockManager.addOnClockChangedListener(mClockChangedListener); 198 } 199 mStatusBarStateController.addCallback(mStateListener); 200 mSysuiColorExtractor.addOnColorsChangedListener(mColorsListener); 201 updateColors(); 202 } 203 204 @Override onDetachedFromWindow()205 protected void onDetachedFromWindow() { 206 super.onDetachedFromWindow(); 207 if (CUSTOM_CLOCKS_ENABLED) { 208 mClockManager.removeOnClockChangedListener(mClockChangedListener); 209 } 210 mStatusBarStateController.removeCallback(mStateListener); 211 mSysuiColorExtractor.removeOnColorsChangedListener(mColorsListener); 212 setClockPlugin(null); 213 } 214 setClockPlugin(ClockPlugin plugin)215 private void setClockPlugin(ClockPlugin plugin) { 216 // Disconnect from existing plugin. 217 if (mClockPlugin != null) { 218 View smallClockView = mClockPlugin.getView(); 219 if (smallClockView != null && smallClockView.getParent() == mSmallClockFrame) { 220 mSmallClockFrame.removeView(smallClockView); 221 } 222 if (mBigClockContainer != null) { 223 mBigClockContainer.removeAllViews(); 224 updateBigClockVisibility(); 225 } 226 mClockPlugin.onDestroyView(); 227 mClockPlugin = null; 228 } 229 if (plugin == null) { 230 mClockView.setVisibility(View.VISIBLE); 231 mClockViewBold.setVisibility(View.INVISIBLE); 232 mKeyguardStatusArea.setVisibility(View.VISIBLE); 233 return; 234 } 235 // Attach small and big clock views to hierarchy. 236 View smallClockView = plugin.getView(); 237 if (smallClockView != null) { 238 mSmallClockFrame.addView(smallClockView, -1, 239 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 240 ViewGroup.LayoutParams.WRAP_CONTENT)); 241 mClockView.setVisibility(View.GONE); 242 mClockViewBold.setVisibility(View.GONE); 243 } 244 View bigClockView = plugin.getBigClockView(); 245 if (bigClockView != null && mBigClockContainer != null) { 246 mBigClockContainer.addView(bigClockView); 247 updateBigClockVisibility(); 248 } 249 // Hide default clock. 250 if (!plugin.shouldShowStatusArea()) { 251 mKeyguardStatusArea.setVisibility(View.GONE); 252 } 253 // Initialize plugin parameters. 254 mClockPlugin = plugin; 255 mClockPlugin.setStyle(getPaint().getStyle()); 256 mClockPlugin.setTextColor(getCurrentTextColor()); 257 mClockPlugin.setDarkAmount(mDarkAmount); 258 if (mColorPalette != null) { 259 mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); 260 } 261 } 262 263 /** 264 * Set container for big clock face appearing behind NSSL and KeyguardStatusView. 265 */ setBigClockContainer(ViewGroup container)266 public void setBigClockContainer(ViewGroup container) { 267 if (mClockPlugin != null && container != null) { 268 View bigClockView = mClockPlugin.getBigClockView(); 269 if (bigClockView != null) { 270 container.addView(bigClockView); 271 } 272 } 273 mBigClockContainer = container; 274 updateBigClockVisibility(); 275 } 276 277 /** 278 * It will also update plugin setStyle if plugin is connected. 279 */ setStyle(Style style)280 public void setStyle(Style style) { 281 mClockView.getPaint().setStyle(style); 282 mClockViewBold.getPaint().setStyle(style); 283 if (mClockPlugin != null) { 284 mClockPlugin.setStyle(style); 285 } 286 } 287 288 /** 289 * It will also update plugin setTextColor if plugin is connected. 290 */ setTextColor(int color)291 public void setTextColor(int color) { 292 mClockView.setTextColor(color); 293 mClockViewBold.setTextColor(color); 294 if (mClockPlugin != null) { 295 mClockPlugin.setTextColor(color); 296 } 297 } 298 setShowCurrentUserTime(boolean showCurrentUserTime)299 public void setShowCurrentUserTime(boolean showCurrentUserTime) { 300 mClockView.setShowCurrentUserTime(showCurrentUserTime); 301 mClockViewBold.setShowCurrentUserTime(showCurrentUserTime); 302 } 303 setTextSize(int unit, float size)304 public void setTextSize(int unit, float size) { 305 mClockView.setTextSize(unit, size); 306 } 307 setFormat12Hour(CharSequence format)308 public void setFormat12Hour(CharSequence format) { 309 mClockView.setFormat12Hour(format); 310 mClockViewBold.setFormat12Hour(format); 311 } 312 setFormat24Hour(CharSequence format)313 public void setFormat24Hour(CharSequence format) { 314 mClockView.setFormat24Hour(format); 315 mClockViewBold.setFormat24Hour(format); 316 } 317 318 /** 319 * Set the amount (ratio) that the device has transitioned to doze. 320 * 321 * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. 322 */ setDarkAmount(float darkAmount)323 public void setDarkAmount(float darkAmount) { 324 mDarkAmount = darkAmount; 325 if (mClockPlugin != null) { 326 mClockPlugin.setDarkAmount(darkAmount); 327 } 328 } 329 getPaint()330 public Paint getPaint() { 331 return mClockView.getPaint(); 332 } 333 getCurrentTextColor()334 public int getCurrentTextColor() { 335 return mClockView.getCurrentTextColor(); 336 } 337 getTextSize()338 public float getTextSize() { 339 return mClockView.getTextSize(); 340 } 341 342 /** 343 * Returns the preferred Y position of the clock. 344 * 345 * @param totalHeight Height of the parent container. 346 * @return preferred Y position. 347 */ getPreferredY(int totalHeight)348 int getPreferredY(int totalHeight) { 349 if (mClockPlugin != null) { 350 return mClockPlugin.getPreferredY(totalHeight); 351 } else { 352 return totalHeight / 2; 353 } 354 } 355 356 /** 357 * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm. 358 */ refresh()359 public void refresh() { 360 mClockView.refresh(); 361 mClockViewBold.refresh(); 362 if (mClockPlugin != null) { 363 mClockPlugin.onTimeTick(); 364 } 365 if (Build.IS_DEBUGGABLE) { 366 // Log for debugging b/130888082 (sysui waking up, but clock not updating) 367 Log.d(TAG, "Updating clock: " + mClockView.getText()); 368 } 369 } 370 371 /** 372 * Notifies that the time zone has changed. 373 */ onTimeZoneChanged(TimeZone timeZone)374 public void onTimeZoneChanged(TimeZone timeZone) { 375 if (mClockPlugin != null) { 376 mClockPlugin.onTimeZoneChanged(timeZone); 377 } 378 } 379 updateColors()380 private void updateColors() { 381 ColorExtractor.GradientColors colors = mSysuiColorExtractor.getColors( 382 WallpaperManager.FLAG_LOCK); 383 mSupportsDarkText = colors.supportsDarkText(); 384 mColorPalette = colors.getColorPalette(); 385 if (mClockPlugin != null) { 386 mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); 387 } 388 } 389 updateBigClockVisibility()390 private void updateBigClockVisibility() { 391 if (mBigClockContainer == null) { 392 return; 393 } 394 final boolean inDisplayState = mStatusBarState == StatusBarState.KEYGUARD 395 || mStatusBarState == StatusBarState.SHADE_LOCKED; 396 final int visibility = 397 inDisplayState && mBigClockContainer.getChildCount() != 0 ? View.VISIBLE 398 : View.GONE; 399 if (mBigClockContainer.getVisibility() != visibility) { 400 mBigClockContainer.setVisibility(visibility); 401 } 402 } 403 404 /** 405 * Sets if the keyguard slice is showing a center-aligned header. We need a smaller clock in 406 * these cases. 407 */ setKeyguardShowingHeader(boolean hasHeader)408 void setKeyguardShowingHeader(boolean hasHeader) { 409 if (mShowingHeader == hasHeader || hasCustomClock()) { 410 return; 411 } 412 mShowingHeader = hasHeader; 413 414 float smallFontSize = mContext.getResources().getDimensionPixelSize( 415 R.dimen.widget_small_font_size); 416 float bigFontSize = mContext.getResources().getDimensionPixelSize( 417 R.dimen.widget_big_font_size); 418 mClockTransition.setScale(smallFontSize / bigFontSize); 419 mBoldClockTransition.setScale(bigFontSize / smallFontSize); 420 421 // End any current transitions before starting a new transition so that the new transition 422 // starts from a good state instead of a potentially bad intermediate state arrived at 423 // during a transition animation. 424 TransitionManager.endTransitions((ViewGroup) mClockView.getParent()); 425 426 if (hasHeader) { 427 // After the transition, make the default clock GONE so that it doesn't make the 428 // KeyguardStatusView appear taller in KeyguardClockPositionAlgorithm and elsewhere. 429 mTransition.addListener(new TransitionListenerAdapter() { 430 @Override 431 public void onTransitionEnd(Transition transition) { 432 super.onTransitionEnd(transition); 433 // Check that header is actually showing. I saw issues where this event was 434 // fired after the big clock transitioned back to visible, which causes the time 435 // to completely disappear. 436 if (mShowingHeader) { 437 mClockView.setVisibility(View.GONE); 438 } 439 transition.removeListener(this); 440 } 441 }); 442 } 443 444 TransitionManager.beginDelayedTransition((ViewGroup) mClockView.getParent(), mTransition); 445 mClockView.setVisibility(hasHeader ? View.INVISIBLE : View.VISIBLE); 446 mClockViewBold.setVisibility(hasHeader ? View.VISIBLE : View.INVISIBLE); 447 int paddingBottom = mContext.getResources().getDimensionPixelSize(hasHeader 448 ? R.dimen.widget_vertical_padding_clock : R.dimen.title_clock_padding); 449 mClockView.setPadding(mClockView.getPaddingLeft(), mClockView.getPaddingTop(), 450 mClockView.getPaddingRight(), paddingBottom); 451 mClockViewBold.setPadding(mClockViewBold.getPaddingLeft(), mClockViewBold.getPaddingTop(), 452 mClockViewBold.getPaddingRight(), paddingBottom); 453 } 454 455 @VisibleForTesting(otherwise = VisibleForTesting.NONE) getClockChangedListener()456 ClockManager.ClockChangedListener getClockChangedListener() { 457 return mClockChangedListener; 458 } 459 460 @VisibleForTesting(otherwise = VisibleForTesting.NONE) getStateListener()461 StatusBarStateController.StateListener getStateListener() { 462 return mStateListener; 463 } 464 dump(FileDescriptor fd, PrintWriter pw, String[] args)465 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 466 pw.println("KeyguardClockSwitch:"); 467 pw.println(" mClockPlugin: " + mClockPlugin); 468 pw.println(" mClockView: " + mClockView); 469 pw.println(" mClockViewBold: " + mClockViewBold); 470 pw.println(" mSmallClockFrame: " + mSmallClockFrame); 471 pw.println(" mBigClockContainer: " + mBigClockContainer); 472 pw.println(" mKeyguardStatusArea: " + mKeyguardStatusArea); 473 pw.println(" mDarkAmount: " + mDarkAmount); 474 pw.println(" mShowingHeader: " + mShowingHeader); 475 pw.println(" mSupportsDarkText: " + mSupportsDarkText); 476 pw.println(" mColorPalette: " + Arrays.toString(mColorPalette)); 477 } 478 479 /** 480 * {@link Visibility} transformation that scales the view while it is disappearing/appearing and 481 * transitions suddenly at a cutoff fraction during the animation. 482 */ 483 private class ClockVisibilityTransition extends android.transition.Visibility { 484 485 private static final String PROPNAME_VISIBILITY = "systemui:keyguard:visibility"; 486 487 private float mCutoff; 488 private float mScale; 489 490 /** 491 * Constructs a transition that switches between visible/invisible at a cutoff and scales in 492 * size while appearing/disappearing. 493 */ ClockVisibilityTransition()494 ClockVisibilityTransition() { 495 setCutoff(1f); 496 setScale(1f); 497 } 498 499 /** 500 * Sets the transition point between visible/invisible. 501 * 502 * @param cutoff The fraction in [0, 1] when the view switches between visible/invisible. 503 * @return This transition object 504 */ setCutoff(float cutoff)505 public ClockVisibilityTransition setCutoff(float cutoff) { 506 mCutoff = cutoff; 507 return this; 508 } 509 510 /** 511 * Sets the scale factor applied while appearing/disappearing. 512 * 513 * @param scale Scale factor applied while appearing/disappearing. When factor is less than 514 * one, the view will shrink while disappearing. When it is greater than one, 515 * the view will expand while disappearing. 516 * @return This transition object 517 */ setScale(float scale)518 public ClockVisibilityTransition setScale(float scale) { 519 mScale = scale; 520 return this; 521 } 522 523 @Override captureStartValues(TransitionValues transitionValues)524 public void captureStartValues(TransitionValues transitionValues) { 525 super.captureStartValues(transitionValues); 526 captureVisibility(transitionValues); 527 } 528 529 @Override captureEndValues(TransitionValues transitionValues)530 public void captureEndValues(TransitionValues transitionValues) { 531 super.captureStartValues(transitionValues); 532 captureVisibility(transitionValues); 533 } 534 captureVisibility(TransitionValues transitionValues)535 private void captureVisibility(TransitionValues transitionValues) { 536 transitionValues.values.put(PROPNAME_VISIBILITY, 537 transitionValues.view.getVisibility()); 538 } 539 540 @Override onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)541 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 542 TransitionValues endValues) { 543 if (!sceneRoot.isShown()) { 544 return null; 545 } 546 final float cutoff = mCutoff; 547 final int startVisibility = View.INVISIBLE; 548 final int endVisibility = (int) endValues.values.get(PROPNAME_VISIBILITY); 549 final float startScale = mScale; 550 final float endScale = 1f; 551 return createAnimator(view, cutoff, startVisibility, endVisibility, startScale, 552 endScale); 553 } 554 555 @Override onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)556 public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, 557 TransitionValues endValues) { 558 if (!sceneRoot.isShown()) { 559 return null; 560 } 561 final float cutoff = 1f - mCutoff; 562 final int startVisibility = View.VISIBLE; 563 final int endVisibility = (int) endValues.values.get(PROPNAME_VISIBILITY); 564 final float startScale = 1f; 565 final float endScale = mScale; 566 return createAnimator(view, cutoff, startVisibility, endVisibility, startScale, 567 endScale); 568 } 569 createAnimator(View view, float cutoff, int startVisibility, int endVisibility, float startScale, float endScale)570 private Animator createAnimator(View view, float cutoff, int startVisibility, 571 int endVisibility, float startScale, float endScale) { 572 view.setPivotY(view.getHeight() - view.getPaddingBottom()); 573 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); 574 animator.addUpdateListener(animation -> { 575 final float fraction = animation.getAnimatedFraction(); 576 if (fraction > cutoff) { 577 view.setVisibility(endVisibility); 578 } 579 final float scale = MathUtils.lerp(startScale, endScale, fraction); 580 view.setScaleX(scale); 581 view.setScaleY(scale); 582 }); 583 animator.addListener(new AnimatorListenerAdapter() { 584 @Override 585 public void onAnimationStart(Animator animation) { 586 super.onAnimationStart(animation); 587 view.setVisibility(startVisibility); 588 animation.removeListener(this); 589 } 590 }); 591 addListener(new TransitionListenerAdapter() { 592 @Override 593 public void onTransitionEnd(Transition transition) { 594 view.setVisibility(endVisibility); 595 view.setScaleX(1f); 596 view.setScaleY(1f); 597 transition.removeListener(this); 598 } 599 }); 600 return animator; 601 } 602 } 603 } 604