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