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