Merge "Disable trust agent settings when disabled by policy"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 564bf1f..4269f5a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -158,6 +158,17 @@
                 android:value="true" />
         </activity>
 
+        <activity android:name="AirplaneModeVoiceActivity"
+                android:label="@string/wireless_networks_settings_title"
+                android:theme="@android:style/Theme.Material.Light.Voice"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="android.settings.VOICE_CONTROL_AIRPLANE_MODE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.VOICE" />
+            </intent-filter>
+        </activity>
+
         <!-- Top-level settings -->
 
         <activity android:name="Settings$WifiSettingsActivity"
@@ -696,6 +707,10 @@
                 <action android:name="android.settings.ZEN_MODE_AUTOMATION_SETTINGS" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.ACTION_CONDITION_PROVIDER_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -729,6 +744,26 @@
                 android:value="true" />
         </activity>
 
+        <activity android:name="Settings$ZenModeExternalRuleSettingsActivity"
+                android:exported="true"
+                android:taskAffinity="">
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.ZEN_MODE_EXTERNAL_RULE_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="com.android.settings.SHORTCUT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.notification.ZenModeExternalRuleSettings" />
+            <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID"
+                android:resource="@id/notification_settings" />
+            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
+                android:value="true" />
+        </activity>
+
         <activity android:name="Settings$HomeSettingsActivity"
                 android:label="@string/home_settings"
                 android:taskAffinity="">
@@ -972,6 +1007,16 @@
                 android:value="com.android.settings.notification.NotificationStation" />
         </activity>
 
+        <activity android:name=".notification.ZenModeVoiceActivity"
+                android:theme="@android:style/Theme.Material.Light"
+                android:label="@string/zen_mode_settings_title">
+            <intent-filter>
+                <action android:name="android.settings.VOICE_CONTROL_DO_NOT_DISTURB_MODE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.VOICE" />
+            </intent-filter>
+        </activity>
+
         <!--
         <activity android:name="Settings$AppOpsSummaryActivity"
                 android:label="@string/app_ops_settings"
@@ -1850,6 +1895,17 @@
                 android:value="true" />
         </activity>
 
+        <activity android:name=".fuelguage.BatterySaverModeVoiceActivity"
+                android:label="@string/power_usage_summary_title"
+                android:theme="@android:style/Theme.Material.Light.Voice"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="android.settings.VOICE_CONTROL_BATTERY_SAVER_MODE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.VOICE" />
+            </intent-filter>
+        </activity>
+
         <activity android:name="Settings$AccountSettingsActivity"
             android:label="@string/account_settings_title"
             android:taskAffinity=""
diff --git a/res/drawable/bg_circle_blue.xml b/res/drawable/bg_circle_blue.xml
new file mode 100644
index 0000000..7f2cb1d
--- /dev/null
+++ b/res/drawable/bg_circle_blue.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2015 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="@color/blue" />
+</shape>
diff --git a/res/layout/voice_interaction.xml b/res/layout/voice_interaction.xml
new file mode 100644
index 0000000..13c4341
--- /dev/null
+++ b/res/layout/voice_interaction.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/fragment_root"
+    android:paddingLeft="8dp"
+    android:paddingRight="8dp"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+</FrameLayout>
diff --git a/res/layout/voice_item_row.xml b/res/layout/voice_item_row.xml
new file mode 100644
index 0000000..8576a57
--- /dev/null
+++ b/res/layout/voice_item_row.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+    <LinearLayout
+        android:id="@+id/row_one"
+        android:orientation="horizontal"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentStart="true"
+        android:layout_marginTop="20dp"
+        android:layout_marginBottom="20dp">
+
+        <TextView
+            android:layout_width="0px"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:id="@+id/voice_item_label" />
+
+        <TextView
+            android:layout_width="100px"
+            android:layout_height="100px"
+            android:gravity="center_horizontal|center_vertical"
+            android:background="@drawable/bg_circle_blue"
+            android:textAppearance="?android:attr/textAppearanceMediumInverse"
+            android:textStyle="bold"
+            android:id="@+id/voice_item_position" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/zen_rule_name.xml b/res/layout/zen_rule_name.xml
index 62f51ea..a192c83 100755
--- a/res/layout/zen_rule_name.xml
+++ b/res/layout/zen_rule_name.xml
@@ -13,7 +13,8 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content" >
 
@@ -29,4 +30,34 @@
 
     </EditText>
 
-</FrameLayout>
+    <RadioGroup
+        android:id="@+id/rule_types"
+        android:visibility="gone"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="22dp"
+        android:layout_marginRight="22dp"
+        android:layout_marginTop="16dp"
+        android:orientation="vertical"
+        android:checkedButton="@+id/rule_type_schedule" >
+
+        <RadioButton android:id="@+id/rule_type_schedule"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/zen_schedule_rule_type_name" />
+
+        <RadioButton android:id="@+id/rule_type_2"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <RadioButton android:id="@+id/rule_type_3"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <RadioButton android:id="@+id/rule_type_4"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+    </RadioGroup>
+
+</LinearLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 2d0c741..0d0637c 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,6 +17,7 @@
 <resources>
     <color name="black">#000</color>
     <color name="red">#F00</color>
+    <color name="blue">#00F</color>
 
     <color name="material_empty_color_light">#FFCED7DB</color>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d8e346c..7c13e8f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -5283,7 +5283,7 @@
     <!-- Title for add user confirmation dialog [CHAR LIMIT=30] -->
     <string name="user_add_user_title">Add new user?</string>
     <!-- Message for add user confirmation dialog - long version. [CHAR LIMIT=none] -->
-    <string name="user_add_user_message_long">You can share this device with other people by creating additional users. Each user has their own space, which they can customize with their own apps, wallpaper, and so on. Users can also adjust device settings like Wi\u2011Fi that affect everyone.\n\nWhen you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users.</string>
+    <string name="user_add_user_message_long">You can share this device with other people by creating additional users. Each user has their own space, which they can customize with apps, wallpaper, and so on. Users can also adjust device settings like Wi\u2011Fi that affect everyone.\n\nWhen you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users.</string>
     <!-- Message for add user confirmation dialog - short version. [CHAR LIMIT=none] -->
     <string name="user_add_user_message_short">When you add a new user, that person needs to set up their space.\n\nAny user can update apps for all other users. </string>
     <!-- Title of dialog to setup a new user [CHAR LIMIT=30] -->
@@ -5898,11 +5898,23 @@
     <!-- [CHAR LIMIT=40] Zen mode settings: Delete rule dialog button caption -->
     <string name="zen_mode_delete_rule_button">Delete</string>
 
+    <!-- [CHAR LIMIT=40] Zen mode settings: External rule type -->
+    <string name="zen_mode_rule_type">Rule type</string>
+
+    <!-- [CHAR LIMIT=40] Zen mode settings: External rule type name if unknown -->
+    <string name="zen_mode_rule_type_unknown">Unknown</string>
+
+    <!-- [CHAR LIMIT=40] Zen mode settings: Configure external rule -->
+    <string name="zen_mode_configure_rule">Configure rule</string>
+
+    <!-- [CHAR LIMIT=40] Zen mode settings: Schedule rule type name -->
+    <string name="zen_schedule_rule_type_name">Schedule rule</string>
+
     <!-- [CHAR LIMIT=40] Zen mode settings: Text to display if rule isn't found  -->
     <string name="zen_mode_rule_not_found_text">Rule not found.</string>
 
     <!-- [CHAR LIMIT=40] Zen mode settings: Rule summary template (when enabled)  -->
-    <string name="zen_mode_rule_summary_template"><xliff:g id="days" example="Sun - Thu">%1$s</xliff:g> / <xliff:g id="timerange" example="10:00 PM to 7:30 AM">%2$s</xliff:g> / <xliff:g id="mode" example="Alarms only">%3$s</xliff:g></string>
+    <string name="zen_mode_rule_summary_combination"><xliff:g id="description" example="Sun - Thu">%1$s</xliff:g> / <xliff:g id="mode" example="Alarms only">%2$s</xliff:g></string>
 
     <!-- [CHAR LIMIT=40] Zen mode settings: Timebased rule days option title -->
     <string name="zen_mode_schedule_rule_days">Days</string>
@@ -5979,6 +5991,93 @@
     <!-- [CHAR LIMIT=60] Zen mode settings: End time option: Summary text value format when end time = next day -->
     <string name="zen_mode_end_time_next_day_summary_format"><xliff:g id="formatted_time">%s</xliff:g> next day</string>
 
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Prompt read for interruption type -->
+    <string name="zen_mode_interruptions_voice_prompt">When would you like to be interrupted?</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Prompt read for zen mode duration -->
+    <string name="zen_mode_duration_voice_prompt">For how long?</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Comma delimited synonyms for important interriuptions -->
+    <string name="zen_mode_option_important_voice_synonyms">important,priority,priority notifications</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Comma delimited synonyms for alarm interriuptions -->
+    <string name="zen_mode_option_alarms_voice_synonyms">alarms</string>
+
+    <!--  [CHAR LIMIT=60] Zen mode voice: Off [CHAR LIMIT=60] -->
+    <string name="zen_mode_option_off">Off</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Comma delimited synonyms for off interriuptions -->
+    <string name="zen_mode_option_off_voice_synonyms">off,all,everything</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Comma delimited synonyms for no interriuptions -->
+    <string name="zen_mode_option_no_interruptions_voice_synonyms">none,nothing,no interruptions</string>
+
+    <!-- [CHAR LIMIT=40] Zen mode voice: Label for indefinite mode duration -->
+    <string name="zen_mode_duration_indefinte_voice_label">Indefinitely</string>
+
+    <!-- [CHAR LIMIT=40] Zen mode voice: Label for duration in minutes -->
+    <plurals name="zen_mode_duration_minutes_voice_label">
+       <item quantity="one"><xliff:g id="count" example="1">%d</xliff:g> minute</item>
+       <item quantity="other"><xliff:g id="count" example="10">%d</xliff:g> minutes</item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=40] Zen mode voice: Label for duration in hours -->
+    <plurals name="zen_mode_duration_hours_voice_label">
+       <item quantity="one"><xliff:g id="count" example="1">%d</xliff:g> hour</item>
+       <item quantity="other"><xliff:g id="count" example="10">%d</xliff:g> hours</item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice - spoken summary: important only duration indefinite. -->
+    <string name="zen_mode_summary_priority_indefinitely">Change to priority notifications only indefinitely</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice- spoken summary: important only duration minutes. -->
+    <plurals name="zen_mode_summary_priority_by_minute">
+        <item quantity="one">Change to priority notifications only for one minute until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+        <item quantity="other">Change to priority notifications only for <xliff:g id="duration" example="2">%1$d</xliff:g> minutes until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice- spoken summary: important only duration hours. -->
+    <plurals name="zen_mode_summary_priority_by_hour">
+        <item quantity="one">Change to priority notifications only for one hour until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+        <item quantity="other">Change to priority notifications only for <xliff:g id="duration" example="2">%1$d</xliff:g> hours (until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g>)</item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice - spoken summary: alarms only duration indefinite. -->
+    <string name="zen_mode_summary_alarams_only_indefinite">Change to alarms only indefinitely</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice- spoken summary: alarms only duration minutes. -->
+    <plurals name="zen_mode_summary_alarms_only_by_minute">
+        <item quantity="one">Change to alarms only for one minute until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+        <item quantity="other">Change to alarms only for <xliff:g id="duration" example="2">%1$d</xliff:g> minutes (until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g>)</item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice- spoken summary: alarms only duration hours. -->
+    <plurals name="zen_mode_summary_alarms_only_by_hour">
+        <item quantity="one">Change to alarms only for one hour until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+        <item quantity="other">Change to alarms only for <xliff:g id="duration" example="2">%1$d</xliff:g> hours until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice - spoken summary: no interruptions duration indefinite. -->
+    <string name="zen_mode_summary_no_interruptions_indefinite">Change to don\'t interrupt indefinitely</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice- spoken summary: alarms only duration minutes. -->
+    <plurals name="zen_mode_summary_no_interruptions_by_minute">
+        <item quantity="one">Change to don\'t interrupt for one minute until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+        <item quantity="other">Change to don\'t interrupt for <xliff:g id="duration" example="2">%1$d</xliff:g> minutes (until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g>)</item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice- spoken summary: alarms only duration hours. -->
+    <plurals name="zen_mode_summary_no_interruptions_by_hour">
+        <item quantity="one">Change to don\'t interrupt for one hour until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+        <item quantity="other">Change to don\'t interrupt for <xliff:g id="duration" example="2">%1$d</xliff:g> hours until <xliff:g id="formattedTime" example="10:00 PM">%2$s</xliff:g></item>
+    </plurals>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice - spoken summary: off. -->
+    <string name="zen_mode_summary_always">Change to always interrupt</string>
+
+    <!-- [CHAR LIMIT=NONE] Zen mode voice: Comma delimited synonyms for indefinte duration -->
+    <string name="zen_mode_duration_indefinite_voice_synonyms">forever</string>
+
     <!-- [CHAR LIMIT=20] Notifications settings: Apps section header -->
     <string name="notification_settings_apps_title">App notifications</string>
 
