1 /* 2 * Copyright (C) 2017 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.systemui.keyguard; 18 19 import android.annotation.AnyThread; 20 import android.app.AlarmManager; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.graphics.Typeface; 28 import android.graphics.drawable.Icon; 29 import android.icu.text.DateFormat; 30 import android.icu.text.DisplayContext; 31 import android.media.MediaMetadata; 32 import android.media.session.PlaybackState; 33 import android.net.Uri; 34 import android.os.Handler; 35 import android.os.Trace; 36 import android.provider.Settings; 37 import android.service.notification.ZenModeConfig; 38 import android.text.TextUtils; 39 import android.text.style.StyleSpan; 40 import android.util.Log; 41 42 import androidx.core.graphics.drawable.IconCompat; 43 import androidx.slice.Slice; 44 import androidx.slice.SliceProvider; 45 import androidx.slice.builders.ListBuilder; 46 import androidx.slice.builders.ListBuilder.RowBuilder; 47 import androidx.slice.builders.SliceAction; 48 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.keyguard.KeyguardUpdateMonitor; 51 import com.android.keyguard.KeyguardUpdateMonitorCallback; 52 import com.android.systemui.SystemUIAppComponentFactoryBase; 53 import com.android.systemui.dagger.qualifiers.Background; 54 import com.android.systemui.media.NotificationMediaManager; 55 import com.android.systemui.plugins.statusbar.StatusBarStateController; 56 import com.android.systemui.res.R; 57 import com.android.systemui.settings.UserTracker; 58 import com.android.systemui.statusbar.StatusBarState; 59 import com.android.systemui.statusbar.phone.DozeParameters; 60 import com.android.systemui.statusbar.phone.KeyguardBypassController; 61 import com.android.systemui.statusbar.policy.NextAlarmController; 62 import com.android.systemui.statusbar.policy.ZenModeController; 63 import com.android.systemui.util.wakelock.SettableWakeLock; 64 import com.android.systemui.util.wakelock.WakeLock; 65 import com.android.systemui.util.wakelock.WakeLockLogger; 66 67 import java.util.Date; 68 import java.util.Locale; 69 import java.util.TimeZone; 70 import java.util.concurrent.TimeUnit; 71 72 import javax.inject.Inject; 73 74 /** 75 * Simple Slice provider that shows the current date. 76 * 77 * Injection is handled by {@link SystemUIAppComponentFactoryBase} + 78 * {@link com.android.systemui.dagger.GlobalRootComponent#inject(KeyguardSliceProvider)}. 79 */ 80 public class KeyguardSliceProvider extends SliceProvider implements 81 NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback, 82 NotificationMediaManager.MediaListener, StatusBarStateController.StateListener, 83 SystemUIAppComponentFactoryBase.ContextInitializer { 84 85 private static final String TAG = "KgdSliceProvider"; 86 87 private static final StyleSpan BOLD_STYLE = new StyleSpan(Typeface.BOLD); 88 public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main"; 89 private static final String KEYGUARD_HEADER_URI = 90 "content://com.android.systemui.keyguard/header"; 91 public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date"; 92 public static final String KEYGUARD_NEXT_ALARM_URI = 93 "content://com.android.systemui.keyguard/alarm"; 94 public static final String KEYGUARD_DND_URI = "content://com.android.systemui.keyguard/dnd"; 95 public static final String KEYGUARD_MEDIA_URI = 96 "content://com.android.systemui.keyguard/media"; 97 public static final String KEYGUARD_ACTION_URI = 98 "content://com.android.systemui.keyguard/action"; 99 100 /** 101 * Only show alarms that will ring within N hours. 102 */ 103 @VisibleForTesting 104 static final int ALARM_VISIBILITY_HOURS = 12; 105 106 private static final Object sInstanceLock = new Object(); 107 private static KeyguardSliceProvider sInstance; 108 109 protected final Uri mSliceUri; 110 protected final Uri mHeaderUri; 111 protected final Uri mDateUri; 112 protected final Uri mAlarmUri; 113 protected final Uri mDndUri; 114 protected final Uri mMediaUri; 115 private final Date mCurrentTime = new Date(); 116 private final Handler mHandler; 117 private final Handler mMediaHandler; 118 private final AlarmManager.OnAlarmListener mUpdateNextAlarm = this::updateNextAlarm; 119 @Inject 120 public DozeParameters mDozeParameters; 121 @VisibleForTesting 122 protected SettableWakeLock mMediaWakeLock; 123 @Inject 124 public ZenModeController mZenModeController; 125 private String mDatePattern; 126 private DateFormat mDateFormat; 127 private String mLastText; 128 private boolean mRegistered; 129 private String mNextAlarm; 130 @Inject 131 public NextAlarmController mNextAlarmController; 132 @Inject 133 public AlarmManager mAlarmManager; 134 @Inject 135 public ContentResolver mContentResolver; 136 private AlarmManager.AlarmClockInfo mNextAlarmInfo; 137 private PendingIntent mPendingIntent; 138 @Inject 139 public NotificationMediaManager mMediaManager; 140 @Inject 141 public StatusBarStateController mStatusBarStateController; 142 @Inject 143 public KeyguardBypassController mKeyguardBypassController; 144 @Inject 145 public KeyguardUpdateMonitor mKeyguardUpdateMonitor; 146 @Inject 147 UserTracker mUserTracker; 148 private CharSequence mMediaTitle; 149 private CharSequence mMediaArtist; 150 protected boolean mDozing; 151 private int mStatusBarState; 152 private boolean mMediaIsVisible; 153 private SystemUIAppComponentFactoryBase.ContextAvailableCallback mContextAvailableCallback; 154 @Inject 155 WakeLockLogger mWakeLockLogger; 156 @Inject 157 @Background 158 Handler mBgHandler; 159 160 /** 161 * Receiver responsible for time ticking and updating the date format. 162 */ 163 @VisibleForTesting 164 final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 165 @Override 166 public void onReceive(Context context, Intent intent) { 167 final String action = intent.getAction(); 168 if (Intent.ACTION_DATE_CHANGED.equals(action)) { 169 synchronized (this) { 170 updateClockLocked(); 171 } 172 } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { 173 synchronized (this) { 174 cleanDateFormatLocked(); 175 } 176 } 177 } 178 }; 179 180 @VisibleForTesting 181 final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = 182 new KeyguardUpdateMonitorCallback() { 183 @Override 184 public void onTimeChanged() { 185 synchronized (this) { 186 updateClockLocked(); 187 } 188 } 189 190 @Override 191 public void onTimeZoneChanged(TimeZone timeZone) { 192 synchronized (this) { 193 cleanDateFormatLocked(); 194 } 195 } 196 }; 197 getAttachedInstance()198 public static KeyguardSliceProvider getAttachedInstance() { 199 return KeyguardSliceProvider.sInstance; 200 } 201 KeyguardSliceProvider()202 public KeyguardSliceProvider() { 203 mHandler = new Handler(); 204 mMediaHandler = new Handler(); 205 mSliceUri = Uri.parse(KEYGUARD_SLICE_URI); 206 mHeaderUri = Uri.parse(KEYGUARD_HEADER_URI); 207 mDateUri = Uri.parse(KEYGUARD_DATE_URI); 208 mAlarmUri = Uri.parse(KEYGUARD_NEXT_ALARM_URI); 209 mDndUri = Uri.parse(KEYGUARD_DND_URI); 210 mMediaUri = Uri.parse(KEYGUARD_MEDIA_URI); 211 } 212 213 @AnyThread 214 @Override onBindSlice(Uri sliceUri)215 public Slice onBindSlice(Uri sliceUri) { 216 Slice slice = null; 217 try { 218 Trace.beginSection("KeyguardSliceProvider#onBindSlice"); 219 synchronized (this) { 220 ListBuilder builder = new ListBuilder(getContext(), mSliceUri, 221 ListBuilder.INFINITY); 222 if (needsMediaLocked()) { 223 addMediaLocked(builder); 224 } else { 225 builder.addRow(new RowBuilder(mDateUri).setTitle(mLastText)); 226 } 227 addNextAlarmLocked(builder); 228 addZenModeLocked(builder); 229 addPrimaryActionLocked(builder); 230 slice = builder.build(); 231 } 232 } catch (IllegalStateException e) { 233 Log.w(TAG, "Could not initialize slice", e); 234 } finally { 235 Trace.endSection(); 236 } 237 return slice; 238 } 239 needsMediaLocked()240 protected boolean needsMediaLocked() { 241 boolean keepWhenAwake = mKeyguardBypassController != null 242 && mKeyguardBypassController.getBypassEnabled() && mDozeParameters.getAlwaysOn(); 243 // Show header if music is playing and the status bar is in the shade state. This way, an 244 // animation isn't necessary when pressing power and transitioning to AOD. 245 boolean keepWhenShade = mStatusBarState == StatusBarState.SHADE && mMediaIsVisible; 246 return !TextUtils.isEmpty(mMediaTitle) && mMediaIsVisible && (mDozing || keepWhenAwake 247 || keepWhenShade); 248 } 249 addMediaLocked(ListBuilder listBuilder)250 protected void addMediaLocked(ListBuilder listBuilder) { 251 if (TextUtils.isEmpty(mMediaTitle)) { 252 return; 253 } 254 listBuilder.setHeader(new ListBuilder.HeaderBuilder(mHeaderUri).setTitle(mMediaTitle)); 255 256 if (!TextUtils.isEmpty(mMediaArtist)) { 257 RowBuilder albumBuilder = new RowBuilder(mMediaUri); 258 albumBuilder.setTitle(mMediaArtist); 259 260 Icon mediaIcon = mMediaManager == null ? null : mMediaManager.getMediaIcon(); 261 IconCompat mediaIconCompat = mediaIcon == null ? null 262 : IconCompat.createFromIcon(getContext(), mediaIcon); 263 if (mediaIconCompat != null) { 264 albumBuilder.addEndItem(mediaIconCompat, ListBuilder.ICON_IMAGE); 265 } 266 267 listBuilder.addRow(albumBuilder); 268 } 269 } 270 addPrimaryActionLocked(ListBuilder builder)271 protected void addPrimaryActionLocked(ListBuilder builder) { 272 // Add simple action because API requires it; Keyguard handles presenting 273 // its own slices so this action + icon are actually never used. 274 IconCompat icon = IconCompat.createWithResource(getContext(), 275 R.drawable.ic_access_alarms_big); 276 SliceAction action = SliceAction.createDeeplink(mPendingIntent, icon, 277 ListBuilder.ICON_IMAGE, mLastText); 278 RowBuilder primaryActionRow = new RowBuilder(Uri.parse(KEYGUARD_ACTION_URI)) 279 .setPrimaryAction(action); 280 builder.addRow(primaryActionRow); 281 } 282 addNextAlarmLocked(ListBuilder builder)283 protected void addNextAlarmLocked(ListBuilder builder) { 284 if (TextUtils.isEmpty(mNextAlarm)) { 285 return; 286 } 287 IconCompat alarmIcon = IconCompat.createWithResource(getContext(), 288 R.drawable.ic_access_alarms_big); 289 RowBuilder alarmRowBuilder = new RowBuilder(mAlarmUri) 290 .setTitle(mNextAlarm) 291 .addEndItem(alarmIcon, ListBuilder.ICON_IMAGE); 292 builder.addRow(alarmRowBuilder); 293 } 294 295 /** 296 * Add zen mode (DND) icon to slice if it's enabled. 297 * @param builder The slice builder. 298 */ addZenModeLocked(ListBuilder builder)299 protected void addZenModeLocked(ListBuilder builder) { 300 if (!isDndOn()) { 301 return; 302 } 303 RowBuilder dndBuilder = new RowBuilder(mDndUri) 304 .setContentDescription(getContext().getResources() 305 .getString(R.string.accessibility_quick_settings_dnd)) 306 .addEndItem( 307 IconCompat.createWithResource(getContext(), R.drawable.stat_sys_dnd), 308 ListBuilder.ICON_IMAGE); 309 builder.addRow(dndBuilder); 310 } 311 312 /** 313 * Return true if DND is enabled. 314 */ isDndOn()315 protected boolean isDndOn() { 316 return mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF; 317 } 318 319 @Override onCreateSliceProvider()320 public boolean onCreateSliceProvider() { 321 mContextAvailableCallback.onContextAvailable(getContext()); 322 if (mMediaManager == null) { 323 Log.e(TAG, "Dagger injection failed, cannot start. See any above warnings with string: " 324 + "\"No injector for class\""); 325 return false; 326 } 327 mMediaWakeLock = new SettableWakeLock( 328 WakeLock.createPartial(getContext(), mWakeLockLogger, "media"), "media"); 329 synchronized (KeyguardSliceProvider.sInstanceLock) { 330 KeyguardSliceProvider oldInstance = KeyguardSliceProvider.sInstance; 331 if (oldInstance != null) { 332 oldInstance.onDestroy(); 333 } 334 mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern); 335 mPendingIntent = PendingIntent.getActivity(getContext(), 0, 336 new Intent(getContext(), KeyguardSliceProvider.class), 337 PendingIntent.FLAG_IMMUTABLE); 338 mMediaManager.addCallback(this); 339 mStatusBarStateController.addCallback(this); 340 mNextAlarmController.addCallback(this); 341 mZenModeController.addCallback(this); 342 KeyguardSliceProvider.sInstance = this; 343 registerClockUpdate(); 344 updateClockLocked(); 345 } 346 return true; 347 } 348 349 @VisibleForTesting onDestroy()350 protected void onDestroy() { 351 synchronized (KeyguardSliceProvider.sInstanceLock) { 352 mNextAlarmController.removeCallback(this); 353 mZenModeController.removeCallback(this); 354 mMediaWakeLock.setAcquired(false); 355 mAlarmManager.cancel(mUpdateNextAlarm); 356 if (mRegistered) { 357 mRegistered = false; 358 mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); 359 getContext().unregisterReceiver(mIntentReceiver); 360 } 361 KeyguardSliceProvider.sInstance = null; 362 } 363 } 364 365 @Override onZenChanged(int zen)366 public void onZenChanged(int zen) { 367 notifyChange(); 368 } 369 370 @Override onConfigChanged(ZenModeConfig config)371 public void onConfigChanged(ZenModeConfig config) { 372 notifyChange(); 373 } 374 updateNextAlarm()375 private void updateNextAlarm() { 376 synchronized (this) { 377 if (withinNHoursLocked(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) { 378 String pattern = android.text.format.DateFormat.is24HourFormat(getContext(), 379 mUserTracker.getUserId()) ? "HH:mm" : "h:mm"; 380 mNextAlarm = android.text.format.DateFormat.format(pattern, 381 mNextAlarmInfo.getTriggerTime()).toString(); 382 } else { 383 mNextAlarm = ""; 384 } 385 } 386 notifyChange(); 387 } 388 withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours)389 private boolean withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours) { 390 if (alarmClockInfo == null) { 391 return false; 392 } 393 394 long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours); 395 return mNextAlarmInfo.getTriggerTime() <= limit; 396 } 397 398 /** 399 * Registers a broadcast receiver for clock updates, include date, time zone and manually 400 * changing the date/time via the settings app. 401 */ 402 @VisibleForTesting registerClockUpdate()403 protected void registerClockUpdate() { 404 synchronized (this) { 405 if (mRegistered) { 406 return; 407 } 408 409 IntentFilter filter = new IntentFilter(); 410 filter.addAction(Intent.ACTION_DATE_CHANGED); 411 filter.addAction(Intent.ACTION_LOCALE_CHANGED); 412 mBgHandler.post(() -> { 413 getContext().registerReceiver(mIntentReceiver, filter, null /* permission*/, 414 null /* scheduler */); 415 }); 416 mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); 417 mRegistered = true; 418 } 419 } 420 421 @VisibleForTesting isRegistered()422 boolean isRegistered() { 423 synchronized (this) { 424 return mRegistered; 425 } 426 } 427 updateClockLocked()428 protected void updateClockLocked() { 429 final String text = getFormattedDateLocked(); 430 if (!text.equals(mLastText)) { 431 mLastText = text; 432 notifyChange(); 433 } 434 } 435 getFormattedDateLocked()436 protected String getFormattedDateLocked() { 437 if (mDateFormat == null) { 438 final Locale l = Locale.getDefault(); 439 DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l); 440 // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of 441 // CAPITALIZATION_FOR_STANDALONE is to address 442 // https://unicode-org.atlassian.net/browse/ICU-21631 443 // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE 444 format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE); 445 mDateFormat = format; 446 } 447 mCurrentTime.setTime(System.currentTimeMillis()); 448 return mDateFormat.format(mCurrentTime); 449 } 450 451 @VisibleForTesting cleanDateFormatLocked()452 void cleanDateFormatLocked() { 453 mDateFormat = null; 454 } 455 456 @Override onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm)457 public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { 458 synchronized (this) { 459 mNextAlarmInfo = nextAlarm; 460 mAlarmManager.cancel(mUpdateNextAlarm); 461 462 long triggerAt = mNextAlarmInfo == null ? -1 : mNextAlarmInfo.getTriggerTime() 463 - TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS); 464 if (triggerAt > 0) { 465 mAlarmManager.setExact(AlarmManager.RTC, triggerAt, "lock_screen_next_alarm", 466 mUpdateNextAlarm, mHandler); 467 } 468 } 469 updateNextAlarm(); 470 } 471 472 /** 473 * Called whenever new media metadata is available. 474 * @param metadata New metadata. 475 */ 476 @Override onPrimaryMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state)477 public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, 478 @PlaybackState.State int state) { 479 synchronized (this) { 480 boolean nextVisible = NotificationMediaManager.isPlayingState(state); 481 mMediaHandler.removeCallbacksAndMessages(null); 482 if (mMediaIsVisible && !nextVisible && mStatusBarState != StatusBarState.SHADE) { 483 // We need to delay this event for a few millis when stopping to avoid jank in the 484 // animation. The media app might not send its update when buffering, and the slice 485 // would end up without a header for 0.5 second. 486 mMediaWakeLock.setAcquired(true); 487 mMediaHandler.postDelayed(() -> { 488 synchronized (this) { 489 updateMediaStateLocked(metadata, state); 490 mMediaWakeLock.setAcquired(false); 491 } 492 }, 2000); 493 } else { 494 mMediaWakeLock.setAcquired(false); 495 updateMediaStateLocked(metadata, state); 496 } 497 } 498 } 499 updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state)500 private void updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state) { 501 boolean nextVisible = NotificationMediaManager.isPlayingState(state); 502 CharSequence title = null; 503 if (metadata != null) { 504 title = metadata.getText(MediaMetadata.METADATA_KEY_TITLE); 505 if (TextUtils.isEmpty(title)) { 506 title = getContext().getResources().getString(R.string.music_controls_no_title); 507 } 508 } 509 CharSequence artist = metadata == null ? null : metadata.getText( 510 MediaMetadata.METADATA_KEY_ARTIST); 511 512 if (nextVisible == mMediaIsVisible && TextUtils.equals(title, mMediaTitle) 513 && TextUtils.equals(artist, mMediaArtist)) { 514 return; 515 } 516 mMediaTitle = title; 517 mMediaArtist = artist; 518 mMediaIsVisible = nextVisible; 519 notifyChange(); 520 } 521 notifyChange()522 protected void notifyChange() { 523 mBgHandler.post(() -> { 524 try { 525 mContentResolver.notifyChange(mSliceUri, null /* observer */); 526 } catch (Exception e) { 527 Log.e(TAG, "Error on mContentResolver.notifyChange()", e); 528 } 529 }); 530 } 531 532 @Override onDozingChanged(boolean isDozing)533 public void onDozingChanged(boolean isDozing) { 534 final boolean notify; 535 synchronized (this) { 536 boolean neededMedia = needsMediaLocked(); 537 mDozing = isDozing; 538 notify = neededMedia != needsMediaLocked(); 539 } 540 if (notify) { 541 notifyChange(); 542 } 543 } 544 545 @Override onStateChanged(int newState)546 public void onStateChanged(int newState) { 547 final boolean notify; 548 synchronized (this) { 549 boolean needsMedia = needsMediaLocked(); 550 mStatusBarState = newState; 551 notify = needsMedia != needsMediaLocked(); 552 } 553 if (notify) { 554 notifyChange(); 555 } 556 } 557 558 @Override setContextAvailableCallback( SystemUIAppComponentFactoryBase.ContextAvailableCallback callback)559 public void setContextAvailableCallback( 560 SystemUIAppComponentFactoryBase.ContextAvailableCallback callback) { 561 mContextAvailableCallback = callback; 562 } 563 } 564