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