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