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 17 package com.android.systemui.qs.tiles; 18 19 import static android.provider.Settings.Global.ZEN_MODE_ALARMS; 20 import static android.provider.Settings.Global.ZEN_MODE_OFF; 21 22 import android.app.AlertDialog; 23 import android.app.Dialog; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.UserManager; 34 import android.provider.Settings; 35 import android.provider.Settings.Global; 36 import android.service.notification.ZenModeConfig; 37 import android.service.notification.ZenModeConfig.ZenRule; 38 import android.service.quicksettings.Tile; 39 import android.text.TextUtils; 40 import android.util.Slog; 41 import android.view.LayoutInflater; 42 import android.view.View; 43 import android.view.View.OnAttachStateChangeListener; 44 import android.view.ViewGroup; 45 import android.widget.Switch; 46 import android.widget.Toast; 47 48 import androidx.annotation.Nullable; 49 50 import com.android.internal.logging.MetricsLogger; 51 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 52 import com.android.settingslib.notification.EnableZenModeDialog; 53 import com.android.systemui.Prefs; 54 import com.android.systemui.R; 55 import com.android.systemui.SysUIToast; 56 import com.android.systemui.animation.DialogLaunchAnimator; 57 import com.android.systemui.dagger.qualifiers.Background; 58 import com.android.systemui.dagger.qualifiers.Main; 59 import com.android.systemui.plugins.ActivityStarter; 60 import com.android.systemui.plugins.FalsingManager; 61 import com.android.systemui.plugins.qs.DetailAdapter; 62 import com.android.systemui.plugins.qs.QSTile.BooleanState; 63 import com.android.systemui.plugins.statusbar.StatusBarStateController; 64 import com.android.systemui.qs.QSHost; 65 import com.android.systemui.qs.SecureSetting; 66 import com.android.systemui.qs.logging.QSLogger; 67 import com.android.systemui.qs.tileimpl.QSTileImpl; 68 import com.android.systemui.statusbar.phone.SystemUIDialog; 69 import com.android.systemui.statusbar.policy.ZenModeController; 70 import com.android.systemui.util.settings.SecureSettings; 71 import com.android.systemui.volume.ZenModePanel; 72 73 import javax.inject.Inject; 74 75 /** Quick settings tile: Do not disturb **/ 76 public class DndTile extends QSTileImpl<BooleanState> { 77 78 private static final Intent ZEN_SETTINGS = 79 new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); 80 81 private static final Intent ZEN_PRIORITY_SETTINGS = 82 new Intent(Settings.ACTION_ZEN_MODE_PRIORITY_SETTINGS); 83 84 private final ZenModeController mController; 85 private final DndDetailAdapter mDetailAdapter; 86 private final SharedPreferences mSharedPreferences; 87 private final SecureSetting mSettingZenDuration; 88 private final DialogLaunchAnimator mDialogLaunchAnimator; 89 90 private boolean mListening; 91 private boolean mShowingDetail; 92 93 @Inject DndTile( QSHost host, @Background Looper backgroundLooper, @Main Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, ZenModeController zenModeController, @Main SharedPreferences sharedPreferences, SecureSettings secureSettings, DialogLaunchAnimator dialogLaunchAnimator )94 public DndTile( 95 QSHost host, 96 @Background Looper backgroundLooper, 97 @Main Handler mainHandler, 98 FalsingManager falsingManager, 99 MetricsLogger metricsLogger, 100 StatusBarStateController statusBarStateController, 101 ActivityStarter activityStarter, 102 QSLogger qsLogger, 103 ZenModeController zenModeController, 104 @Main SharedPreferences sharedPreferences, 105 SecureSettings secureSettings, 106 DialogLaunchAnimator dialogLaunchAnimator 107 ) { 108 super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, 109 statusBarStateController, activityStarter, qsLogger); 110 mController = zenModeController; 111 mSharedPreferences = sharedPreferences; 112 mDetailAdapter = new DndDetailAdapter(); 113 mController.observe(getLifecycle(), mZenCallback); 114 mDialogLaunchAnimator = dialogLaunchAnimator; 115 mSettingZenDuration = new SecureSetting(secureSettings, mUiHandler, 116 Settings.Secure.ZEN_DURATION, getHost().getUserId()) { 117 @Override 118 protected void handleValueChanged(int value, boolean observedChange) { 119 refreshState(); 120 } 121 }; 122 } 123 setVisible(Context context, boolean visible)124 public static void setVisible(Context context, boolean visible) { 125 Prefs.putBoolean(context, Prefs.Key.DND_TILE_VISIBLE, visible); 126 } 127 isVisible(SharedPreferences prefs)128 public static boolean isVisible(SharedPreferences prefs) { 129 return prefs.getBoolean(Prefs.Key.DND_TILE_VISIBLE, false /* defaultValue */); 130 } 131 setCombinedIcon(Context context, boolean combined)132 public static void setCombinedIcon(Context context, boolean combined) { 133 Prefs.putBoolean(context, Prefs.Key.DND_TILE_COMBINED_ICON, combined); 134 } 135 isCombinedIcon(SharedPreferences sharedPreferences)136 public static boolean isCombinedIcon(SharedPreferences sharedPreferences) { 137 return sharedPreferences.getBoolean(Prefs.Key.DND_TILE_COMBINED_ICON, 138 false /* defaultValue */); 139 } 140 141 @Override getDetailAdapter()142 public DetailAdapter getDetailAdapter() { 143 return mDetailAdapter; 144 } 145 146 @Override newTileState()147 public BooleanState newTileState() { 148 return new BooleanState(); 149 } 150 151 @Override getLongClickIntent()152 public Intent getLongClickIntent() { 153 return ZEN_SETTINGS; 154 } 155 156 @Override handleClick(@ullable View view)157 protected void handleClick(@Nullable View view) { 158 // Zen is currently on 159 if (mState.value) { 160 mController.setZen(ZEN_MODE_OFF, null, TAG); 161 } else { 162 enableZenMode(view); 163 } 164 } 165 166 @Override handleUserSwitch(int newUserId)167 protected void handleUserSwitch(int newUserId) { 168 super.handleUserSwitch(newUserId); 169 mSettingZenDuration.setUserId(newUserId); 170 } 171 enableZenMode(@ullable View view)172 private void enableZenMode(@Nullable View view) { 173 int zenDuration = mSettingZenDuration.getValue(); 174 boolean showOnboarding = Settings.Secure.getInt(mContext.getContentResolver(), 175 Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0 176 && Settings.Secure.getInt(mContext.getContentResolver(), 177 Settings.Secure.ZEN_SETTINGS_UPDATED, 0) != 1; 178 if (showOnboarding) { 179 // don't show on-boarding again or notification ever 180 Settings.Secure.putInt(mContext.getContentResolver(), 181 Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0); 182 // turn on DND 183 mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); 184 // show on-boarding screen 185 Intent intent = new Intent(Settings.ZEN_MODE_ONBOARDING); 186 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 187 mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); 188 } else { 189 switch (zenDuration) { 190 case Settings.Secure.ZEN_DURATION_PROMPT: 191 mUiHandler.post(() -> { 192 Dialog dialog = makeZenModeDialog(); 193 if (view != null) { 194 mDialogLaunchAnimator.showFromView(dialog, view, false); 195 } else { 196 dialog.show(); 197 } 198 }); 199 break; 200 case Settings.Secure.ZEN_DURATION_FOREVER: 201 mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); 202 break; 203 default: 204 Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration, 205 mHost.getUserId(), true).id; 206 mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, 207 conditionId, TAG); 208 } 209 } 210 } 211 makeZenModeDialog()212 private Dialog makeZenModeDialog() { 213 AlertDialog dialog = new EnableZenModeDialog(mContext, R.style.Theme_SystemUI_Dialog, 214 true /* cancelIsNeutral */).createDialog(); 215 SystemUIDialog.applyFlags(dialog); 216 SystemUIDialog.setShowForAllUsers(dialog, true); 217 SystemUIDialog.registerDismissListener(dialog); 218 SystemUIDialog.setDialogSize(dialog); 219 return dialog; 220 } 221 222 @Override handleSecondaryClick(@ullable View view)223 protected void handleSecondaryClick(@Nullable View view) { 224 if (mController.isVolumeRestricted()) { 225 // Collapse the panels, so the user can see the toast. 226 mHost.collapsePanels(); 227 SysUIToast.makeText(mContext, mContext.getString( 228 com.android.internal.R.string.error_message_change_not_allowed), 229 Toast.LENGTH_LONG).show(); 230 return; 231 } 232 if (!mState.value) { 233 // Because of the complexity of the zen panel, it needs to be shown after 234 // we turn on zen below. 235 mController.addCallback(new ZenModeController.Callback() { 236 @Override 237 public void onZenChanged(int zen) { 238 mController.removeCallback(this); 239 showDetail(true); 240 } 241 }); 242 mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); 243 } else { 244 showDetail(true); 245 } 246 } 247 248 @Override getTileLabel()249 public CharSequence getTileLabel() { 250 return mContext.getString(R.string.quick_settings_dnd_label); 251 } 252 253 @Override handleUpdateState(BooleanState state, Object arg)254 protected void handleUpdateState(BooleanState state, Object arg) { 255 if (mController == null) return; 256 final int zen = arg instanceof Integer ? (Integer) arg : mController.getZen(); 257 final boolean newValue = zen != ZEN_MODE_OFF; 258 final boolean valueChanged = state.value != newValue; 259 if (state.slash == null) state.slash = new SlashState(); 260 state.dualTarget = true; 261 state.value = newValue; 262 state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; 263 state.slash.isSlashed = !state.value; 264 state.label = getTileLabel(); 265 state.secondaryLabel = TextUtils.emptyIfNull(ZenModeConfig.getDescription(mContext, 266 zen != Global.ZEN_MODE_OFF, mController.getConfig(), false)); 267 state.icon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_dnd); 268 checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_ADJUST_VOLUME); 269 // Keeping the secondaryLabel in contentDescription instead of stateDescription is easier 270 // to understand. 271 switch (zen) { 272 case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: 273 state.contentDescription = 274 mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " 275 + state.secondaryLabel; 276 break; 277 case Global.ZEN_MODE_NO_INTERRUPTIONS: 278 state.contentDescription = 279 mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " + 280 mContext.getString(R.string.accessibility_quick_settings_dnd_none_on) 281 + ", " + state.secondaryLabel; 282 break; 283 case ZEN_MODE_ALARMS: 284 state.contentDescription = 285 mContext.getString(R.string.accessibility_quick_settings_dnd) + ", " + 286 mContext.getString(R.string.accessibility_quick_settings_dnd_alarms_on) 287 + ", " + state.secondaryLabel; 288 break; 289 default: 290 state.contentDescription = mContext.getString( 291 R.string.accessibility_quick_settings_dnd); 292 break; 293 } 294 if (valueChanged) { 295 fireToggleStateChanged(state.value); 296 } 297 state.dualLabelContentDescription = mContext.getResources().getString( 298 R.string.accessibility_quick_settings_open_settings, getTileLabel()); 299 state.expandedAccessibilityClassName = Switch.class.getName(); 300 state.forceExpandIcon = 301 mSettingZenDuration.getValue() == Settings.Secure.ZEN_DURATION_PROMPT; 302 } 303 304 @Override getMetricsCategory()305 public int getMetricsCategory() { 306 return MetricsEvent.QS_DND; 307 } 308 309 @Override composeChangeAnnouncement()310 protected String composeChangeAnnouncement() { 311 if (mState.value) { 312 return mContext.getString(R.string.accessibility_quick_settings_dnd_changed_on); 313 } else { 314 return mContext.getString(R.string.accessibility_quick_settings_dnd_changed_off); 315 } 316 } 317 318 @Override handleSetListening(boolean listening)319 public void handleSetListening(boolean listening) { 320 super.handleSetListening(listening); 321 if (mListening == listening) return; 322 mListening = listening; 323 if (mListening) { 324 Prefs.registerListener(mContext, mPrefListener); 325 } else { 326 Prefs.unregisterListener(mContext, mPrefListener); 327 } 328 mSettingZenDuration.setListening(listening); 329 } 330 331 @Override handleDestroy()332 protected void handleDestroy() { 333 super.handleDestroy(); 334 mSettingZenDuration.setListening(false); 335 } 336 337 @Override isAvailable()338 public boolean isAvailable() { 339 return isVisible(mSharedPreferences); 340 } 341 342 private final OnSharedPreferenceChangeListener mPrefListener 343 = new OnSharedPreferenceChangeListener() { 344 @Override 345 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, 346 @Prefs.Key String key) { 347 if (Prefs.Key.DND_TILE_COMBINED_ICON.equals(key) || 348 Prefs.Key.DND_TILE_VISIBLE.equals(key)) { 349 refreshState(); 350 } 351 } 352 }; 353 354 private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { 355 public void onZenChanged(int zen) { 356 refreshState(zen); 357 if (isShowingDetail()) { 358 mDetailAdapter.updatePanel(); 359 } 360 } 361 362 @Override 363 public void onConfigChanged(ZenModeConfig config) { 364 if (isShowingDetail()) { 365 mDetailAdapter.updatePanel(); 366 } 367 } 368 }; 369 370 private final class DndDetailAdapter implements DetailAdapter, OnAttachStateChangeListener { 371 372 private ZenModePanel mZenPanel; 373 private boolean mAuto; 374 375 @Override getTitle()376 public CharSequence getTitle() { 377 return mContext.getString(R.string.quick_settings_dnd_label); 378 } 379 380 @Override getToggleState()381 public Boolean getToggleState() { 382 return mState.value; 383 } 384 385 @Override getSettingsIntent()386 public Intent getSettingsIntent() { 387 return ZEN_SETTINGS; 388 } 389 390 @Override setToggleState(boolean state)391 public void setToggleState(boolean state) { 392 MetricsLogger.action(mContext, MetricsEvent.QS_DND_TOGGLE, state); 393 if (!state) { 394 mController.setZen(ZEN_MODE_OFF, null, TAG); 395 mAuto = false; 396 } else { 397 mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG); 398 } 399 } 400 401 @Override getMetricsCategory()402 public int getMetricsCategory() { 403 return MetricsEvent.QS_DND_DETAILS; 404 } 405 406 @Override createDetailView(Context context, View convertView, ViewGroup parent)407 public View createDetailView(Context context, View convertView, ViewGroup parent) { 408 mZenPanel = convertView != null ? (ZenModePanel) convertView 409 : (ZenModePanel) LayoutInflater.from(context).inflate( 410 R.layout.zen_mode_panel, parent, false); 411 if (convertView == null) { 412 mZenPanel.init(mController); 413 mZenPanel.addOnAttachStateChangeListener(this); 414 mZenPanel.setCallback(mZenModePanelCallback); 415 mZenPanel.setEmptyState(R.drawable.ic_qs_dnd_detail_empty, R.string.dnd_is_off); 416 } 417 updatePanel(); 418 return mZenPanel; 419 } 420 updatePanel()421 private void updatePanel() { 422 if (mZenPanel == null) return; 423 mAuto = false; 424 if (mController.getZen() == ZEN_MODE_OFF) { 425 mZenPanel.setState(ZenModePanel.STATE_OFF); 426 } else { 427 ZenModeConfig config = mController.getConfig(); 428 String summary = ""; 429 if (config.manualRule != null && config.manualRule.enabler != null) { 430 summary = getOwnerCaption(config.manualRule.enabler); 431 } 432 for (ZenRule automaticRule : config.automaticRules.values()) { 433 if (automaticRule.isAutomaticActive()) { 434 if (summary.isEmpty()) { 435 summary = mContext.getString(R.string.qs_dnd_prompt_auto_rule, 436 automaticRule.name); 437 } else { 438 summary = mContext.getString(R.string.qs_dnd_prompt_auto_rule_app); 439 } 440 } 441 } 442 if (summary.isEmpty()) { 443 mZenPanel.setState(ZenModePanel.STATE_MODIFY); 444 } else { 445 mAuto = true; 446 mZenPanel.setState(ZenModePanel.STATE_AUTO_RULE); 447 mZenPanel.setAutoText(summary); 448 } 449 } 450 } 451 getOwnerCaption(String owner)452 private String getOwnerCaption(String owner) { 453 final PackageManager pm = mContext.getPackageManager(); 454 try { 455 final ApplicationInfo info = pm.getApplicationInfo(owner, 0); 456 if (info != null) { 457 final CharSequence seq = info.loadLabel(pm); 458 if (seq != null) { 459 final String str = seq.toString().trim(); 460 return mContext.getString(R.string.qs_dnd_prompt_app, str); 461 } 462 } 463 } catch (Throwable e) { 464 Slog.w(TAG, "Error loading owner caption", e); 465 } 466 return ""; 467 } 468 469 @Override onViewAttachedToWindow(View v)470 public void onViewAttachedToWindow(View v) { 471 mShowingDetail = true; 472 } 473 474 @Override onViewDetachedFromWindow(View v)475 public void onViewDetachedFromWindow(View v) { 476 mShowingDetail = false; 477 mZenPanel = null; 478 } 479 } 480 481 private final ZenModePanel.Callback mZenModePanelCallback = new ZenModePanel.Callback() { 482 @Override 483 public void onPrioritySettings() { 484 mActivityStarter.postStartActivityDismissingKeyguard( 485 ZEN_PRIORITY_SETTINGS, 0); 486 } 487 488 @Override 489 public void onInteraction() { 490 // noop 491 } 492 493 @Override 494 public void onExpanded(boolean expanded) { 495 // noop 496 } 497 }; 498 499 } 500