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