diff --git a/res/xml/zen_mode_external_rule_settings.xml b/res/xml/zen_mode_external_rule_settings.xml
new file mode 100644
index 0000000..e244dc8
--- /dev/null
+++ b/res/xml/zen_mode_external_rule_settings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2015 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="zen_mode_external_rule_settings" >
+
+    <!-- Rule name -->
+    <Preference
+        android:key="rule_name"
+        android:title="@string/zen_mode_rule_name"
+        android:persistent="false" />
+
+    <!-- Rule type -->
+    <Preference
+        android:key="type"
+        android:title="@string/zen_mode_rule_type"
+        android:persistent="false" />
+
+    <!-- Configure -->
+    <Preference
+        android:key="configure"
+        android:title="@string/zen_mode_configure_rule"
+        android:persistent="false" />
+
+    <!-- Zen mode -->
+    <com.android.settings.DropDownPreference
+        android:key="zen_mode"
+        android:title="@string/zen_mode_settings_title"
+        android:persistent="false" />
+
+</PreferenceScreen>
diff --git a/src/com/android/settings/AirplaneModeVoiceActivity.java b/src/com/android/settings/AirplaneModeVoiceActivity.java
index 3ab0c37..e0649e4 100644
--- a/src/com/android/settings/AirplaneModeVoiceActivity.java
+++ b/src/com/android/settings/AirplaneModeVoiceActivity.java
@@ -20,6 +20,8 @@
 import android.provider.Settings;
 import android.util.Log;
 
+import com.android.settings.utils.VoiceSettingsActivity;
+
 /**
  * Activity for modifying the {@link Settings.Global#AIRPLANE_MODE_ON AIRPLANE_MODE_ON}
  * setting using the Voice Interaction API.
@@ -27,14 +29,14 @@
 public class AirplaneModeVoiceActivity extends VoiceSettingsActivity {
     private static final String TAG = "AirplaneModeVoiceActivity";
 
-    protected void onVoiceSettingInteraction(Intent intent) {
+    protected boolean onVoiceSettingInteraction(Intent intent) {
         if (intent.hasExtra(Settings.EXTRA_AIRPLANE_MODE_ENABLED)) {
-            boolean enabled =
-                    intent.getBooleanExtra(Settings.EXTRA_AIRPLANE_MODE_ENABLED, false);
             Settings.Global.putInt(getContentResolver(),
-                    Settings.Global.AIRPLANE_MODE_ON, enabled ? 1 : 0);
+                    Settings.Global.AIRPLANE_MODE_ON,
+                    intent.getBooleanExtra(Settings.EXTRA_AIRPLANE_MODE_ENABLED, false) ? 1 : 0);
         } else {
             Log.v(TAG, "Missing airplane mode extra");
         }
+        return true;
     }
 }
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index fe0df59..ba08036 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -98,6 +98,7 @@
     public static class ZenModePrioritySettingsActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeAutomationSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeScheduleRuleSettingsActivity extends SettingsActivity { /* empty */ }
+    public static class ZenModeExternalRuleSettingsActivity extends SettingsActivity { /* empty */ }
     public static class NotificationSettingsActivity extends SettingsActivity { /* empty */ }
     public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
     public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index 7bfd249..0fdf04b 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -95,11 +95,11 @@
 import com.android.settings.nfc.AndroidBeam;
 import com.android.settings.nfc.PaymentSettings;
 import com.android.settings.notification.AppNotificationSettings;
-import com.android.settings.notification.ConditionProviderSettings;
 import com.android.settings.notification.NotificationAccessSettings;
 import com.android.settings.notification.NotificationSettings;
 import com.android.settings.notification.NotificationStation;
 import com.android.settings.notification.OtherSoundSettings;
+import com.android.settings.notification.ZenModeExternalRuleSettings;
 import com.android.settings.notification.ZenModeSettings;
 import com.android.settings.notification.ZenModeScheduleRuleSettings;
 import com.android.settings.print.PrintJobSettingsFragment;
@@ -320,7 +320,6 @@
             DreamSettings.class.getName(),
             UserSettings.class.getName(),
             NotificationAccessSettings.class.getName(),
-            ConditionProviderSettings.class.getName(),
             PrintSettingsFragment.class.getName(),
             PrintJobSettingsFragment.class.getName(),
             TrustedCredentialsSettings.class.getName(),
@@ -337,6 +336,7 @@
             ApnSettings.class.getName(),
             WifiCallingSettings.class.getName(),
             ZenModeScheduleRuleSettings.class.getName(),
+            ZenModeExternalRuleSettings.class.getName(),
     };
 
 
diff --git a/src/com/android/settings/VoiceSettingsActivity.java b/src/com/android/settings/VoiceSettingsActivity.java
deleted file mode 100644
index b5e8ede..0000000
--- a/src/com/android/settings/VoiceSettingsActivity.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.settings;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * Activity for modifying a setting using the Voice Interaction API. This activity
- * MUST only modify the setting if the intent was sent using
- * {@link android.service.voice.VoiceInteractionSession#startVoiceActivity startVoiceActivity}.
- */
-abstract public class VoiceSettingsActivity extends Activity {
-
-    private static final String TAG = "VoiceSettingsActivity";
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        if (isVoiceInteraction()) {
-            // Only permit if this is a voice interaction.
-            onVoiceSettingInteraction(getIntent());
-        } else {
-            Log.v(TAG, "Cannot modify settings without voice interaction");
-        }
-        finish();
-    }
-
-    /**
-     * Modify the setting as a voice interaction. The activity will finish
-     * after this method is called.
-     */
-    abstract protected void onVoiceSettingInteraction(Intent intent);
-}
diff --git a/src/com/android/settings/fuelgauge/BatterySaverModeVoiceActivity.java b/src/com/android/settings/fuelgauge/BatterySaverModeVoiceActivity.java
new file mode 100644
index 0000000..4494887
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/BatterySaverModeVoiceActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.settings.fuelguage;
+
+import static android.provider.Settings.EXTRA_BATTERY_SAVER_MODE_ENABLED;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settings.utils.VoiceSettingsActivity;
+
+/**
+ * Activity for modifying the {@link android.os.PowerManager} power save mode
+ * setting using the Voice Interaction API.
+ */
+public class BatterySaverModeVoiceActivity extends VoiceSettingsActivity {
+    private static final String TAG = "BatterySaverModeVoiceActivity";
+
+    protected boolean onVoiceSettingInteraction(Intent intent) {
+        if (intent.hasExtra(EXTRA_BATTERY_SAVER_MODE_ENABLED)) {
+            PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+            if (powerManager.setPowerSaveMode(
+                    intent.getBooleanExtra(EXTRA_BATTERY_SAVER_MODE_ENABLED, false))) {
+                notifySuccess(null);
+            } else {
+                Log.v(TAG, "Unable to set power mode");
+                notifyFailure(null);
+            }
+        } else {
+            Log.v(TAG, "Missing battery saver mode extra");
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/settings/notification/ConditionProviderSettings.java b/src/com/android/settings/notification/ConditionProviderSettings.java
deleted file mode 100644
index 76576ab..0000000
--- a/src/com/android/settings/notification/ConditionProviderSettings.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.settings.notification;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.provider.Settings;
-import android.service.notification.ConditionProviderService;
-
-public class ConditionProviderSettings extends ManagedServiceSettings {
-    private static final String TAG = ConditionProviderSettings.class.getSimpleName();
-    private static final Config CONFIG = getConditionProviderConfig();
-
-    private static Config getConditionProviderConfig() {
-        final Config c = new Config();
-        c.tag = TAG;
-        c.setting = Settings.Secure.ENABLED_CONDITION_PROVIDERS;
-        c.intentAction = ConditionProviderService.SERVICE_INTERFACE;
-        c.permission = android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE;
-        c.noun = "condition provider";
-        return c;
-    }
-
-    @Override
-    protected Config getConfig() {
-        return CONFIG;
-    }
-
-    public static int getProviderCount(PackageManager pm) {
-        return getServicesCount(CONFIG, pm);
-    }
-
-    public static int getEnabledProviderCount(Context context) {
-        return getEnabledServicesCount(CONFIG, context);
-    }
-}
diff --git a/src/com/android/settings/notification/ManagedServiceSettings.java b/src/com/android/settings/notification/ManagedServiceSettings.java
index 7be644e..cc9e734 100644
--- a/src/com/android/settings/notification/ManagedServiceSettings.java
+++ b/src/com/android/settings/notification/ManagedServiceSettings.java
@@ -16,28 +16,17 @@
 
 package com.android.settings.notification;
 
-import android.app.ActivityManager;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
 import android.app.ListFragment;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
-import android.database.ContentObserver;
-import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import android.util.Slog;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -49,17 +38,15 @@
 
 import com.android.settings.R;
 
-import java.util.HashSet;
 import java.util.List;
 
 public abstract class ManagedServiceSettings extends ListFragment {
     private static final boolean SHOW_PACKAGE_NAME = false;
 
     private final Config mConfig;
-    private PackageManager mPM;
-    private ContentResolver mCR;
 
-    private final HashSet<ComponentName> mEnabledServices = new HashSet<ComponentName>();
+    private PackageManager mPM;
+    private ServiceListing mServiceListing;
     private ServiceListAdapter mListAdapter;
 
     abstract protected Config getConfig();
@@ -68,19 +55,65 @@
         mConfig = getConfig();
     }
 
-    private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            updateList();
-        }
-    };
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
 
-    private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            updateList();
+        mPM = getActivity().getPackageManager();
+        mServiceListing = new ServiceListing(getActivity(), mConfig);
+        mServiceListing.addCallback(new ServiceListing.Callback() {
+            @Override
+            public void onServicesReloaded(List<ServiceInfo> services) {
+                updateList(services);
+            }
+        });
+        mListAdapter = new ServiceListAdapter(getActivity());
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View v =  inflater.inflate(R.layout.managed_service_settings, container, false);
+        TextView empty = (TextView) v.findViewById(android.R.id.empty);
+        empty.setText(mConfig.emptyText);
+        return v;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mServiceListing.reload();
+        mServiceListing.setListening(true);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mServiceListing.setListening(false);
+    }
+
+    private void updateList(List<ServiceInfo> services) {
+        mListAdapter.clear();
+        mListAdapter.addAll(services);
+        mListAdapter.sort(new PackageItemInfo.DisplayNameComparator(mPM));
+
+        getListView().setAdapter(mListAdapter);
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        ServiceInfo info = mListAdapter.getItem(position);
+        final ComponentName cn = new ComponentName(info.packageName, info.name);
+        if (mServiceListing.isEnabled(cn)) {
+            // the simple version: disabling
+            mServiceListing.setEnabled(cn, false);
+        } else {
+            // show a scary dialog
+            new ScaryWarningDialogFragment()
+                .setServiceInfo(cn, info.loadLabel(mPM).toString())
+                .show(getFragmentManager(), "dialog");
         }
-    };
+    }
 
     public class ScaryWarningDialogFragment extends DialogFragment {
         static final String KEY_COMPONENT = "c";
@@ -99,7 +132,8 @@
             super.onCreate(savedInstanceState);
             final Bundle args = getArguments();
             final String label = args.getString(KEY_LABEL);
-            final ComponentName cn = ComponentName.unflattenFromString(args.getString(KEY_COMPONENT));
+            final ComponentName cn = ComponentName.unflattenFromString(args
+                    .getString(KEY_COMPONENT));
 
             final String title = getResources().getString(mConfig.warningDialogTitle, label);
             final String summary = getResources().getString(mConfig.warningDialogSummary, label);
@@ -110,8 +144,7 @@
                     .setPositiveButton(android.R.string.ok,
                             new DialogInterface.OnClickListener() {
                                 public void onClick(DialogInterface dialog, int id) {
-                                    mEnabledServices.add(cn);
-                                    saveEnabledServices();
+                                    mServiceListing.setEnabled(cn, true);
                                 }
                             })
                     .setNegativeButton(android.R.string.cancel,
@@ -124,151 +157,6 @@
         }
     }
 
