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