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