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