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