OSDN Git Service

Do not disturb: persist user config.
authorJohn Spurlock <jspurlock@google.com>
Mon, 21 Apr 2014 01:52:01 +0000 (21:52 -0400)
committerJohn Spurlock <jspurlock@google.com>
Tue, 22 Apr 2014 15:01:24 +0000 (11:01 -0400)
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
core/java/android/provider/Settings.java
core/java/android/service/notification/ZenModeConfig.aidl [new file with mode: 0644]
core/java/android/service/notification/ZenModeConfig.java [new file with mode: 0644]
core/res/res/values/symbols.xml
core/res/res/xml/default_zen_mode_config.xml [new file with mode: 0644]
packages/SystemUI/res/values/strings.xml
packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java
packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java
services/core/java/com/android/server/notification/NotificationManagerService.java
services/core/java/com/android/server/notification/ZenModeHelper.java [new file with mode: 0644]

index bb6eeda..8681f5c 100644 (file)
@@ -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
index 1e202ca..2ce6210 100644 (file)
@@ -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 (file)
index 0000000..c73b75e
--- /dev/null
@@ -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 (file)
index 0000000..925ddcf
--- /dev/null
@@ -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<ZenModeConfig> CREATOR
+            = new Parcelable.Creator<ZenModeConfig>() {
+        @Override
+        public ZenModeConfig createFromParcel(Parcel source) {
+            return new ZenModeConfig(source);
+        }
+
+        @Override
+        public ZenModeConfig[] newArray(int size) {
+            return new ZenModeConfig[size];
+        }
+    };
+}
index 26efe36..f916128 100644 (file)
   <java-symbol type="xml" name="sms_short_codes" />
   <java-symbol type="xml" name="audio_assets" />
   <java-symbol type="xml" name="global_keys" />
+  <java-symbol type="xml" name="default_zen_mode_config" />
 
   <java-symbol type="raw" name="accessibility_gestures" />
   <java-symbol type="raw" name="incognito_mode_start_page" />
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 (file)
index 0000000..1bdc1ec
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<!-- Default configuration for zen mode.  See android.service.notification.ZenModeConfig. -->
+<zen version="1">
+    <allow calls="false" messages="false" />
+    <sleep startHour="22" startMin="0" endHour="7" endMin="0" />
+</zen>
index 72d14fe..f3c956c 100644 (file)
     <!-- Zen mode: Summary notification content text. [CHAR LIMIT=NONE] -->
     <string name="zen_mode_notification_text">Touch to show</string>
 
+    <!-- Zen mode: Short title. [CHAR LIMIT=40] -->
+    <string name="zen_mode_title">Do not disturb</string>
+
     <!-- Text for overflow card on Keyguard when there is not enough space for all notifications on Keyguard. [CHAR LIMIT=12] -->
     <plurals name="keyguard_more_overflow_text">
         <item quantity="other">%d more</item>
index 9b25046..e1ef83a 100644 (file)
@@ -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);
     }
index 49cf78b..c1662ba 100644 (file)
@@ -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);
index 5b597a3..c8bdb4c 100644 (file)
@@ -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<String> CALL_PACKAGES = new HashSet<String>(Arrays.asList(
-            "com.google.android.dialer",
-            "com.android.phone"
-            ));
-    private static final Set<String> ALARM_PACKAGES = new HashSet<String>(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<StatusBarNotification> 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 (file)
index 0000000..80f5b5c
--- /dev/null
@@ -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<String> CALL_PACKAGES = new HashSet<String>(Arrays.asList(
+            "com.google.android.dialer",
+            "com.android.phone"
+            ));
+    private static final Set<String> MESSAGE_PACKAGES = new HashSet<String>(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();
+    }
+}