ZenModeHelper.java revision d5092bcb11c28f656eb470bd9b74585287fd75b4
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 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 "com.android.example.notificationshowcase" 83 )); 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 boolean shouldIntercept(NotificationRecord record) { 133 if (mZenMode != Global.ZEN_MODE_OFF) { 134 if (isSystem(record)) { 135 return false; 136 } 137 if (isAlarm(record)) { 138 return false; 139 } 140 // audience has veto power over all following rules 141 if (!audienceMatches(record)) { 142 return true; 143 } 144 if (isCall(record)) { 145 return !mConfig.allowCalls; 146 } 147 if (isMessage(record)) { 148 return !mConfig.allowMessages; 149 } 150 return true; 151 } 152 return false; 153 } 154 155 public int getZenMode() { 156 return mZenMode; 157 } 158 159 public void setZenMode(int zenModeValue) { 160 Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zenModeValue); 161 } 162 163 public void updateZenMode() { 164 final int mode = Global.getInt(mContext.getContentResolver(), 165 Global.ZEN_MODE, Global.ZEN_MODE_OFF); 166 if (mode != mZenMode) { 167 Slog.d(TAG, String.format("updateZenMode: %s -> %s", 168 Global.zenModeToString(mZenMode), 169 Global.zenModeToString(mode))); 170 } 171 mZenMode = mode; 172 final boolean zen = mZenMode != Global.ZEN_MODE_OFF; 173 final String[] exceptionPackages = null; // none (for now) 174 175 // call restrictions 176 final boolean muteCalls = zen && !mConfig.allowCalls; 177 mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING, 178 muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, 179 exceptionPackages); 180 mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING, 181 muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, 182 exceptionPackages); 183 184 // restrict vibrations with no hints 185 mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE, 186 zen ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, 187 exceptionPackages); 188 dispatchOnZenModeChanged(); 189 } 190 191 public boolean allowDisable(int what, IBinder token, String pkg) { 192 // TODO(cwren): delete this API before the next release. Bug:15344099 193 if (CALL_PACKAGES.contains(pkg)) { 194 return mZenMode == Global.ZEN_MODE_OFF || mConfig.allowCalls; 195 } 196 return true; 197 } 198 199 public void dump(PrintWriter pw, String prefix) { 200 pw.print(prefix); pw.print("mZenMode="); 201 pw.println(Global.zenModeToString(mZenMode)); 202 pw.print(prefix); pw.print("mConfig="); pw.println(mConfig); 203 pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig); 204 } 205 206 public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException { 207 final ZenModeConfig config = ZenModeConfig.readXml(parser); 208 if (config != null) { 209 setConfig(config); 210 } 211 } 212 213 public void writeXml(XmlSerializer out) throws IOException { 214 mConfig.writeXml(out); 215 } 216 217 public ZenModeConfig getConfig() { 218 return mConfig; 219 } 220 221 public boolean setConfig(ZenModeConfig config) { 222 if (config == null || !config.isValid()) return false; 223 if (config.equals(mConfig)) return true; 224 mConfig = config; 225 Slog.d(TAG, "mConfig=" + mConfig); 226 dispatchOnConfigChanged(); 227 final String val = Integer.toString(mConfig.hashCode()); 228 Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); 229 updateAlarms(); 230 updateZenMode(); 231 return true; 232 } 233 234 private void dispatchOnConfigChanged() { 235 for (Callback callback : mCallbacks) { 236 callback.onConfigChanged(); 237 } 238 } 239 240 private void dispatchOnZenModeChanged() { 241 for (Callback callback : mCallbacks) { 242 callback.onZenModeChanged(); 243 } 244 } 245 246 private boolean isSystem(NotificationRecord record) { 247 return SYSTEM_PACKAGES.contains(record.sbn.getPackageName()) 248 && Notification.CATEGORY_SYSTEM.equals(record.getNotification().category); 249 } 250 251 private boolean isAlarm(NotificationRecord record) { 252 return ALARM_PACKAGES.contains(record.sbn.getPackageName()); 253 } 254 255 private boolean isCall(NotificationRecord record) { 256 return CALL_PACKAGES.contains(record.sbn.getPackageName()); 257 } 258 259 private boolean isMessage(NotificationRecord record) { 260 return MESSAGE_PACKAGES.contains(record.sbn.getPackageName()); 261 } 262 263 private boolean audienceMatches(NotificationRecord record) { 264 switch (mConfig.allowFrom) { 265 case ZenModeConfig.SOURCE_ANYONE: 266 return true; 267 case ZenModeConfig.SOURCE_CONTACT: 268 return record.getContactAffinity() >= ValidateNotificationPeople.VALID_CONTACT; 269 case ZenModeConfig.SOURCE_STAR: 270 return record.getContactAffinity() >= ValidateNotificationPeople.STARRED_CONTACT; 271 default: 272 Slog.w(TAG, "Encountered unknown source: " + mConfig.allowFrom); 273 return true; 274 } 275 } 276 277 private void updateAlarms() { 278 updateAlarm(ACTION_ENTER_ZEN, REQUEST_CODE_ENTER, 279 mConfig.sleepStartHour, mConfig.sleepStartMinute); 280 updateAlarm(ACTION_EXIT_ZEN, REQUEST_CODE_EXIT, 281 mConfig.sleepEndHour, mConfig.sleepEndMinute); 282 } 283 284 private void updateAlarm(String action, int requestCode, int hr, int min) { 285 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 286 final long now = System.currentTimeMillis(); 287 final Calendar c = Calendar.getInstance(); 288 c.setTimeInMillis(now); 289 c.set(Calendar.HOUR_OF_DAY, hr); 290 c.set(Calendar.MINUTE, min); 291 c.set(Calendar.SECOND, 0); 292 c.set(Calendar.MILLISECOND, 0); 293 if (c.getTimeInMillis() <= now) { 294 c.add(Calendar.DATE, 1); 295 } 296 final long time = c.getTimeInMillis(); 297 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, 298 new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT); 299 alarms.cancel(pendingIntent); 300 if (mConfig.sleepMode != null) { 301 Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s", 302 action, ts(time), time - now, ts(now))); 303 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 304 } 305 } 306 307 private static String ts(long time) { 308 return new Date(time) + " (" + time + ")"; 309 } 310 311 public static boolean isWeekend(long time, int offsetDays) { 312 final Calendar c = Calendar.getInstance(); 313 c.setTimeInMillis(time); 314 if (offsetDays != 0) { 315 c.add(Calendar.DATE, offsetDays); 316 } 317 final int day = c.get(Calendar.DAY_OF_WEEK); 318 return day == Calendar.SATURDAY || day == Calendar.SUNDAY; 319 } 320 321 private class SettingsObserver extends ContentObserver { 322 private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE); 323 324 public SettingsObserver(Handler handler) { 325 super(handler); 326 } 327 328 public void observe() { 329 final ContentResolver resolver = mContext.getContentResolver(); 330 resolver.registerContentObserver(ZEN_MODE, false /*notifyForDescendents*/, this); 331 update(null); 332 } 333 334 @Override 335 public void onChange(boolean selfChange, Uri uri) { 336 update(uri); 337 } 338 339 public void update(Uri uri) { 340 if (ZEN_MODE.equals(uri)) { 341 updateZenMode(); 342 } 343 } 344 } 345 346 private class ZenBroadcastReceiver extends BroadcastReceiver { 347 @Override 348 public void onReceive(Context context, Intent intent) { 349 if (ACTION_ENTER_ZEN.equals(intent.getAction())) { 350 setZenMode(intent, 1, Global.ZEN_MODE_ON); 351 } else if (ACTION_EXIT_ZEN.equals(intent.getAction())) { 352 setZenMode(intent, 0, Global.ZEN_MODE_OFF); 353 } 354 } 355 356 private void setZenMode(Intent intent, int wkendOffsetDays, int zenModeValue) { 357 final long schTime = intent.getLongExtra(EXTRA_TIME, 0); 358 final long now = System.currentTimeMillis(); 359 Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", 360 intent.getAction(), ts(schTime), ts(now), now - schTime)); 361 362 final boolean skip = ZenModeConfig.SLEEP_MODE_WEEKNIGHTS.equals(mConfig.sleepMode) && 363 isWeekend(schTime, wkendOffsetDays); 364 365 if (skip) { 366 Slog.d(TAG, "Skipping zen mode update for the weekend"); 367 } else { 368 ZenModeHelper.this.setZenMode(zenModeValue); 369 } 370 updateAlarms(); 371 } 372 } 373 374 public static class Callback { 375 void onConfigChanged() {} 376 void onZenModeChanged() {} 377 } 378} 379