ZenModeHelper.java revision e77bb36d48b6b8b5c3bb6a1195aca469bb237919
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 setZenMode(int zenModeValue) {
136        Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zenModeValue);
137    }
138
139    public void updateZenMode() {
140        final int mode = Global.getInt(mContext.getContentResolver(),
141                Global.ZEN_MODE, Global.ZEN_MODE_OFF);
142        if (mode != mZenMode) {
143            Slog.d(TAG, String.format("updateZenMode: %s -> %s",
144                    Global.zenModeToString(mZenMode),
145                    Global.zenModeToString(mode)));
146        }
147        mZenMode = mode;
148        final boolean zen = mZenMode != Global.ZEN_MODE_OFF;
149        final String[] exceptionPackages = null; // none (for now)
150
151        // call restrictions
152        final boolean muteCalls = zen && !mConfig.allowCalls;
153        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING,
154                muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
155                exceptionPackages);
156        mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING,
157                muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
158                exceptionPackages);
159
160        // restrict vibrations with no hints
161        mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE,
162                zen ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED,
163                exceptionPackages);
164    }
165
166    public boolean allowDisable(int what, IBinder token, String pkg) {
167        if (isCall(pkg, null)) {
168            return mZenMode == Global.ZEN_MODE_OFF || mConfig.allowCalls;
169        }
170        return true;
171    }
172
173    public void dump(PrintWriter pw, String prefix) {
174        pw.print(prefix); pw.print("mZenMode=");
175        pw.println(Global.zenModeToString(mZenMode));
176        pw.print(prefix); pw.print("mConfig="); pw.println(mConfig);
177        pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig);
178    }
179
180    public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException {
181        final ZenModeConfig config = ZenModeConfig.readXml(parser);
182        if (config != null) {
183            setConfig(config);
184        }
185    }
186
187    public void writeXml(XmlSerializer out) throws IOException {
188        mConfig.writeXml(out);
189    }
190
191    public ZenModeConfig getConfig() {
192        return mConfig;
193    }
194
195    public boolean setConfig(ZenModeConfig config) {
196        if (config == null || !config.isValid()) return false;
197        if (config.equals(mConfig)) return true;
198        mConfig = config;
199        Slog.d(TAG, "mConfig=" + mConfig);
200        if (mCallback != null) mCallback.onConfigChanged();
201        final String val = Integer.toString(mConfig.hashCode());
202        Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val);
203        updateAlarms();
204        updateZenMode();
205        return true;
206    }
207
208    private boolean isCall(String pkg, Notification n) {
209        return CALL_PACKAGES.contains(pkg);
210    }
211
212    private boolean isMessage(String pkg, Notification n) {
213        return MESSAGE_PACKAGES.contains(pkg);
214    }
215
216    private void updateAlarms() {
217        updateAlarm(ACTION_ENTER_ZEN, REQUEST_CODE_ENTER,
218                mConfig.sleepStartHour, mConfig.sleepStartMinute);
219        updateAlarm(ACTION_EXIT_ZEN, REQUEST_CODE_EXIT,
220                mConfig.sleepEndHour, mConfig.sleepEndMinute);
221    }
222
223    private void updateAlarm(String action, int requestCode, int hr, int min) {
224        final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
225        final long now = System.currentTimeMillis();
226        final Calendar c = Calendar.getInstance();
227        c.setTimeInMillis(now);
228        c.set(Calendar.HOUR_OF_DAY, hr);
229        c.set(Calendar.MINUTE, min);
230        c.set(Calendar.SECOND, 0);
231        c.set(Calendar.MILLISECOND, 0);
232        if (c.getTimeInMillis() <= now) {
233            c.add(Calendar.DATE, 1);
234        }
235        final long time = c.getTimeInMillis();
236        final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode,
237                new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT);
238        alarms.cancel(pendingIntent);
239        if (mConfig.sleepMode != null) {
240            Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s",
241                    action, ts(time), time - now, ts(now)));
242            alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent);
243        }
244    }
245
246    private static String ts(long time) {
247        return new Date(time) + " (" + time + ")";
248    }
249
250    public static boolean isWeekend(long time, int offsetDays) {
251        final Calendar c = Calendar.getInstance();
252        c.setTimeInMillis(time);
253        if (offsetDays != 0) {
254            c.add(Calendar.DATE, offsetDays);
255        }
256        final int day = c.get(Calendar.DAY_OF_WEEK);
257        return day == Calendar.SATURDAY || day == Calendar.SUNDAY;
258    }
259
260    private class SettingsObserver extends ContentObserver {
261        private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE);
262
263        public SettingsObserver(Handler handler) {
264            super(handler);
265        }
266
267        public void observe() {
268            final ContentResolver resolver = mContext.getContentResolver();
269            resolver.registerContentObserver(ZEN_MODE, false /*notifyForDescendents*/, this);
270            update(null);
271        }
272
273        @Override
274        public void onChange(boolean selfChange, Uri uri) {
275            update(uri);
276        }
277
278        public void update(Uri uri) {
279            if (ZEN_MODE.equals(uri)) {
280                updateZenMode();
281            }
282        }
283    }
284
285    private class ZenBroadcastReceiver extends BroadcastReceiver {
286        @Override
287        public void onReceive(Context context, Intent intent) {
288            if (ACTION_ENTER_ZEN.equals(intent.getAction())) {
289                setZenMode(intent, 1, Global.ZEN_MODE_ON);
290            } else if (ACTION_EXIT_ZEN.equals(intent.getAction())) {
291                setZenMode(intent, 0, Global.ZEN_MODE_OFF);
292            }
293        }
294
295        private void setZenMode(Intent intent, int wkendOffsetDays, int zenModeValue) {
296            final long schTime = intent.getLongExtra(EXTRA_TIME, 0);
297            final long now = System.currentTimeMillis();
298            Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s",
299                    intent.getAction(), ts(schTime), ts(now), now - schTime));
300
301            final boolean skip = ZenModeConfig.SLEEP_MODE_WEEKNIGHTS.equals(mConfig.sleepMode) &&
302                    isWeekend(schTime, wkendOffsetDays);
303
304            if (skip) {
305                Slog.d(TAG, "Skipping zen mode update for the weekend");
306            } else {
307                ZenModeHelper.this.setZenMode(zenModeValue);
308            }
309            updateAlarms();
310        }
311    }
312
313    public interface Callback {
314        void onConfigChanged();
315    }
316}
317