-    @Override
-    public void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        mPM = getActivity().getPackageManager();
-        mCR = getActivity().getContentResolver();
-        mListAdapter = new ServiceListAdapter(getActivity());
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        View v =  inflater.inflate(R.layout.managed_service_settings, container, false);
-        TextView empty = (TextView) v.findViewById(android.R.id.empty);
-        empty.setText(mConfig.emptyText);
-        return v;
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        updateList();
-
-        // listen for package changes
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
-        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
-        filter.addDataScheme("package");
-        getActivity().registerReceiver(mPackageReceiver, filter);
-
-        mCR.registerContentObserver(Settings.Secure.getUriFor(mConfig.setting),
-                false, mSettingsObserver);
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-
-        getActivity().unregisterReceiver(mPackageReceiver);
-        mCR.unregisterContentObserver(mSettingsObserver);
-    }
-
-    private void loadEnabledServices() {
-        mEnabledServices.clear();
-        final String flat = Settings.Secure.getString(mCR, mConfig.setting);
-        if (flat != null && !"".equals(flat)) {
-            final String[] names = flat.split(":");
-            for (int i = 0; i < names.length; i++) {
-                final ComponentName cn = ComponentName.unflattenFromString(names[i]);
-                if (cn != null) {
-                    mEnabledServices.add(cn);
-                }
-            }
-        }
-    }
-
-    private void saveEnabledServices() {
-        StringBuilder sb = null;
-        for (ComponentName cn : mEnabledServices) {
-            if (sb == null) {
-                sb = new StringBuilder();
-            } else {
-                sb.append(':');
-            }
-            sb.append(cn.flattenToString());
-        }
-        Settings.Secure.putString(mCR,
-                mConfig.setting,
-                sb != null ? sb.toString() : "");
-    }
-
-    private void updateList() {
-        loadEnabledServices();
-
-        getServices(mConfig, mListAdapter, mPM);
-        mListAdapter.sort(new PackageItemInfo.DisplayNameComparator(mPM));
-
-        getListView().setAdapter(mListAdapter);
-    }
-
-    protected static int getEnabledServicesCount(Config config, Context context) {
-        final String flat = Settings.Secure.getString(context.getContentResolver(), config.setting);
-        if (flat == null || "".equals(flat)) return 0;
-        final String[] components = flat.split(":");
-        return components.length;
-    }
-
-    protected static int getServicesCount(Config c, PackageManager pm) {
-        return getServices(c, null, pm);
-    }
-
-    private static int getServices(Config c, ArrayAdapter<ServiceInfo> adapter, PackageManager pm) {
-        int services = 0;
-        if (adapter != null) {
-            adapter.clear();
-        }
-        final int user = ActivityManager.getCurrentUser();
-
-        List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
-                new Intent(c.intentAction),
-                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
-                user);
-
-        for (int i = 0, count = installedServices.size(); i < count; i++) {
-            ResolveInfo resolveInfo = installedServices.get(i);
-            ServiceInfo info = resolveInfo.serviceInfo;
-
-            if (!c.permission.equals(info.permission)) {
-                Slog.w(c.tag, "Skipping " + c.noun + " service "
-                        + info.packageName + "/" + info.name
-                        + ": it does not require the permission "
-                        + c.permission);
-                continue;
-            }
-            if (adapter != null) {
-                adapter.add(info);
-            }
-            services++;
-        }
-        return services;
-    }
-
-    private boolean isServiceEnabled(ServiceInfo info) {
-        final ComponentName cn = new ComponentName(info.packageName, info.name);
-        return mEnabledServices.contains(cn);
-    }
-
-    @Override
-    public void onListItemClick(ListView l, View v, int position, long id) {
-        ServiceInfo info = mListAdapter.getItem(position);
-        final ComponentName cn = new ComponentName(info.packageName, info.name);
-        if (mEnabledServices.contains(cn)) {
-            // the simple version: disabling
-            mEnabledServices.remove(cn);
-            saveEnabledServices();
-        } else {
-            // show a scary dialog
-            new ScaryWarningDialogFragment()
-                .setServiceInfo(cn, info.loadLabel(mPM).toString())
-                .show(getFragmentManager(), "dialog");
-        }
-    }
-
     private static class ViewHolder {
         ImageView icon;
         TextView name;
@@ -327,7 +215,8 @@
             } else {
                 vh.description.setVisibility(View.GONE);
             }
-            vh.checkbox.setChecked(isServiceEnabled(info));
+            final ComponentName cn = new ComponentName(info.packageName, info.name);
+            vh.checkbox.setChecked(mServiceListing.isEnabled(cn));
         }
     }
 
diff --git a/src/com/android/settings/notification/NotificationAccessSettings.java b/src/com/android/settings/notification/NotificationAccessSettings.java
index ced71a4..5104d4a 100644
--- a/src/com/android/settings/notification/NotificationAccessSettings.java
+++ b/src/com/android/settings/notification/NotificationAccessSettings.java
@@ -46,10 +46,10 @@
     }
 
     public static int getListenersCount(PackageManager pm) {
-        return getServicesCount(CONFIG, pm);
+        return ServiceListing.getServicesCount(CONFIG, pm);
     }
 
     public static int getEnabledListenersCount(Context context) {
-        return getEnabledServicesCount(CONFIG, context);
+        return ServiceListing.getEnabledServicesCount(CONFIG, context);
     }
 }
diff --git a/src/com/android/settings/notification/ServiceListing.java b/src/com/android/settings/notification/ServiceListing.java
new file mode 100644
index 0000000..d296139
--- /dev/null
+++ b/src/com/android/settings/notification/ServiceListing.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2015 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.settings.notification;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.settings.notification.ManagedServiceSettings.Config;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+public class ServiceListing {
+    private final ContentResolver mContentResolver;
+    private final Context mContext;
+    private final Config mConfig;
+    private final HashSet<ComponentName> mEnabledServices = new HashSet<ComponentName>();
+    private final List<ServiceInfo> mServices = new ArrayList<ServiceInfo>();
+    private final List<Callback> mCallbacks = new ArrayList<Callback>();
+
+    private boolean mListening;
+
+    public ServiceListing(Context context, Config config) {
+        mContext = context;
+        mConfig = config;
+        mContentResolver = context.getContentResolver();
+    }
+
+    public void addCallback(Callback callback) {
+        mCallbacks.add(callback);
+    }
+
+    public void removeCallback(Callback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void setListening(boolean listening) {
+        if (mListening == listening) return;
+        mListening = listening;
+        if (mListening) {
+            // listen for package changes
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+            filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+            filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+            filter.addDataScheme("package");
+            mContext.registerReceiver(mPackageReceiver, filter);
+            mContentResolver.registerContentObserver(Settings.Secure.getUriFor(mConfig.setting),
+                    false, mSettingsObserver);
+        } else {
+            mContext.unregisterReceiver(mPackageReceiver);
+            mContentResolver.unregisterContentObserver(mSettingsObserver);
+        }
+    }
+
+    public static int getEnabledServicesCount(Config config, Context context) {
+        final String flat = Settings.Secure.getString(context.getContentResolver(), config.setting);
+        if (flat == null || "".equals(flat)) return 0;
+        final String[] components = flat.split(":");
+        return components.length;
+    }
+
+    public static int getServicesCount(Config c, PackageManager pm) {
+        return getServices(c, null, pm);
+    }
+
+    public static ServiceInfo findService(Context context, Config config, final ComponentName cn) {
+        final ServiceListing listing = new ServiceListing(context, config);
+        final List<ServiceInfo> services = listing.reload();
+        for (ServiceInfo service : services) {
+            final ComponentName serviceCN = new ComponentName(service.packageName, service.name);
+            if (serviceCN.equals(cn)) {
+                return service;
+            }
+        }
+        return null;
+    }
+
+    private static int getServices(Config c, List<ServiceInfo> list, PackageManager pm) {
+        int services = 0;
+        if (list != null) {
+            list.clear();
+        }
+        final int user = ActivityManager.getCurrentUser();
+
+        List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
+                new Intent(c.intentAction),
+                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+                user);
+
+        for (int i = 0, count = installedServices.size(); i < count; i++) {
+            ResolveInfo resolveInfo = installedServices.get(i);
+            ServiceInfo info = resolveInfo.serviceInfo;
+
+            if (!c.permission.equals(info.permission)) {
+                Slog.w(c.tag, "Skipping " + c.noun + " service "
+                        + info.packageName + "/" + info.name
+                        + ": it does not require the permission "
+                        + c.permission);
+                continue;
+            }
+            if (list != null) {
+                list.add(info);
+            }
+            services++;
+        }
+        return services;
+    }
+
+    private void saveEnabledServices() {
+        StringBuilder sb = null;
+        for (ComponentName cn : mEnabledServices) {
+            if (sb == null) {
+                sb = new StringBuilder();
+            } else {
+                sb.append(':');
+            }
+            sb.append(cn.flattenToString());
+        }
+        Settings.Secure.putString(mContentResolver, mConfig.setting,
+                sb != null ? sb.toString() : "");
+    }
+
+    private void loadEnabledServices() {
+        mEnabledServices.clear();
+        final String flat = Settings.Secure.getString(mContentResolver, mConfig.setting);
+        if (flat != null && !"".equals(flat)) {
+            final String[] names = flat.split(":");
+            for (int i = 0; i < names.length; i++) {
+                final ComponentName cn = ComponentName.unflattenFromString(names[i]);
+                if (cn != null) {
+                    mEnabledServices.add(cn);
+                }
+            }
+        }
+    }
+
+    public List<ServiceInfo> reload() {
+        loadEnabledServices();
+        getServices(mConfig, mServices, mContext.getPackageManager());
+        for (Callback callback : mCallbacks) {
+            callback.onServicesReloaded(mServices);
+        }
+        return mServices;
+    }
+
+    public boolean isEnabled(ComponentName cn) {
+        return mEnabledServices.contains(cn);
+    }
+
+    public void setEnabled(ComponentName cn, boolean enabled) {
+        if (enabled) {
+            mEnabledServices.add(cn);
+        } else {
+            mEnabledServices.remove(cn);
+        }
+        saveEnabledServices();
+    }
+
+    private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            reload();
+        }
+    };
+
+    private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            reload();
+        }
+    };
+
+    public interface Callback {
+        void onServicesReloaded(List<ServiceInfo> services);
+    }
+}
diff --git a/src/com/android/settings/notification/ZenModeAutomationSettings.java b/src/com/android/settings/notification/ZenModeAutomationSettings.java
index c43d99e..f2ee71d 100644
--- a/src/com/android/settings/notification/ZenModeAutomationSettings.java
+++ b/src/com/android/settings/notification/ZenModeAutomationSettings.java
@@ -18,12 +18,16 @@
 
 import static android.service.notification.ZenModeConfig.ALL_DAYS;
 
