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