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