1 package com.android.keyguard; 2 3 import android.animation.Animator; 4 import android.animation.AnimatorListenerAdapter; 5 import android.animation.AnimatorSet; 6 import android.animation.ObjectAnimator; 7 import android.content.Context; 8 import android.graphics.Paint; 9 import android.graphics.Paint.Style; 10 import android.util.AttributeSet; 11 import android.util.TypedValue; 12 import android.view.View; 13 import android.view.ViewGroup; 14 import android.widget.FrameLayout; 15 import android.widget.RelativeLayout; 16 17 import androidx.annotation.IntDef; 18 import androidx.annotation.VisibleForTesting; 19 20 import com.android.internal.colorextraction.ColorExtractor; 21 import com.android.keyguard.dagger.KeyguardStatusViewScope; 22 import com.android.systemui.R; 23 import com.android.systemui.animation.Interpolators; 24 import com.android.systemui.plugins.ClockPlugin; 25 26 import java.io.FileDescriptor; 27 import java.io.PrintWriter; 28 import java.lang.annotation.Retention; 29 import java.lang.annotation.RetentionPolicy; 30 import java.util.Arrays; 31 import java.util.TimeZone; 32 33 /** 34 * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. 35 */ 36 @KeyguardStatusViewScope 37 public class KeyguardClockSwitch extends RelativeLayout { 38 39 private static final String TAG = "KeyguardClockSwitch"; 40 41 private static final long CLOCK_OUT_MILLIS = 150; 42 private static final long CLOCK_IN_MILLIS = 200; 43 private static final long STATUS_AREA_MOVE_MILLIS = 350; 44 45 @IntDef({LARGE, SMALL}) 46 @Retention(RetentionPolicy.SOURCE) 47 public @interface ClockSize { } 48 49 public static final int LARGE = 0; 50 public static final int SMALL = 1; 51 52 /** 53 * Optional/alternative clock injected via plugin. 54 */ 55 private ClockPlugin mClockPlugin; 56 57 /** 58 * Frame for small/large clocks 59 */ 60 private FrameLayout mClockFrame; 61 private FrameLayout mLargeClockFrame; 62 private AnimatableClockView mClockView; 63 private AnimatableClockView mLargeClockView; 64 65 private View mStatusArea; 66 private int mSmartspaceTopOffset; 67 68 /** 69 * Maintain state so that a newly connected plugin can be initialized. 70 */ 71 private float mDarkAmount; 72 73 /** 74 * Indicates which clock is currently displayed - should be one of {@link ClockSize}. 75 * Use null to signify it is uninitialized. 76 */ 77 @ClockSize private Integer mDisplayedClockSize = null; 78 79 @VisibleForTesting AnimatorSet mClockInAnim = null; 80 @VisibleForTesting AnimatorSet mClockOutAnim = null; 81 private ObjectAnimator mStatusAreaAnim = null; 82 83 /** 84 * If the Keyguard Slice has a header (big center-aligned text.) 85 */ 86 private boolean mSupportsDarkText; 87 private int[] mColorPalette; 88 89 private int mClockSwitchYAmount; 90 @VisibleForTesting boolean mChildrenAreLaidOut = false; 91 KeyguardClockSwitch(Context context, AttributeSet attrs)92 public KeyguardClockSwitch(Context context, AttributeSet attrs) { 93 super(context, attrs); 94 } 95 96 /** 97 * Apply dp changes on font/scale change 98 */ onDensityOrFontScaleChanged()99 public void onDensityOrFontScaleChanged() { 100 mLargeClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources() 101 .getDimensionPixelSize(R.dimen.large_clock_text_size)); 102 mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources() 103 .getDimensionPixelSize(R.dimen.clock_text_size)); 104 105 mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize( 106 R.dimen.keyguard_clock_switch_y_shift); 107 108 mSmartspaceTopOffset = mContext.getResources().getDimensionPixelSize( 109 R.dimen.keyguard_smartspace_top_offset); 110 } 111 112 /** 113 * Returns if this view is presenting a custom clock, or the default implementation. 114 */ hasCustomClock()115 public boolean hasCustomClock() { 116 return mClockPlugin != null; 117 } 118 119 @Override onFinishInflate()120 protected void onFinishInflate() { 121 super.onFinishInflate(); 122 123 mClockFrame = findViewById(R.id.lockscreen_clock_view); 124 mClockView = findViewById(R.id.animatable_clock_view); 125 mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); 126 mLargeClockView = findViewById(R.id.animatable_clock_view_large); 127 mStatusArea = findViewById(R.id.keyguard_status_area); 128 129 onDensityOrFontScaleChanged(); 130 } 131 setClockPlugin(ClockPlugin plugin, int statusBarState)132 void setClockPlugin(ClockPlugin plugin, int statusBarState) { 133 // Disconnect from existing plugin. 134 if (mClockPlugin != null) { 135 View smallClockView = mClockPlugin.getView(); 136 if (smallClockView != null && smallClockView.getParent() == mClockFrame) { 137 mClockFrame.removeView(smallClockView); 138 } 139 View bigClockView = mClockPlugin.getBigClockView(); 140 if (bigClockView != null && bigClockView.getParent() == mLargeClockFrame) { 141 mLargeClockFrame.removeView(bigClockView); 142 } 143 mClockPlugin.onDestroyView(); 144 mClockPlugin = null; 145 } 146 if (plugin == null) { 147 mClockView.setVisibility(View.VISIBLE); 148 mLargeClockView.setVisibility(View.VISIBLE); 149 return; 150 } 151 // Attach small and big clock views to hierarchy. 152 View smallClockView = plugin.getView(); 153 if (smallClockView != null) { 154 mClockFrame.addView(smallClockView, -1, 155 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 156 ViewGroup.LayoutParams.WRAP_CONTENT)); 157 mClockView.setVisibility(View.GONE); 158 } 159 View bigClockView = plugin.getBigClockView(); 160 if (bigClockView != null) { 161 mLargeClockFrame.addView(bigClockView); 162 mLargeClockView.setVisibility(View.GONE); 163 } 164 165 // Initialize plugin parameters. 166 mClockPlugin = plugin; 167 mClockPlugin.setStyle(getPaint().getStyle()); 168 mClockPlugin.setTextColor(getCurrentTextColor()); 169 mClockPlugin.setDarkAmount(mDarkAmount); 170 if (mColorPalette != null) { 171 mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); 172 } 173 } 174 175 /** 176 * It will also update plugin setStyle if plugin is connected. 177 */ setStyle(Style style)178 public void setStyle(Style style) { 179 if (mClockPlugin != null) { 180 mClockPlugin.setStyle(style); 181 } 182 } 183 184 /** 185 * It will also update plugin setTextColor if plugin is connected. 186 */ setTextColor(int color)187 public void setTextColor(int color) { 188 if (mClockPlugin != null) { 189 mClockPlugin.setTextColor(color); 190 } 191 } 192 animateClockChange(boolean useLargeClock)193 private void animateClockChange(boolean useLargeClock) { 194 if (mClockInAnim != null) mClockInAnim.cancel(); 195 if (mClockOutAnim != null) mClockOutAnim.cancel(); 196 if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); 197 198 View in, out; 199 int direction = 1; 200 float statusAreaYTranslation; 201 if (useLargeClock) { 202 out = mClockFrame; 203 in = mLargeClockFrame; 204 if (indexOfChild(in) == -1) addView(in); 205 direction = -1; 206 statusAreaYTranslation = mClockFrame.getTop() - mStatusArea.getTop() 207 + mSmartspaceTopOffset; 208 } else { 209 in = mClockFrame; 210 out = mLargeClockFrame; 211 statusAreaYTranslation = 0f; 212 213 // Must remove in order for notifications to appear in the proper place 214 removeView(out); 215 } 216 217 mClockOutAnim = new AnimatorSet(); 218 mClockOutAnim.setDuration(CLOCK_OUT_MILLIS); 219 mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 220 mClockOutAnim.playTogether( 221 ObjectAnimator.ofFloat(out, View.ALPHA, 0f), 222 ObjectAnimator.ofFloat(out, View.TRANSLATION_Y, 0, 223 direction * -mClockSwitchYAmount)); 224 mClockOutAnim.addListener(new AnimatorListenerAdapter() { 225 public void onAnimationEnd(Animator animation) { 226 mClockOutAnim = null; 227 } 228 }); 229 230 in.setAlpha(0); 231 in.setVisibility(View.VISIBLE); 232 mClockInAnim = new AnimatorSet(); 233 mClockInAnim.setDuration(CLOCK_IN_MILLIS); 234 mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 235 mClockInAnim.playTogether(ObjectAnimator.ofFloat(in, View.ALPHA, 1f), 236 ObjectAnimator.ofFloat(in, View.TRANSLATION_Y, direction * mClockSwitchYAmount, 0)); 237 mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2); 238 mClockInAnim.addListener(new AnimatorListenerAdapter() { 239 public void onAnimationEnd(Animator animation) { 240 mClockInAnim = null; 241 } 242 }); 243 244 mClockInAnim.start(); 245 mClockOutAnim.start(); 246 247 mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y, 248 statusAreaYTranslation); 249 mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS); 250 mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 251 mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { 252 public void onAnimationEnd(Animator animation) { 253 mStatusAreaAnim = null; 254 } 255 }); 256 mStatusAreaAnim.start(); 257 } 258 259 /** 260 * Set the amount (ratio) that the device has transitioned to doze. 261 * 262 * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. 263 */ setDarkAmount(float darkAmount)264 public void setDarkAmount(float darkAmount) { 265 mDarkAmount = darkAmount; 266 if (mClockPlugin != null) { 267 mClockPlugin.setDarkAmount(darkAmount); 268 } 269 } 270 271 /** 272 * Display the desired clock and hide the other one 273 * 274 * @return true if desired clock appeared and false if it was already visible 275 */ switchToClock(@lockSize int clockSize)276 boolean switchToClock(@ClockSize int clockSize) { 277 if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) { 278 return false; 279 } 280 281 // let's make sure clock is changed only after all views were laid out so we can 282 // translate them properly 283 if (mChildrenAreLaidOut) { 284 animateClockChange(clockSize == LARGE); 285 } 286 287 mDisplayedClockSize = clockSize; 288 return true; 289 } 290 291 @Override onLayout(boolean changed, int l, int t, int r, int b)292 protected void onLayout(boolean changed, int l, int t, int r, int b) { 293 super.onLayout(changed, l, t, r, b); 294 295 if (mDisplayedClockSize != null && !mChildrenAreLaidOut) { 296 animateClockChange(mDisplayedClockSize == LARGE); 297 } 298 299 mChildrenAreLaidOut = true; 300 } 301 getPaint()302 public Paint getPaint() { 303 return mClockView.getPaint(); 304 } 305 getCurrentTextColor()306 public int getCurrentTextColor() { 307 return mClockView.getCurrentTextColor(); 308 } 309 getTextSize()310 public float getTextSize() { 311 return mClockView.getTextSize(); 312 } 313 314 /** 315 * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm. 316 */ refresh()317 public void refresh() { 318 if (mClockPlugin != null) { 319 mClockPlugin.onTimeTick(); 320 } 321 } 322 323 /** 324 * Notifies that the time zone has changed. 325 */ onTimeZoneChanged(TimeZone timeZone)326 public void onTimeZoneChanged(TimeZone timeZone) { 327 if (mClockPlugin != null) { 328 mClockPlugin.onTimeZoneChanged(timeZone); 329 } 330 } 331 332 /** 333 * Notifies that the time format has changed. 334 * 335 * @param timeFormat "12" for 12-hour format, "24" for 24-hour format 336 */ onTimeFormatChanged(String timeFormat)337 public void onTimeFormatChanged(String timeFormat) { 338 if (mClockPlugin != null) { 339 mClockPlugin.onTimeFormatChanged(timeFormat); 340 } 341 } 342 updateColors(ColorExtractor.GradientColors colors)343 void updateColors(ColorExtractor.GradientColors colors) { 344 mSupportsDarkText = colors.supportsDarkText(); 345 mColorPalette = colors.getColorPalette(); 346 if (mClockPlugin != null) { 347 mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette); 348 } 349 } 350 dump(FileDescriptor fd, PrintWriter pw, String[] args)351 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 352 pw.println("KeyguardClockSwitch:"); 353 pw.println(" mClockPlugin: " + mClockPlugin); 354 pw.println(" mClockFrame: " + mClockFrame); 355 pw.println(" mLargeClockFrame: " + mLargeClockFrame); 356 pw.println(" mStatusArea: " + mStatusArea); 357 pw.println(" mDarkAmount: " + mDarkAmount); 358 pw.println(" mSupportsDarkText: " + mSupportsDarkText); 359 pw.println(" mColorPalette: " + Arrays.toString(mColorPalette)); 360 pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); 361 } 362 } 363