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