+import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.ServiceInfo;
 import android.os.Bundle;
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceScreen;
+import android.provider.Settings;
 import android.provider.Settings.Global;
+import android.service.notification.ConditionProviderService;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.service.notification.ZenModeConfig.ZenRule;
@@ -35,30 +39,39 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.settings.R;
+import com.android.settings.notification.ManagedServiceSettings.Config;
+import com.android.settings.notification.ZenRuleNameDialog.RuleInfo;
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
+import java.util.List;
 import java.util.TreeSet;
 
 public class ZenModeAutomationSettings extends ZenModeSettingsBase {
     private static final SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("EEE");
 
+    static final Config CONFIG = getConditionProviderConfig();
+
     private final Calendar mCalendar = Calendar.getInstance();
 
+    private ServiceListing mServiceListing;
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
-
         setHasOptionsMenu(true);
-
         addPreferencesFromResource(R.xml.zen_mode_automation_settings);
+        mServiceListing = new ServiceListing(mContext, CONFIG);
+        mServiceListing.addCallback(mServiceListingCallback);
+        mServiceListing.reload();
+        mServiceListing.setListening(true);
     }
 
-    private void showRule(String ruleId, String ruleName) {
-        if (DEBUG) Log.d(TAG, "showRule " + ruleId + " name=" + ruleName);
-        mContext.startActivity(new Intent(ZenModeScheduleRuleSettings.ACTION)
-                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
-                .putExtra(ZenModeScheduleRuleSettings.EXTRA_RULE_ID, ruleId));
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mServiceListing.setListening(false);
+        mServiceListing.removeCallback(mServiceListingCallback);
     }
 
     @Override
@@ -75,29 +88,6 @@
         return super.onOptionsItemSelected(item);
     }
 
-    private void showAddRuleDialog() {
-        new ZenRuleNameDialog(mContext, "", mConfig.getAutomaticRuleNames()) {
-            @Override
-            public void onOk(String ruleName) {
-                final ScheduleInfo schedule = new ScheduleInfo();
-                schedule.days = ZenModeConfig.ALL_DAYS;
-                schedule.startHour = 22;
-                schedule.endHour = 7;
-                final ZenRule rule = new ZenRule();
-                rule.name = ruleName;
-                rule.enabled = true;
-                rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-                rule.conditionId = ZenModeConfig.toScheduleConditionId(schedule);
-                final ZenModeConfig newConfig = mConfig.copy();
-                final String ruleId = newConfig.newRuleId();
-                newConfig.automaticRules.put(ruleId, rule);
-                if (setZenModeConfig(newConfig)) {
-                    showRule(ruleId, rule.name);
-                }
-            }
-        }.show();
-    }
-
     @Override
     protected void onZenModeChanged() {
         // don't care
@@ -114,15 +104,42 @@
         updateControls();
     }
 
