From 056c519df1dfb8fdc57daddfdf09bc0e1ffddac4 Mon Sep 17 00:00:00 2001 From: John Spurlock Date: Sun, 20 Apr 2014 21:52:01 -0400 Subject: [PATCH] Do not disturb: persist user config. Load and store user configuration for do not disturb. Separate out service-related aspects into new helper. Make config availble over NoMan for settings. Implement phone + message based filtering (package whitelist for now). Implement automatic enter/exit zen mode overnight scheduler. Bug:14211946 Change-Id: Ib28aab0e4c5c9a5fd0b950b2884b1ab618fdfeca --- core/java/android/app/INotificationManager.aidl | 4 + core/java/android/provider/Settings.java | 7 + .../service/notification/ZenModeConfig.aidl | 20 ++ .../service/notification/ZenModeConfig.java | 234 ++++++++++++++++ core/res/res/values/symbols.xml | 1 + core/res/res/xml/default_zen_mode_config.xml | 24 ++ packages/SystemUI/res/values/strings.xml | 3 + .../statusbar/phone/QuickSettingsModel.java | 2 +- .../systemui/statusbar/phone/ZenModeView.java | 4 +- .../notification/NotificationManagerService.java | 227 +++++++-------- .../android/server/notification/ZenModeHelper.java | 312 +++++++++++++++++++++ 11 files changed, 715 insertions(+), 123 deletions(-) create mode 100644 core/java/android/service/notification/ZenModeConfig.aidl create mode 100644 core/java/android/service/notification/ZenModeConfig.java create mode 100644 core/res/res/xml/default_zen_mode_config.xml create mode 100644 services/core/java/com/android/server/notification/ZenModeHelper.java diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index bb6eeda2dd3a..8681f5c85b12 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -23,6 +23,7 @@ import android.app.Notification; import android.content.ComponentName; import android.content.Intent; import android.service.notification.INotificationListener; +import android.service.notification.ZenModeConfig; /** {@hide} */ interface INotificationManager @@ -49,4 +50,7 @@ interface INotificationManager StatusBarNotification[] getActiveNotificationsFromListener(in INotificationListener token, in String[] keys); String[] getActiveNotificationKeysFromListener(in INotificationListener token); + + ZenModeConfig getZenModeConfig(); + boolean setZenModeConfig(in ZenModeConfig config); } \ No newline at end of file diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1e202ca722d4..2ce6210cb6b4 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6120,6 +6120,13 @@ public final class Settings { } /** + * Opaque value, changes when persisted zen mode configuration changes. + * + * @hide + */ + public static final String ZEN_MODE_CONFIG_ETAG = "zen_mode_config_etag"; + + /** * Defines global heads up toggle. One of HEADS_UP_OFF, HEADS_UP_ON. * * @hide diff --git a/core/java/android/service/notification/ZenModeConfig.aidl b/core/java/android/service/notification/ZenModeConfig.aidl new file mode 100644 index 000000000000..c73b75ef821f --- /dev/null +++ b/core/java/android/service/notification/ZenModeConfig.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.notification; + +parcelable ZenModeConfig; + diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java new file mode 100644 index 000000000000..925ddcfc79df --- /dev/null +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.notification; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Objects; + +/** + * Persisted configuration for zen mode. + * + * @hide + */ +public class ZenModeConfig implements Parcelable { + + public static final String SLEEP_MODE_NIGHTS = "nights"; + public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights"; + + private static final int XML_VERSION = 1; + private static final String ZEN_TAG = "zen"; + private static final String ZEN_ATT_VERSION = "version"; + private static final String ALLOW_TAG = "allow"; + private static final String ALLOW_ATT_CALLS = "calls"; + private static final String ALLOW_ATT_MESSAGES = "messages"; + private static final String SLEEP_TAG = "sleep"; + private static final String SLEEP_ATT_MODE = "mode"; + + private static final String SLEEP_ATT_START_HR = "startHour"; + private static final String SLEEP_ATT_START_MIN = "startMin"; + private static final String SLEEP_ATT_END_HR = "endHour"; + private static final String SLEEP_ATT_END_MIN = "endMin"; + + public boolean allowCalls; + public boolean allowMessages; + + public String sleepMode; + public int sleepStartHour; + public int sleepStartMinute; + public int sleepEndHour; + public int sleepEndMinute; + + public ZenModeConfig() { } + + public ZenModeConfig(Parcel source) { + allowCalls = source.readInt() == 1; + allowMessages = source.readInt() == 1; + if (source.readInt() == 1) { + sleepMode = source.readString(); + } + sleepStartHour = source.readInt(); + sleepStartMinute = source.readInt(); + sleepEndHour = source.readInt(); + sleepEndMinute = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(allowCalls ? 1 : 0); + dest.writeInt(allowMessages ? 1 : 0); + if (sleepMode != null) { + dest.writeInt(1); + dest.writeString(sleepMode); + } else { + dest.writeInt(0); + } + dest.writeInt(sleepStartHour); + dest.writeInt(sleepStartMinute); + dest.writeInt(sleepEndHour); + dest.writeInt(sleepEndMinute); + } + + @Override + public String toString() { + return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[') + .append("allowCalls=").append(allowCalls) + .append(",allowMessages=").append(allowMessages) + .append(",sleepMode=").append(sleepMode) + .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute) + .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute) + .append(']').toString(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ZenModeConfig)) return false; + if (o == this) return true; + final ZenModeConfig other = (ZenModeConfig) o; + return other.allowCalls == allowCalls + && other.allowMessages == allowMessages + && Objects.equals(other.sleepMode, sleepMode) + && other.sleepStartHour == sleepStartHour + && other.sleepStartMinute == sleepStartMinute + && other.sleepEndHour == sleepEndHour + && other.sleepEndMinute == sleepEndMinute; + } + + @Override + public int hashCode() { + return Objects.hash(allowCalls, allowMessages, sleepMode, sleepStartHour, + sleepStartMinute, sleepEndHour, sleepEndMinute); + } + + public boolean isValid() { + return isValidHour(sleepStartHour) && isValidMinute(sleepStartMinute) + && isValidHour(sleepEndHour) && isValidMinute(sleepEndMinute) + && (sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS) + || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS)); + } + + public static ZenModeConfig readXml(XmlPullParser parser) + throws XmlPullParserException, IOException { + int type = parser.getEventType(); + if (type != XmlPullParser.START_TAG) return null; + String tag = parser.getName(); + if (!ZEN_TAG.equals(tag)) return null; + final ZenModeConfig rt = new ZenModeConfig(); + final int version = Integer.parseInt(parser.getAttributeValue(null, ZEN_ATT_VERSION)); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + tag = parser.getName(); + if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) return rt; + if (type == XmlPullParser.START_TAG) { + if (ALLOW_TAG.equals(tag)) { + rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false); + rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false); + } else if (SLEEP_TAG.equals(tag)) { + final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); + rt.sleepMode = (SLEEP_MODE_NIGHTS.equals(mode) + || SLEEP_MODE_WEEKNIGHTS.equals(mode)) ? mode : null; + final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0); + final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0); + final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0); + final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0); + rt.sleepStartHour = isValidHour(startHour) ? startHour : 0; + rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0; + rt.sleepEndHour = isValidHour(endHour) ? endHour : 0; + rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0; + } + } + } + return rt; + } + + public void writeXml(XmlSerializer out) throws IOException { + out.startTag(null, ZEN_TAG); + out.attribute(null, ZEN_ATT_VERSION, Integer.toString(XML_VERSION)); + + out.startTag(null, ALLOW_TAG); + out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls)); + out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages)); + out.endTag(null, ALLOW_TAG); + + out.startTag(null, SLEEP_TAG); + if (sleepMode != null) { + out.attribute(null, SLEEP_ATT_MODE, sleepMode); + } + out.attribute(null, SLEEP_ATT_START_HR, Integer.toString(sleepStartHour)); + out.attribute(null, SLEEP_ATT_START_MIN, Integer.toString(sleepStartMinute)); + out.attribute(null, SLEEP_ATT_END_HR, Integer.toString(sleepEndHour)); + out.attribute(null, SLEEP_ATT_END_MIN, Integer.toString(sleepEndMinute)); + out.endTag(null, SLEEP_TAG); + + out.endTag(null, ZEN_TAG); + } + + public static boolean isValidHour(int val) { + return val >= 0 && val < 24; + } + + public static boolean isValidMinute(int val) { + return val >= 0 && val < 60; + } + + private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) { + final String val = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(val)) return defValue; + return Boolean.valueOf(val); + } + + private static int safeInt(XmlPullParser parser, String att, int defValue) { + final String val = parser.getAttributeValue(null, att); + if (TextUtils.isEmpty(val)) return defValue; + return Integer.valueOf(val); + } + + @Override + public int describeContents() { + return 0; + } + + public ZenModeConfig copy() { + final Parcel parcel = Parcel.obtain(); + try { + writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new ZenModeConfig(parcel); + } finally { + parcel.recycle(); + } + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public ZenModeConfig createFromParcel(Parcel source) { + return new ZenModeConfig(source); + } + + @Override + public ZenModeConfig[] newArray(int size) { + return new ZenModeConfig[size]; + } + }; +} diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 26efe36a5a7a..f91612829f4d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1230,6 +1230,7 @@ + diff --git a/core/res/res/xml/default_zen_mode_config.xml b/core/res/res/xml/default_zen_mode_config.xml new file mode 100644 index 000000000000..1bdc1ec6b735 --- /dev/null +++ b/core/res/res/xml/default_zen_mode_config.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 72d14fefce1f..f3c956ce70e5 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -539,6 +539,9 @@ Touch to show + + Do not disturb + %d more diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java index 9b2504629720..e1ef83a9e754 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java @@ -608,7 +608,7 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); mZenModeState.enabled = mode != Settings.Global.ZEN_MODE_OFF; mZenModeState.zenMode = mode; - mZenModeState.label = ZenModeView.MODE_LABEL; + mZenModeState.label = mContext.getString(R.string.zen_mode_title); mZenModeState.iconId = R.drawable.stat_sys_zen_limited; mZenModeCallback.refreshView(mZenModeTile, mZenModeState); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java index 49cf78b9d824..c1662bac3667 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java @@ -42,13 +42,13 @@ import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; +import com.android.systemui.R; import com.android.systemui.statusbar.phone.ZenModeView.Adapter.ExitCondition; public class ZenModeView extends RelativeLayout { private static final String TAG = ZenModeView.class.getSimpleName(); private static final boolean DEBUG = false; - public static final String MODE_LABEL = "Limited interruptions"; public static final int BACKGROUND = 0xff282828; private static final Typeface CONDENSED = @@ -91,7 +91,7 @@ public class ZenModeView extends RelativeLayout { LayoutParams lp = null; mModeText = new TextView(mContext); - mModeText.setText(MODE_LABEL); + mModeText.setText(R.string.zen_mode_title); mModeText.setId(android.R.id.title); mModeText.setTextColor(GRAY); mModeText.setTypeface(CONDENSED); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 5b597a3cf329..c8bdb4c3a2c9 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -52,6 +52,7 @@ import android.media.IRingtonePlayer; import android.net.Uri; import android.os.Binder; import android.os.Build; +import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -64,6 +65,7 @@ import android.provider.Settings; import android.service.notification.INotificationListener; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; +import android.service.notification.ZenModeConfig; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; @@ -78,6 +80,7 @@ import android.widget.Toast; import com.android.internal.R; import com.android.internal.notification.NotificationScorer; +import com.android.internal.util.FastXmlSerializer; import com.android.server.EventLogTags; import com.android.server.notification.NotificationUsageStats.SingleNotificationStats; import com.android.server.statusbar.StatusBarManagerInternal; @@ -87,11 +90,13 @@ import com.android.server.lights.LightsManager; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Array; @@ -115,6 +120,7 @@ public class NotificationManagerService extends SystemService { // message codes static final int MESSAGE_TIMEOUT = 2; + static final int MESSAGE_SAVE_POLICY_FILE = 3; static final int LONG_DELAY = 3500; // 3.5 seconds static final int SHORT_DELAY = 2000; // 2 seconds @@ -209,15 +215,6 @@ public class NotificationManagerService extends SystemService { private final NotificationUsageStats mUsageStats = new NotificationUsageStats(); - private int mZenMode; - // temporary, until we update apps to provide metadata - private static final Set CALL_PACKAGES = new HashSet(Arrays.asList( - "com.google.android.dialer", - "com.android.phone" - )); - private static final Set ALARM_PACKAGES = new HashSet(Arrays.asList( - "com.google.android.deskclock" - )); private static final String EXTRA_INTERCEPT = "android.intercept"; // Profiles of the current user. @@ -421,53 +418,82 @@ public class NotificationManagerService extends SystemService { Archive mArchive = new Archive(); - private void loadBlockDb() { - synchronized(mBlockedPackages) { - if (mPolicyFile == null) { - File dir = new File("/data/system"); - mPolicyFile = new AtomicFile(new File(dir, "notification_policy.xml")); + private void loadPolicyFile() { + synchronized(mPolicyFile) { + mBlockedPackages.clear(); - mBlockedPackages.clear(); - - FileInputStream infile = null; - try { - infile = mPolicyFile.openRead(); - final XmlPullParser parser = Xml.newPullParser(); - parser.setInput(infile, null); - - int type; - String tag; - int version = DB_VERSION; - while ((type = parser.next()) != END_DOCUMENT) { - tag = parser.getName(); - if (type == START_TAG) { - if (TAG_BODY.equals(tag)) { - version = Integer.parseInt( - parser.getAttributeValue(null, ATTR_VERSION)); - } else if (TAG_BLOCKED_PKGS.equals(tag)) { - while ((type = parser.next()) != END_DOCUMENT) { - tag = parser.getName(); - if (TAG_PACKAGE.equals(tag)) { - mBlockedPackages.add( - parser.getAttributeValue(null, ATTR_NAME)); - } else if (TAG_BLOCKED_PKGS.equals(tag) && type == END_TAG) { - break; - } + FileInputStream infile = null; + try { + infile = mPolicyFile.openRead(); + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(infile, null); + + int type; + String tag; + int version = DB_VERSION; + while ((type = parser.next()) != END_DOCUMENT) { + tag = parser.getName(); + if (type == START_TAG) { + if (TAG_BODY.equals(tag)) { + version = Integer.parseInt( + parser.getAttributeValue(null, ATTR_VERSION)); + } else if (TAG_BLOCKED_PKGS.equals(tag)) { + while ((type = parser.next()) != END_DOCUMENT) { + tag = parser.getName(); + if (TAG_PACKAGE.equals(tag)) { + mBlockedPackages.add( + parser.getAttributeValue(null, ATTR_NAME)); + } else if (TAG_BLOCKED_PKGS.equals(tag) && type == END_TAG) { + break; } } } } - } catch (FileNotFoundException e) { - // No data yet - } catch (IOException e) { - Log.wtf(TAG, "Unable to read blocked notifications database", e); - } catch (NumberFormatException e) { - Log.wtf(TAG, "Unable to parse blocked notifications database", e); - } catch (XmlPullParserException e) { - Log.wtf(TAG, "Unable to parse blocked notifications database", e); - } finally { - IoUtils.closeQuietly(infile); + mZenModeHelper.readXml(parser); } + } catch (FileNotFoundException e) { + // No data yet + } catch (IOException e) { + Log.wtf(TAG, "Unable to read notification policy", e); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Unable to parse notification policy", e); + } catch (XmlPullParserException e) { + Log.wtf(TAG, "Unable to parse notification policy", e); + } finally { + IoUtils.closeQuietly(infile); + } + } + } + + public void savePolicyFile() { + mHandler.removeMessages(MESSAGE_SAVE_POLICY_FILE); + mHandler.sendEmptyMessage(MESSAGE_SAVE_POLICY_FILE); + } + + private void handleSavePolicyFile() { + Slog.d(TAG, "handleSavePolicyFile"); + synchronized (mPolicyFile) { + final FileOutputStream stream; + try { + stream = mPolicyFile.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Failed to save policy file", e); + return; + } + + try { + final XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, "utf-8"); + out.startDocument(null, true); + out.startTag(null, TAG_BODY); + out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION)); + mZenModeHelper.writeXml(out); + out.endTag(null, TAG_BODY); + out.endDocument(); + mPolicyFile.finishWrite(stream); + } catch (IOException e) { + Slog.w(TAG, "Failed to save policy file, restoring backup", e); + mPolicyFile.failWrite(stream); } } } @@ -1066,10 +1092,7 @@ public class NotificationManagerService extends SystemService { @Override public boolean allowDisable(int what, IBinder token, String pkg) { - if (isCall(pkg, null)) { - return mZenMode == Settings.Global.ZEN_MODE_OFF; - } - return true; + return mZenModeHelper.allowDisable(what, token, pkg); } @Override @@ -1194,9 +1217,6 @@ public class NotificationManagerService extends SystemService { private final Uri ENABLED_NOTIFICATION_LISTENERS_URI = Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); - private final Uri ZEN_MODE - = Settings.Global.getUriFor(Settings.Global.ZEN_MODE); - SettingsObserver(Handler handler) { super(handler); } @@ -1207,8 +1227,6 @@ public class NotificationManagerService extends SystemService { false, this, UserHandle.USER_ALL); resolver.registerContentObserver(ENABLED_NOTIFICATION_LISTENERS_URI, false, this, UserHandle.USER_ALL); - resolver.registerContentObserver(ZEN_MODE, - false, this); update(null); } @@ -1229,13 +1247,11 @@ public class NotificationManagerService extends SystemService { if (uri == null || ENABLED_NOTIFICATION_LISTENERS_URI.equals(uri)) { rebindListenerServices(); } - if (ZEN_MODE.equals(uri)) { - updateZenMode(); - } } } private SettingsObserver mSettingsObserver; + private ZenModeHelper mZenModeHelper; static long[] getLongArray(Resources r, int resid, int maxlen, long[] def) { int[] ar = r.getIntArray(resid); @@ -1261,6 +1277,15 @@ public class NotificationManagerService extends SystemService { mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); mHandler = new WorkerHandler(); + mZenModeHelper = new ZenModeHelper(getContext(), mHandler); + mZenModeHelper.setCallback(new ZenModeHelper.Callback() { + @Override + public void onConfigChanged() { + savePolicyFile(); + } + }); + final File systemDir = new File(Environment.getDataDirectory(), "system"); + mPolicyFile = new AtomicFile(new File(systemDir, "notification_policy.xml")); importOldBlockDb(); @@ -1297,7 +1322,7 @@ public class NotificationManagerService extends SystemService { Settings.Global.DEVICE_PROVISIONED, 0)) { mDisableNotificationAlerts = true; } - updateZenMode(); + mZenModeHelper.updateZenMode(); updateCurrentProfilesCache(getContext()); @@ -1350,7 +1375,7 @@ public class NotificationManagerService extends SystemService { * Read the old XML-based app block database and import those blockages into the AppOps system. */ private void importOldBlockDb() { - loadBlockDb(); + loadPolicyFile(); PackageManager pm = getContext().getPackageManager(); for (String pkg : mBlockedPackages) { @@ -1363,9 +1388,6 @@ public class NotificationManagerService extends SystemService { } } mBlockedPackages.clear(); - if (mPolicyFile != null) { - mPolicyFile.delete(); - } } @Override @@ -1745,6 +1767,18 @@ public class NotificationManagerService extends SystemService { } @Override + public ZenModeConfig getZenModeConfig() { + checkCallerIsSystem(); + return mZenModeHelper.getConfig(); + } + + @Override + public boolean setZenModeConfig(ZenModeConfig config) { + checkCallerIsSystem(); + return mZenModeHelper.setConfig(config); + } + + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { @@ -1825,7 +1859,6 @@ public class NotificationManagerService extends SystemService { pw.println(" mSoundNotification=" + mSoundNotification); pw.println(" mVibrateNotification=" + mVibrateNotification); pw.println(" mDisableNotificationAlerts=" + mDisableNotificationAlerts); - pw.println(" mZenMode=" + Settings.Global.zenModeToString(mZenMode)); pw.println(" mSystemReady=" + mSystemReady); pw.println(" mArchive=" + mArchive.toString()); Iterator iter = mArchive.descendingIterator(); @@ -1841,6 +1874,8 @@ public class NotificationManagerService extends SystemService { pw.println("\n Usage Stats:"); mUsageStats.dump(pw, " "); + pw.println("\n Zen Mode:"); + mZenModeHelper.dump(pw, " "); } } @@ -1973,7 +2008,7 @@ public class NotificationManagerService extends SystemService { } // Is this notification intercepted by zen mode? - final boolean intercept = shouldIntercept(pkg, notification); + final boolean intercept = mZenModeHelper.shouldIntercept(pkg, notification); notification.extras.putBoolean(EXTRA_INTERCEPT, intercept); // Should this notification make noise, vibe, or use the LED? @@ -2358,6 +2393,9 @@ public class NotificationManagerService extends SystemService { case MESSAGE_TIMEOUT: handleTimeout((ToastRecord)msg.obj); break; + case MESSAGE_SAVE_POLICY_FILE: + handleSavePolicyFile(); + break; } } } @@ -2722,42 +2760,6 @@ public class NotificationManagerService extends SystemService { } } - private void updateZenMode() { - final int mode = Settings.Global.getInt(getContext().getContentResolver(), - Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF); - if (mode != mZenMode) { - Slog.d(TAG, String.format("updateZenMode: %s -> %s", - Settings.Global.zenModeToString(mZenMode), - Settings.Global.zenModeToString(mode))); - } - mZenMode = mode; - - final String[] exceptionPackages = null; // none (for now) - - // call restrictions - final boolean muteCalls = mZenMode != Settings.Global.ZEN_MODE_OFF; - mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING, - muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, - exceptionPackages); - mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING, - muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, - exceptionPackages); - - // alarm restrictions - final boolean muteAlarms = false; // TODO until we save user config - mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_ALARM, - muteAlarms ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, - exceptionPackages); - mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_ALARM, - muteAlarms ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, - exceptionPackages); - - // restrict vibrations with no hints - mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE, - (muteAlarms || muteCalls) ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, - exceptionPackages); - } - private void updateCurrentProfilesCache(Context context) { UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); if (userManager != null) { @@ -2788,19 +2790,4 @@ public class NotificationManagerService extends SystemService { return mCurrentProfiles.get(userId) != null; } } - - private boolean isCall(String pkg, Notification n) { - return CALL_PACKAGES.contains(pkg); - } - - private boolean isAlarm(String pkg, Notification n) { - return ALARM_PACKAGES.contains(pkg); - } - - private boolean shouldIntercept(String pkg, Notification n) { - if (mZenMode != Settings.Global.ZEN_MODE_OFF) { - return !isAlarm(pkg, n); - } - return false; - } } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java new file mode 100644 index 000000000000..80f5b5c1d541 --- /dev/null +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -0,0 +1,312 @@ +/** + * Copyright (c) 2014, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import android.app.AlarmManager; +import android.app.AppOpsManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.provider.Settings.Global; +import android.service.notification.ZenModeConfig; +import android.util.Slog; + +import com.android.internal.R; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +/** + * NotificationManagerService helper for functionality related to zen mode. + */ +public class ZenModeHelper { + private static final String TAG = "ZenModeHelper"; + + private static final String ACTION_ENTER_ZEN = "enter_zen"; + private static final int REQUEST_CODE_ENTER = 100; + private static final String ACTION_EXIT_ZEN = "exit_zen"; + private static final int REQUEST_CODE_EXIT = 101; + private static final String EXTRA_TIME = "time"; + + private final Context mContext; + private final Handler mHandler; + private final SettingsObserver mSettingsObserver; + private final AppOpsManager mAppOps; + private final ZenModeConfig mDefaultConfig; + + private Callback mCallback; + private int mZenMode; + private ZenModeConfig mConfig; + + // temporary, until we update apps to provide metadata + private static final Set CALL_PACKAGES = new HashSet(Arrays.asList( + "com.google.android.dialer", + "com.android.phone" + )); + private static final Set MESSAGE_PACKAGES = new HashSet(Arrays.asList( + "com.google.android.talk", + "com.android.mms" + )); + + public ZenModeHelper(Context context, Handler handler) { + mContext = context; + mHandler = handler; + mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mDefaultConfig = readDefaultConfig(context.getResources()); + mConfig = mDefaultConfig; + mSettingsObserver = new SettingsObserver(mHandler); + mSettingsObserver.observe(); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_ENTER_ZEN); + filter.addAction(ACTION_EXIT_ZEN); + mContext.registerReceiver(new ZenBroadcastReceiver(), filter); + } + + public static ZenModeConfig readDefaultConfig(Resources resources) { + XmlResourceParser parser = null; + try { + parser = resources.getXml(R.xml.default_zen_mode_config); + while (parser.next() != XmlPullParser.END_DOCUMENT) { + final ZenModeConfig config = ZenModeConfig.readXml(parser); + if (config != null) return config; + } + } catch (Exception e) { + Slog.w(TAG, "Error reading default zen mode config from resource", e); + } finally { + IoUtils.closeQuietly(parser); + } + return new ZenModeConfig(); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public boolean shouldIntercept(String pkg, Notification n) { + if (mZenMode != Global.ZEN_MODE_OFF) { + if (isCall(pkg, n)) { + return !mConfig.allowCalls; + } + if (isMessage(pkg, n)) { + return !mConfig.allowMessages; + } + return true; + } + return false; + } + + public void updateZenMode() { + final int mode = Global.getInt(mContext.getContentResolver(), + Global.ZEN_MODE, Global.ZEN_MODE_OFF); + if (mode != mZenMode) { + Slog.d(TAG, String.format("updateZenMode: %s -> %s", + Global.zenModeToString(mZenMode), + Global.zenModeToString(mode))); + } + mZenMode = mode; + final boolean zen = mZenMode != Global.ZEN_MODE_OFF; + final String[] exceptionPackages = null; // none (for now) + + // call restrictions + final boolean muteCalls = zen && !mConfig.allowCalls; + mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.STREAM_RING, + muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, + exceptionPackages); + mAppOps.setRestriction(AppOpsManager.OP_PLAY_AUDIO, AudioManager.STREAM_RING, + muteCalls ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, + exceptionPackages); + + // restrict vibrations with no hints + mAppOps.setRestriction(AppOpsManager.OP_VIBRATE, AudioManager.USE_DEFAULT_STREAM_TYPE, + zen ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED, + exceptionPackages); + } + + public boolean allowDisable(int what, IBinder token, String pkg) { + if (isCall(pkg, null)) { + return mZenMode == Global.ZEN_MODE_OFF || mConfig.allowCalls; + } + return true; + } + + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("mZenMode="); + pw.println(Global.zenModeToString(mZenMode)); + pw.print(prefix); pw.print("mConfig="); pw.println(mConfig); + pw.print(prefix); pw.print("mDefaultConfig="); pw.println(mDefaultConfig); + } + + public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException { + final ZenModeConfig config = ZenModeConfig.readXml(parser); + if (config != null) { + setConfig(config); + } + } + + public void writeXml(XmlSerializer out) throws IOException { + mConfig.writeXml(out); + } + + public ZenModeConfig getConfig() { + return mConfig; + } + + public boolean setConfig(ZenModeConfig config) { + if (config == null || !config.isValid()) return false; + if (config.equals(mConfig)) return true; + mConfig = config; + Slog.d(TAG, "mConfig=" + mConfig); + if (mCallback != null) mCallback.onConfigChanged(); + final String val = Integer.toString(mConfig.hashCode()); + Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); + updateAlarms(); + updateZenMode(); + return true; + } + + private boolean isCall(String pkg, Notification n) { + return CALL_PACKAGES.contains(pkg); + } + + private boolean isMessage(String pkg, Notification n) { + return MESSAGE_PACKAGES.contains(pkg); + } + + private void updateAlarms() { + updateAlarm(ACTION_ENTER_ZEN, REQUEST_CODE_ENTER, + mConfig.sleepStartHour, mConfig.sleepStartMinute); + updateAlarm(ACTION_EXIT_ZEN, REQUEST_CODE_EXIT, + mConfig.sleepEndHour, mConfig.sleepEndMinute); + } + + private void updateAlarm(String action, int requestCode, int hr, int min) { + final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + final long now = System.currentTimeMillis(); + final Calendar c = Calendar.getInstance(); + c.setTimeInMillis(now); + c.set(Calendar.HOUR_OF_DAY, hr); + c.set(Calendar.MINUTE, min); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + if (c.getTimeInMillis() <= now) { + c.add(Calendar.DATE, 1); + } + final long time = c.getTimeInMillis(); + final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, requestCode, + new Intent(action).putExtra(EXTRA_TIME, time), PendingIntent.FLAG_UPDATE_CURRENT); + alarms.cancel(pendingIntent); + if (mConfig.sleepMode != null) { + Slog.d(TAG, String.format("Scheduling %s for %s, %s in the future, now=%s", + action, ts(time), time - now, ts(now))); + alarms.setExact(AlarmManager.RTC_WAKEUP, time, pendingIntent); + } + } + + private static String ts(long time) { + return new Date(time) + " (" + time + ")"; + } + + public static boolean isWeekend(long time, int offsetDays) { + final Calendar c = Calendar.getInstance(); + c.setTimeInMillis(time); + if (offsetDays != 0) { + c.add(Calendar.DATE, offsetDays); + } + final int day = c.get(Calendar.DAY_OF_WEEK); + return day == Calendar.SATURDAY || day == Calendar.SUNDAY; + } + + private class SettingsObserver extends ContentObserver { + private final Uri ZEN_MODE = Global.getUriFor(Global.ZEN_MODE); + + public SettingsObserver(Handler handler) { + super(handler); + } + + public void observe() { + final ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(ZEN_MODE, false /*notifyForDescendents*/, this); + update(null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(uri); + } + + public void update(Uri uri) { + if (ZEN_MODE.equals(uri)) { + updateZenMode(); + } + } + } + + private class ZenBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_ENTER_ZEN.equals(intent.getAction())) { + setZenMode(intent, 1, Global.ZEN_MODE_ON); + } else if (ACTION_EXIT_ZEN.equals(intent.getAction())) { + setZenMode(intent, 0, Global.ZEN_MODE_OFF); + } + } + + private void setZenMode(Intent intent, int wkendOffsetDays, int zenModeValue) { + final long schTime = intent.getLongExtra(EXTRA_TIME, 0); + final long now = System.currentTimeMillis(); + Slog.d(TAG, String.format("%s scheduled for %s, fired at %s, delta=%s", + intent.getAction(), ts(schTime), ts(now), now - schTime)); + + final boolean skip = ZenModeConfig.SLEEP_MODE_WEEKNIGHTS.equals(mConfig.sleepMode) && + isWeekend(schTime, wkendOffsetDays); + + if (skip) { + Slog.d(TAG, "Skipping zen mode update for the weekend"); + } else { + Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zenModeValue); + } + updateAlarms(); + } + } + + public interface Callback { + void onConfigChanged(); + } +} -- 2.11.0