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