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