ZenModeHelper.java revision 99f963ea04d7a86219ece00c356f3f6bce33b6d6
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.PendingIntent; 22import android.content.BroadcastReceiver; 23import android.content.ContentResolver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.content.res.Resources; 28import android.content.res.XmlResourceParser; 29import android.database.ContentObserver; 30import android.media.AudioManager; 31import android.net.Uri; 32import android.os.Handler; 33import android.os.IBinder; 34import android.provider.Settings.Global; 35import android.service.notification.ZenModeConfig; 36import android.util.Slog; 37 38import com.android.internal.R; 39 40import libcore.io.IoUtils; 41 42import org.xmlpull.v1.XmlPullParser; 43import org.xmlpull.v1.XmlPullParserException; 44import org.xmlpull.v1.XmlSerializer; 45 46import java.io.IOException; 47import java.io.PrintWriter; 48import java.util.ArrayList; 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 private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); 73 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 "com.android.example.notificationshowcase" 82 )); 83 private static final Set<String> MESSAGE_PACKAGES = new HashSet<String>(Arrays.asList( 84 "com.google.android.talk", 85 "com.android.mms", 86 "com.android.example.notificationshowcase" 87 )); 88 private static final Set<String> ALARM_PACKAGES = new HashSet<String>(Arrays.asList( 89 "com.google.android.deskclock" 90 )); 91 92 public ZenModeHelper(Context context, Handler handler) { 93 mContext = context; 94 mHandler = handler; 95 mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 96 mDefaultConfig = readDefaultConfig(context.getResources()); 97 mConfig = mDefaultConfig; 98 mSettingsObserver = new SettingsObserver(mHandler); 99 mSettingsObserver.observe(); 100 101 final IntentFilter filter = new IntentFilter(); 102 filter.addAction(ACTION_ENTER_ZEN); 103 filter.addAction(ACTION_EXIT_ZEN); 104 mContext.registerReceiver(new ZenBroadcastReceiver(), filter); 105 } 106 107 public static ZenModeConfig readDefaultConfig(Resources resources) { 108 XmlResourceParser parser = null; 109 try { 110 parser = resources.getXml(R.xml.default_zen_mode_config); 111 while (parser.next() != XmlPullParser.END_DOCUMENT) { 112 final ZenModeConfig config = ZenModeConfig.readXml(parser); 113 if (config != null) return config; 114 } 115 } catch (Exception e) { 116 Slog.w(TAG, "Error reading default zen mode config from resource", e); 117 } finally { 118 IoUtils.closeQuietly(parser); 119 } 120 return new ZenModeConfig(); 121 } 122 123 public void addCallback(Callback callback) { 124 mCallbacks.add(callback); 125 } 126 127 public boolean shouldIntercept(NotificationRecord record, boolean previouslySeen) { 128 if (mZenMode != Global.ZEN_MODE_OFF) { 129 if (previouslySeen && !record.isIntercepted()) { 130 // notifications never transition from not intercepted to intercepted 131 return false; 132 } 133 if (isAlarm(record)) { 134 return false; 135 } 136 // audience has veto power over all following rules 137 if (!audienceMatches(record)) { 138 return true; 139 } 140 if (isCall(record)) { 141 return !mConfig.allowCalls; 142 } 143 if (isMessage(record)) { 144 return !mConfig.allowMessages; 145 } 146 return true; 147 } 148 return false; 149 } 150 151 public int getZenMode() { 152 return mZenMode; 153 } 154 155 public void setZenMode(int zenModeValue) { 156 Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zenModeValue); 157 } 158 159 public void updateZenMode() { 160 final int mode = Global.getInt(mContext.getContentResolver(), 161 Global.ZEN_MODE, Global.ZEN_MODE_OFF); 162 if (mode != mZenMode) { 163 Slog.d(TAG, String.format("updateZenMode: %s -> %s", 164 Global.zenModeToString(mZenMode), 165 Global.zenModeToString(mode))); 166 } 167 mZenMode = mode; 168 final boolean zen = mZenMode != Global.ZEN_MODE_OFF; 169 final String[] exceptionPackages = null; // none (for now) 170 171 // call restrictions 172 final boolean muteCalls = zen && !mConfig.allowCalls; 173 mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING, 174 muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, 175 exceptionPackages); 176 mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING, 177 muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, 178 exceptionPackages); 179 180 // restrict vibrations with no hints 181 mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE, 182 zen ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, 183 exceptionPackages); 184 dispatchOnZenModeChanged(); 185 } 186 187 public boolean allowDisable(int what, IBinder token, String pkg) { 188 // TODO(cwren): delete this API before the next release. Bug:15344099 189 if (CALL_PACKAGES.contains(pkg)) { 190 return mZenMode == Global.ZEN_MODE_OFF || mConfig.allowCalls; 191 } 192 return true; 193 } 194 195 public void dump(PrintWriter pw, String prefix) { 196 pw.print(prefix); pw.print("mZenMode="); 197 pw.println(Global.zenModeToString(mZenMode)); 198 pw.print(prefix); pw.print("mConfig="); pw.println(mConfig); 199 pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig); 200 } 201 202 public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException { 203 final ZenModeConfig config = ZenModeConfig.readXml(parser); 204 if (config != null) { 205 setConfig(config); 206 } 207 } 208 209 public void writeXml(XmlSerializer out) throws IOException { 210 mConfig.writeXml(out); 211 } 212 213 public ZenModeConfig getConfig() { 214 return mConfig; 215 } 216 217 public boolean setConfig(ZenModeConfig config) { 218 if (config == null || !config.isValid()) return false; 219 if (config.equals(mConfig)) return true; 220 mConfig = config; 221 Slog.d(TAG, "mConfig=" + mConfig); 222 dispatchOnConfigChanged(); 223 final String val = Integer.toString(mConfig.hashCode()); 224 Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); 225 updateAlarms(); 226 updateZenMode(); 227 return true; 228 } 229 230 private void dispatchOnConfigChanged() { 231 for (Callback callback : mCallbacks) { 232 callback.onConfigChanged(); 233 } 234 } 235 236 private void dispatchOnZenModeChanged() { 237 for (Callback callback : mCallbacks) { 238 callback.onZenModeChanged(); 239 } 240 } 241 242 private boolean isAlarm(NotificationRecord record) { 243 return ALARM_PACKAGES.contains(record.sbn.getPackageName()); 244 } 245 246 private boolean isCall(NotificationRecord record) { 247 return CALL_PACKAGES.contains(record.sbn.getPackageName()); 248 } 249 250 private boolean isMessage(NotificationRecord record) { 251 return MESSAGE_PACKAGES.contains(record.sbn.getPackageName()); 252 } 253 254 private boolean audienceMatches(NotificationRecord record) { 255 switch (mConfig.allowFrom) { 256 case ZenModeConfig.SOURCE_ANYONE: 257 return true; 258 case ZenModeConfig.SOURCE_CONTACT: 259 return record.getContactAffinity() >= ValidateNotificationPeople.VALID_CONTACT; 260 case ZenModeConfig.SOURCE_STAR: 261 return record.getContactAffinity() >= ValidateNotificationPeople.STARRED_CONTACT; 262 default: 263 Slog.w(TAG, "Encountered unknown source: " + mConfig.allowFrom); 264 return true; 265 } 266 } 267 268 private void updateAlarms() { 269 updateAlarm(ACTION_ENTER_ZEN, REQUEST_CODE_ENTER, 270 mConfig.sleepStartHour, mConfig.sleepStartMinute); 271 updateAlarm(ACTION_EXIT_ZEN, REQUEST_CODE_EXIT, 272 mConfig.sleepEndHour, mConfig.sleepEndMinute); 273 } 274 275 private void updateAlarm(String action, int requestCode, int hr, int min) { 276 final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); 277 final long now = System.currentTimeMillis(); 278 final Calendar c = Calendar.getInstance(); 279 c.setTimeInMillis(now); 280 c.set(Calendar.HOUR_OF_DAY, hr); 281 c.set(Calendar.MINUTE, min); 282 c.set(Calendar.SECOND, 0); 283 c.set(Calendar.MILLISECOND, 0); 284 if (c.getTimeInMillis() <= now) { 285 c.add(Calendar.DATE, 1); 286 } 287 final long time = c.getTimeInMillis(); 288 final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, 289 new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT); 290 alarms.cancel(pendingIntent); 291 if (mConfig.sleepMode != null) { 292 Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s", 293 action, ts(time), time - now, ts(now))); 294 alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); 295 } 296 } 297 298 private static String ts(long time) { 299 return new Date(time) + " (" + time + ")"; 300 } 301 302 public static boolean isWeekend(long time, int offsetDays) { 303 final Calendar c = Calendar.getInstance(); 304 c.setTimeInMillis(time); 305 if (offsetDays != 0) { 306 c.add(Calendar.DATE, offsetDays); 307 } 308 final int day = c.get(Calendar.DAY_OF_WEEK); 309 return day == Calendar.SATURDAY || day == Calendar.SUNDAY; 310 } 311 312 private class SettingsObserver extends ContentObserver { 313 private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE); 314 315 public SettingsObserver(Handler handler) { 316 super(handler); 317 } 318 319 public void observe() { 320 final ContentResolver resolver = mContext.getContentResolver(); 321 resolver.registerContentObserver(ZEN_MODE, false /*notifyForDescendents*/, this); 322 update(null); 323 } 324 325 @Override 326 public void onChange(boolean selfChange, Uri uri) { 327 update(uri); 328 } 329 330 public void update(Uri uri) { 331 if (ZEN_MODE.equals(uri)) { 332 updateZenMode(); 333 } 334 } 335 } 336 337 private class ZenBroadcastReceiver extends BroadcastReceiver { 338 @Override 339 public void onReceive(Context context, Intent intent) { 340 if (ACTION_ENTER_ZEN.equals(intent.getAction())) { 341 setZenMode(intent, 1, Global.ZEN_MODE_ON); 342 } else if (ACTION_EXIT_ZEN.equals(intent.getAction())) { 343 setZenMode(intent, 0, Global.ZEN_MODE_OFF); 344 } 345 } 346 347 private void setZenMode(Intent intent, int wkendOffsetDays, int zenModeValue) { 348 final long schTime = intent.getLongExtra(EXTRA_TIME, 0); 349 final long now = System.currentTimeMillis(); 350 Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", 351 intent.getAction(), ts(schTime), ts(now), now - schTime)); 352 353 final boolean skip = ZenModeConfig.SLEEP_MODE_WEEKNIGHTS.equals(mConfig.sleepMode) && 354 isWeekend(schTime, wkendOffsetDays); 355 356 if (skip) { 357 Slog.d(TAG, "Skipping zen mode update for the weekend"); 358 } else { 359 ZenModeHelper.this.setZenMode(zenModeValue); 360 } 361 updateAlarms(); 362 } 363 } 364 365 public static class Callback { 366 void onConfigChanged() {} 367 void onZenModeChanged() {} 368 } 369} 370