Add activities to support voice settings.

Adds Voice Interaction Activities for:
- Do not disturb mode (with interaction)
- Battery Saver mode
- Airplane mode

Change-Id: I4480dc3a30975d94b71714ff58fbeebddfbc1c58
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index af9c3b4..7112f13 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"
@@ -972,6 +983,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"
@@ -1848,6 +1869,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/values/colors.xml b/res/values/colors.xml
index 97ba2ed..6e97a2a 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 cc01c45..133f6f5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -5989,6 +5989,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/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/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/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/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));
+    }
+}