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