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.plugins.statusbar.StatusBarStateController; 55 import com.android.systemui.res.R; 56 import com.android.systemui.settings.UserTracker; 57 import com.android.systemui.statusbar.NotificationMediaManager; 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 mMediaWakeLock = new SettableWakeLock( 323 WakeLock.createPartial(getContext(), mWakeLockLogger, "media"), "media"); 324 synchronized (KeyguardSliceProvider.sInstanceLock) { 325 KeyguardSliceProvider oldInstance = KeyguardSliceProvider.sInstance; 326 if (oldInstance != null) { 327 oldInstance.onDestroy(); 328 } 329 mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern); 330 mPendingIntent = PendingIntent.getActivity(getContext(), 0, 331 new Intent(getContext(), KeyguardSliceProvider.class), 332 PendingIntent.FLAG_IMMUTABLE); 333 mMediaManager.addCallback(this); 334 mStatusBarStateController.addCallback(this); 335 mNextAlarmController.addCallback(this); 336 mZenModeController.addCallback(this); 337 KeyguardSliceProvider.sInstance = this; 338 registerClockUpdate(); 339 updateClockLocked(); 340 } 341 return true; 342 } 343 344 @VisibleForTesting onDestroy()345 protected void onDestroy() { 346 synchronized (KeyguardSliceProvider.sInstanceLock) { 347 mNextAlarmController.removeCallback(this); 348 mZenModeController.removeCallback(this); 349 mMediaWakeLock.setAcquired(false); 350 mAlarmManager.cancel(mUpdateNextAlarm); 351 if (mRegistered) { 352 mRegistered = false; 353 mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); 354 getContext().unregisterReceiver(mIntentReceiver); 355 } 356 KeyguardSliceProvider.sInstance = null; 357 } 358 } 359 360 @Override onZenChanged(int zen)361 public void onZenChanged(int zen) { 362 notifyChange(); 363 } 364 365 @Override onConfigChanged(ZenModeConfig config)366 public void onConfigChanged(ZenModeConfig config) { 367 notifyChange(); 368 } 369 updateNextAlarm()370 private void updateNextAlarm() { 371 synchronized (this) { 372 if (withinNHoursLocked(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) { 373 String pattern = android.text.format.DateFormat.is24HourFormat(getContext(), 374 mUserTracker.getUserId()) ? "HH:mm" : "h:mm"; 375 mNextAlarm = android.text.format.DateFormat.format(pattern, 376 mNextAlarmInfo.getTriggerTime()).toString(); 377 } else { 378 mNextAlarm = ""; 379 } 380 } 381 notifyChange(); 382 } 383 withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours)384 private boolean withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours) { 385 if (alarmClockInfo == null) { 386 return false; 387 } 388 389 long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours); 390 return mNextAlarmInfo.getTriggerTime() <= limit; 391 } 392 393 /** 394 * Registers a broadcast receiver for clock updates, include date, time zone and manually 395 * changing the date/time via the settings app. 396 */ 397 @VisibleForTesting registerClockUpdate()398 protected void registerClockUpdate() { 399 synchronized (this) { 400 if (mRegistered) { 401 return; 402 } 403 404 IntentFilter filter = new IntentFilter(); 405 filter.addAction(Intent.ACTION_DATE_CHANGED); 406 filter.addAction(Intent.ACTION_LOCALE_CHANGED); 407 mBgHandler.post(() -> { 408 getContext().registerReceiver(mIntentReceiver, filter, null /* permission*/, 409 null /* scheduler */); 410 }); 411 mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); 412 mRegistered = true; 413 } 414 } 415 416 @VisibleForTesting isRegistered()417 boolean isRegistered() { 418 synchronized (this) { 419 return mRegistered; 420 } 421 } 422 updateClockLocked()423 protected void updateClockLocked() { 424 final String text = getFormattedDateLocked(); 425 if (!text.equals(mLastText)) { 426 mLastText = text; 427 notifyChange(); 428 } 429 } 430 getFormattedDateLocked()431 protected String getFormattedDateLocked() { 432 if (mDateFormat == null) { 433 final Locale l = Locale.getDefault(); 434 DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l); 435 // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of 436 // CAPITALIZATION_FOR_STANDALONE is to address 437 // https://unicode-org.atlassian.net/browse/ICU-21631 438 // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE 439 format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE); 440 mDateFormat = format; 441 } 442 mCurrentTime.setTime(System.currentTimeMillis()); 443 return mDateFormat.format(mCurrentTime); 444 } 445 446 @VisibleForTesting cleanDateFormatLocked()447 void cleanDateFormatLocked() { 448 mDateFormat = null; 449 } 450 451 @Override onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm)452 public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { 453 synchronized (this) { 454 mNextAlarmInfo = nextAlarm; 455 mAlarmManager.cancel(mUpdateNextAlarm); 456 457 long triggerAt = mNextAlarmInfo == null ? -1 : mNextAlarmInfo.getTriggerTime() 458 - TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS); 459 if (triggerAt > 0) { 460 mAlarmManager.setExact(AlarmManager.RTC, triggerAt, "lock_screen_next_alarm", 461 mUpdateNextAlarm, mHandler); 462 } 463 } 464 updateNextAlarm(); 465 } 466 467 /** 468 * Called whenever new media metadata is available. 469 * @param metadata New metadata. 470 */ 471 @Override onPrimaryMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state)472 public void onPrimaryMetadataOrStateChanged(MediaMetadata metadata, 473 @PlaybackState.State int state) { 474 synchronized (this) { 475 boolean nextVisible = NotificationMediaManager.isPlayingState(state); 476 mMediaHandler.removeCallbacksAndMessages(null); 477 if (mMediaIsVisible && !nextVisible && mStatusBarState != StatusBarState.SHADE) { 478 // We need to delay this event for a few millis when stopping to avoid jank in the 479 // animation. The media app might not send its update when buffering, and the slice 480 // would end up without a header for 0.5 second. 481 mMediaWakeLock.setAcquired(true); 482 mMediaHandler.postDelayed(() -> { 483 synchronized (this) { 484 updateMediaStateLocked(metadata, state); 485 mMediaWakeLock.setAcquired(false); 486 } 487 }, 2000); 488 } else { 489 mMediaWakeLock.setAcquired(false); 490 updateMediaStateLocked(metadata, state); 491 } 492 } 493 } 494 updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state)495 private void updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state) { 496 boolean nextVisible = NotificationMediaManager.isPlayingState(state); 497 CharSequence title = null; 498 if (metadata != null) { 499 title = metadata.getText(MediaMetadata.METADATA_KEY_TITLE); 500 if (TextUtils.isEmpty(title)) { 501 title = getContext().getResources().getString(R.string.music_controls_no_title); 502 } 503 } 504 CharSequence artist = metadata == null ? null : metadata.getText( 505 MediaMetadata.METADATA_KEY_ARTIST); 506 507 if (nextVisible == mMediaIsVisible && TextUtils.equals(title, mMediaTitle) 508 && TextUtils.equals(artist, mMediaArtist)) { 509 return; 510 } 511 mMediaTitle = title; 512 mMediaArtist = artist; 513 mMediaIsVisible = nextVisible; 514 notifyChange(); 515 } 516 notifyChange()517 protected void notifyChange() { 518 mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */)); 519 } 520 521 @Override onDozingChanged(boolean isDozing)522 public void onDozingChanged(boolean isDozing) { 523 final boolean notify; 524 synchronized (this) { 525 boolean neededMedia = needsMediaLocked(); 526 mDozing = isDozing; 527 notify = neededMedia != needsMediaLocked(); 528 } 529 if (notify) { 530 notifyChange(); 531 } 532 } 533 534 @Override onStateChanged(int newState)535 public void onStateChanged(int newState) { 536 final boolean notify; 537 synchronized (this) { 538 boolean needsMedia = needsMediaLocked(); 539 mStatusBarState = newState; 540 notify = needsMedia != needsMediaLocked(); 541 } 542 if (notify) { 543 notifyChange(); 544 } 545 } 546 547 @Override setContextAvailableCallback( SystemUIAppComponentFactoryBase.ContextAvailableCallback callback)548 public void setContextAvailableCallback( 549 SystemUIAppComponentFactoryBase.ContextAvailableCallback callback) { 550 mContextAvailableCallback = callback; 551 } 552 } 553