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