+    private void showAddRuleDialog() {
+        new ZenRuleNameDialog(mContext, mServiceListing, null, mConfig.getAutomaticRuleNames()) {
+            @Override
+            public void onOk(String ruleName, RuleInfo ri) {
+                final ZenRule rule = new ZenRule();
+                rule.name = ruleName;
+                rule.enabled = true;
+                rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+                rule.conditionId = ri.defaultConditionId;
+                rule.component = ri.serviceComponent;
+                final ZenModeConfig newConfig = mConfig.copy();
+                final String ruleId = newConfig.newRuleId();
+                newConfig.automaticRules.put(ruleId, rule);
+                if (setZenModeConfig(newConfig)) {
+                    showRule(ri.settingsAction, ri.configurationActivity, ruleId, rule.name);
+                }
+            }
+        }.show();
+    }
+
+    private void showRule(String settingsAction, ComponentName configurationActivity,
+            String ruleId, String ruleName) {
+        if (DEBUG) Log.d(TAG, "showRule " + ruleId + " name=" + ruleName);
+        mContext.startActivity(new Intent(settingsAction)
+                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                .putExtra(ZenModeRuleSettingsBase.EXTRA_RULE_ID, ruleId));
+    }
+
     private void updateControls() {
         final PreferenceScreen root = getPreferenceScreen();
         root.removeAll();
-
         if (mConfig == null) return;
         for (int i = 0; i < mConfig.automaticRules.size(); i++) {
             final String id = mConfig.automaticRules.keyAt(i);
             final ZenRule rule = mConfig.automaticRules.valueAt(i);
-            if (!ZenModeConfig.isValidScheduleConditionId(rule.conditionId)) continue;
+            final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId(rule.conditionId);
             final Preference p = new Preference(mContext);
             p.setTitle(rule.name);
             p.setSummary(computeRuleSummary(rule));
@@ -130,7 +147,9 @@
             p.setOnPreferenceClickListener(new OnPreferenceClickListener() {
                 @Override
                 public boolean onPreferenceClick(Preference preference) {
-                    showRule(id, rule.name);
+                    final String action = isSchedule ? ZenModeScheduleRuleSettings.ACTION
+                            : ZenModeExternalRuleSettings.ACTION;
+                    showRule(action, null, id, rule.name);
                     return true;
                 }
             });
@@ -146,13 +165,16 @@
     private String computeRuleSummary(ZenRule rule) {
         if (rule == null || !rule.enabled) return getString(R.string.switch_off_text);
         final ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
-        if (schedule == null) return getString(R.string.switch_on_text);
-        final String days = computeContiguousDayRanges(schedule.days);
-        final String start = getTime(schedule.startHour, schedule.startMinute);
-        final String end = getTime(schedule.endHour, schedule.endMinute);
-        final String time = getString(R.string.summary_range_verbal_combination, start, end);
         final String mode = ZenModeSettings.computeZenModeCaption(getResources(), rule.zenMode);
-        return getString(R.string.zen_mode_rule_summary_template, days, time, mode);
+        String summary = getString(R.string.switch_on_text);
+        if (schedule != null) {
+            final String days = computeContiguousDayRanges(schedule.days);
+            final String start = getTime(schedule.startHour, schedule.startMinute);
+            final String end = getTime(schedule.endHour, schedule.endMinute);
+            final String time = getString(R.string.summary_range_verbal_combination, start, end);
+            summary = getString(R.string.zen_mode_rule_summary_combination, days, time);
+        }
+        return getString(R.string.zen_mode_rule_summary_combination, summary, mode);
     }
 
     private String getTime(int hour, int minute) {
@@ -199,4 +221,30 @@
         return DAY_FORMAT.format(mCalendar.getTime());
     }
 
+    private static Config getConditionProviderConfig() {
+        final Config c = new Config();
+        c.tag = TAG;
+        c.setting = Settings.Secure.ENABLED_CONDITION_PROVIDERS;
+        c.intentAction = ConditionProviderService.SERVICE_INTERFACE;
+        c.permission = android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE;
+        c.noun = "condition provider";
+        return c;
+    }
+
+    private final ServiceListing.Callback mServiceListingCallback = new ServiceListing.Callback() {
+        @Override
+        public void onServicesReloaded(List<ServiceInfo> services) {
+            for (ServiceInfo service : services) {
+                final RuleInfo ri = ZenModeExternalRuleSettings.getRuleInfo(service);
+                if (ri != null && ri.serviceComponent != null
+                        && ri.settingsAction == ZenModeExternalRuleSettings.ACTION) {
+                    if (!mServiceListing.isEnabled(ri.serviceComponent)) {
+                        Log.i(TAG, "Enabling external condition provider: " + ri.serviceComponent);
+                        mServiceListing.setEnabled(ri.serviceComponent, true);
+                    }
+                }
+            }
+        }
+    };
+
 }
diff --git a/src/com/android/settings/notification/ZenModeExternalRuleSettings.java b/src/com/android/settings/notification/ZenModeExternalRuleSettings.java
new file mode 100644
index 0000000..9f9dc8a
--- /dev/null
+++ b/src/com/android/settings/notification/ZenModeExternalRuleSettings.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 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.settings.notification;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.net.Uri;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.service.notification.ZenModeConfig.ZenRule;
+import android.util.Log;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.settings.R;
+import com.android.settings.notification.ZenRuleNameDialog.RuleInfo;
+
+public class ZenModeExternalRuleSettings extends ZenModeRuleSettingsBase {
+    private static final String KEY_TYPE = "type";
+    private static final String KEY_CONFIGURE = "configure";
+
+    public static final String ACTION = Settings.ACTION_ZEN_MODE_EXTERNAL_RULE_SETTINGS;
+    private static final int REQUEST_CODE_CONFIGURE = 1;
+
+    private static final String MD_RULE_TYPE = "automatic.ruleType";
+    private static final String MD_DEFAULT_CONDITION_ID = "automatic.defaultConditionId";
+    private static final String MD_CONFIGURATION_ACTIVITY = "automatic.configurationActivity";
+    private static final String EXTRA_CONDITION_ID = "automatic.conditionId";
+
+    private Preference mType;
+    private Preference mConfigure;
+
+    @Override
+    protected boolean setRule(ZenRule rule) {
+        return rule != null;
+    }
+
+    @Override
+    protected String getZenModeDependency() {
+        return null;
+    }
+
+    @Override
+    protected void onCreateInternal() {
+        addPreferencesFromResource(R.xml.zen_mode_external_rule_settings);
+        final PreferenceScreen root = getPreferenceScreen();
+        final ServiceInfo si = ServiceListing.findService(mContext,
+                ZenModeAutomationSettings.CONFIG, mRule.component);
+        if (DEBUG) Log.d(TAG, "ServiceInfo: " + si);
+        final RuleInfo ri = getRuleInfo(si);
+        if (DEBUG) Log.d(TAG, "RuleInfo: " + ri);
+        mType = root.findPreference(KEY_TYPE);
+        if (ri == null) {
+            mType.setSummary(R.string.zen_mode_rule_type_unknown);
+        } else {
+            mType.setSummary(ri.caption);
+        }
+
+        mConfigure = root.findPreference(KEY_CONFIGURE);
+        if (ri == null || ri.configurationActivity == null) {
+            mConfigure.setEnabled(false);
+        } else {
+            mConfigure.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+                @Override
+                public boolean onPreferenceClick(Preference preference) {
+                    startActivityForResult(new Intent().setComponent(ri.configurationActivity),
+                            REQUEST_CODE_CONFIGURE);
+                    return true;
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (requestCode == REQUEST_CODE_CONFIGURE) {
+            if (resultCode == Activity.RESULT_OK && data != null) {
+                final Uri conditionId = data.getParcelableExtra(EXTRA_CONDITION_ID);
+                if (conditionId != null && !conditionId.equals(mRule.conditionId)) {
+                    updateRule(conditionId);
+                }
+            }
+        }
+    }
+
+    public static RuleInfo getRuleInfo(ServiceInfo si) {
+        if (si == null || si.metaData == null) return null;
+        final String ruleType = si.metaData.getString(MD_RULE_TYPE);
+        final String defaultConditionId = si.metaData.getString(MD_DEFAULT_CONDITION_ID);
+        final String configurationActivity = si.metaData.getString(MD_CONFIGURATION_ACTIVITY);
+        if (ruleType != null && !ruleType.trim().isEmpty() && defaultConditionId != null) {
+            final RuleInfo ri = new RuleInfo();
+            ri.serviceComponent = new ComponentName(si.packageName, si.name);
+            ri.settingsAction = ZenModeExternalRuleSettings.ACTION;
+            ri.caption = ruleType;
+            ri.defaultConditionId = Uri.parse(defaultConditionId);
+            if (configurationActivity != null) {
+                ri.configurationActivity = ComponentName.unflattenFromString(configurationActivity);
+            }
+            return ri;
+        }
+        return null;
+    }
+
+    @Override
+    protected void updateControlsInternal() {
+        // everything done up front
+    }
+
+    @Override
+    protected int getMetricsCategory() {
+        return MetricsLogger.NOTIFICATION_ZEN_MODE_EXTERNAL_RULE;
+    }
+
+}
diff --git a/src/com/android/settings/notification/ZenModeRuleSettingsBase.java b/src/com/android/settings/notification/ZenModeRuleSettingsBase.java
new file mode 100644
index 0000000..f6bc75f
--- /dev/null
+++ b/src/com/android/settings/notification/ZenModeRuleSettingsBase.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2015 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.settings.notification;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceScreen;
+import android.provider.Settings.Global;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import com.android.settings.DropDownPreference;
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.widget.SwitchBar;
+
+public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase
+        implements SwitchBar.OnSwitchChangeListener {
+    protected static final String TAG = ZenModeSettingsBase.TAG;
+    protected static final boolean DEBUG = ZenModeSettingsBase.DEBUG;
+
+    public static final String EXTRA_RULE_ID = "rule_id";
+    private static final String KEY_RULE_NAME = "rule_name";
+    private static final String KEY_ZEN_MODE = "zen_mode";
+
+    protected Context mContext;
+    protected boolean mDisableListeners;
+    protected ZenRule mRule;
+
+    private String mRuleId;
+    private boolean mDeleting;
+    private Preference mRuleName;
+    private SwitchBar mSwitchBar;
+    private DropDownPreference mZenMode;
+
+    abstract protected void onCreateInternal();
+    abstract protected boolean setRule(ZenRule rule);
+    abstract protected String getZenModeDependency();
+    abstract protected void updateControlsInternal();
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        mContext = getActivity();
+
+        final Intent intent = getActivity().getIntent();
+        if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + intent);
+        if (intent == null) {
+            Log.w(TAG, "No intent");
+            toastAndFinish();
+            return;
+        }
+
+        mRuleId = intent.getStringExtra(EXTRA_RULE_ID);
+        if (DEBUG) Log.d(TAG, "mRuleId=" + mRuleId);
+        if (refreshRuleOrFinish()) {
+            return;
+        }
+
+        setHasOptionsMenu(true);
+
+        onCreateInternal();
+
+        final PreferenceScreen root = getPreferenceScreen();
+        mRuleName = root.findPreference(KEY_RULE_NAME);
+        mRuleName.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                showRuleNameDialog();
+                return true;
+            }
+        });
+
+        mZenMode = (DropDownPreference) root.findPreference(KEY_ZEN_MODE);
+        mZenMode.addItem(R.string.zen_mode_option_important_interruptions,
+                Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        mZenMode.addItem(R.string.zen_mode_option_alarms, Global.ZEN_MODE_ALARMS);
+        mZenMode.addItem(R.string.zen_mode_option_no_interruptions,
+                Global.ZEN_MODE_NO_INTERRUPTIONS);
+        mZenMode.setCallback(new DropDownPreference.Callback() {
+            @Override
+            public boolean onItemSelected(int pos, Object value) {
+                if (mDisableListeners) return true;
+                final int zenMode = (Integer) value;
+                if (zenMode == mRule.zenMode) return true;
+                if (DEBUG) Log.d(TAG, "onPrefChange zenMode=" + zenMode);
+                mRule.zenMode = zenMode;
+                setZenModeConfig(mConfig);
+                return true;
+            }
+        });
+        mZenMode.setOrder(10);  // sort at the bottom of the category
+        mZenMode.setDependency(getZenModeDependency());
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        updateControls();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        final SettingsActivity activity = (SettingsActivity) getActivity();
+        mSwitchBar = activity.getSwitchBar();
+        mSwitchBar.addOnSwitchChangeListener(this);
+        mSwitchBar.show();
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mSwitchBar.removeOnSwitchChangeListener(this);
+        mSwitchBar.hide();
+    }
+
+    @Override
+    public void onSwitchChanged(Switch switchView, boolean isChecked) {
+        if (DEBUG) Log.d(TAG, "onSwitchChanged " + isChecked);
+        if (mDisableListeners) return;
+        final boolean enabled = isChecked;
+        if (enabled == mRule.enabled) return;
+        if (DEBUG) Log.d(TAG, "onSwitchChanged enabled=" + enabled);
+        mRule.enabled = enabled;
+        mRule.snoozing = false;
+        setZenModeConfig(mConfig);
+    }
+
+    protected void updateRule(Uri newConditionId) {
+        mRule.conditionId = newConditionId;
+        mRule.condition = null;
+        mRule.snoozing = false;
+        setZenModeConfig(mConfig);
+    }
+
+    @Override
+    protected void onZenModeChanged() {
+        // noop
+    }
+
+    @Override
+    protected void onZenModeConfigChanged() {
+        if (!refreshRuleOrFinish()) {
+            updateControls();
+        }
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (DEBUG) Log.d(TAG, "onCreateOptionsMenu");
+        inflater.inflate(R.menu.zen_mode_rule, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (DEBUG) Log.d(TAG, "onOptionsItemSelected " + item.getItemId());
+        if (item.getItemId() == R.id.delete) {
+            showDeleteRuleDialog();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void showRuleNameDialog() {
+        new ZenRuleNameDialog(mContext, null, mRule.name, mConfig.getAutomaticRuleNames()) {
+            @Override
+            public void onOk(String ruleName, RuleInfo type) {
+                final ZenModeConfig newConfig = mConfig.copy();
+                final ZenRule rule = newConfig.automaticRules.get(mRuleId);
+                if (rule == null) return;
+                rule.name = ruleName;
+                setZenModeConfig(newConfig);
+            }
+        }.show();
+    }
+
+    private boolean refreshRuleOrFinish() {
+        mRule = mConfig.automaticRules.get(mRuleId);
+        if (DEBUG) Log.d(TAG, "mRule=" + mRule);
+        if (!setRule(mRule)) {
+            toastAndFinish();
+            return true;
+        }
+        return false;
+    }
+
+    private void showDeleteRuleDialog() {
+        new AlertDialog.Builder(mContext)
+                .setMessage(getString(R.string.zen_mode_delete_rule_confirmation, mRule.name))
+                .setNegativeButton(R.string.cancel, null)
+                .setPositiveButton(R.string.zen_mode_delete_rule_button, new OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        mDeleting = true;
+                        mConfig.automaticRules.remove(mRuleId);
+                        setZenModeConfig(mConfig);
+                    }
+                })
+                .show();
+    }
+
+    private void toastAndFinish() {
+        if (!mDeleting) {
+            Toast.makeText(mContext, R.string.zen_mode_rule_not_found_text, Toast.LENGTH_SHORT)
+                    .show();
+        }
+        getActivity().finish();
+    }
+
+    private void updateRuleName() {
+        getActivity().setTitle(mRule.name);
+        mRuleName.setSummary(mRule.name);
+    }
+
+    private void updateControls() {
+        mDisableListeners = true;
+        updateRuleName();
+        updateControlsInternal();
+        mZenMode.setSelectedValue(mRule.zenMode);
+        mDisableListeners = false;
+        if (mSwitchBar != null) {
+            mSwitchBar.setChecked(mRule.enabled);
+        }
+    }
+
+}
diff --git a/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java b/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java
index f0f0294..f7015d3 100644
--- a/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java
+++ b/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java
@@ -25,141 +25,58 @@
 import android.app.TimePickerDialog;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
 import android.content.DialogInterface.OnDismissListener;
-import android.content.Intent;
 import android.os.Bundle;
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceScreen;
 import android.provider.Settings;
-import android.provider.Settings.Global;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.service.notification.ZenModeConfig.ZenRule;
 import android.text.format.DateFormat;
 import android.util.Log;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.widget.Switch;
 import android.widget.TimePicker;
-import android.widget.Toast;
 
 import com.android.internal.logging.MetricsLogger;
-import com.android.settings.DropDownPreference;
 import com.android.settings.R;
-import com.android.settings.SettingsActivity;
-import com.android.settings.widget.SwitchBar;
 
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Calendar;
 
