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