DndTile.java revision 312ad02a75aea55a38a273c385da46757d38a384
1/* 2 * Copyright (C) 2015 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 17package com.android.systemui.qs.tiles; 18 19import static android.provider.Settings.Global.ZEN_MODE_ALARMS; 20import static android.provider.Settings.Global.ZEN_MODE_OFF; 21 22import android.app.AlarmManager; 23import android.app.AlarmManager.AlarmClockInfo; 24import android.app.Dialog; 25import android.content.BroadcastReceiver; 26import android.content.Context; 27import android.content.Intent; 28import android.content.IntentFilter; 29import android.content.SharedPreferences; 30import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 31import android.content.pm.ApplicationInfo; 32import android.content.pm.PackageManager; 33import android.net.Uri; 34import android.os.UserManager; 35import android.provider.Settings; 36import android.provider.Settings.Global; 37import android.service.notification.ScheduleCalendar; 38import android.service.notification.ZenModeConfig; 39import android.service.notification.ZenModeConfig.ZenRule; 40import android.service.quicksettings.Tile; 41import android.util.Slog; 42import android.view.LayoutInflater; 43import android.view.View; 44import android.view.View.OnAttachStateChangeListener; 45import android.view.ViewGroup; 46import android.view.WindowManager; 47import android.widget.Switch; 48import android.widget.Toast; 49 50import com.android.internal.logging.MetricsLogger; 51import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 52import com.android.settingslib.notification.EnableZenModeDialog; 53import com.android.systemui.Dependency; 54import com.android.systemui.Prefs; 55import com.android.systemui.R; 56import com.android.systemui.SysUIToast; 57import com.android.systemui.plugins.ActivityStarter; 58import com.android.systemui.plugins.qs.DetailAdapter; 59import com.android.systemui.plugins.qs.QSTile; 60import com.android.systemui.plugins.qs.QSTile.BooleanState; 61import com.android.systemui.qs.QSHost; 62import com.android.systemui.qs.tileimpl.QSTileImpl; 63import com.android.systemui.statusbar.phone.SystemUIDialog; 64import com.android.systemui.statusbar.policy.ZenModeController; 65import com.android.systemui.volume.ZenModePanel; 66 67/** Quick settings tile: Do not disturb **/ 68public class DndTile extends QSTileImpl<BooleanState> { 69 70 private static final Intent ZEN_SETTINGS = 71 new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); 72 73 private static final Intent ZEN_PRIORITY_SETTINGS = 74 new Intent(Settings.ACTION_ZEN_MODE_PRIORITY_SETTINGS); 75 76 private static final String ACTION_SET_VISIBLE = "com.android.systemui.dndtile.SET_VISIBLE"; 77 private static final String EXTRA_VISIBLE = "visible"; 78 79 private static final QSTile.Icon TOTAL_SILENCE = 80 ResourceIcon.get(R.drawable.ic_qs_dnd_on_total_silence); 81 82 private final ZenModeController mController; 83 private final DndDetailAdapter mDetailAdapter; 84 85 private boolean mListening; 86 private boolean mShowingDetail; 87 private boolean mReceiverRegistered; 88 89 public DndTile(QSHost host) { 90 super(host); 91 mController = Dependency.get(ZenModeController.class); 92 mDetailAdapter = new DndDetailAdapter(); 93 mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_SET_VISIBLE)); 94 mReceiverRegistered = true; 95 } 96 97 @Override 98 protected void handleDestroy() { 99 super.handleDestroy(); 100 if (mReceiverRegistered) { 101 mContext.unregisterReceiver(mReceiver); 102 mReceiverRegistered = false; 103 } 104 } 105 106 public static void setVisible(Context context, boolean visible) { 107 Prefs.putBoolean(context, Prefs.Key.DND_TILE_VISIBLE, visible); 108 } 109 110 public static boolean isVisible(Context context) { 111 return Prefs.getBoolean(context, Prefs.Key.DND_TILE_VISIBLE, false /* defaultValue */); 112 } 113 114 public static void setCombinedIcon(Context context, boolean combined) { 115 Prefs.putBoolean(context, Prefs.Key.DND_TILE_COMBINED_ICON, combined); 116 } 117 118 public static boolean isCombinedIcon(Context context) { 119 return Prefs.getBoolean(context, Prefs.Key.DND_TILE_COMBINED_ICON, 120 false /* defaultValue */); 121 } 122 123 @Override 124 public DetailAdapter getDetailAdapter() { 125 return mDetailAdapter; 126 } 127 128 @Override 129 public BooleanState newTileState() { 130 return new BooleanState(); 131 } 132 133 @Override 134 public Intent getLongClickIntent() { 135 return ZEN_SETTINGS; 136 } 137 138 @Override 139 protected void handleClick() { 140 // Zen is currently on 141 if (mState.value) { 142 mController.setZen(ZEN_MODE_OFF, null, TAG); 143 } else { 144 showDetail(true); 145 } 146 } 147 148 @Override 149 public void showDetail(boolean show) { 150 mUiHandler.post(() -> { 151 Dialog mDialog = new EnableZenModeDialog(mContext).createDialog(); 152 mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 153 SystemUIDialog.setShowForAllUsers(mDialog, true); 154 SystemUIDialog.registerDismissListener(mDialog); 155 SystemUIDialog.setWindowOnTop(mDialog); 156 mUiHandler.post(() -> mDialog.show()); 157 mHost.collapsePanels(); 158 }); 159 } 160 161 @Override 162 protected void handleSecondaryClick() { 163 if (mController.isVolumeRestricted()) { 164 // Collapse the panels, so the user can see the toast. 165 mHost.collapsePanels(); 166 SysUIToast.makeText(mContext, mContext.getString( 167 com.android.internal.R.string.error_message_change_not_allowed), 168 Toast.LENGTH_LONG).show(); 169 return; 170 } 171 if (!mState.value) { 172 // Because of the complexity of the zen panel, it needs to be shown after 173 // we turn on zen below. 174 mController.addCallback(new ZenModeController.Callback() { 175 @Override 176 public void onZenChanged(int zen) { 177 mController.removeCallback(this); 178 showDetail(true); 179 } 180 }); 181 mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); 182 } else { 183 showDetail(true); 184 } 185 } 186 187 @Override 188 public CharSequence getTileLabel() { 189 return mContext.getString(R.string.quick_settings_dnd_label); 190 } 191 192 @Override 193 protected void handleUpdateState(BooleanState state, Object arg) { 194 final int zen = arg instanceof Integer ? (Integer) arg : mController.getZen(); 195 final boolean newValue = zen != ZEN_MODE_OFF; 196 final boolean valueChanged = state.value != newValue; 197 if (state.slash == null) state.slash = new SlashState(); 198 state.dualTarget = true; 199 state.value = newValue; 200 state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; 201 state.slash.isSlashed = !state.value; 202 state.label = getTileLabel(); 203 state.secondaryLabel = getSecondaryLabel(zen != Global.ZEN_MODE_OFF); 204 checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_ADJUST_VOLUME); 205 switch (zen) { 206 case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: 207 state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); 208 state.contentDescription = mContext.getString( 209 R.string.accessibility_quick_settings_dnd_priority_on); 210 break; 211 case Global.ZEN_MODE_NO_INTERRUPTIONS: 212 state.icon = TOTAL_SILENCE; 213 state.contentDescription = mContext.getString( 214 R.string.accessibility_quick_settings_dnd_none_on); 215 break; 216 case ZEN_MODE_ALARMS: 217 state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); 218 state.contentDescription = mContext.getString( 219 R.string.accessibility_quick_settings_dnd_alarms_on); 220 break; 221 default: 222 state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on); 223 state.contentDescription = mContext.getString( 224 R.string.accessibility_quick_settings_dnd); 225 break; 226 } 227 if (valueChanged) { 228 fireToggleStateChanged(state.value); 229 } 230 state.dualLabelContentDescription = mContext.getResources().getString( 231 R.string.accessibility_quick_settings_open_settings, getTileLabel()); 232 state.expandedAccessibilityClassName = Switch.class.getName(); 233 } 234 235 /** 236 * Returns the secondary label to use for the given instance of do not disturb. 237 * - If turned on manually and end time is known, returns end time. 238 * - If turned on by an automatic rule, returns the automatic rule name. 239 * - If on due to an app, returns the app name. 240 * - If there's a combination of rules/apps that trigger, then shows the one that will 241 * last the longest if applicable. 242 * @return null if do not disturb is off. 243 */ 244 private String getSecondaryLabel(boolean zenOn) { 245 if (!zenOn) { 246 return null; 247 } 248 249 ZenModeConfig config = mController.getConfig(); 250 String secondaryText = ""; 251 long latestEndTime = -1; 252 253 // DND turned on by manual rule 254 if (config.manualRule != null) { 255 final Uri id = config.manualRule.conditionId; 256 if (config.manualRule.enabler != null) { 257 // app triggered manual rule 258 String appName = ZenModeConfig.getOwnerCaption(mContext, config.manualRule.enabler); 259 if (!appName.isEmpty()) { 260 secondaryText = appName; 261 } 262 } else { 263 if (id == null) { 264 // Do not disturb manually triggered to remain on forever until turned off 265 // No subtext 266 return null; 267 } else { 268 latestEndTime = ZenModeConfig.tryParseCountdownConditionId(id); 269 if (latestEndTime > 0) { 270 final CharSequence formattedTime = ZenModeConfig.getFormattedTime(mContext, 271 latestEndTime, ZenModeConfig.isToday(latestEndTime), 272 mContext.getUserId()); 273 secondaryText = mContext.getString(R.string.qs_dnd_until, formattedTime); 274 } 275 } 276 } 277 } 278 279 // DND turned on by an automatic rule 280 for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { 281 if (automaticRule.isAutomaticActive()) { 282 if (ZenModeConfig.isValidEventConditionId(automaticRule.conditionId) || 283 ZenModeConfig.isValidScheduleConditionId(automaticRule.conditionId)) { 284 // set text if automatic rule end time is the latest active rule end time 285 long endTime = parseAutomaticRuleEndTime(automaticRule.conditionId); 286 if (endTime > latestEndTime) { 287 latestEndTime = endTime; 288 secondaryText = automaticRule.name; 289 } 290 } else { 291 // set text if 3rd party rule 292 return automaticRule.name; 293 } 294 } 295 } 296 297 return !secondaryText.equals("") ? secondaryText : null; 298 } 299 300 private long parseAutomaticRuleEndTime(Uri id) { 301 if (ZenModeConfig.isValidEventConditionId(id)) { 302 // cannot look up end times for events 303 return Long.MAX_VALUE; 304 } 305 306 if (ZenModeConfig.isValidScheduleConditionId(id)) { 307 ScheduleCalendar schedule = ZenModeConfig.toScheduleCalendar(id); 308 long endTimeMs = schedule.getNextChangeTime(System.currentTimeMillis()); 309 310 // check if automatic rule will end on next alarm 311 if (schedule.exitAtAlarm()) { 312 long nextAlarm = getNextAlarm(mContext); 313 schedule.maybeSetNextAlarm(System.currentTimeMillis(), nextAlarm); 314 if (schedule.shouldExitForAlarm(endTimeMs)) { 315 return nextAlarm; 316 } 317 } 318 319 return endTimeMs; 320 } 321 322 return -1; 323 } 324 325 private long getNextAlarm(Context context) { 326 final AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 327 final AlarmClockInfo info = alarms.getNextAlarmClock(mContext.getUserId()); 328 return info != null ? info.getTriggerTime() : 0; 329 } 330 331 @Override 332 public int getMetricsCategory() { 333 return MetricsEvent.QS_DND; 334 } 335 336 @Override 337 protected String composeChangeAnnouncement() { 338 if (mState.value) { 339 return mContext.getString(R.string.accessibility_quick_settings_dnd_changed_on); 340 } else { 341 return mContext.getString(R.string.accessibility_quick_settings_dnd_changed_off); 342 } 343 } 344 345 @Override 346 public void handleSetListening(boolean listening) { 347 if (mListening == listening) return; 348 mListening = listening; 349 if (mListening) { 350 mController.addCallback(mZenCallback); 351 Prefs.registerListener(mContext, mPrefListener); 352 } else { 353 mController.removeCallback(mZenCallback); 354 Prefs.unregisterListener(mContext, mPrefListener); 355 } 356 } 357 358 @Override 359 public boolean isAvailable() { 360 return isVisible(mContext); 361 } 362 363 private final OnSharedPreferenceChangeListener mPrefListener 364 = new OnSharedPreferenceChangeListener() { 365 @Override 366 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, 367 @Prefs.Key String key) { 368 if (Prefs.Key.DND_TILE_COMBINED_ICON.equals(key) || 369 Prefs.Key.DND_TILE_VISIBLE.equals(key)) { 370 refreshState(); 371 } 372 } 373 }; 374 375 private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { 376 public void onZenChanged(int zen) { 377 refreshState(zen); 378 if (isShowingDetail()) { 379 mDetailAdapter.updatePanel(); 380 } 381 } 382 383 @Override 384 public void onConfigChanged(ZenModeConfig config) { 385 if (isShowingDetail()) { 386 mDetailAdapter.updatePanel(); 387 } 388 } 389 }; 390 391 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 392 @Override 393 public void onReceive(Context context, Intent intent) { 394 final boolean visible = intent.getBooleanExtra(EXTRA_VISIBLE, false); 395 setVisible(mContext, visible); 396 refreshState(); 397 } 398 }; 399 400 private final class DndDetailAdapter implements DetailAdapter, OnAttachStateChangeListener { 401 402 private ZenModePanel mZenPanel; 403 private boolean mAuto; 404 405 @Override 406 public CharSequence getTitle() { 407 return mContext.getString(R.string.quick_settings_dnd_label); 408 } 409 410 @Override 411 public Boolean getToggleState() { 412 return mState.value; 413 } 414 415 @Override 416 public Intent getSettingsIntent() { 417 return ZEN_SETTINGS; 418 } 419 420 @Override 421 public void setToggleState(boolean state) { 422 MetricsLogger.action(mContext, MetricsEvent.QS_DND_TOGGLE, state); 423 if (!state) { 424 mController.setZen(ZEN_MODE_OFF, null, TAG); 425 mAuto = false; 426 } else { 427 mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); 428 } 429 } 430 431 @Override 432 public int getMetricsCategory() { 433 return MetricsEvent.QS_DND_DETAILS; 434 } 435 436 @Override 437 public View createDetailView(Context context, View convertView, ViewGroup parent) { 438 mZenPanel = convertView != null ? (ZenModePanel) convertView 439 : (ZenModePanel) LayoutInflater.from(context).inflate( 440 R.layout.zen_mode_panel, parent, false); 441 if (convertView == null) { 442 mZenPanel.init(mController); 443 mZenPanel.addOnAttachStateChangeListener(this); 444 mZenPanel.setCallback(mZenModePanelCallback); 445 mZenPanel.setEmptyState(R.drawable.ic_qs_dnd_detail_empty, R.string.dnd_is_off); 446 } 447 updatePanel(); 448 return mZenPanel; 449 } 450 451 private void updatePanel() { 452 if (mZenPanel == null) return; 453 mAuto = false; 454 if (mController.getZen() == ZEN_MODE_OFF) { 455 mZenPanel.setState(ZenModePanel.STATE_OFF); 456 } else { 457 ZenModeConfig config = mController.getConfig(); 458 String summary = ""; 459 if (config.manualRule != null && config.manualRule.enabler != null) { 460 summary = getOwnerCaption(config.manualRule.enabler); 461 } 462 for (ZenRule automaticRule : config.automaticRules.values()) { 463 if (automaticRule.isAutomaticActive()) { 464 if (summary.isEmpty()) { 465 summary = mContext.getString(R.string.qs_dnd_prompt_auto_rule, 466 automaticRule.name); 467 } else { 468 summary = mContext.getString(R.string.qs_dnd_prompt_auto_rule_app); 469 } 470 } 471 } 472 if (summary.isEmpty()) { 473 mZenPanel.setState(ZenModePanel.STATE_MODIFY); 474 } else { 475 mAuto = true; 476 mZenPanel.setState(ZenModePanel.STATE_AUTO_RULE); 477 mZenPanel.setAutoText(summary); 478 } 479 } 480 } 481 482 private String getOwnerCaption(String owner) { 483 final PackageManager pm = mContext.getPackageManager(); 484 try { 485 final ApplicationInfo info = pm.getApplicationInfo(owner, 0); 486 if (info != null) { 487 final CharSequence seq = info.loadLabel(pm); 488 if (seq != null) { 489 final String str = seq.toString().trim(); 490 return mContext.getString(R.string.qs_dnd_prompt_app, str); 491 } 492 } 493 } catch (Throwable e) { 494 Slog.w(TAG, "Error loading owner caption", e); 495 } 496 return ""; 497 } 498 499 @Override 500 public void onViewAttachedToWindow(View v) { 501 mShowingDetail = true; 502 } 503 504 @Override 505 public void onViewDetachedFromWindow(View v) { 506 mShowingDetail = false; 507 mZenPanel = null; 508 } 509 } 510 511 private final ZenModePanel.Callback mZenModePanelCallback = new ZenModePanel.Callback() { 512 @Override 513 public void onPrioritySettings() { 514 Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( 515 ZEN_PRIORITY_SETTINGS, 0); 516 } 517 518 @Override 519 public void onInteraction() { 520 // noop 521 } 522 523 @Override 524 public void onExpanded(boolean expanded) { 525 // noop 526 } 527 }; 528 529} 530