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.app.ActivityManager; 20 import android.app.IActivityManager; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Color; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.RemoteException; 27 import android.os.UserHandle; 28 import android.text.TextUtils; 29 import android.text.format.DateFormat; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.Slog; 33 import android.util.TypedValue; 34 import android.view.View; 35 import android.widget.GridLayout; 36 import android.widget.LinearLayout; 37 import android.widget.TextView; 38 39 import androidx.core.graphics.ColorUtils; 40 41 import com.android.internal.widget.LockPatternUtils; 42 import com.android.systemui.Dependency; 43 import com.android.systemui.R; 44 import com.android.systemui.statusbar.policy.ConfigurationController; 45 46 import java.io.FileDescriptor; 47 import java.io.PrintWriter; 48 import java.util.Locale; 49 import java.util.TimeZone; 50 51 public class KeyguardStatusView extends GridLayout implements 52 ConfigurationController.ConfigurationListener { 53 private static final boolean DEBUG = KeyguardConstants.DEBUG; 54 private static final String TAG = "KeyguardStatusView"; 55 private static final int MARQUEE_DELAY_MS = 2000; 56 57 private final LockPatternUtils mLockPatternUtils; 58 private final IActivityManager mIActivityManager; 59 60 private LinearLayout mStatusViewContainer; 61 private TextView mLogoutView; 62 private KeyguardClockSwitch mClockView; 63 private TextView mOwnerInfo; 64 private KeyguardSliceView mKeyguardSlice; 65 private View mNotificationIcons; 66 private Runnable mPendingMarqueeStart; 67 private Handler mHandler; 68 69 private boolean mPulsing; 70 private float mDarkAmount = 0; 71 private int mTextColor; 72 73 /** 74 * Bottom margin that defines the margin between bottom of smart space and top of notification 75 * icons on AOD. 76 */ 77 private int mIconTopMargin; 78 private int mIconTopMarginWithHeader; 79 private boolean mShowingHeader; 80 81 private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { 82 83 @Override 84 public void onTimeChanged() { 85 refreshTime(); 86 } 87 88 @Override 89 public void onTimeZoneChanged(TimeZone timeZone) { 90 updateTimeZone(timeZone); 91 } 92 93 @Override 94 public void onKeyguardVisibilityChanged(boolean showing) { 95 if (showing) { 96 if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); 97 refreshTime(); 98 updateOwnerInfo(); 99 updateLogoutView(); 100 } 101 } 102 103 @Override 104 public void onStartedWakingUp() { 105 setEnableMarquee(true); 106 } 107 108 @Override 109 public void onFinishedGoingToSleep(int why) { 110 setEnableMarquee(false); 111 } 112 113 @Override 114 public void onUserSwitchComplete(int userId) { 115 refreshFormat(); 116 updateOwnerInfo(); 117 updateLogoutView(); 118 } 119 120 @Override 121 public void onLogoutEnabledChanged() { 122 updateLogoutView(); 123 } 124 }; 125 KeyguardStatusView(Context context)126 public KeyguardStatusView(Context context) { 127 this(context, null, 0); 128 } 129 KeyguardStatusView(Context context, AttributeSet attrs)130 public KeyguardStatusView(Context context, AttributeSet attrs) { 131 this(context, attrs, 0); 132 } 133 KeyguardStatusView(Context context, AttributeSet attrs, int defStyle)134 public KeyguardStatusView(Context context, AttributeSet attrs, int defStyle) { 135 super(context, attrs, defStyle); 136 mIActivityManager = ActivityManager.getService(); 137 mLockPatternUtils = new LockPatternUtils(getContext()); 138 mHandler = new Handler(Looper.myLooper()); 139 onDensityOrFontScaleChanged(); 140 } 141 142 /** 143 * If we're presenting a custom clock of just the default one. 144 */ hasCustomClock()145 public boolean hasCustomClock() { 146 return mClockView.hasCustomClock(); 147 } 148 149 /** 150 * Set whether or not the lock screen is showing notifications. 151 */ setHasVisibleNotifications(boolean hasVisibleNotifications)152 public void setHasVisibleNotifications(boolean hasVisibleNotifications) { 153 mClockView.setHasVisibleNotifications(hasVisibleNotifications); 154 } 155 setEnableMarquee(boolean enabled)156 private void setEnableMarquee(boolean enabled) { 157 if (DEBUG) Log.v(TAG, "Schedule setEnableMarquee: " + (enabled ? "Enable" : "Disable")); 158 if (enabled) { 159 if (mPendingMarqueeStart == null) { 160 mPendingMarqueeStart = () -> { 161 setEnableMarqueeImpl(true); 162 mPendingMarqueeStart = null; 163 }; 164 mHandler.postDelayed(mPendingMarqueeStart, MARQUEE_DELAY_MS); 165 } 166 } else { 167 if (mPendingMarqueeStart != null) { 168 mHandler.removeCallbacks(mPendingMarqueeStart); 169 mPendingMarqueeStart = null; 170 } 171 setEnableMarqueeImpl(false); 172 } 173 } 174 setEnableMarqueeImpl(boolean enabled)175 private void setEnableMarqueeImpl(boolean enabled) { 176 if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee"); 177 if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled); 178 } 179 180 @Override onFinishInflate()181 protected void onFinishInflate() { 182 super.onFinishInflate(); 183 mStatusViewContainer = findViewById(R.id.status_view_container); 184 mLogoutView = findViewById(R.id.logout); 185 mNotificationIcons = findViewById(R.id.clock_notification_icon_container); 186 if (mLogoutView != null) { 187 mLogoutView.setOnClickListener(this::onLogoutClicked); 188 } 189 190 mClockView = findViewById(R.id.keyguard_clock_container); 191 mClockView.setShowCurrentUserTime(true); 192 if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) { 193 mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext)); 194 } 195 mOwnerInfo = findViewById(R.id.owner_info); 196 mKeyguardSlice = findViewById(R.id.keyguard_status_area); 197 mTextColor = mClockView.getCurrentTextColor(); 198 199 mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged); 200 onSliceContentChanged(); 201 202 boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive(); 203 setEnableMarquee(shouldMarquee); 204 refreshFormat(); 205 updateOwnerInfo(); 206 updateLogoutView(); 207 updateDark(); 208 } 209 210 /** 211 * Moves clock, adjusting margins when slice content changes. 212 */ onSliceContentChanged()213 private void onSliceContentChanged() { 214 final boolean hasHeader = mKeyguardSlice.hasHeader(); 215 mClockView.setKeyguardShowingHeader(hasHeader); 216 if (mShowingHeader == hasHeader) { 217 return; 218 } 219 mShowingHeader = hasHeader; 220 if (mNotificationIcons != null) { 221 // Update top margin since header has appeared/disappeared. 222 MarginLayoutParams params = (MarginLayoutParams) mNotificationIcons.getLayoutParams(); 223 params.setMargins(params.leftMargin, 224 hasHeader ? mIconTopMarginWithHeader : mIconTopMargin, 225 params.rightMargin, 226 params.bottomMargin); 227 mNotificationIcons.setLayoutParams(params); 228 } 229 } 230 231 @Override onLayout(boolean changed, int left, int top, int right, int bottom)232 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 233 super.onLayout(changed, left, top, right, bottom); 234 layoutOwnerInfo(); 235 } 236 237 @Override onDensityOrFontScaleChanged()238 public void onDensityOrFontScaleChanged() { 239 if (mClockView != null) { 240 mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 241 getResources().getDimensionPixelSize(R.dimen.widget_big_font_size)); 242 } 243 if (mOwnerInfo != null) { 244 mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX, 245 getResources().getDimensionPixelSize(R.dimen.widget_label_font_size)); 246 } 247 loadBottomMargin(); 248 } 249 dozeTimeTick()250 public void dozeTimeTick() { 251 refreshTime(); 252 mKeyguardSlice.refresh(); 253 } 254 refreshTime()255 private void refreshTime() { 256 mClockView.refresh(); 257 } 258 updateTimeZone(TimeZone timeZone)259 private void updateTimeZone(TimeZone timeZone) { 260 mClockView.onTimeZoneChanged(timeZone); 261 } 262 refreshFormat()263 private void refreshFormat() { 264 Patterns.update(mContext); 265 mClockView.setFormat12Hour(Patterns.clockView12); 266 mClockView.setFormat24Hour(Patterns.clockView24); 267 } 268 getLogoutButtonHeight()269 public int getLogoutButtonHeight() { 270 if (mLogoutView == null) { 271 return 0; 272 } 273 return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0; 274 } 275 getClockTextSize()276 public float getClockTextSize() { 277 return mClockView.getTextSize(); 278 } 279 280 /** 281 * Returns the preferred Y position of the clock. 282 * 283 * @param totalHeight The height available to position the clock. 284 * @return Y position of clock. 285 */ getClockPreferredY(int totalHeight)286 public int getClockPreferredY(int totalHeight) { 287 return mClockView.getPreferredY(totalHeight); 288 } 289 updateLogoutView()290 private void updateLogoutView() { 291 if (mLogoutView == null) { 292 return; 293 } 294 mLogoutView.setVisibility(shouldShowLogout() ? VISIBLE : GONE); 295 // Logout button will stay in language of user 0 if we don't set that manually. 296 mLogoutView.setText(mContext.getResources().getString( 297 com.android.internal.R.string.global_action_logout)); 298 } 299 updateOwnerInfo()300 private void updateOwnerInfo() { 301 if (mOwnerInfo == null) return; 302 String info = mLockPatternUtils.getDeviceOwnerInfo(); 303 if (info == null) { 304 // Use the current user owner information if enabled. 305 final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled( 306 KeyguardUpdateMonitor.getCurrentUser()); 307 if (ownerInfoEnabled) { 308 info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser()); 309 } 310 } 311 mOwnerInfo.setText(info); 312 updateDark(); 313 } 314 315 @Override onAttachedToWindow()316 protected void onAttachedToWindow() { 317 super.onAttachedToWindow(); 318 KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback); 319 Dependency.get(ConfigurationController.class).addCallback(this); 320 } 321 322 @Override onDetachedFromWindow()323 protected void onDetachedFromWindow() { 324 super.onDetachedFromWindow(); 325 KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback); 326 Dependency.get(ConfigurationController.class).removeCallback(this); 327 } 328 329 @Override onLocaleListChanged()330 public void onLocaleListChanged() { 331 refreshFormat(); 332 } 333 dump(FileDescriptor fd, PrintWriter pw, String[] args)334 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 335 pw.println("KeyguardStatusView:"); 336 pw.println(" mOwnerInfo: " + (mOwnerInfo == null 337 ? "null" : mOwnerInfo.getVisibility() == VISIBLE)); 338 pw.println(" mPulsing: " + mPulsing); 339 pw.println(" mDarkAmount: " + mDarkAmount); 340 pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); 341 if (mLogoutView != null) { 342 pw.println(" logout visible: " + (mLogoutView.getVisibility() == VISIBLE)); 343 } 344 if (mClockView != null) { 345 mClockView.dump(fd, pw, args); 346 } 347 if (mKeyguardSlice != null) { 348 mKeyguardSlice.dump(fd, pw, args); 349 } 350 } 351 loadBottomMargin()352 private void loadBottomMargin() { 353 mIconTopMargin = getResources().getDimensionPixelSize(R.dimen.widget_vertical_padding); 354 mIconTopMarginWithHeader = getResources().getDimensionPixelSize( 355 R.dimen.widget_vertical_padding_with_header); 356 } 357 358 // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. 359 // This is an optimization to ensure we only recompute the patterns when the inputs change. 360 private static final class Patterns { 361 static String clockView12; 362 static String clockView24; 363 static String cacheKey; 364 update(Context context)365 static void update(Context context) { 366 final Locale locale = Locale.getDefault(); 367 final Resources res = context.getResources(); 368 final String clockView12Skel = res.getString(R.string.clock_12hr_format); 369 final String clockView24Skel = res.getString(R.string.clock_24hr_format); 370 final String key = locale.toString() + clockView12Skel + clockView24Skel; 371 if (key.equals(cacheKey)) return; 372 373 clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); 374 // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton 375 // format. The following code removes the AM/PM indicator if we didn't want it. 376 if (!clockView12Skel.contains("a")) { 377 clockView12 = clockView12.replaceAll("a", "").trim(); 378 } 379 380 clockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); 381 382 // Use fancy colon. 383 clockView24 = clockView24.replace(':', '\uee01'); 384 clockView12 = clockView12.replace(':', '\uee01'); 385 386 cacheKey = key; 387 } 388 } 389 setDarkAmount(float darkAmount)390 public void setDarkAmount(float darkAmount) { 391 if (mDarkAmount == darkAmount) { 392 return; 393 } 394 mDarkAmount = darkAmount; 395 mClockView.setDarkAmount(darkAmount); 396 updateDark(); 397 } 398 updateDark()399 private void updateDark() { 400 boolean dark = mDarkAmount == 1; 401 if (mLogoutView != null) { 402 mLogoutView.setAlpha(dark ? 0 : 1); 403 } 404 405 if (mOwnerInfo != null) { 406 boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText()); 407 mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE); 408 layoutOwnerInfo(); 409 } 410 411 final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); 412 mKeyguardSlice.setDarkAmount(mDarkAmount); 413 mClockView.setTextColor(blendedTextColor); 414 } 415 layoutOwnerInfo()416 private void layoutOwnerInfo() { 417 if (mOwnerInfo != null && mOwnerInfo.getVisibility() != GONE) { 418 // Animate owner info during wake-up transition 419 mOwnerInfo.setAlpha(1f - mDarkAmount); 420 421 float ratio = mDarkAmount; 422 // Calculate how much of it we should crop in order to have a smooth transition 423 int collapsed = mOwnerInfo.getTop() - mOwnerInfo.getPaddingTop(); 424 int expanded = mOwnerInfo.getBottom() + mOwnerInfo.getPaddingBottom(); 425 int toRemove = (int) ((expanded - collapsed) * ratio); 426 setBottom(getMeasuredHeight() - toRemove); 427 if (mNotificationIcons != null) { 428 // We're using scrolling in order not to overload the translation which is used 429 // when appearing the icons 430 mNotificationIcons.setScrollY(toRemove); 431 } 432 } else if (mNotificationIcons != null){ 433 mNotificationIcons.setScrollY(0); 434 } 435 } 436 setPulsing(boolean pulsing)437 public void setPulsing(boolean pulsing) { 438 if (mPulsing == pulsing) { 439 return; 440 } 441 mPulsing = pulsing; 442 } 443 shouldShowLogout()444 private boolean shouldShowLogout() { 445 return KeyguardUpdateMonitor.getInstance(mContext).isLogoutEnabled() 446 && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM; 447 } 448 onLogoutClicked(View view)449 private void onLogoutClicked(View view) { 450 int currentUserId = KeyguardUpdateMonitor.getCurrentUser(); 451 try { 452 mIActivityManager.switchUser(UserHandle.USER_SYSTEM); 453 mIActivityManager.stopUser(currentUserId, true /*force*/, null); 454 } catch (RemoteException re) { 455 Log.e(TAG, "Failed to logout user", re); 456 } 457 } 458 } 459