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.policy.NextAlarmController; 57 import com.android.systemui.statusbar.policy.NextAlarmControllerImpl; 58 import com.android.systemui.statusbar.policy.ZenModeController; 59 import com.android.systemui.statusbar.policy.ZenModeControllerImpl; 60 import com.android.systemui.util.wakelock.SettableWakeLock; 61 import com.android.systemui.util.wakelock.WakeLock; 62 63 import java.util.Date; 64 import java.util.HashSet; 65 import java.util.Locale; 66 import java.util.TimeZone; 67 import java.util.concurrent.TimeUnit; 68 69 /** 70 * Simple Slice provider that shows the current date. 71 */ 72 public class KeyguardSliceProvider extends SliceProvider implements 73 NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback, 74 NotificationMediaManager.MediaListener, StatusBarStateController.StateListener { 75 76 private static final StyleSpan BOLD_STYLE = new StyleSpan(Typeface.BOLD); 77 public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main"; 78 private static final String KEYGUARD_HEADER_URI = 79 "content://com.android.systemui.keyguard/header"; 80 public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date"; 81 public static final String KEYGUARD_NEXT_ALARM_URI = 82 "content://com.android.systemui.keyguard/alarm"; 83 public static final String KEYGUARD_DND_URI = "content://com.android.systemui.keyguard/dnd"; 84 public static final String KEYGUARD_MEDIA_URI = 85 "content://com.android.systemui.keyguard/media"; 86 public static final String KEYGUARD_ACTION_URI = 87 "content://com.android.systemui.keyguard/action"; 88 89 /** 90 * Only show alarms that will ring within N hours. 91 */ 92 @VisibleForTesting 93 static final int ALARM_VISIBILITY_HOURS = 12; 94 95 private static KeyguardSliceProvider sInstance; 96 97 protected final Uri mSliceUri; 98 protected final Uri mHeaderUri; 99 protected final Uri mDateUri; 100 protected final Uri mAlarmUri; 101 protected final Uri mDndUri; 102 protected final Uri mMediaUri; 103 private final Date mCurrentTime = new Date(); 104 private final Handler mHandler; 105 private final AlarmManager.OnAlarmListener mUpdateNextAlarm = this::updateNextAlarm; 106 private final HashSet<Integer> mMediaInvisibleStates; 107 private final Object mMediaToken = new Object(); 108 @VisibleForTesting 109 protected SettableWakeLock mMediaWakeLock; 110 @VisibleForTesting 111 protected ZenModeController mZenModeController; 112 private String mDatePattern; 113 private DateFormat mDateFormat; 114 private String mLastText; 115 private boolean mRegistered; 116 private String mNextAlarm; 117 private NextAlarmController mNextAlarmController; 118 @VisibleForTesting 119 protected AlarmManager mAlarmManager; 120 @VisibleForTesting 121 protected ContentResolver mContentResolver; 122 private AlarmManager.AlarmClockInfo mNextAlarmInfo; 123 private PendingIntent mPendingIntent; 124 protected NotificationMediaManager mMediaManager; 125 private StatusBarStateController mStatusBarStateController; 126 private CharSequence mMediaTitle; 127 private CharSequence mMediaArtist; 128 protected boolean mDozing; 129 private int mStatusBarState; 130 private boolean mMediaIsVisible; 131 132 /** 133 * Receiver responsible for time ticking and updating the date format. 134 */ 135 @VisibleForTesting 136 final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 137 @Override 138 public void onReceive(Context context, Intent intent) { 139 final String action = intent.getAction(); 140 if (Intent.ACTION_DATE_CHANGED.equals(action)) { 141 synchronized (this) { 142 updateClockLocked(); 143 } 144 } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { 145 synchronized (this) { 146 cleanDateFormatLocked(); 147 } 148 } 149 } 150 }; 151 152 @VisibleForTesting 153 final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = 154 new KeyguardUpdateMonitorCallback() { 155 @Override 156 public void onTimeChanged() { 157 synchronized (this) { 158 updateClockLocked(); 159 } 160 } 161 162 @Override 163 public void onTimeZoneChanged(TimeZone timeZone) { 164 synchronized (this) { 165 cleanDateFormatLocked(); 166 } 167 } 168 }; 169 KeyguardSliceProvider()170 public KeyguardSliceProvider() { 171 this(new Handler()); 172 } 173 getAttachedInstance()174 public static KeyguardSliceProvider getAttachedInstance() { 175 return KeyguardSliceProvider.sInstance; 176 } 177 178 @VisibleForTesting KeyguardSliceProvider(Handler handler)179 KeyguardSliceProvider(Handler handler) { 180 mHandler = handler; 181 mSliceUri = Uri.parse(KEYGUARD_SLICE_URI); 182 mHeaderUri = Uri.parse(KEYGUARD_HEADER_URI); 183 mDateUri = Uri.parse(KEYGUARD_DATE_URI); 184 mAlarmUri = Uri.parse(KEYGUARD_NEXT_ALARM_URI); 185 mDndUri = Uri.parse(KEYGUARD_DND_URI); 186 mMediaUri = Uri.parse(KEYGUARD_MEDIA_URI); 187 188 mMediaInvisibleStates = new HashSet<>(); 189 mMediaInvisibleStates.add(PlaybackState.STATE_NONE); 190 mMediaInvisibleStates.add(PlaybackState.STATE_STOPPED); 191 mMediaInvisibleStates.add(PlaybackState.STATE_PAUSED); 192 } 193 194 /** 195 * Initialize dependencies that don't exist during {@link android.content.ContentProvider} 196 * instantiation. 197 * 198 * @param mediaManager {@link NotificationMediaManager} singleton. 199 * @param statusBarStateController {@link StatusBarStateController} singleton. 200 */ initDependencies( NotificationMediaManager mediaManager, StatusBarStateController statusBarStateController)201 public void initDependencies( 202 NotificationMediaManager mediaManager, 203 StatusBarStateController statusBarStateController) { 204 mMediaManager = mediaManager; 205 mMediaManager.addCallback(this); 206 mStatusBarStateController = statusBarStateController; 207 mStatusBarStateController.addCallback(this); 208 } 209 210 @AnyThread 211 @Override onBindSlice(Uri sliceUri)212 public Slice onBindSlice(Uri sliceUri) { 213 Trace.beginSection("KeyguardSliceProvider#onBindSlice"); 214 Slice slice; 215 synchronized (this) { 216 ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY); 217 if (needsMediaLocked()) { 218 addMediaLocked(builder); 219 } else { 220 builder.addRow(new RowBuilder(mDateUri).setTitle(mLastText)); 221 } 222 addNextAlarmLocked(builder); 223 addZenModeLocked(builder); 224 addPrimaryActionLocked(builder); 225 slice = builder.build(); 226 } 227 Trace.endSection(); 228 return slice; 229 } 230 needsMediaLocked()231 protected boolean needsMediaLocked() { 232 // Show header if music is playing and the status bar is in the shade state. This way, an 233 // animation isn't necessary when pressing power and transitioning to AOD. 234 boolean keepWhenShade = mStatusBarState == StatusBarState.SHADE && mMediaIsVisible; 235 return !TextUtils.isEmpty(mMediaTitle) && mMediaIsVisible && (mDozing || 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, new Intent(), 0); 323 mMediaWakeLock = new SettableWakeLock(WakeLock.createPartial(getContext(), "media"), 324 "media"); 325 KeyguardSliceProvider.sInstance = this; 326 registerClockUpdate(); 327 updateClockLocked(); 328 } 329 return true; 330 } 331 332 @VisibleForTesting onDestroy()333 protected void onDestroy() { 334 synchronized (this) { 335 mNextAlarmController.removeCallback(this); 336 mZenModeController.removeCallback(this); 337 mMediaWakeLock.setAcquired(false); 338 mAlarmManager.cancel(mUpdateNextAlarm); 339 if (mRegistered) { 340 mRegistered = false; 341 getKeyguardUpdateMonitor().removeCallback(mKeyguardUpdateMonitorCallback); 342 getContext().unregisterReceiver(mIntentReceiver); 343 } 344 } 345 } 346 347 @Override onZenChanged(int zen)348 public void onZenChanged(int zen) { 349 notifyChange(); 350 } 351 352 @Override onConfigChanged(ZenModeConfig config)353 public void onConfigChanged(ZenModeConfig config) { 354 notifyChange(); 355 } 356 updateNextAlarm()357 private void updateNextAlarm() { 358 synchronized (this) { 359 if (withinNHoursLocked(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) { 360 String pattern = android.text.format.DateFormat.is24HourFormat(getContext(), 361 ActivityManager.getCurrentUser()) ? "HH:mm" : "h:mm"; 362 mNextAlarm = android.text.format.DateFormat.format(pattern, 363 mNextAlarmInfo.getTriggerTime()).toString(); 364 } else { 365 mNextAlarm = ""; 366 } 367 } 368 notifyChange(); 369 } 370 withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours)371 private boolean withinNHoursLocked(AlarmManager.AlarmClockInfo alarmClockInfo, int hours) { 372 if (alarmClockInfo == null) { 373 return false; 374 } 375 376 long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours); 377 return mNextAlarmInfo.getTriggerTime() <= limit; 378 } 379 380 /** 381 * Registers a broadcast receiver for clock updates, include date, time zone and manually 382 * changing the date/time via the settings app. 383 */ 384 @VisibleForTesting registerClockUpdate()385 protected void registerClockUpdate() { 386 synchronized (this) { 387 if (mRegistered) { 388 return; 389 } 390 391 IntentFilter filter = new IntentFilter(); 392 filter.addAction(Intent.ACTION_DATE_CHANGED); 393 filter.addAction(Intent.ACTION_LOCALE_CHANGED); 394 getContext().registerReceiver(mIntentReceiver, filter, null /* permission*/, 395 null /* scheduler */); 396 getKeyguardUpdateMonitor().registerCallback(mKeyguardUpdateMonitorCallback); 397 mRegistered = true; 398 } 399 } 400 401 @VisibleForTesting isRegistered()402 boolean isRegistered() { 403 synchronized (this) { 404 return mRegistered; 405 } 406 } 407 updateClockLocked()408 protected void updateClockLocked() { 409 final String text = getFormattedDateLocked(); 410 if (!text.equals(mLastText)) { 411 mLastText = text; 412 notifyChange(); 413 } 414 } 415 getFormattedDateLocked()416 protected String getFormattedDateLocked() { 417 if (mDateFormat == null) { 418 final Locale l = Locale.getDefault(); 419 DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l); 420 format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE); 421 mDateFormat = format; 422 } 423 mCurrentTime.setTime(System.currentTimeMillis()); 424 return mDateFormat.format(mCurrentTime); 425 } 426 427 @VisibleForTesting cleanDateFormatLocked()428 void cleanDateFormatLocked() { 429 mDateFormat = null; 430 } 431 432 @Override onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm)433 public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { 434 synchronized (this) { 435 mNextAlarmInfo = nextAlarm; 436 mAlarmManager.cancel(mUpdateNextAlarm); 437 438 long triggerAt = mNextAlarmInfo == null ? -1 : mNextAlarmInfo.getTriggerTime() 439 - TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS); 440 if (triggerAt > 0) { 441 mAlarmManager.setExact(AlarmManager.RTC, triggerAt, "lock_screen_next_alarm", 442 mUpdateNextAlarm, mHandler); 443 } 444 } 445 updateNextAlarm(); 446 } 447 448 @VisibleForTesting getKeyguardUpdateMonitor()449 protected KeyguardUpdateMonitor getKeyguardUpdateMonitor() { 450 return KeyguardUpdateMonitor.getInstance(getContext()); 451 } 452 453 /** 454 * Called whenever new media metadata is available. 455 * @param metadata New metadata. 456 */ 457 @Override onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state)458 public void onMetadataOrStateChanged(MediaMetadata metadata, @PlaybackState.State int state) { 459 synchronized (this) { 460 boolean nextVisible = !mMediaInvisibleStates.contains(state); 461 mHandler.removeCallbacksAndMessages(mMediaToken); 462 if (mMediaIsVisible && !nextVisible && mStatusBarState != StatusBarState.SHADE) { 463 // We need to delay this event for a few millis when stopping to avoid jank in the 464 // animation. The media app might not send its update when buffering, and the slice 465 // would end up without a header for 0.5 second. 466 mMediaWakeLock.setAcquired(true); 467 mHandler.postDelayed(() -> { 468 updateMediaStateLocked(metadata, state); 469 mMediaWakeLock.setAcquired(false); 470 }, mMediaToken, 2000); 471 } else { 472 mMediaWakeLock.setAcquired(false); 473 updateMediaStateLocked(metadata, state); 474 } 475 } 476 } 477 updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state)478 private void updateMediaStateLocked(MediaMetadata metadata, @PlaybackState.State int state) { 479 boolean nextVisible = !mMediaInvisibleStates.contains(state); 480 CharSequence title = null; 481 if (metadata != null) { 482 title = metadata.getText(MediaMetadata.METADATA_KEY_TITLE); 483 if (TextUtils.isEmpty(title)) { 484 title = getContext().getResources().getString(R.string.music_controls_no_title); 485 } 486 } 487 CharSequence artist = metadata == null ? null : metadata.getText( 488 MediaMetadata.METADATA_KEY_ARTIST); 489 490 if (nextVisible == mMediaIsVisible && TextUtils.equals(title, mMediaTitle) 491 && TextUtils.equals(artist, mMediaArtist)) { 492 return; 493 } 494 mMediaTitle = title; 495 mMediaArtist = artist; 496 mMediaIsVisible = nextVisible; 497 notifyChange(); 498 } 499 notifyChange()500 protected void notifyChange() { 501 mContentResolver.notifyChange(mSliceUri, null /* observer */); 502 } 503 504 @Override onDozingChanged(boolean isDozing)505 public void onDozingChanged(boolean isDozing) { 506 final boolean notify; 507 synchronized (this) { 508 boolean neededMedia = needsMediaLocked(); 509 mDozing = isDozing; 510 notify = neededMedia != needsMediaLocked(); 511 } 512 if (notify) { 513 notifyChange(); 514 } 515 } 516 517 @Override onStateChanged(int newState)518 public void onStateChanged(int newState) { 519 final boolean notify; 520 synchronized (this) { 521 boolean needsMedia = needsMediaLocked(); 522 mStatusBarState = newState; 523 notify = needsMedia != needsMediaLocked(); 524 } 525 if (notify) { 526 notifyChange(); 527 } 528 } 529 } 530