1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.keyguard; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.app.ActivityManager; 22 import android.app.IActivityManager; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Color; 26 import android.graphics.Paint; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.RemoteException; 30 import android.os.UserHandle; 31 import android.support.v4.graphics.ColorUtils; 32 import android.text.TextUtils; 33 import android.text.format.DateFormat; 34 import android.util.ArraySet; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.util.Slog; 38 import android.util.TypedValue; 39 import android.view.View; 40 import android.widget.GridLayout; 41 import android.widget.RelativeLayout; 42 import android.widget.TextClock; 43 import android.widget.TextView; 44 45 import com.android.internal.widget.LockPatternUtils; 46 import com.android.internal.widget.ViewClippingUtil; 47 import com.android.systemui.Dependency; 48 import com.android.systemui.Interpolators; 49 import com.android.systemui.statusbar.policy.ConfigurationController; 50 import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; 51 52 import com.google.android.collect.Sets; 53 54 import java.util.Locale; 55 56 public class KeyguardStatusView extends GridLayout implements 57 ConfigurationController.ConfigurationListener, View.OnLayoutChangeListener { 58 private static final boolean DEBUG = KeyguardConstants.DEBUG; 59 private static final String TAG = "KeyguardStatusView"; 60 private static final int MARQUEE_DELAY_MS = 2000; 61 62 private final LockPatternUtils mLockPatternUtils; 63 private final IActivityManager mIActivityManager; 64 private final float mSmallClockScale; 65 66 private TextView mLogoutView; 67 private TextClock mClockView; 68 private View mClockSeparator; 69 private TextView mOwnerInfo; 70 private KeyguardSliceView mKeyguardSlice; 71 private Runnable mPendingMarqueeStart; 72 private Handler mHandler; 73 74 private ArraySet<View> mVisibleInDoze; 75 private boolean mPulsing; 76 private float mDarkAmount = 0; 77 private int mTextColor; 78 private float mWidgetPadding; 79 private int mLastLayoutHeight; 80 81 private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { 82 83 @Override 84 public void onTimeChanged() { 85 refreshTime(); 86 } 87 88 @Override 89 public void onKeyguardVisibilityChanged(boolean showing) { 90 if (showing) { 91 if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); 92 refreshTime(); 93 updateOwnerInfo(); 94 updateLogoutView(); 95 } 96 } 97 98 @Override 99 public void onStartedWakingUp() { 100 setEnableMarquee(true); 101 } 102 103 @Override 104 public void onFinishedGoingToSleep(int why) { 105 setEnableMarquee(false); 106 } 107 108 @Override 109 public void onUserSwitchComplete(int userId) { 110 refreshFormat(); 111 updateOwnerInfo(); 112 updateLogoutView(); 113 } 114 115 @Override 116 public void onLogoutEnabledChanged() { 117 updateLogoutView(); 118 } 119 }; 120 KeyguardStatusView(Context context)121 public KeyguardStatusView(Context context) { 122 this(context, null, 0); 123 } 124 KeyguardStatusView(Context context, AttributeSet attrs)125 public KeyguardStatusView(Context context, AttributeSet attrs) { 126 this(context, attrs, 0); 127 } 128 KeyguardStatusView(Context context, AttributeSet attrs, int defStyle)129 public KeyguardStatusView(Context context, AttributeSet attrs, int defStyle) { 130 super(context, attrs, defStyle); 131 mIActivityManager = ActivityManager.getService(); 132 mLockPatternUtils = new LockPatternUtils(getContext()); 133 mHandler = new Handler(Looper.myLooper()); 134 mSmallClockScale = getResources().getDimension(R.dimen.widget_small_font_size) 135 / getResources().getDimension(R.dimen.widget_big_font_size); 136 onDensityOrFontScaleChanged(); 137 } 138 setEnableMarquee(boolean enabled)139 private void setEnableMarquee(boolean enabled) { 140 if (DEBUG) Log.v(TAG, "Schedule setEnableMarquee: " + (enabled ? "Enable" : "Disable")); 141 if (enabled) { 142 if (mPendingMarqueeStart == null) { 143 mPendingMarqueeStart = () -> { 144 setEnableMarqueeImpl(true); 145 mPendingMarqueeStart = null; 146 }; 147 mHandler.postDelayed(mPendingMarqueeStart, MARQUEE_DELAY_MS); 148 } 149 } else { 150 if (mPendingMarqueeStart != null) { 151 mHandler.removeCallbacks(mPendingMarqueeStart); 152 mPendingMarqueeStart = null; 153 } 154 setEnableMarqueeImpl(false); 155 } 156 } 157 setEnableMarqueeImpl(boolean enabled)158 private void setEnableMarqueeImpl(boolean enabled) { 159 if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee"); 160 if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled); 161 } 162 163 @Override onFinishInflate()164 protected void onFinishInflate() { 165 super.onFinishInflate(); 166 mLogoutView = findViewById(R.id.logout); 167 if (mLogoutView != null) { 168 mLogoutView.setOnClickListener(this::onLogoutClicked); 169 } 170 171 mClockView = findViewById(R.id.clock_view); 172 mClockView.setShowCurrentUserTime(true); 173 if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) { 174 mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext)); 175 } 176 mOwnerInfo = findViewById(R.id.owner_info); 177 mKeyguardSlice = findViewById(R.id.keyguard_status_area); 178 mClockSeparator = findViewById(R.id.clock_separator); 179 mVisibleInDoze = Sets.newArraySet(mClockView, mKeyguardSlice); 180 mTextColor = mClockView.getCurrentTextColor(); 181 182 int clockStroke = getResources().getDimensionPixelSize(R.dimen.widget_small_font_stroke); 183 mClockView.getPaint().setStrokeWidth(clockStroke); 184 mClockView.addOnLayoutChangeListener(this); 185 mClockSeparator.addOnLayoutChangeListener(this); 186 mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged); 187 onSliceContentChanged(); 188 189 boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive(); 190 setEnableMarquee(shouldMarquee); 191 refreshFormat(); 192 updateOwnerInfo(); 193 updateLogoutView(); 194 updateDark(); 195 196 // Disable elegant text height because our fancy colon makes the ymin value huge for no 197 // reason. 198 mClockView.setElegantTextHeight(false); 199 } 200 onSliceContentChanged()201 private void onSliceContentChanged() { 202 boolean smallClock = mKeyguardSlice.hasHeader() || mPulsing; 203 float clockScale = smallClock ? mSmallClockScale : 1; 204 205 RelativeLayout.LayoutParams layoutParams = 206 (RelativeLayout.LayoutParams) mClockView.getLayoutParams(); 207 int height = mClockView.getHeight(); 208 layoutParams.bottomMargin = (int) -(height - (clockScale * height)); 209 mClockView.setLayoutParams(layoutParams); 210 211 layoutParams = (RelativeLayout.LayoutParams) mClockSeparator.getLayoutParams(); 212 layoutParams.topMargin = smallClock ? (int) mWidgetPadding : 0; 213 layoutParams.bottomMargin = layoutParams.topMargin; 214 mClockSeparator.setLayoutParams(layoutParams); 215 } 216 217 /** 218 * Animate clock and its separator when necessary. 219 */ 220 @Override onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)221 public void onLayoutChange(View view, int left, int top, int right, int bottom, 222 int oldLeft, int oldTop, int oldRight, int oldBottom) { 223 int heightOffset = mPulsing ? 0 : getHeight() - mLastLayoutHeight; 224 boolean hasHeader = mKeyguardSlice.hasHeader(); 225 boolean smallClock = hasHeader || mPulsing; 226 long duration = KeyguardSliceView.DEFAULT_ANIM_DURATION; 227 long delay = smallClock ? 0 : duration / 4; 228 229 boolean shouldAnimate = mKeyguardSlice.getLayoutTransition() != null 230 && mKeyguardSlice.getLayoutTransition().isRunning(); 231 if (view == mClockView) { 232 float clockScale = smallClock ? mSmallClockScale : 1; 233 Paint.Style style = smallClock ? Paint.Style.FILL_AND_STROKE : Paint.Style.FILL; 234 mClockView.animate().cancel(); 235 if (shouldAnimate) { 236 mClockView.setY(oldTop + heightOffset); 237 mClockView.animate() 238 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 239 .setDuration(duration) 240 .setListener(new ClipChildrenAnimationListener()) 241 .setStartDelay(delay) 242 .y(top) 243 .scaleX(clockScale) 244 .scaleY(clockScale) 245 .withEndAction(() -> { 246 mClockView.getPaint().setStyle(style); 247 mClockView.invalidate(); 248 }) 249 .start(); 250 } else { 251 mClockView.setY(top); 252 mClockView.setScaleX(clockScale); 253 mClockView.setScaleY(clockScale); 254 mClockView.getPaint().setStyle(style); 255 mClockView.invalidate(); 256 } 257 } else if (view == mClockSeparator) { 258 boolean hasSeparator = hasHeader && !mPulsing; 259 float alpha = hasSeparator ? 1 : 0; 260 mClockSeparator.animate().cancel(); 261 if (shouldAnimate) { 262 boolean isAwake = mDarkAmount != 0; 263 mClockSeparator.setY(oldTop + heightOffset); 264 mClockSeparator.animate() 265 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 266 .setDuration(duration) 267 .setListener(isAwake ? null : new KeepAwakeAnimationListener(getContext())) 268 .setStartDelay(delay) 269 .y(top) 270 .alpha(alpha) 271 .start(); 272 } else { 273 mClockSeparator.setY(top); 274 mClockSeparator.setAlpha(alpha); 275 } 276 } 277 } 278 279 @Override onLayout(boolean changed, int left, int top, int right, int bottom)280 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 281 super.onLayout(changed, left, top, right, bottom); 282 mClockView.setPivotX(mClockView.getWidth() / 2); 283 mClockView.setPivotY(0); 284 mLastLayoutHeight = getHeight(); 285 layoutOwnerInfo(); 286 } 287 288 @Override onDensityOrFontScaleChanged()289 public void onDensityOrFontScaleChanged() { 290 mWidgetPadding = getResources().getDimension(R.dimen.widget_vertical_padding); 291 if (mClockView != null) { 292 mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 293 getResources().getDimensionPixelSize(R.dimen.widget_big_font_size)); 294 mClockView.getPaint().setStrokeWidth( 295 getResources().getDimensionPixelSize(R.dimen.widget_small_font_stroke)); 296 } 297 if (mOwnerInfo != null) { 298 mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX, 299 getResources().getDimensionPixelSize(R.dimen.widget_label_font_size)); 300 } 301 } 302 dozeTimeTick()303 public void dozeTimeTick() { 304 refreshTime(); 305 mKeyguardSlice.refresh(); 306 } 307 refreshTime()308 private void refreshTime() { 309 mClockView.refresh(); 310 } 311 refreshFormat()312 private void refreshFormat() { 313 Patterns.update(mContext); 314 mClockView.setFormat12Hour(Patterns.clockView12); 315 mClockView.setFormat24Hour(Patterns.clockView24); 316 } 317 getLogoutButtonHeight()318 public int getLogoutButtonHeight() { 319 if (mLogoutView == null) { 320 return 0; 321 } 322 return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0; 323 } 324 getClockTextSize()325 public float getClockTextSize() { 326 return mClockView.getTextSize(); 327 } 328 updateLogoutView()329 private void updateLogoutView() { 330 if (mLogoutView == null) { 331 return; 332 } 333 mLogoutView.setVisibility(shouldShowLogout() ? VISIBLE : GONE); 334 // Logout button will stay in language of user 0 if we don't set that manually. 335 mLogoutView.setText(mContext.getResources().getString( 336 com.android.internal.R.string.global_action_logout)); 337 } 338 updateOwnerInfo()339 private void updateOwnerInfo() { 340 if (mOwnerInfo == null) return; 341 String info = mLockPatternUtils.getDeviceOwnerInfo(); 342 if (info == null) { 343 // Use the current user owner information if enabled. 344 final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled( 345 KeyguardUpdateMonitor.getCurrentUser()); 346 if (ownerInfoEnabled) { 347 info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser()); 348 } 349 } 350 mOwnerInfo.setText(info); 351 } 352 353 @Override onAttachedToWindow()354 protected void onAttachedToWindow() { 355 super.onAttachedToWindow(); 356 KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback); 357 Dependency.get(ConfigurationController.class).addCallback(this); 358 } 359 360 @Override onDetachedFromWindow()361 protected void onDetachedFromWindow() { 362 super.onDetachedFromWindow(); 363 KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback); 364 Dependency.get(ConfigurationController.class).removeCallback(this); 365 } 366 367 @Override onLocaleListChanged()368 public void onLocaleListChanged() { 369 refreshFormat(); 370 } 371 372 @Override hasOverlappingRendering()373 public boolean hasOverlappingRendering() { 374 return false; 375 } 376 377 // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. 378 // This is an optimization to ensure we only recompute the patterns when the inputs change. 379 private static final class Patterns { 380 static String clockView12; 381 static String clockView24; 382 static String cacheKey; 383 update(Context context)384 static void update(Context context) { 385 final Locale locale = Locale.getDefault(); 386 final Resources res = context.getResources(); 387 final String clockView12Skel = res.getString(R.string.clock_12hr_format); 388 final String clockView24Skel = res.getString(R.string.clock_24hr_format); 389 final String key = locale.toString() + clockView12Skel + clockView24Skel; 390 if (key.equals(cacheKey)) return; 391 392 clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); 393 // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton 394 // format. The following code removes the AM/PM indicator if we didn't want it. 395 if (!clockView12Skel.contains("a")) { 396 clockView12 = clockView12.replaceAll("a", "").trim(); 397 } 398 399 clockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); 400 401 // Use fancy colon. 402 clockView24 = clockView24.replace(':', '\uee01'); 403 clockView12 = clockView12.replace(':', '\uee01'); 404 405 cacheKey = key; 406 } 407 } 408 setDarkAmount(float darkAmount)409 public void setDarkAmount(float darkAmount) { 410 if (mDarkAmount == darkAmount) { 411 return; 412 } 413 mDarkAmount = darkAmount; 414 updateDark(); 415 } 416 updateDark()417 private void updateDark() { 418 boolean dark = mDarkAmount == 1; 419 if (mLogoutView != null) { 420 mLogoutView.setAlpha(dark ? 0 : 1); 421 } 422 423 if (mOwnerInfo != null) { 424 boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText()); 425 mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE); 426 layoutOwnerInfo(); 427 } 428 429 final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); 430 updateDozeVisibleViews(); 431 mKeyguardSlice.setDarkAmount(mDarkAmount); 432 mClockView.setTextColor(blendedTextColor); 433 mClockSeparator.setBackgroundColor(blendedTextColor); 434 } 435 layoutOwnerInfo()436 private void layoutOwnerInfo() { 437 if (mOwnerInfo != null && mOwnerInfo.getVisibility() != GONE) { 438 // Animate owner info during wake-up transition 439 mOwnerInfo.setAlpha(1f - mDarkAmount); 440 441 float ratio = mDarkAmount; 442 // Calculate how much of it we should crop in order to have a smooth transition 443 int collapsed = mOwnerInfo.getTop() - mOwnerInfo.getPaddingTop(); 444 int expanded = mOwnerInfo.getBottom() + mOwnerInfo.getPaddingBottom(); 445 int toRemove = (int) ((expanded - collapsed) * ratio); 446 setBottom(getMeasuredHeight() - toRemove); 447 } 448 } 449 setPulsing(boolean pulsing, boolean animate)450 public void setPulsing(boolean pulsing, boolean animate) { 451 mPulsing = pulsing; 452 mKeyguardSlice.setPulsing(pulsing, animate); 453 updateDozeVisibleViews(); 454 } 455 updateDozeVisibleViews()456 private void updateDozeVisibleViews() { 457 for (View child : mVisibleInDoze) { 458 child.setAlpha(mDarkAmount == 1 && mPulsing ? 0.8f : 1); 459 } 460 } 461 shouldShowLogout()462 private boolean shouldShowLogout() { 463 return KeyguardUpdateMonitor.getInstance(mContext).isLogoutEnabled() 464 && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM; 465 } 466 onLogoutClicked(View view)467 private void onLogoutClicked(View view) { 468 int currentUserId = KeyguardUpdateMonitor.getCurrentUser(); 469 try { 470 mIActivityManager.switchUser(UserHandle.USER_SYSTEM); 471 mIActivityManager.stopUser(currentUserId, true /*force*/, null); 472 } catch (RemoteException re) { 473 Log.e(TAG, "Failed to logout user", re); 474 } 475 } 476 477 private class ClipChildrenAnimationListener extends AnimatorListenerAdapter implements 478 ViewClippingUtil.ClippingParameters { 479 ClipChildrenAnimationListener()480 ClipChildrenAnimationListener() { 481 ViewClippingUtil.setClippingDeactivated(mClockView, true /* deactivated */, 482 this /* clippingParams */); 483 } 484 485 @Override onAnimationEnd(Animator animation)486 public void onAnimationEnd(Animator animation) { 487 ViewClippingUtil.setClippingDeactivated(mClockView, false /* deactivated */, 488 this /* clippingParams */); 489 } 490 491 @Override shouldFinish(View view)492 public boolean shouldFinish(View view) { 493 return view == getParent(); 494 } 495 } 496 } 497