ZenModeHelper.java revision 056c519df1dfb8fdc57daddfdf09bc0e1ffddac4
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.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
73    private Callback mCallback;
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            ));
82    private static final Set<String> MESSAGE_PACKAGES = new HashSet<String>(Arrays.asList(
83            "com.google.android.talk",
84            "com.android.mms"
85            ));
86
87    public ZenModeHelper(Context context, Handler handler) {
88        mContext = context;
89        mHandler = handler;
90        mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
91        mDefaultConfig = readDefaultConfig(context.getResources());
92        mConfig = mDefaultConfig;
93        mSettingsObserver = new SettingsObserver(mHandler);
94        mSettingsObserver.observe();
95
96        final IntentFilter filter = new IntentFilter();
97        filter.addAction(ACTION_ENTER_ZEN);
98        filter.addAction(ACTION_EXIT_ZEN);
99        mContext.registerReceiver(new ZenBroadcastReceiver(), filter);
100    }
101
102    public static ZenModeConfig readDefaultConfig(Resources resources) {
103        XmlResourceParser parser = null;
104        try {
105            parser = resources.getXml(R.xml.default_zen_mode_config);
106            while (parser.next() != XmlPullParser.END_DOCUMENT) {
107                final ZenModeConfig config = ZenModeConfig.readXml(parser);
108                if (config != null) return config;
109            }
110        } catch (Exception e) {
111            Slog.w(TAG, "Error reading default zen mode config from resource", e);
112        } finally {
113            IoUtils.closeQuietly(parser);
114        }
115        return new ZenModeConfig();
116    }
117
118    public void setCallback(Callback callback) {
119        mCallback = callback;
120    }
121
122    public boolean shouldIntercept(String pkg, Notification n) {
123        if (mZenMode != Global.ZEN_MODE_OFF) {
124            if (isCall(pkg, n)) {
125                return !mConfig.allowCalls;
126            }
127            if (isMessage(pkg, n)) {
128                return !mConfig.allowMessages;
129            }
130            return true;
131        }
132        return false;
133    }
134
135    public void updateZenMode() {
136        final int mode = Global.getInt(mContext.getContentResolver(),
137                Global.ZEN_MODE, Global.ZEN_MODE_OFF);
138        if (mode != mZenMode) {
139            Slog.d(TAG, String.format("updateZenMode: %s -> %s",
140                    Global.zenModeToString(mZenMode),
141                    Global.zenModeToString(mode)));
142        }
143        mZenMode = mode;
144        final boolean zen = mZenMode != Global.ZEN_MODE_OFF;
145        final String[] exceptionPackages = null; // none (for now)
146
147        // call restrictions
148        final boolean muteCalls = zen && !mConfig.allowCalls;
149        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING,
150                muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
151                exceptionPackages);
152        mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING,
153                muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
154                exceptionPackages);
155
156        // restrict vibrations with no hints
157        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE,
158                zen ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
159                exceptionPackages);
160    }
161
162    public boolean allowDisable(int what, IBinder token, String pkg) {
163        if (isCall(pkg, null)) {
164            return mZenMode == Global.ZEN_MODE_OFF || mConfig.allowCalls;
165        }
166        return true;
167    }
168
169    public void dump(PrintWriter pw, String prefix) {
170        pw.print(prefix); pw.print("mZenMode=");
171        pw.println(Global.zenModeToString(mZenMode));
172        pw.print(prefix); pw.print("mConfig="); pw.println(mConfig);
173        pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig);
174    }
175
176    public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException {
177        final ZenModeConfig config = ZenModeConfig.readXml(parser);
178        if (config != null) {
179            setConfig(config);
180        }
181    }
182
183    public void writeXml(XmlSerializer out) throws IOException {
184        mConfig.writeXml(out);
185    }
186
187    public ZenModeConfig getConfig() {
188        return mConfig;
189    }
190
191    public boolean setConfig(ZenModeConfig config) {
192        if (config == null || !config.isValid()) return false;
193        if (config.equals(mConfig)) return true;
194        mConfig = config;
195        Slog.d(TAG, "mConfig=" + mConfig);
196        if (mCallback != null) mCallback.onConfigChanged();
197        final String val = Integer.toString(mConfig.hashCode());
198        Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val);
199        updateAlarms();
200        updateZenMode();
201        return true;
202    }
203
204    private boolean isCall(String pkg, Notification n) {
205        return CALL_PACKAGES.contains(pkg);
206    }
207
208    private boolean isMessage(String pkg, Notification n) {
209        return MESSAGE_PACKAGES.contains(pkg);
210    }
211
212    private void updateAlarms() {
213        updateAlarm(ACTION_ENTER_ZEN, REQUEST_CODE_ENTER,
214                mConfig.sleepStartHour, mConfig.sleepStartMinute);
215        updateAlarm(ACTION_EXIT_ZEN, REQUEST_CODE_EXIT,
216                mConfig.sleepEndHour, mConfig.sleepEndMinute);
217    }
218
219    private void updateAlarm(String action, int requestCode, int hr, int min) {
220        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
221        final long now = System.currentTimeMillis();
222        final Calendar c = Calendar.getInstance();
223        c.setTimeInMillis(now);
224        c.set(Calendar.HOUR_OF_DAY, hr);
225        c.set(Calendar.MINUTE, min);
226        c.set(Calendar.SECOND, 0);
227        c.set(Calendar.MILLISECOND, 0);
228        if (c.getTimeInMillis() <= now) {
229            c.add(Calendar.DATE, 1);
230        }
231        final long time = c.getTimeInMillis();
232        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode,
233                new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT);
234        alarms.cancel(pendingIntent);
235        if (mConfig.sleepMode != null) {
236            Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s",
237                    action, ts(time), time - now, ts(now)));
238            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
239        }
240    }
241
242    private static String ts(long time) {
243        return new Date(time) + " (" + time + ")";
244    }
245
246    public static boolean isWeekend(long time, int offsetDays) {
247        final Calendar c = Calendar.getInstance();
248        c.setTimeInMillis(time);
249        if (offsetDays != 0) {
250            c.add(Calendar.DATE, offsetDays);
251        }
252        final int day = c.get(Calendar.DAY_OF_WEEK);
253        return day == Calendar.SATURDAY || day == Calendar.SUNDAY;
254    }
255
256    private class SettingsObserver extends ContentObserver {
257        private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE);
258
259        public SettingsObserver(Handler handler) {
260            super(handler);
261        }
262
263        public void observe() {
264            final ContentResolver resolver = mContext.getContentResolver();
265            resolver.registerContentObserver(ZEN_MODE, false /*notifyForDescendents*/, this);
266            update(null);
267        }
268
269        @Override
270        public void onChange(boolean selfChange, Uri uri) {
271            update(uri);
272        }
273
274        public void update(Uri uri) {
275            if (ZEN_MODE.equals(uri)) {
276                updateZenMode();
277            }
278        }
279    }
280
281    private class ZenBroadcastReceiver extends BroadcastReceiver {
282        @Override
283        public void onReceive(Context context, Intent intent) {
284            if (ACTION_ENTER_ZEN.equals(intent.getAction())) {
285                setZenMode(intent, 1, Global.ZEN_MODE_ON);
286            } else if (ACTION_EXIT_ZEN.equals(intent.getAction())) {
287                setZenMode(intent, 0, Global.ZEN_MODE_OFF);
288            }
289        }
290
291        private void setZenMode(Intent intent, int wkendOffsetDays, int zenModeValue) {
292            final long schTime = intent.getLongExtra(EXTRA_TIME, 0);
293            final long now = System.currentTimeMillis();
294            Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s",
295                    intent.getAction(), ts(schTime), ts(now), now - schTime));
296
297            final boolean skip = ZenModeConfig.SLEEP_MODE_WEEKNIGHTS.equals(mConfig.sleepMode) &&
298                    isWeekend(schTime, wkendOffsetDays);
299
300            if (skip) {
301                Slog.d(TAG, "Skipping zen mode update for the weekend");
302            } else {
303                Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zenModeValue);
304            }
305            updateAlarms();
306        }
307    }
308
309    public interface Callback {
310        void onConfigChanged();
311    }
312}
313