1 package com.android.keyguard; 2 3 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; 4 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; 5 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.animation.AnimatorSet; 10 import android.animation.ObjectAnimator; 11 import android.content.Context; 12 import android.graphics.Canvas; 13 import android.graphics.Rect; 14 import android.util.AttributeSet; 15 import android.view.View; 16 import android.view.ViewGroup; 17 import android.widget.RelativeLayout; 18 19 import androidx.annotation.IntDef; 20 import androidx.annotation.VisibleForTesting; 21 import androidx.core.content.res.ResourcesCompat; 22 23 import com.android.app.animation.Interpolators; 24 import com.android.keyguard.dagger.KeyguardStatusViewScope; 25 import com.android.systemui.keyguard.MigrateClocksToBlueprint; 26 import com.android.systemui.log.LogBuffer; 27 import com.android.systemui.log.core.LogLevel; 28 import com.android.systemui.plugins.clocks.ClockController; 29 import com.android.systemui.res.R; 30 import com.android.systemui.shared.clocks.DefaultClockController; 31 32 import java.io.PrintWriter; 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 36 /** 37 * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. 38 */ 39 @KeyguardStatusViewScope 40 public class KeyguardClockSwitch extends RelativeLayout { 41 42 private static final String TAG = "KeyguardClockSwitch"; 43 public static final String MISSING_CLOCK_ID = "CLOCK_MISSING"; 44 45 private static final long CLOCK_OUT_MILLIS = 133; 46 private static final long CLOCK_IN_MILLIS = 167; 47 public static final long CLOCK_IN_START_DELAY_MILLIS = 133; 48 private static final long STATUS_AREA_START_DELAY_MILLIS = 0; 49 private static final long STATUS_AREA_MOVE_UP_MILLIS = 967; 50 private static final long STATUS_AREA_MOVE_DOWN_MILLIS = 467; 51 private static final float SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER = 1.4f; 52 private static final float SMARTSPACE_TOP_PADDING_MULTIPLIER = 2.625f; 53 54 @IntDef({LARGE, SMALL}) 55 @Retention(RetentionPolicy.SOURCE) 56 public @interface ClockSize { } 57 58 public static final int LARGE = 0; 59 public static final int SMALL = 1; 60 // compensate for translation of parents subject to device screen 61 // In this case, the translation comes from KeyguardStatusView 62 public int screenOffsetYPadding = 0; 63 64 /** Returns a region for the large clock to position itself, based on the given parent. */ getLargeClockRegion(ViewGroup parent)65 public static Rect getLargeClockRegion(ViewGroup parent) { 66 int largeClockTopMargin = parent.getResources() 67 .getDimensionPixelSize( 68 com.android.systemui.customization.R.dimen.keyguard_large_clock_top_margin); 69 int targetHeight = parent.getResources() 70 .getDimensionPixelSize( 71 com.android.systemui.customization.R.dimen.large_clock_text_size) 72 * 2; 73 int top = parent.getHeight() / 2 - targetHeight / 2 74 + largeClockTopMargin / 2; 75 return new Rect( 76 parent.getLeft(), 77 top, 78 parent.getRight(), 79 top + targetHeight); 80 } 81 82 /** Returns a region for the small clock to position itself, based on the given parent. */ getSmallClockRegion(ViewGroup parent)83 public static Rect getSmallClockRegion(ViewGroup parent) { 84 int targetHeight = parent.getResources() 85 .getDimensionPixelSize( 86 com.android.systemui.customization.R.dimen.small_clock_text_size); 87 return new Rect( 88 parent.getLeft(), 89 parent.getTop(), 90 parent.getRight(), 91 parent.getTop() + targetHeight); 92 } 93 94 /** 95 * Frame for small/large clocks 96 */ 97 private KeyguardClockFrame mSmallClockFrame; 98 private KeyguardClockFrame mLargeClockFrame; 99 private ClockController mClock; 100 101 // It's bc_smartspace_view, assigned by KeyguardClockSwitchController 102 // to get the top padding for translating smartspace for weather clock 103 private View mSmartspace; 104 105 // Smartspace in weather clock is translated by this value 106 // to compensate for the position invisible dateWeatherView 107 private int mSmartspaceTop = -1; 108 109 private KeyguardStatusAreaView mStatusArea; 110 private int mSmartspaceTopOffset; 111 private float mWeatherClockSmartspaceScaling = 1f; 112 private int mWeatherClockSmartspaceTranslateX = 0; 113 private int mWeatherClockSmartspaceTranslateY = 0; 114 private int mDrawAlpha = 255; 115 116 private int mStatusBarHeight = 0; 117 118 /** 119 * Maintain state so that a newly connected plugin can be initialized. 120 */ 121 private float mDarkAmount; 122 private boolean mSplitShadeCentered = false; 123 124 /** 125 * Indicates which clock is currently displayed - should be one of {@link ClockSize}. 126 * Use null to signify it is uninitialized. 127 */ 128 @ClockSize private Integer mDisplayedClockSize = null; 129 130 @VisibleForTesting AnimatorSet mClockInAnim = null; 131 @VisibleForTesting AnimatorSet mClockOutAnim = null; 132 @VisibleForTesting AnimatorSet mStatusAreaAnim = null; 133 134 private int mClockSwitchYAmount; 135 @VisibleForTesting boolean mChildrenAreLaidOut = false; 136 @VisibleForTesting boolean mAnimateOnLayout = true; 137 private LogBuffer mLogBuffer = null; 138 KeyguardClockSwitch(Context context, AttributeSet attrs)139 public KeyguardClockSwitch(Context context, AttributeSet attrs) { 140 super(context, attrs); 141 } 142 143 /** 144 * Apply dp changes on configuration change 145 */ onConfigChanged()146 public void onConfigChanged() { 147 mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( 148 R.dimen.keyguard_clock_switch_y_shift); 149 mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize( 150 R.dimen.keyguard_smartspace_top_offset) 151 * mContext.getResources().getConfiguration().fontScale 152 / mContext.getResources().getDisplayMetrics().density 153 * SMARTSPACE_TOP_PADDING_MULTIPLIER); 154 mWeatherClockSmartspaceScaling = ResourcesCompat.getFloat( 155 mContext.getResources(), R.dimen.weather_clock_smartspace_scale); 156 mWeatherClockSmartspaceTranslateX = mContext.getResources().getDimensionPixelSize( 157 R.dimen.weather_clock_smartspace_translateX); 158 mWeatherClockSmartspaceTranslateY = mContext.getResources().getDimensionPixelSize( 159 R.dimen.weather_clock_smartspace_translateY); 160 mStatusBarHeight = mContext.getResources().getDimensionPixelSize( 161 R.dimen.status_bar_height); 162 updateStatusArea(/* animate= */false); 163 } 164 165 /** Get bc_smartspace_view from KeyguardClockSwitchController 166 * Use its top to decide the translation value */ setSmartspace(View smartspace)167 public void setSmartspace(View smartspace) { 168 mSmartspace = smartspace; 169 } 170 171 /** Sets whether the large clock is being shown on a connected display. */ setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay)172 public void setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay) { 173 if (mClock != null) { 174 mClock.getLargeClock().getEvents().onSecondaryDisplayChanged(onSecondaryDisplay); 175 } 176 } 177 178 /** 179 * Enable or disable split shade specific positioning 180 */ setSplitShadeCentered(boolean splitShadeCentered)181 public void setSplitShadeCentered(boolean splitShadeCentered) { 182 if (mSplitShadeCentered != splitShadeCentered) { 183 mSplitShadeCentered = splitShadeCentered; 184 updateStatusArea(/* animate= */true); 185 } 186 } 187 getSplitShadeCentered()188 public boolean getSplitShadeCentered() { 189 return mSplitShadeCentered; 190 } 191 192 @Override onFinishInflate()193 protected void onFinishInflate() { 194 super.onFinishInflate(); 195 if (!MigrateClocksToBlueprint.isEnabled()) { 196 mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); 197 mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); 198 mStatusArea = findViewById(R.id.keyguard_status_area); 199 } else { 200 removeView(findViewById(R.id.lockscreen_clock_view)); 201 removeView(findViewById(R.id.lockscreen_clock_view_large)); 202 } 203 onConfigChanged(); 204 } 205 206 @Override onSetAlpha(int alpha)207 protected boolean onSetAlpha(int alpha) { 208 mDrawAlpha = alpha; 209 return true; 210 } 211 212 @Override dispatchDraw(Canvas canvas)213 protected void dispatchDraw(Canvas canvas) { 214 KeyguardClockFrame.saveCanvasAlpha( 215 this, canvas, mDrawAlpha, 216 c -> { 217 super.dispatchDraw(c); 218 return kotlin.Unit.INSTANCE; 219 }); 220 } 221 setLogBuffer(LogBuffer logBuffer)222 public void setLogBuffer(LogBuffer logBuffer) { 223 mLogBuffer = logBuffer; 224 } 225 getLogBuffer()226 public LogBuffer getLogBuffer() { 227 return mLogBuffer; 228 } 229 230 /** Returns the id of the currently rendering clock */ getClockId()231 public String getClockId() { 232 if (mClock == null) { 233 return MISSING_CLOCK_ID; 234 } 235 return mClock.getConfig().getId(); 236 } 237 setClock(ClockController clock, int statusBarState)238 void setClock(ClockController clock, int statusBarState) { 239 mClock = clock; 240 241 // Disconnect from existing plugin. 242 mSmallClockFrame.removeAllViews(); 243 mLargeClockFrame.removeAllViews(); 244 245 if (clock == null) { 246 if (mLogBuffer != null) { 247 mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown"); 248 } 249 return; 250 } 251 252 // Attach small and big clock views to hierarchy. 253 if (mLogBuffer != null) { 254 mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch"); 255 } 256 mSmallClockFrame.addView(clock.getSmallClock().getView()); 257 mLargeClockFrame.addView(clock.getLargeClock().getView()); 258 updateClockTargetRegions(); 259 updateStatusArea(/* animate= */false); 260 } 261 updateStatusArea(boolean animate)262 private void updateStatusArea(boolean animate) { 263 if (mDisplayedClockSize != null && mChildrenAreLaidOut) { 264 updateClockViews(mDisplayedClockSize == LARGE, animate); 265 } 266 } 267 updateClockTargetRegions()268 void updateClockTargetRegions() { 269 if (MigrateClocksToBlueprint.isEnabled()) { 270 return; 271 } 272 if (mClock != null) { 273 if (mSmallClockFrame.isLaidOut()) { 274 Rect targetRegion = getSmallClockRegion(mSmallClockFrame); 275 mClock.getSmallClock().getEvents().onTargetRegionChanged(targetRegion); 276 } 277 278 if (mLargeClockFrame.isLaidOut()) { 279 Rect targetRegion = getLargeClockRegion(mLargeClockFrame); 280 if (mClock instanceof DefaultClockController) { 281 mClock.getLargeClock().getEvents().onTargetRegionChanged( 282 targetRegion); 283 } else { 284 mClock.getLargeClock().getEvents().onTargetRegionChanged( 285 new Rect( 286 targetRegion.left, 287 targetRegion.top - screenOffsetYPadding, 288 targetRegion.right, 289 targetRegion.bottom - screenOffsetYPadding)); 290 } 291 } 292 } 293 } 294 updateClockViews(boolean useLargeClock, boolean animate)295 private void updateClockViews(boolean useLargeClock, boolean animate) { 296 if (mLogBuffer != null) { 297 mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> { 298 msg.setBool1(useLargeClock); 299 msg.setBool2(animate); 300 msg.setBool3(mChildrenAreLaidOut); 301 return kotlin.Unit.INSTANCE; 302 }, (msg) -> "updateClockViews" 303 + "; useLargeClock=" + msg.getBool1() 304 + "; animate=" + msg.getBool2() 305 + "; mChildrenAreLaidOut=" + msg.getBool3()); 306 } 307 308 if (mClockInAnim != null) mClockInAnim.cancel(); 309 if (mClockOutAnim != null) mClockOutAnim.cancel(); 310 if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); 311 312 mClockInAnim = null; 313 mClockOutAnim = null; 314 mStatusAreaAnim = null; 315 316 View in, out; 317 // statusAreaYTranslation uses for the translation for both mStatusArea and mSmallClockFrame 318 // statusAreaClockTranslateY only uses for mStatusArea 319 float statusAreaYTranslation, statusAreaClockScale = 1f; 320 float statusAreaClockTranslateX = 0f, statusAreaClockTranslateY = 0f; 321 float clockInYTranslation, clockOutYTranslation; 322 if (useLargeClock) { 323 out = mSmallClockFrame; 324 in = mLargeClockFrame; 325 if (indexOfChild(in) == -1) addView(in, 0); 326 statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop() 327 + mSmartspaceTopOffset; 328 // TODO: Load from clock config when less risky 329 if (mClock != null 330 && mClock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay()) { 331 statusAreaClockScale = mWeatherClockSmartspaceScaling; 332 statusAreaClockTranslateX = mWeatherClockSmartspaceTranslateX; 333 if (mSplitShadeCentered) { 334 statusAreaClockTranslateX *= SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER; 335 } 336 337 // On large weather clock, 338 // top padding for time is status bar height from top of the screen. 339 // On small one, 340 // it's screenOffsetYPadding (translationY for KeyguardStatusView), 341 // Cause smartspace is positioned according to the smallClockFrame 342 // we need to translate the difference between bottom of large clock and small clock 343 // Also, we need to counter offset the empty date weather view, mSmartspaceTop 344 // mWeatherClockSmartspaceTranslateY is only for Felix 345 statusAreaClockTranslateY = mStatusBarHeight - 0.6F * mSmallClockFrame.getHeight() 346 - mSmartspaceTop - screenOffsetYPadding 347 - statusAreaYTranslation + mWeatherClockSmartspaceTranslateY; 348 } 349 clockInYTranslation = 0; 350 clockOutYTranslation = 0; // Small clock translation is handled with statusArea 351 } else { 352 in = mSmallClockFrame; 353 out = mLargeClockFrame; 354 statusAreaYTranslation = 0f; 355 clockInYTranslation = 0f; 356 clockOutYTranslation = mClockSwitchYAmount * -1f; 357 358 // Must remove in order for notifications to appear in the proper place, ideally this 359 // would happen after the out animation runs, but we can't guarantee that the 360 // nofications won't enter only after the out animation runs. 361 removeView(out); 362 } 363 364 if (!animate) { 365 out.setAlpha(0f); 366 out.setTranslationY(clockOutYTranslation); 367 out.setVisibility(INVISIBLE); 368 in.setAlpha(1f); 369 in.setTranslationY(clockInYTranslation); 370 in.setVisibility(VISIBLE); 371 mStatusArea.setScaleX(statusAreaClockScale); 372 mStatusArea.setScaleY(statusAreaClockScale); 373 mStatusArea.setTranslateXFromClockDesign(statusAreaClockTranslateX); 374 mStatusArea.setTranslateYFromClockDesign(statusAreaClockTranslateY); 375 mStatusArea.setTranslateYFromClockSize(statusAreaYTranslation); 376 mSmallClockFrame.setTranslationY(statusAreaYTranslation); 377 return; 378 } 379 380 mClockOutAnim = new AnimatorSet(); 381 mClockOutAnim.setDuration(CLOCK_OUT_MILLIS); 382 mClockOutAnim.setInterpolator(Interpolators.LINEAR); 383 mClockOutAnim.playTogether( 384 ObjectAnimator.ofFloat(out, ALPHA, 0f), 385 ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation)); 386 mClockOutAnim.addListener(new AnimatorListenerAdapter() { 387 public void onAnimationEnd(Animator animation) { 388 if (mClockOutAnim == animation) { 389 out.setVisibility(INVISIBLE); 390 mClockOutAnim = null; 391 } 392 } 393 }); 394 395 in.setVisibility(View.VISIBLE); 396 mClockInAnim = new AnimatorSet(); 397 mClockInAnim.setDuration(CLOCK_IN_MILLIS); 398 mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 399 mClockInAnim.playTogether( 400 ObjectAnimator.ofFloat(in, ALPHA, 1f), 401 ObjectAnimator.ofFloat(in, TRANSLATION_Y, clockInYTranslation)); 402 mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS); 403 mClockInAnim.addListener(new AnimatorListenerAdapter() { 404 public void onAnimationEnd(Animator animation) { 405 if (mClockInAnim == animation) { 406 mClockInAnim = null; 407 } 408 } 409 }); 410 411 mStatusAreaAnim = new AnimatorSet(); 412 mStatusAreaAnim.setStartDelay(STATUS_AREA_START_DELAY_MILLIS); 413 mStatusAreaAnim.setDuration( 414 useLargeClock ? STATUS_AREA_MOVE_UP_MILLIS : STATUS_AREA_MOVE_DOWN_MILLIS); 415 mStatusAreaAnim.setInterpolator(Interpolators.EMPHASIZED); 416 mStatusAreaAnim.playTogether( 417 ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_SIZE.getProperty(), 418 statusAreaYTranslation), 419 ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation), 420 ObjectAnimator.ofFloat(mStatusArea, SCALE_X, statusAreaClockScale), 421 ObjectAnimator.ofFloat(mStatusArea, SCALE_Y, statusAreaClockScale), 422 ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_X_CLOCK_DESIGN.getProperty(), 423 statusAreaClockTranslateX), 424 ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_DESIGN.getProperty(), 425 statusAreaClockTranslateY)); 426 mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { 427 public void onAnimationEnd(Animator animation) { 428 if (mStatusAreaAnim == animation) { 429 mStatusAreaAnim = null; 430 } 431 } 432 }); 433 434 mClockInAnim.start(); 435 mClockOutAnim.start(); 436 mStatusAreaAnim.start(); 437 } 438 439 /** 440 * Display the desired clock and hide the other one 441 * 442 * @return true if desired clock appeared and false if it was already visible 443 */ switchToClock(@lockSize int clockSize, boolean animate)444 boolean switchToClock(@ClockSize int clockSize, boolean animate) { 445 if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) { 446 return false; 447 } 448 449 // let's make sure clock is changed only after all views were laid out so we can 450 // translate them properly 451 if (mChildrenAreLaidOut) { 452 updateClockViews(clockSize == LARGE, animate); 453 } 454 455 mDisplayedClockSize = clockSize; 456 return true; 457 } 458 459 @Override onLayout(boolean changed, int l, int t, int r, int b)460 protected void onLayout(boolean changed, int l, int t, int r, int b) { 461 super.onLayout(changed, l, t, r, b); 462 // TODO: b/305022530 463 if (mClock != null && mClock.getConfig().getId().equals("DIGITAL_CLOCK_METRO")) { 464 mClock.getEvents().onColorPaletteChanged(mContext.getResources()); 465 } 466 467 if (changed) { 468 post(() -> updateClockTargetRegions()); 469 } 470 471 if (mSmartspace != null && mSmartspaceTop != mSmartspace.getTop() 472 && mDisplayedClockSize != null) { 473 mSmartspaceTop = mSmartspace.getTop(); 474 post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); 475 } 476 477 if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { 478 post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout)); 479 } 480 mChildrenAreLaidOut = true; 481 } 482 dump(PrintWriter pw, String[] args)483 public void dump(PrintWriter pw, String[] args) { 484 pw.println("KeyguardClockSwitch:"); 485 pw.println(" mSmallClockFrame = " + mSmallClockFrame); 486 if (mSmallClockFrame != null) { 487 pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha()); 488 } 489 pw.println(" mLargeClockFrame = " + mLargeClockFrame); 490 if (mLargeClockFrame != null) { 491 pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha()); 492 } 493 pw.println(" mStatusArea = " + mStatusArea); 494 pw.println(" mDisplayedClockSize = " + mDisplayedClockSize); 495 } 496 } 497