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