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