-public class ZenModeScheduleRuleSettings extends ZenModeSettingsBase
-        implements SwitchBar.OnSwitchChangeListener {
-    private static final String TAG = ZenModeSettingsBase.TAG;
-    private static final boolean DEBUG = ZenModeSettingsBase.DEBUG;
-
-    private static final String KEY_RULE_NAME = "rule_name";
+public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase {
     private static final String KEY_DAYS = "days";
     private static final String KEY_START_TIME = "start_time";
     private static final String KEY_END_TIME = "end_time";
-    private static final String KEY_ZEN_MODE = "zen_mode";
 
     private static final SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("EEE");
 
     public static final String ACTION = Settings.ACTION_ZEN_MODE_SCHEDULE_RULE_SETTINGS;
-    public static final String EXTRA_RULE_ID = "rule_id";
 
-    private Context mContext;
-    private boolean mDisableListeners;
-    private SwitchBar mSwitchBar;
-    private Preference mRuleName;
     private Preference mDays;
     private TimePickerPreference mStart;
     private TimePickerPreference mEnd;
-    private DropDownPreference mZenMode;
 
-    private String mRuleId;
-    private ZenRule mRule;
     private ScheduleInfo mSchedule;
-    private boolean mDeleting;
 
     @Override
-    protected void onZenModeChanged() {
-        // noop
-    }
-
-    @Override
-    protected void onZenModeConfigChanged() {
-        if (!refreshRuleOrFinish()) {
-            updateControls();
-        }
-    }
-
-    private boolean refreshRuleOrFinish() {
-        mRule = mConfig.automaticRules.get(mRuleId);
-        if (DEBUG) Log.d(TAG, "mRule=" + mRule);
-        mSchedule = mRule != null ? ZenModeConfig.tryParseScheduleConditionId(mRule.conditionId)
+    protected boolean setRule(ZenRule rule) {
+        mSchedule = rule != null ? ZenModeConfig.tryParseScheduleConditionId(rule.conditionId)
                 : null;
-        if (mSchedule == null) {
-            toastAndFinish();
-            return true;
-        }
-        return false;
+        return mSchedule != null;
     }
 
     @Override
-    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
-        if (DEBUG) Log.d(TAG, "onCreateOptionsMenu");
-        inflater.inflate(R.menu.zen_mode_rule, menu);
+    protected String getZenModeDependency() {
+        return mDays.getKey();
     }
 
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        if (DEBUG) Log.d(TAG, "onOptionsItemSelected " + item.getItemId());
-        if (item.getItemId() == R.id.delete) {
-            showDeleteRuleDialog();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    @Override
-    public void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        mContext = getActivity();
-
-        final Intent intent = getActivity().getIntent();
-        if (DEBUG) Log.d(TAG, "onCreate getIntent()=" + intent);
-        if (intent == null) {
-            Log.w(TAG, "No intent");
-            toastAndFinish();
-            return;
-        }
-
-        mRuleId = intent.getStringExtra(EXTRA_RULE_ID);
-        if (DEBUG) Log.d(TAG, "mRuleId=" + mRuleId);
-        if (refreshRuleOrFinish()) {
-            return;
-        }
-
+    protected void onCreateInternal() {
         addPreferencesFromResource(R.xml.zen_mode_schedule_rule_settings);
         final PreferenceScreen root = getPreferenceScreen();
 
-        setHasOptionsMenu(true);
-
-        mRuleName = root.findPreference(KEY_RULE_NAME);
-        mRuleName.setOnPreferenceClickListener(new OnPreferenceClickListener() {
-            @Override
-            public boolean onPreferenceClick(Preference preference) {
-                showRuleNameDialog();
-                return true;
-            }
-        });
-
         mDays = root.findPreference(KEY_DAYS);
         mDays.setOnPreferenceClickListener(new OnPreferenceClickListener() {
             @Override
@@ -186,10 +103,7 @@
                 if (DEBUG) Log.d(TAG, "onPrefChange start h=" + hour + " m=" + minute);
                 mSchedule.startHour = hour;
                 mSchedule.startMinute = minute;
-                mRule.conditionId = ZenModeConfig.toScheduleConditionId(mSchedule);
-                mRule.condition = null;
-                mRule.snoozing = false;
-                setZenModeConfig(mConfig);
+                updateRule(ZenModeConfig.toScheduleConditionId(mSchedule));
                 return true;
             }
         });
@@ -211,63 +125,12 @@
                 if (DEBUG) Log.d(TAG, "onPrefChange end h=" + hour + " m=" + minute);
                 mSchedule.endHour = hour;
                 mSchedule.endMinute = minute;
-                mRule.conditionId = ZenModeConfig.toScheduleConditionId(mSchedule);
-                mRule.condition = null;
-                mRule.snoozing = false;
-                setZenModeConfig(mConfig);
+                updateRule(ZenModeConfig.toScheduleConditionId(mSchedule));
                 return true;
             }
         });
         root.addPreference(mEnd);
         mEnd.setDependency(mDays.getKey());
-
-        mZenMode = (DropDownPreference) root.findPreference(KEY_ZEN_MODE);
-        mZenMode.addItem(R.string.zen_mode_option_important_interruptions, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS);
-        mZenMode.addItem(R.string.zen_mode_option_alarms, Global.ZEN_MODE_ALARMS);
-        mZenMode.addItem(R.string.zen_mode_option_no_interruptions, Global.ZEN_MODE_NO_INTERRUPTIONS);
-        mZenMode.setCallback(new DropDownPreference.Callback() {
-            @Override
-            public boolean onItemSelected(int pos, Object value) {
-                if (mDisableListeners) return true;
-                final int zenMode = (Integer) value;
-                if (zenMode == mRule.zenMode) return true;
-                if (DEBUG) Log.d(TAG, "onPrefChange zenMode=" + zenMode);
-                mRule.zenMode = zenMode;
-                setZenModeConfig(mConfig);
-                return true;
-            }
-        });
-        mZenMode.setOrder(10);  // sort at the bottom of the category
-        mZenMode.setDependency(mDays.getKey());
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-
-        final SettingsActivity activity = (SettingsActivity) getActivity();
-        mSwitchBar = activity.getSwitchBar();
-        mSwitchBar.addOnSwitchChangeListener(this);
-        mSwitchBar.show();
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        mSwitchBar.removeOnSwitchChangeListener(this);
-        mSwitchBar.hide();
-    }
-
-    @Override
-    public void onSwitchChanged(Switch switchView, boolean isChecked) {
-        if (DEBUG) Log.d(TAG, "onSwitchChanged " + isChecked);
-        if (mDisableListeners) return;
-        final boolean enabled = isChecked;
-        if (enabled == mRule.enabled) return;
-        if (DEBUG) Log.d(TAG, "onSwitchChanged enabled=" + enabled);
-        mRule.enabled = enabled;
-        mRule.snoozing = false;
-        setZenModeConfig(mConfig);
     }
 
     private void updateDays() {
@@ -308,28 +171,11 @@
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
-        updateControls();
-    }
-
-    private void updateRuleName() {
-        getActivity().setTitle(mRule.name);
-        mRuleName.setSummary(mRule.name);
-    }
-
-    private void updateControls() {
-        mDisableListeners = true;
-        updateRuleName();
+    protected void updateControlsInternal() {
         updateDays();
         mStart.setTime(mSchedule.startHour, mSchedule.startMinute);
         mEnd.setTime(mSchedule.endHour, mSchedule.endMinute);
-        mZenMode.setSelectedValue(mRule.zenMode);
-        mDisableListeners = false;
         updateEndSummary();
-        if (mSwitchBar != null) {
-            mSwitchBar.setChecked(mRule.enabled);
-        }
     }
 
     @Override
@@ -337,34 +183,6 @@
         return MetricsLogger.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE;
     }
 
-    private void showDeleteRuleDialog() {
-        new AlertDialog.Builder(mContext)
-                .setMessage(getString(R.string.zen_mode_delete_rule_confirmation, mRule.name))
-                .setNegativeButton(R.string.cancel, null)
-                .setPositiveButton(R.string.zen_mode_delete_rule_button, new OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                        mDeleting = true;
-                        mConfig.automaticRules.remove(mRuleId);
-                        setZenModeConfig(mConfig);
-                    }
-                })
-                .show();
-    }
-
-    private void showRuleNameDialog() {
-        new ZenRuleNameDialog(mContext, mRule.name, mConfig.getAutomaticRuleNames()) {
-            @Override
-            public void onOk(String ruleName) {
-                final ZenModeConfig newConfig = mConfig.copy();
-                final ZenRule rule = newConfig.automaticRules.get(mRuleId);
-                if (rule == null) return;
-                rule.name = ruleName;
-                setZenModeConfig(newConfig);
-            }
-        }.show();
-    }
-
     private void showDaysDialog() {
         new AlertDialog.Builder(mContext)
                 .setTitle(R.string.zen_mode_schedule_rule_days)
@@ -375,10 +193,7 @@
                           if (Arrays.equals(days, mSchedule.days)) return;
                           if (DEBUG) Log.d(TAG, "days.onChanged days=" + Arrays.asList(days));
                           mSchedule.days = days;
-                          mRule.conditionId = ZenModeConfig.toScheduleConditionId(mSchedule);
-                          mRule.condition = null;
-                          mRule.snoozing = false;
-                          setZenModeConfig(mConfig);
+                          updateRule(ZenModeConfig.toScheduleConditionId(mSchedule));
                       }
                 })
                 .setOnDismissListener(new OnDismissListener() {
@@ -391,14 +206,6 @@
                 .show();
     }
 
-    private void toastAndFinish() {
-        if (!mDeleting) {
-            Toast.makeText(mContext, R.string.zen_mode_rule_not_found_text, Toast.LENGTH_SHORT)
-                    .show();
-        }
-        getActivity().finish();
-    }
-
     private static class TimePickerPreference extends Preference {
         private final Context mContext;
 
@@ -474,4 +281,5 @@
             boolean onSetTime(int hour, int minute);
         }
     }
+
 }
