ZenModeHelper.java revision ae641c9ccd3f81214cee54a5f13804f1765187ad
1/**
2 * Copyright (c) 2014, 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.server.notification;
18
19import android.app.AlarmManager;
20import android.app.AppOpsManager;
21import android.app.Notification;
22import android.app.PendingIntent;
23import android.content.BroadcastReceiver;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.res.Resources;
29import android.content.res.XmlResourceParser;
30import android.database.ContentObserver;
31import android.media.AudioManager;
32import android.net.Uri;
33import android.os.Handler;
34import android.os.IBinder;
35import android.provider.Settings.Global;
36import android.service.notification.ZenModeConfig;
37import android.util.Slog;
38
39import com.android.internal.R;
40
41import libcore.io.IoUtils;
42
43import org.xmlpull.v1.XmlPullParser;
44import org.xmlpull.v1.XmlPullParserException;
45import org.xmlpull.v1.XmlSerializer;
46
47import java.io.IOException;
48import java.io.PrintWriter;
49import java.util.ArrayList;
50import java.util.Arrays;
51import java.util.Calendar;
52import java.util.Date;
53import java.util.HashSet;
54import java.util.Set;
55
56/**
57 * NotificationManagerService helper for functionality related to zen mode.
58 */
59public class ZenModeHelper {
60    private static final String TAG = "ZenModeHelper";
61
62    private static final String ACTION_ENTER_ZEN = "enter_zen";
63    private static final int REQUEST_CODE_ENTER = 100;
64    private static final String ACTION_EXIT_ZEN = "exit_zen";
65    private static final int REQUEST_CODE_EXIT = 101;
66    private static final String EXTRA_TIME = "time";
67
68    private final Context mContext;
69    private final Handler mHandler;
70    private final SettingsObserver mSettingsObserver;
71    private final AppOpsManager mAppOps;
72    private final ZenModeConfig mDefaultConfig;
73    private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
74
75    private int mZenMode;
76    private ZenModeConfig mConfig;
77    private AudioManager mAudioManager;
78    private int mPreviousRingerMode = -1;
79
80    // temporary, until we update apps to provide metadata
81    private static final Set<String> CALL_PACKAGES = new HashSet<String>(Arrays.asList(
82            "com.google.android.dialer",
83            "com.android.phone",
84            "com.android.example.notificationshowcase"
85            ));
86    private static final Set<String> MESSAGE_PACKAGES = new HashSet<String>(Arrays.asList(
87            "com.google.android.talk",
88            "com.android.mms",
89            "com.android.example.notificationshowcase"
90            ));
91    private static final Set<String> ALARM_PACKAGES = new HashSet<String>(Arrays.asList(
92            "com.google.android.deskclock"
93            ));
94    private static final Set<String> SYSTEM_PACKAGES = new HashSet<String>(Arrays.asList(
95            "android",
96            "com.android.systemui"
97            ));
98
99    public ZenModeHelper(Context context, Handler handler) {
100        mContext = context;
101        mHandler = handler;
102        mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
103        mDefaultConfig = readDefaultConfig(context.getResources());
104        mConfig = mDefaultConfig;
105        mSettingsObserver = new SettingsObserver(mHandler);
106        mSettingsObserver.observe();
107
108        final IntentFilter filter = new IntentFilter();
109        filter.addAction(ACTION_ENTER_ZEN);
110        filter.addAction(ACTION_EXIT_ZEN);
111        mContext.registerReceiver(new ZenBroadcastReceiver(), filter);
112    }
113
114    public static ZenModeConfig readDefaultConfig(Resources resources) {
115        XmlResourceParser parser = null;
116        try {
117            parser = resources.getXml(R.xml.default_zen_mode_config);
118            while (parser.next() != XmlPullParser.END_DOCUMENT) {
119                final ZenModeConfig config = ZenModeConfig.readXml(parser);
120                if (config != null) return config;
121            }
122        } catch (Exception e) {
123            Slog.w(TAG, "Error reading default zen mode config from resource", e);
124        } finally {
125            IoUtils.closeQuietly(parser);
126        }
127        return new ZenModeConfig();
128    }
129
130    public void addCallback(Callback callback) {
131        mCallbacks.add(callback);
132    }
133
134    public void setAudioManager(AudioManager audioManager) {
135        mAudioManager = audioManager;
136    }
137
138    public boolean shouldIntercept(NotificationRecord record) {
139        if (mZenMode != Global.ZEN_MODE_OFF) {
140            if (isSystem(record)) {
141                return false;
142            }
143            if (isAlarm(record)) {
144                return mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
145            }
146            // audience has veto power over all following rules
147            if (!audienceMatches(record)) {
148                return true;
149            }
150            if (isCall(record)) {
151                return !mConfig.allowCalls;
152            }
153            if (isMessage(record)) {
154                return !mConfig.allowMessages;
155            }
156            return true;
157        }
158        return false;
159    }
160
161    public int getZenMode() {
162        return mZenMode;
163    }
164
165    public void setZenMode(int zenModeValue) {
166        Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zenModeValue);
167    }
168
169    public void updateZenMode() {
170        final int mode = Global.getInt(mContext.getContentResolver(),
171                Global.ZEN_MODE, Global.ZEN_MODE_OFF);
172        if (mode != mZenMode) {
173            Slog.d(TAG, String.format("updateZenMode: %s -> %s",
174                    Global.zenModeToString(mZenMode),
175                    Global.zenModeToString(mode)));
176        }
177        mZenMode = mode;
178        final boolean zen = mZenMode != Global.ZEN_MODE_OFF;
179        final String[] exceptionPackages = null; // none (for now)
180
181        // call restrictions
182        final boolean muteCalls = zen && !mConfig.allowCalls;
183        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING,
184                muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
185                exceptionPackages);
186        mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING,
187                muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
188                exceptionPackages);
189
190        // restrict vibrations with no hints
191        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE,
192                zen ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
193                exceptionPackages);
194
195        // alarm restrictions
196        final boolean muteAlarms = mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
197        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_ALARM,
198                muteAlarms ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
199                exceptionPackages);
200        mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_ALARM,
201                muteAlarms ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
202                exceptionPackages);
203
204        // force ringer mode into compliance
205        if (mAudioManager != null) {
206            int ringerMode = mAudioManager.getRingerMode();
207            if (mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) {
208                if (ringerMode != AudioManager.RINGER_MODE_SILENT) {
209                    mPreviousRingerMode = ringerMode;
210                    Slog.d(TAG, "Silencing ringer");
211                    mAudioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT);
212                }
213            } else {
214                if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
215                    Slog.d(TAG, "Unsilencing ringer");
216                    mAudioManager.setRingerMode(mPreviousRingerMode != -1 ? mPreviousRingerMode
217                            : AudioManager.RINGER_MODE_NORMAL);
218                    mPreviousRingerMode = -1;
219                }
220            }
221        }
222        dispatchOnZenModeChanged();
223    }
224
225    public boolean allowDisable(int what, IBinder token, String pkg) {
226        // TODO(cwren): delete this API before the next release. Bug:15344099
227        if (CALL_PACKAGES.contains(pkg)) {
228            return mZenMode == Global.ZEN_MODE_OFF || mConfig.allowCalls;
229        }
230        return true;
231    }
232
233    public void dump(PrintWriter pw, String prefix) {
234        pw.print(prefix); pw.print("mZenMode=");
235        pw.println(Global.zenModeToString(mZenMode));
236        pw.print(prefix); pw.print("mConfig="); pw.println(mConfig);
237        pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig);
238        pw.print(prefix); pw.print("mPreviousRingerMode="); pw.println(mPreviousRingerMode);
239    }
240
241    public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException {
242        final ZenModeConfig config = ZenModeConfig.readXml(parser);
243        if (config != null) {
244            setConfig(config);
245        }
246    }
247
248    public void writeXml(XmlSerializer out) throws IOException {
249        mConfig.writeXml(out);
250    }
251
252    public ZenModeConfig getConfig() {
253        return mConfig;
254    }
255
256    public boolean setConfig(ZenModeConfig config) {
257        if (config == null || !config.isValid()) return false;
258        if (config.equals(mConfig)) return true;
259        mConfig = config;
260        Slog.d(TAG, "mConfig=" + mConfig);
261        dispatchOnConfigChanged();
262        final String val = Integer.toString(mConfig.hashCode());
263        Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val);
264        updateAlarms();
265        updateZenMode();
266        return true;
267    }
268
269    private void dispatchOnConfigChanged() {
270        for (Callback callback : mCallbacks) {
271            callback.onConfigChanged();
272        }
273    }
274
275    private void dispatchOnZenModeChanged() {
276        for (Callback callback : mCallbacks) {
277            callback.onZenModeChanged();
278        }
279    }
280
281    private boolean isSystem(NotificationRecord record) {
282        return SYSTEM_PACKAGES.contains(record.sbn.getPackageName())
283                && Notification.CATEGORY_SYSTEM.equals(record.getNotification().category);
284    }
285
286    private boolean isAlarm(NotificationRecord record) {
287        return ALARM_PACKAGES.contains(record.sbn.getPackageName());
288    }
289
290    private boolean isCall(NotificationRecord record) {
291        return CALL_PACKAGES.contains(record.sbn.getPackageName());
292    }
293
294    private boolean isMessage(NotificationRecord record) {
295        return MESSAGE_PACKAGES.contains(record.sbn.getPackageName());
296    }
297
298    private boolean audienceMatches(NotificationRecord record) {
299        switch (mConfig.allowFrom) {
300            case ZenModeConfig.SOURCE_ANYONE:
301                return true;
302            case ZenModeConfig.SOURCE_CONTACT:
303                return record.getContactAffinity() >= ValidateNotificationPeople.VALID_CONTACT;
304            case ZenModeConfig.SOURCE_STAR:
305                return record.getContactAffinity() >= ValidateNotificationPeople.STARRED_CONTACT;
306            default:
307                Slog.w(TAG, "Encountered unknown source: " + mConfig.allowFrom);
308                return true;
309        }
310    }
311
312    private void updateAlarms() {
313        updateAlarm(ACTION_ENTER_ZEN, REQUEST_CODE_ENTER,
314                mConfig.sleepStartHour, mConfig.sleepStartMinute);
315        updateAlarm(ACTION_EXIT_ZEN, REQUEST_CODE_EXIT,
316                mConfig.sleepEndHour, mConfig.sleepEndMinute);
317    }
318
319    private void updateAlarm(String action, int requestCode, int hr, int min) {
320        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
321        final long now = System.currentTimeMillis();
322        final Calendar c = Calendar.getInstance();
323        c.setTimeInMillis(now);
324        c.set(Calendar.HOUR_OF_DAY, hr);
325        c.set(Calendar.MINUTE, min);
326        c.set(Calendar.SECOND, 0);
327        c.set(Calendar.MILLISECOND, 0);
328        if (c.getTimeInMillis() <= now) {
329            c.add(Calendar.DATE, 1);
330        }
331        final long time = c.getTimeInMillis();
332        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode,
333                new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT);
334        alarms.cancel(pendingIntent);
335        if (mConfig.sleepMode != null) {
336            Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s",
337                    action, ts(time), time - now, ts(now)));
338            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
339        }
340    }
341
342    private static String ts(long time) {
343        return new Date(time) + " (" + time + ")";
344    }
345
346    private class SettingsObserver extends ContentObserver {
347        private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE);
348
349        public SettingsObserver(Handler handler) {
350            super(handler);
351        }
352
353        public void observe() {
354            final ContentResolver resolver = mContext.getContentResolver();
355            resolver.registerContentObserver(ZEN_MODE, false /*notifyForDescendents*/, this);
356            update(null);
357        }
358
359        @Override
360        public void onChange(boolean selfChange, Uri uri) {
361            update(uri);
362        }
363
364        public void update(Uri uri) {
365            if (ZEN_MODE.equals(uri)) {
366                updateZenMode();
367            }
368        }
369    }
370
371    private class ZenBroadcastReceiver extends BroadcastReceiver {
372        private final Calendar mCalendar = Calendar.getInstance();
373
374        @Override
375        public void onReceive(Context context, Intent intent) {
376            if (ACTION_ENTER_ZEN.equals(intent.getAction())) {
377                setZenMode(intent, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
378            } else if (ACTION_EXIT_ZEN.equals(intent.getAction())) {
379                setZenMode(intent, Global.ZEN_MODE_OFF);
380            }
381        }
382
383        private void setZenMode(Intent intent, int zenModeValue) {
384            final long schTime = intent.getLongExtra(EXTRA_TIME, 0);
385            final long now = System.currentTimeMillis();
386            Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s",
387                    intent.getAction(), ts(schTime), ts(now), now - schTime));
388
389            final int[] days = ZenModeConfig.tryParseDays(mConfig.sleepMode);
390            if (days != null) {
391                final int day = getDayOfWeek(schTime);
392                for (int i = 0; i < days.length; i++) {
393                    if (days[i] == day) {
394                        Slog.d(TAG, "Enter downtime, day=" + day + " days=" + Arrays.asList(days));
395                        ZenModeHelper.this.setZenMode(zenModeValue);
396                        break;
397                    } else {
398                        Slog.d(TAG, "Skip downtime, day=" + day + " days=" + Arrays.asList(days));
399                    }
400                }
401            } else {
402                Slog.d(TAG, "Skip downtime, no days configured");
403            }
404            updateAlarms();
405        }
406
407        private int getDayOfWeek(long time) {
408            mCalendar.setTimeInMillis(time);
409            return mCalendar.get(Calendar.DAY_OF_WEEK);
410        }
411    }
412
413    public static class Callback {
414        void onConfigChanged() {}
415        void onZenModeChanged() {}
416    }
417}
418