diff --git a/src/com/android/settings/notification/ZenModeVoiceActivity.java b/src/com/android/settings/notification/ZenModeVoiceActivity.java
new file mode 100644
index 0000000..c7c1151
--- /dev/null
+++ b/src/com/android/settings/notification/ZenModeVoiceActivity.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2015 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.settings.notification;
+
+import static android.provider.Settings.EXTRA_DO_NOT_DISTURB_MODE_MINUTES;
+import static android.provider.Settings.EXTRA_DO_NOT_DISTURB_MODE_ENABLED;
+
+import com.android.settings.R;
+import com.android.settings.utils.VoiceSelectionAdapter;
+import com.android.settings.utils.VoiceSelection;
+import com.android.settings.utils.VoiceSelectionFragment;
+import com.android.settings.utils.VoiceSettingsActivity;
+
+import android.app.Fragment;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.provider.Settings.Global;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.text.format.DateFormat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Activity for modifying the Zen mode (Do not disturb) by voice
+ * using the Voice Interaction API.
+ */
+public class ZenModeVoiceActivity extends VoiceSettingsActivity {
+    private static final String TAG = "ZenModeVoiceActivity";
+    private static final int MINUTES_MS = 60 * 1000;
+
+    @Override
+    protected boolean onVoiceSettingInteraction(Intent intent) {
+        setContentView(R.layout.voice_interaction);
+        pickNotificationMode(intent);
+        return false;
+    }
+
+    /**
+     * Start a voice interaction to ask what kind of interruptions should
+     * be permitted. The intent can optionally include extra information about the type
+     * of interruptions desired or how long interruptions should be limited to that are
+     * used as hints.
+     */
+    private void pickNotificationMode(final Intent intent) {
+        boolean enabled = intent.getBooleanExtra(EXTRA_DO_NOT_DISTURB_MODE_ENABLED, false);
+        boolean specified = intent.hasExtra(EXTRA_DO_NOT_DISTURB_MODE_ENABLED);
+
+        List<VoiceSelection> states = new ArrayList<VoiceSelection>();
+        if (!specified || enabled) {
+            states.add(new ModeSelection(this, Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                    R.string.zen_mode_option_important_interruptions,
+                    R.string.zen_mode_option_important_voice_synonyms));
+            states.add(new ModeSelection(this, Global.ZEN_MODE_ALARMS,
+                    R.string.zen_mode_option_alarms,
+                    R.string.zen_mode_option_alarms_voice_synonyms));
+            states.add(new ModeSelection(this, Global.ZEN_MODE_NO_INTERRUPTIONS,
+                    R.string.zen_mode_option_no_interruptions,
+                    R.string.zen_mode_option_no_interruptions_voice_synonyms));
+        }
+        if (!specified || !enabled) {
+            states.add(new ModeSelection(this, Global.ZEN_MODE_OFF,
+                    R.string.zen_mode_option_off,
+                    R.string.zen_mode_option_off_voice_synonyms));
+        }
+        VoiceSelectionFragment fragment = new VoiceSelectionFragment();
+        fragment.setArguments(VoiceSelectionFragment.createArguments(
+                getString(R.string.zen_mode_interruptions_voice_prompt)));
+        fragment.setListAdapter(
+                new VoiceSelectionAdapter(this, R.layout.voice_item_row, states));
+        fragment.setOnItemSelectedHandler(new VoiceSelection.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(int index, VoiceSelection selection) {
+                int mode = ((ModeSelection) selection).mMode;
+                ConditionSelection conditionSelection = getConditionSelection(
+                        intent.getIntExtra(EXTRA_DO_NOT_DISTURB_MODE_MINUTES, 0));
+                if (mode != Global.ZEN_MODE_OFF) {
+                    if (conditionSelection == null) {
+                        pickDuration(selection.getLabel(), mode);
+                        return;
+                    }
+                }
+                setZenModeConfig(mode, conditionSelection.mCondition);
+                notifySuccess(getChangeSummary(mode, conditionSelection));
+                finish();
+            }
+        });
+        showFragment(fragment, "pick_mode_fragment");
+    }
+
+    /**
+     * Start a voice interaction to ask for the zen mode duration.
+     */
+    private void pickDuration(CharSequence label, final int mode) {
+        setTitle(label.toString());
+        List<VoiceSelection> states = new ArrayList<VoiceSelection>();
+        states.add(new ConditionSelection(null, -1,
+              getString(R.string.zen_mode_duration_indefinte_voice_label),
+              getString(R.string.zen_mode_duration_indefinite_voice_synonyms)));
+        for (int i = ZenModeConfig.MINUTE_BUCKETS.length - 1; i >= 0; --i) {
+            states.add(getConditionSelection(ZenModeConfig.MINUTE_BUCKETS[i]));
+        }
+
+        VoiceSelectionFragment fragment = new VoiceSelectionFragment();
+        fragment.setArguments(VoiceSelectionFragment.createArguments(
+                getString(R.string.zen_mode_duration_voice_prompt)));
+        fragment.setListAdapter(
+                new VoiceSelectionAdapter(this, R.layout.voice_item_row, states));
+        fragment.setOnItemSelectedHandler(new VoiceSelection.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(int index, VoiceSelection item) {
+                ConditionSelection selection = ((ConditionSelection) item);
+                setZenModeConfig(mode, selection.mCondition);
+                notifySuccess(getChangeSummary(mode, selection));
+                finish();
+            }
+        });
+        showFragment(fragment, "pick_duration_fragment");
+    }
+
+    private void showFragment(Fragment fragment, String tag) {
+        getFragmentManager()
+                .beginTransaction()
+                .replace(R.id.fragment_root, fragment, tag)
+                .commit();
+    }
+
+    private void setZenModeConfig(int mode, Condition condition) {
+        if (condition != null) {
+            NotificationManager.from(this).setZenMode(mode, condition.id, TAG);
+        } else {
+            NotificationManager.from(this).setZenMode(mode, null, TAG);
+        }
+    }
+
+    /**
+     * Produce a summary of the Zen mode change to be read aloud as TTS.
+     */
+    private CharSequence getChangeSummary(int mode, ConditionSelection duration) {
+        int indefinite = -1;
+        int byMinute = -1;
+        int byHour = -1;
+
+        switch (mode) {
+            case Global.ZEN_MODE_ALARMS:
+                indefinite = R.string.zen_mode_summary_alarams_only_indefinite;
+                byMinute = R.plurals.zen_mode_summary_alarms_only_by_minute;
+                byHour = R.plurals.zen_mode_summary_alarms_only_by_hour;
+                break;
+            case Global.ZEN_MODE_NO_INTERRUPTIONS:
+                indefinite = R.string.zen_mode_summary_no_interruptions_indefinite;
+                byMinute = R.plurals.zen_mode_summary_no_interruptions_by_minute;
+                byHour = R.plurals.zen_mode_summary_no_interruptions_by_hour;
+                break;
+            case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
+                indefinite = R.string.zen_mode_summary_priority_indefinitely;
+                byMinute = R.plurals.zen_mode_summary_priority_by_minute;
+                byHour = R.plurals.zen_mode_summary_priority_by_hour;
+                break;
+            default:
+            case Global.ZEN_MODE_OFF:
+                indefinite = R.string.zen_mode_summary_always;
+                break;
+        };
+
+        if (duration == null || duration.mCondition == null) {
+            return getString(indefinite);
+        }
+
+        long time = System.currentTimeMillis() + duration.mMinutes * MINUTES_MS;
+        String skeleton = DateFormat.is24HourFormat(this, UserHandle.myUserId()) ? "Hm" : "hma";
+        String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
+        CharSequence formattedTime = DateFormat.format(pattern, time);
+        Resources res = getResources();
+
+        if (duration.mMinutes < 60) {
+            return res.getQuantityString(byMinute,
+                    duration.mMinutes, duration.mMinutes, formattedTime);
+        } else {
+            int hours = duration.mMinutes / 60;
+            return res.getQuantityString(byHour, hours, hours, formattedTime);
+        }
+    }
+
+    private ConditionSelection getConditionSelection(int minutes) {
+        Condition condition = ZenModeConfig.toTimeCondition(this, minutes, UserHandle.myUserId());
+        Resources res = getResources();
+        if (minutes <= 0) {
+            return null;
+        } else if (minutes < 60) {
+            String label = res.getQuantityString(R.plurals.zen_mode_duration_minutes_voice_label,
+                    minutes, minutes);
+            return new ConditionSelection(condition, minutes, label, Integer.toString(minutes));
+        } else {
+            int hours = minutes / 60;
+            String label = res.getQuantityString(R.plurals.zen_mode_duration_hours_voice_label,
+                    hours, hours);
+            return new ConditionSelection(condition, minutes, label, Integer.toString(hours));
+        }
+    }
+
+    private static class ConditionSelection extends VoiceSelection {
+        Condition mCondition;
+        int mMinutes;
+
+        public ConditionSelection(Condition condition, int minutes, CharSequence label,
+                CharSequence synonyms) {
+            super(label, synonyms);
+            mMinutes = minutes;
+            mCondition = condition;
+        }
+    }
+
+    private static class ModeSelection extends VoiceSelection {
+        int mMode;
+
+        public ModeSelection(Context context, int mode, int label, int synonyms) {
+            super(context.getString(label), context.getString(synonyms));
+            mMode = mode;
+        }
+    }
+}
diff --git a/src/com/android/settings/notification/ZenRuleNameDialog.java b/src/com/android/settings/notification/ZenRuleNameDialog.java
index b0eaaec..8b44e46 100644
--- a/src/com/android/settings/notification/ZenRuleNameDialog.java
+++ b/src/com/android/settings/notification/ZenRuleNameDialog.java
@@ -17,35 +17,71 @@
 package com.android.settings.notification;
 
 import android.app.AlertDialog;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.pm.ServiceInfo;
+import android.net.Uri;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.ArraySet;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
 
 import com.android.settings.R;
 
+import java.util.List;
+
 public abstract class ZenRuleNameDialog {
+    private static final String TAG = ZenModeSettings.TAG;
+    private static final boolean DEBUG = ZenModeSettings.DEBUG;
+
     private final AlertDialog mDialog;
     private final EditText mEditText;
+    private final RadioGroup mTypes;
     private final ArraySet<String> mExistingNames;
+    private final ServiceListing mServiceListing;
+    private final RuleInfo[] mExternalRules = new RuleInfo[3];
 
-    public ZenRuleNameDialog(Context context, String ruleName, ArraySet<String> existingNames) {
+    public ZenRuleNameDialog(Context context, ServiceListing serviceListing, String ruleName,
+            ArraySet<String> existingNames) {
+        mServiceListing = serviceListing;
         final View v = LayoutInflater.from(context).inflate(R.layout.zen_rule_name, null, false);
         mEditText = (EditText) v.findViewById(R.id.rule_name);
-        mEditText.setText(ruleName);
+        if (ruleName != null) {
+            mEditText.setText(ruleName);
+        }
         mEditText.setSelectAllOnFocus(true);
+        mTypes = (RadioGroup) v.findViewById(R.id.rule_types);
+        if (mServiceListing != null) {
+            bindType(R.id.rule_type_schedule, defaultNewSchedule());
+            bindExternalRules();
+            mServiceListing.addCallback(mServiceListingCallback);
+            mServiceListing.reload();
+        }
         mDialog = new AlertDialog.Builder(context)
                 .setTitle(R.string.zen_mode_rule_name)
                 .setView(v)
                 .setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int which) {
-                        onOk(trimmedText());
+                        onOk(trimmedText(), selectedRuleInfo());
+                    }
+                })
+                .setOnDismissListener(new OnDismissListener() {
+                    @Override
+                    public void onDismiss(DialogInterface dialog) {
+                        if (mServiceListing != null) {
+                            mServiceListing.removeCallback(mServiceListingCallback);
+                        }
                     }
                 })
                 .setNegativeButton(R.string.cancel, null)
@@ -72,17 +108,37 @@
         }
     }
 
-    abstract public void onOk(String ruleName);
-
-    private String trimmedText() {
-        return mEditText.getText() == null ? null : mEditText.getText().toString().trim();
-    }
+    abstract public void onOk(String ruleName, RuleInfo ruleInfo);
 
     public void show() {
         mDialog.show();
         updatePositiveButton();
     }
 
+    private void bindType(int id, RuleInfo ri) {
+        final RadioButton rb = (RadioButton) mTypes.findViewById(id);
+        if (ri == null) {
+            rb.setVisibility(View.GONE);
+            return;
+        }
+        rb.setVisibility(View.VISIBLE);
+        if (ri.caption != null) {
+            rb.setText(ri.caption);
+        }
+        rb.setTag(ri);
+    }
+
+    private RuleInfo selectedRuleInfo() {
+        final int id = mTypes.getCheckedRadioButtonId();
+        if (id == -1) return null;
+        final RadioButton rb = (RadioButton) mTypes.findViewById(id);
+        return (RuleInfo) rb.getTag();
+    }
+
+    private String trimmedText() {
+        return mEditText.getText() == null ? null : mEditText.getText().toString().trim();
+    }
+
     private void updatePositiveButton() {
         final String name = trimmedText();
         final boolean validName = !TextUtils.isEmpty(name)
@@ -90,4 +146,51 @@
         mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validName);
     }
 
+    private static RuleInfo defaultNewSchedule() {
+        final ScheduleInfo schedule = new ScheduleInfo();
+        schedule.days = ZenModeConfig.ALL_DAYS;
+        schedule.startHour = 22;
+        schedule.endHour = 7;
+        final RuleInfo rt = new RuleInfo();
+        rt.settingsAction = ZenModeScheduleRuleSettings.ACTION;
+        rt.defaultConditionId = ZenModeConfig.toScheduleConditionId(schedule);
+        return rt;
+    }
+
+    private void bindExternalRules() {
+        bindType(R.id.rule_type_2, mExternalRules[0]);
+        bindType(R.id.rule_type_3, mExternalRules[1]);
+        bindType(R.id.rule_type_4, mExternalRules[2]);
+        // show radio group if we have at least one external rule type
+        mTypes.setVisibility(mExternalRules[0] != null ? View.VISIBLE : View.GONE);
+    }
+
+    private final ServiceListing.Callback mServiceListingCallback = new ServiceListing.Callback() {
+        @Override
+        public void onServicesReloaded(List<ServiceInfo> services) {
+            if (DEBUG) Log.d(TAG, "Services reloaded: count=" + services.size());
+            mExternalRules[0] = mExternalRules[1] = mExternalRules[2] = null;
+            int i = 0;
+            for (ServiceInfo si : services) {
+                final RuleInfo ri = ZenModeExternalRuleSettings.getRuleInfo(si);
+                if (ri != null) {
+                    mExternalRules[i] = ri;
+                    i++;
+                    if (i == mExternalRules.length) {
+                        break;
+                    }
+                }
+            }
+            bindExternalRules();
+        }
+    };
+
+    public static class RuleInfo {
+        public String caption;
+        public String settingsAction;
+        public Uri defaultConditionId;
+        public ComponentName serviceComponent;
+        public ComponentName configurationActivity;
+    }
+
 }
\ No newline at end of file
diff --git a/src/com/android/settings/utils/VoiceSelection.java b/src/com/android/settings/utils/VoiceSelection.java
new file mode 100644
index 0000000..997d2cc
--- /dev/null
+++ b/src/com/android/settings/utils/VoiceSelection.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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.settings.utils;
+
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+/**
+ * Model for a single item that can be selected by a {@link VoiceSelectionFragment}.
+ * Each item consists of a visual label and several alternative synonyms for the item
+ * that can be used to identify the item by voice.
+ */
+public class VoiceSelection {
+    final CharSequence mLabel;
+    final CharSequence[] mSynonyms;
+
+    /**
+     * Created a new selectable item with a visual label and a set of synonyms.
+     */
+    public VoiceSelection(CharSequence label, CharSequence synonyms) {
+        mLabel = label;
+        mSynonyms = TextUtils.split(synonyms.toString(), ",");
+    }
+
+    /**
+     * Created a new selectable item with a visual label and no synonyms.
+     */
+    public VoiceSelection(CharSequence label) {
+        mLabel = label;
+        mSynonyms = null;
+    }
+
+    public CharSequence getLabel() {
+        return mLabel;
+    }
+
+    public CharSequence[] getSynonyms() {
+        return mSynonyms;
+    }
+
+    Option toOption(int index) {
+        Option result = new Option(mLabel);
+        Bundle extras = new Bundle();
+        extras.putInt("index", index);
+        result.setExtras(extras);
+
+        for (CharSequence synonym : mSynonyms) {
+            result.addSynonym(synonym);
+        }
+        return result;
+    }
+
+    /**
+     * Listener interface for when an item is selected.
+     */
+    public interface OnItemSelectedListener {
+        abstract void onItemSelected(int position, VoiceSelection selection);
+    };
+}
diff --git a/src/com/android/settings/utils/VoiceSelectionAdapter.java b/src/com/android/settings/utils/VoiceSelectionAdapter.java
new file mode 100644
index 0000000..2c060c2
--- /dev/null
+++ b/src/com/android/settings/utils/VoiceSelectionAdapter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 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.settings.utils;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import android.app.Activity;
+import com.android.settings.R;
+
+import java.util.List;
+import android.util.Log;
+
+/**
+ * Array adapter for selecting an item by voice interaction. Each row includes a visual
+ * indication of the 1-indexed position of the item so that a user can easily say
+ * "number 4" to select it.
+ */
+public class VoiceSelectionAdapter extends ArrayAdapter<VoiceSelection> {
+    public VoiceSelectionAdapter(Context context, int resource, List<VoiceSelection> objects) {
+        super(context, resource, objects);
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        VoiceSelection item = getItem(position);
+        View row = convertView;
+        if (row == null) {
+            LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater();
+            row = inflater.inflate(R.layout.voice_item_row, parent, false);
+        }
+
+        TextView label = (TextView) row.findViewById(R.id.voice_item_label);
+        if (label != null) {
+            label.setText(item.getLabel());
+        }
+
+        TextView positionLabel = (TextView) row.findViewById(R.id.voice_item_position);
+        if (positionLabel != null) {
+            positionLabel.setText(Integer.toString(position + 1));
+        }
+
+        return row;
+    }
+};
diff --git a/src/com/android/settings/utils/VoiceSelectionFragment.java b/src/com/android/settings/utils/VoiceSelectionFragment.java
new file mode 100644
index 0000000..c2e80d3
--- /dev/null
+++ b/src/com/android/settings/utils/VoiceSelectionFragment.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 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.settings.utils;
+
+import android.app.ListFragment;
+import android.app.VoiceInteractor;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import java.util.List;
+
+/**
+ * An Activity fragment that presents a set of options as a visual list and also allows
+ * items to be selected by the users voice.
+ */
+public class VoiceSelectionFragment extends ListFragment {
+    private static final String EXTRA_SELECTION_PROMPT = "selection_prompt";
+
+    private CharSequence mPrompt = null;
+    private VoiceInteractor.Request mRequest = null;
+    private VoiceInteractor mVoiceInteractor = null;
+    private VoiceSelection.OnItemSelectedListener mOnItemSelectedListener = null;
+
+    /**
+     * No-args ctor required for fragment.
+     */
+    public VoiceSelectionFragment() {}
+
+    @Override
+    public void onCreate(Bundle args) {
+        super.onCreate(args);
+        mPrompt = getArguments().getCharSequence(EXTRA_SELECTION_PROMPT);
+    }
+
+    /**
+     * Set the prompt spoken when the fragment is presented.
+     */
+    static public Bundle createArguments(CharSequence prompt) {
+        Bundle args = new Bundle();
+        args.putCharSequence(EXTRA_SELECTION_PROMPT, prompt);
+        return args;
+    }
+
+    private VoiceSelection getSelectionAt(int position) {
+        return ((ArrayAdapter<VoiceSelection>) getListAdapter()).getItem(position);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        final int numItems = getListAdapter().getCount();
+        if (numItems <= 0) {
+            return;
+        }
+
+        Option[] options = new Option[numItems];
+        for (int idx = 0; idx < numItems; idx++) {
+            options[idx] = getSelectionAt(idx).toOption(idx);
+        }
+        mRequest = new PickOptionRequest(mPrompt, options, null) {
+            @Override
+            public void onPickOptionResult(boolean isComplete, Option[] options, Bundle args) {
+                if (!isComplete || options == null) {
+                    return;
+                }
+                if (options.length == 1 && mOnItemSelectedListener != null) {
+                    int idx = options[0].getExtras().getInt("index", -1);
+                    mOnItemSelectedListener.onItemSelected(idx, getSelectionAt(idx));
+                } else {
+                    onCancel();
+                }
+            }
+        };
+        mVoiceInteractor = getActivity().getVoiceInteractor();
+        if (mVoiceInteractor != null) {
+            mVoiceInteractor.submitRequest(mRequest);
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mVoiceInteractor = null;
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        if (mRequest != null) {
+            mRequest.cancel();
+            mRequest = null;
+        }
+
+        if (mOnItemSelectedListener != null) {
+          mOnItemSelectedListener.onItemSelected(position, getSelectionAt(position));
+        }
+    }
+
+
+    /**
+     * Sets the selection handler for an item either by voice or by touch.
+     */
+    public void setOnItemSelectedHandler(VoiceSelection.OnItemSelectedListener listener) {
+        mOnItemSelectedListener = listener;
+    }
+
+    /**
+     * Called when the user cancels the interaction. The default implementation is to
+     * finish the activity.
+     */
+    public void onCancel() {
+        getActivity().finish();
+    }
+};
diff --git a/src/com/android/settings/utils/VoiceSettingsActivity.java b/src/com/android/settings/utils/VoiceSettingsActivity.java
new file mode 100644
index 0000000..ac5b8be
--- /dev/null
+++ b/src/com/android/settings/utils/VoiceSettingsActivity.java
@@ -0,0 +1,74 @@
+/*
+ * 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.settings.utils;
+
+import android.app.Activity;
+import android.app.VoiceInteractor;
+import android.app.VoiceInteractor.CompleteVoiceRequest;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Activity for modifying a setting using the Voice Interaction API. This activity
+ * will only allow modifying the setting if the intent was sent using
+ * {@link android.service.voice.VoiceInteractionSession#startVoiceActivity startVoiceActivity}
+ * by the current Voice Interaction Service.
+ */
+abstract public class VoiceSettingsActivity extends Activity {
+
+    private static final String TAG = "VoiceSettingsActivity";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (isVoiceInteraction() || savedInstanceState == null) {
+            // Only permit if this is a voice interaction.
+            if (onVoiceSettingInteraction(getIntent())) {
+                // If it's complete, finish.
+                finish();
+            }
+        } else {
+            Log.v(TAG, "Cannot modify settings without voice interaction");
+            finish();
+        }
+    }
+
+    /**
+     * Modify the setting as a voice interaction. Should return true if the
+     * voice interaction is complete or false if more interaction is required.
+     */
+    abstract protected boolean onVoiceSettingInteraction(Intent intent);
+
+    /**
+     * Send a notification that the interaction was successful. If {@link prompt} is
+     * not null, then it will be read to the user.
+     */
+    protected void notifySuccess(CharSequence prompt) {
+        if (getVoiceInteractor() != null) {
+            getVoiceInteractor().submitRequest(new CompleteVoiceRequest(prompt, null));
+        }
+    }
+
+    /**
+     * Indicates when the setting could not be changed.
+     */
+    protected void notifyFailure(String reason) {
+        getVoiceInteractor().submitRequest(new VoiceInteractor.AbortVoiceRequest(reason, null));
+    }
+}