OSDN Git Service

Unify voice interactor and recognizer settings.
authorDianne Hackborn <hackbod@google.com>
Sat, 19 Jul 2014 02:20:11 +0000 (19:20 -0700)
committerDianne Hackborn <hackbod@google.com>
Tue, 22 Jul 2014 03:14:43 +0000 (20:14 -0700)
There is now one settings UI to select both the new
voice interactor and old voice recognizer.

There are still a few wonky things about this that won't
be resolved until we start requiring that all interactors
specify an associated recognizer service.

Change-Id: Ib702ff717fb28bcb244cb30e49577066ddc9f197

res/values/strings.xml
res/xml/language_settings.xml
res/xml/voice_input_settings.xml [new file with mode: 0644]
src/com/android/settings/VoiceInputOutputSettings.java
src/com/android/settings/voice/VoiceInputHelper.java [new file with mode: 0644]
src/com/android/settings/voice/VoiceInputPreference.java [new file with mode: 0644]
src/com/android/settings/voice/VoiceInputSettings.java [new file with mode: 0644]

index a16c791..bbce4e4 100644 (file)
     <string name="keyboard_settings_title">Android keyboard</string>
     <!-- Title for the 'voice input' category of voice input/output settings -->
     <string name="voice_category">Speech</string>
-    <!-- Title for the voice interactor setting in voice input/output settings -->
-    <string name="voice_interactor_title">Voice input</string>
-    <!-- Title for the link to settings for the chosen voice interactor in voice input/output
-         settings -->
-    <string name="voice_interactor_settings_title">Voice input</string>
-    <!-- Summary for the link to settings for the chosen voice interactor in voice input/output
-         settings.  Would say something like, e.g., "Settings for 'Google'". -->
-    <string name="voice_interactor_settings_summary">Settings for
-        \'<xliff:g id="interactor_name">%s</xliff:g>\'</string>
-    <!-- Label to show for no voice interactor selector -->
-    <string name="no_voice_interactor">None</string>
-    <!-- Title for the voice recognizer setting in voice input/output settings -->
-    <string name="recognizer_title">Voice recognizer</string>
-    <!-- Title for the link to settings for the chosen voice recognizer in voice input/output settings -->
-    <string name="recognizer_settings_title">Voice Search</string>
-    <!-- Summary for the link to settings for the chosen voice recognizer in voice input/output settings.
-         Would say something like, e.g., "Settings for 'Google'". -->
-    <string name="recognizer_settings_summary">Settings for \'<xliff:g id="recognizer_name">%s</xliff:g>\'</string>
+
+    <!-- Voice input settings --><skip />
+    <!-- [CHAR_LIMIT=NONE] Name of the settings item to open the voice input settings. -->
+    <string name="voice_input_settings">Voice input settings</string>
+    <!-- [CHAR_LIMIT=NONE] Title of the screen of the voice input settings -->
+    <string name="voice_input_settings_title">Voice input</string>
+    <!-- [CHAR LIMIT=50] The text for the settings section in which users select
+         a voice interaction or recognition service to use. -->
+    <string name="voice_service_preference_section_title">Voice input services</string>
+    <!-- [CHAR LIMIT=NONE] The summary text for the voice service preference that is
+         a full voice interaction service. -->
+    <string name="voice_interactor_preference_summary">Full voice interaction</string>
+    <!-- [CHAR LIMIT=NONE] The summary text for the voice service preference that is
+         a simple voice recognition service. -->
+    <string name="voice_recognizer_preference_summary">Simple voice recognition</string>
+    <!-- [CHAR_LIMIT=NONE] Warning message about security implications of enabling a
+         voice interaction service, displayed as a dialog
+         message when the user selects to enable a service. -->
+    <string name="voice_interaction_security_warning">This voice input service will be able to
+        control all voice enabled applications on your behalf.
+        It comes from the <xliff:g id="voice_input_service_app_name">%s</xliff:g> application.
+        Enable the use of this service?</string>
 
     <!-- Text-To-Speech (TTS) settings --><skip />
     <!-- Name of the TTS package as listed by the package manager. -->
     <string name="keywords_storage">space disk hard drive</string>
     <string name="keywords_battery">power</string>
     <string name="keywords_spell_checker">spelling</string>
+    <string name="keywords_voice_input">recognizer input speech speak language hands-free hand free recognition offensive word audio history bluetooth headset</string>
     <string name="keywords_text_to_speech_output">rate language default speak speaking</string>
     <string name="keywords_date_and_time">clock</string>
     <string name="keywords_factory_data_reset">wipe delete</string>
     <string name="keywords_accounts">account</string>
     <string name="keywords_users">restriction restrict restricted</string>
     <string name="keywords_keyboard_and_ime">text correction correct sound vibrate auto language gesture suggest suggestion theme offensive word type emoji</string>
-    <string name="keywords_search_voice">language hands-free hand free recognition offensive word audio history bluetooth headset</string>
 
     <!-- NFC Wi-Fi pairing/setup strings-->
 
index b493a0f..9d12857 100644 (file)
             android:key="voice_category"
             android:title="@string/voice_category" >
 
-        <!-- entries, entryValues, and defaultValue will be populated programmatically. -->
-        <ListPreference
-                android:key="voice_interactor"
-                android:title="@string/voice_interactor_title"
-                android:dialogTitle="@string/voice_interactor_title"
-                />
-
-        <!-- An intent for this preference will be populated programmatically. -->
-        <PreferenceScreen
-                android:key="voice_interactor_settings"
-                android:title="@string/voice_interactor_settings_title"
-                />
-
-        <!-- entries, entryValues, and defaultValue will be populated programmatically. -->
-        <ListPreference
-                android:key="recognizer"
-                android:title="@string/recognizer_title"
-                android:dialogTitle="@string/recognizer_title"
-                />
-
-        <!-- An intent for this preference will be populated programmatically. -->
         <PreferenceScreen
-                android:key="recognizer_settings"
-                android:title="@string/recognizer_settings_title"
-                settings:keywords="@string/keywords_search_voice"
+                android:key="voice_input_settings"
+                android:title="@string/voice_input_settings_title"
+                settings:keywords="@string/keywords_voice_input"
+                android:fragment="com.android.settings.voice.VoiceInputSettings"
                 />
 
         <PreferenceScreen
diff --git a/res/xml/voice_input_settings.xml b/res/xml/voice_input_settings.xml
new file mode 100644 (file)
index 0000000..47d72ec
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        android:title="@string/voice_input_settings_title">
+
+    <!-- The contents of this category are filled in by the Java code
+         based on the list of available voice interaction and recognition services. -->
+    <PreferenceCategory android:key="voice_service_preference_section"
+        android:title="@string/voice_service_preference_section_title" />
+
+</PreferenceScreen>
index 64f8a09..e052f8e 100644 (file)
 
 package com.android.settings;
 
-import android.Manifest;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.preference.Preference.OnPreferenceChangeListener;
-import android.provider.Settings;
-import android.service.voice.VoiceInteractionService;
-import android.speech.RecognitionService;
 import android.speech.tts.TtsEngines;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.Xml;
 
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
+import com.android.settings.voice.VoiceInputHelper;
 
 /**
  * Settings screen for voice input/output.
  */
-public class VoiceInputOutputSettings implements OnPreferenceChangeListener {
+public class VoiceInputOutputSettings {
 
     private static final String TAG = "VoiceInputOutputSettings";
 
     private static final String KEY_VOICE_CATEGORY = "voice_category";
-    private static final String KEY_VOICE_INTERACTOR = "voice_interactor";
-    private static final String KEY_VOICE_INTERACTOR_SETTINGS = "voice_interactor_settings";
-    private static final String KEY_RECOGNIZER = "recognizer";
-    private static final String KEY_RECOGNIZER_SETTINGS = "recognizer_settings";
+    private static final String KEY_VOICE_INPUT_SETTINGS = "voice_input_settings";
     private static final String KEY_TTS_SETTINGS = "tts_settings";
 
     private PreferenceGroup mParent;
-    private ListPreference mVoiceInteractionPref;
-    private PreferenceScreen mVoiceInteractionSettingsPref;
     private PreferenceCategory mVoiceCategory;
-    private ListPreference mRecognizerPref;
-    private PreferenceScreen mRecognizerSettingsPref;
+    private Preference mVoiceInputSettingsPref;
     private Preference mTtsSettingsPref;
     private final SettingsPreferenceFragment mFragment;
     private final TtsEngines mTtsEngines;
 
-    private HashMap<String, ResolveInfo> mAvailableVoiceInteractionsMap;
-
-    private HashMap<String, ResolveInfo> mAvailableRecognizersMap;
-
     public VoiceInputOutputSettings(SettingsPreferenceFragment fragment) {
         mFragment = fragment;
         mTtsEngines = new TtsEngines(fragment.getPreferenceScreen().getContext());
@@ -84,28 +50,16 @@ public class VoiceInputOutputSettings implements OnPreferenceChangeListener {
 
         mParent = mFragment.getPreferenceScreen();
         mVoiceCategory = (PreferenceCategory) mParent.findPreference(KEY_VOICE_CATEGORY);
-        mVoiceInteractionPref = (ListPreference) mVoiceCategory.findPreference(
-                KEY_VOICE_INTERACTOR);
-        mVoiceInteractionPref.setOnPreferenceChangeListener(this);
-        mVoiceInteractionSettingsPref = (PreferenceScreen)mVoiceCategory.findPreference(
-                KEY_VOICE_INTERACTOR_SETTINGS);
-        mRecognizerPref = (ListPreference) mVoiceCategory.findPreference(KEY_RECOGNIZER);
-        mRecognizerSettingsPref = (PreferenceScreen)
-                mVoiceCategory.findPreference(KEY_RECOGNIZER_SETTINGS);
-        mRecognizerPref.setOnPreferenceChangeListener(this);
+        mVoiceInputSettingsPref = mVoiceCategory.findPreference(KEY_VOICE_INPUT_SETTINGS);
         mTtsSettingsPref = mVoiceCategory.findPreference(KEY_TTS_SETTINGS);
 
-        mAvailableVoiceInteractionsMap = new HashMap<String, ResolveInfo>();
-        mAvailableRecognizersMap = new HashMap<String, ResolveInfo>();
-
         populateOrRemovePreferences();
     }
 
     private void populateOrRemovePreferences() {
-        boolean hasVoiceInteractionPrefs = populateOrRemoveVoiceInteractionPrefs();
-        boolean hasRecognizerPrefs = populateOrRemoveRecognizerPrefs();
+        boolean hasVoiceInputPrefs = populateOrRemoveVoiceInputPrefs();
         boolean hasTtsPrefs = populateOrRemoveTtsPrefs();
-        if (!hasVoiceInteractionPrefs && !hasRecognizerPrefs && !hasTtsPrefs) {
+        if (!hasVoiceInputPrefs && !hasTtsPrefs) {
             // There were no TTS settings and no recognizer settings,
             // so it should be safe to hide the preference category
             // entirely.
@@ -113,68 +67,13 @@ public class VoiceInputOutputSettings implements OnPreferenceChangeListener {
         }
     }
 
-    private boolean populateOrRemoveVoiceInteractionPrefs() {
-        List<ResolveInfo> availableVoiceServices =
-                mFragment.getPackageManager().queryIntentServices(
-                        new Intent(VoiceInteractionService.SERVICE_INTERFACE),
-                        PackageManager.GET_META_DATA);
-        for (int i=0; i<availableVoiceServices.size(); i++) {
-            ResolveInfo ri = availableVoiceServices.get(i);
-            if (!Manifest.permission.BIND_VOICE_INTERACTION.equals(ri.serviceInfo.permission)) {
-                availableVoiceServices.remove(i);
-            }
-        }
-        int numAvailable = availableVoiceServices.size();
-
-        if (numAvailable == 0) {
-            mVoiceCategory.removePreference(mVoiceInteractionPref);
-            mVoiceCategory.removePreference(mVoiceInteractionSettingsPref);
+    private boolean populateOrRemoveVoiceInputPrefs() {
+        VoiceInputHelper helper = new VoiceInputHelper(mFragment.getActivity());
+        if (!helper.hasItems()) {
+            mVoiceCategory.removePreference(mVoiceInputSettingsPref);
             return false;
         }
 
-        populateVoiceInteractionPreference(availableVoiceServices);
-
-        // In this case, there was at least one available recognizer so
-        // we populated the settings.
-        return true;
-    }
-
-    private boolean populateOrRemoveRecognizerPrefs() {
-        List<ResolveInfo> availableRecognitionServices =
-                mFragment.getPackageManager().queryIntentServices(
-                        new Intent(RecognitionService.SERVICE_INTERFACE),
-                        PackageManager.GET_META_DATA);
-        int numAvailable = availableRecognitionServices.size();
-
-        if (numAvailable == 0) {
-            mVoiceCategory.removePreference(mRecognizerPref);
-            mVoiceCategory.removePreference(mRecognizerSettingsPref);
-            return false;
-        }
-
-        if (numAvailable == 1) {
-            // Only one recognizer available, so don't show the list of choices, but do
-            // set up the link to settings for the available recognizer.
-            mVoiceCategory.removePreference(mRecognizerPref);
-
-            // But first set up the available recognizers map with just the one recognizer.
-            ResolveInfo resolveInfo = availableRecognitionServices.get(0);
-            String recognizerComponent =
-                new ComponentName(resolveInfo.serviceInfo.packageName,
-                        resolveInfo.serviceInfo.name).flattenToShortString();
-
-            mAvailableRecognizersMap.put(recognizerComponent, resolveInfo);
-
-            String currentSetting = Settings.Secure.getString(
-                    mFragment.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE);
-            updateRecognizerSettingsLink(currentSetting);
-        } else {
-            // Multiple recognizers available, so show the full list of choices.
-            populateRecognizerPreference(availableRecognitionServices);
-        }
-
-        // In this case, there was at least one available recognizer so
-        // we populated the settings.
         return true;
     }
 
@@ -186,222 +85,4 @@ public class VoiceInputOutputSettings implements OnPreferenceChangeListener {
 
         return true;
     }
-
-    private void populateVoiceInteractionPreference(List<ResolveInfo> voiceInteractors) {
-        int size = voiceInteractors.size();
-        CharSequence[] entries = new CharSequence[size+1];
-        CharSequence[] values = new CharSequence[size+1];
-
-        // Get the current value from the secure setting.
-        String currentSetting = Settings.Secure.getString(
-                mFragment.getContentResolver(), Settings.Secure.VOICE_INTERACTION_SERVICE);
-
-        // Iterate through all the available recognizers and load up their info to show
-        // in the preference. Also build up a map of recognizer component names to their
-        // ResolveInfos - we'll need that a little later.
-        for (int i = 0; i < size; i++) {
-            ResolveInfo resolveInfo = voiceInteractors.get(i);
-            String recognizerComponent =
-                    new ComponentName(resolveInfo.serviceInfo.packageName,
-                            resolveInfo.serviceInfo.name).flattenToShortString();
-
-            mAvailableVoiceInteractionsMap.put(recognizerComponent, resolveInfo);
-
-            entries[i] = resolveInfo.loadLabel(mFragment.getPackageManager());
-            values[i] = recognizerComponent;
-        }
-
-        entries[size] = mFragment.getString(R.string.no_voice_interactor);
-        values[size] = "";
-
-        mVoiceInteractionPref.setEntries(entries);
-        mVoiceInteractionPref.setEntryValues(values);
-
-        mVoiceInteractionPref.setDefaultValue(currentSetting);
-        mVoiceInteractionPref.setValue(currentSetting);
-
-        updateVoiceInteractionSettingsLink(currentSetting);
-    }
-
-    private void updateVoiceInteractionSettingsLink(String currentSetting) {
-        ResolveInfo currentRecognizer = mAvailableVoiceInteractionsMap.get(currentSetting);
-        if (currentRecognizer == null) {
-            mVoiceInteractionPref.setSummary(mFragment.getString(R.string.no_voice_interactor));
-            mVoiceInteractionPref.setValue("");
-            return;
-        }
-
-        ServiceInfo si = currentRecognizer.serviceInfo;
-        XmlResourceParser parser = null;
-        String settingsActivity = null;
-        try {
-            parser = si.loadXmlMetaData(mFragment.getPackageManager(),
-                    VoiceInteractionService.SERVICE_META_DATA);
-            if (parser == null) {
-                throw new XmlPullParserException("No " + VoiceInteractionService.SERVICE_META_DATA +
-                        " meta-data for " + si.packageName);
-            }
-
-            Resources res = mFragment.getPackageManager().getResourcesForApplication(
-                    si.applicationInfo);
-
-            AttributeSet attrs = Xml.asAttributeSet(parser);
-
-            int type;
-            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
-                    && type != XmlPullParser.START_TAG) {
-            }
-
-            String nodeName = parser.getName();
-            if (!"voice-interaction-service".equals(nodeName)) {
-                throw new XmlPullParserException(
-                        "Meta-data does not start with voice-interaction-service tag");
-            }
-
-            TypedArray array = res.obtainAttributes(attrs,
-                    com.android.internal.R.styleable.VoiceInteractionService);
-            settingsActivity = array.getString(
-                    com.android.internal.R.styleable.VoiceInteractionService_settingsActivity);
-            array.recycle();
-        } catch (XmlPullParserException e) {
-            Log.e(TAG, "error parsing recognition service meta-data", e);
-        } catch (IOException e) {
-            Log.e(TAG, "error parsing recognition service meta-data", e);
-        } catch (NameNotFoundException e) {
-            Log.e(TAG, "error parsing recognition service meta-data", e);
-        } finally {
-            if (parser != null) parser.close();
-        }
-
-        mVoiceInteractionPref.setSummary(currentRecognizer.loadLabel(
-                mFragment.getPackageManager()));
-        mVoiceInteractionPref.setValue(currentSetting);
-
-        if (settingsActivity == null) {
-            // No settings preference available - hide the preference.
-            Log.w(TAG, "no recognizer settings available for " + si.packageName);
-        } else {
-            Intent i = new Intent(Intent.ACTION_MAIN);
-            i.setComponent(new ComponentName(si.packageName, settingsActivity));
-            mVoiceInteractionSettingsPref.setIntent(i);
-        }
-    }
-
-    private void populateRecognizerPreference(List<ResolveInfo> recognizers) {
-        int size = recognizers.size();
-        CharSequence[] entries = new CharSequence[size];
-        CharSequence[] values = new CharSequence[size];
-        
-        // Get the current value from the secure setting.
-        String currentSetting = Settings.Secure.getString(
-                mFragment.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE);
-        
-        // Iterate through all the available recognizers and load up their info to show
-        // in the preference. Also build up a map of recognizer component names to their
-        // ResolveInfos - we'll need that a little later.
-        for (int i = 0; i < size; i++) {
-            ResolveInfo resolveInfo = recognizers.get(i);
-            String recognizerComponent =
-                    new ComponentName(resolveInfo.serviceInfo.packageName,
-                            resolveInfo.serviceInfo.name).flattenToShortString();
-            
-            mAvailableRecognizersMap.put(recognizerComponent, resolveInfo);
-
-            entries[i] = resolveInfo.loadLabel(mFragment.getPackageManager());
-            values[i] = recognizerComponent;
-        }
-        
-        mRecognizerPref.setEntries(entries);
-        mRecognizerPref.setEntryValues(values);
-        
-        mRecognizerPref.setDefaultValue(currentSetting);
-        mRecognizerPref.setValue(currentSetting);
-        
-        updateRecognizerSettingsLink(currentSetting);
-    }
-
-    private void updateRecognizerSettingsLink(String currentSetting) {
-        ResolveInfo currentRecognizer = mAvailableRecognizersMap.get(currentSetting);
-        if (currentRecognizer == null) return;
-
-        ServiceInfo si = currentRecognizer.serviceInfo;
-        XmlResourceParser parser = null;
-        String settingsActivity = null;
-        try {
-            parser = si.loadXmlMetaData(mFragment.getPackageManager(),
-                    RecognitionService.SERVICE_META_DATA);
-            if (parser == null) {
-                throw new XmlPullParserException("No " + RecognitionService.SERVICE_META_DATA +
-                        " meta-data for " + si.packageName);
-            }
-            
-            Resources res = mFragment.getPackageManager().getResourcesForApplication(
-                    si.applicationInfo);
-            
-            AttributeSet attrs = Xml.asAttributeSet(parser);
-            
-            int type;
-            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
-                    && type != XmlPullParser.START_TAG) {
-            }
-            
-            String nodeName = parser.getName();
-            if (!"recognition-service".equals(nodeName)) {
-                throw new XmlPullParserException(
-                        "Meta-data does not start with recognition-service tag");
-            }
-            
-            TypedArray array = res.obtainAttributes(attrs,
-                    com.android.internal.R.styleable.RecognitionService);
-            settingsActivity = array.getString(
-                    com.android.internal.R.styleable.RecognitionService_settingsActivity);
-            array.recycle();
-        } catch (XmlPullParserException e) {
-            Log.e(TAG, "error parsing recognition service meta-data", e);
-        } catch (IOException e) {
-            Log.e(TAG, "error parsing recognition service meta-data", e);
-        } catch (NameNotFoundException e) {
-            Log.e(TAG, "error parsing recognition service meta-data", e);
-        } finally {
-            if (parser != null) parser.close();
-        }
-        
-        if (settingsActivity == null) {
-            // No settings preference available - hide the preference.
-            Log.w(TAG, "no recognizer settings available for " + si.packageName);
-            mRecognizerSettingsPref.setIntent(null);
-            mVoiceCategory.removePreference(mRecognizerSettingsPref);
-        } else {
-            Intent i = new Intent(Intent.ACTION_MAIN);
-            i.setComponent(new ComponentName(si.packageName, settingsActivity));
-            mRecognizerSettingsPref.setIntent(i);
-            mRecognizerPref.setSummary(currentRecognizer.loadLabel(mFragment.getPackageManager()));
-        }
-    }
-
-    public boolean onPreferenceChange(Preference preference, Object newValue) {
-        if (preference == mVoiceInteractionPref) {
-            String setting = (String) newValue;
-
-            // Put the new value back into secure settings.
-            Settings.Secure.putString(mFragment.getContentResolver(),
-                    Settings.Secure.VOICE_INTERACTION_SERVICE,
-                    setting);
-
-            // Update the settings item so it points to the right settings.
-            updateVoiceInteractionSettingsLink(setting);
-
-        } else  if (preference == mRecognizerPref) {
-            String setting = (String) newValue;
-
-            // Put the new value back into secure settings.
-            Settings.Secure.putString(mFragment.getContentResolver(),
-                    Settings.Secure.VOICE_RECOGNITION_SERVICE,
-                    setting);
-
-            // Update the settings item so it points to the right settings.
-            updateRecognizerSettingsLink(setting);
-        }
-        return true;
-    }
 }
diff --git a/src/com/android/settings/voice/VoiceInputHelper.java b/src/com/android/settings/voice/VoiceInputHelper.java
new file mode 100644 (file)
index 0000000..63b891a
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * 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.voice;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.provider.Settings;
+import android.service.voice.VoiceInteractionService;
+import android.service.voice.VoiceInteractionServiceInfo;
+import android.speech.RecognitionService;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class VoiceInputHelper {
+    static final String TAG = "VoiceInputHelper";
+    final Context mContext;
+
+    final List<ResolveInfo> mAvailableVoiceInteractions;
+    final List<ResolveInfo> mAvailableRecognition;
+
+    static public class BaseInfo implements Comparable {
+        public final ServiceInfo service;
+        public final ComponentName componentName;
+        public final String key;
+        public final ComponentName settings;
+        public final CharSequence label;
+        public final String labelStr;
+        public final CharSequence appLabel;
+
+        public BaseInfo(PackageManager pm, ServiceInfo _service, String _settings) {
+            service = _service;
+            componentName = new ComponentName(_service.packageName, _service.name);
+            key = componentName.flattenToShortString();
+            settings = _settings != null
+                    ? new ComponentName(_service.packageName, _settings) : null;
+            label = _service.loadLabel(pm);
+            labelStr = label.toString();
+            appLabel = _service.applicationInfo.loadLabel(pm);
+        }
+
+        @Override
+        public int compareTo(Object another) {
+            return labelStr.compareTo(((BaseInfo)another).labelStr);
+        }
+    }
+
+    static public class InteractionInfo extends BaseInfo {
+        public final VoiceInteractionServiceInfo serviceInfo;
+
+        public InteractionInfo(PackageManager pm, VoiceInteractionServiceInfo _service) {
+            super(pm, _service.getServiceInfo(), _service.getSettingsActivity());
+            serviceInfo = _service;
+        }
+    }
+
+    static public class RecognizerInfo extends BaseInfo {
+        public RecognizerInfo(PackageManager pm, ServiceInfo _service, String _settings) {
+            super(pm, _service, _settings);
+        }
+    }
+
+    final ArrayList<InteractionInfo> mAvailableInteractionInfos = new ArrayList<>();
+    final ArrayList<RecognizerInfo> mAvailableRecognizerInfos = new ArrayList<>();
+
+    ComponentName mCurrentVoiceInteraction;
+    ComponentName mCurrentRecognizer;
+
+    public VoiceInputHelper(Context context) {
+        mContext = context;
+
+        mAvailableVoiceInteractions = mContext.getPackageManager().queryIntentServices(
+                        new Intent(VoiceInteractionService.SERVICE_INTERFACE),
+                        PackageManager.GET_META_DATA);
+        mAvailableRecognition = mContext.getPackageManager().queryIntentServices(
+                        new Intent(RecognitionService.SERVICE_INTERFACE),
+                        PackageManager.GET_META_DATA);
+    }
+
+    public boolean hasItems() {
+        return mAvailableVoiceInteractions.size() > 0 || mAvailableRecognition.size() > 0;
+    }
+
+    public void buildUi() {
+        // Get the currently selected interactor from the secure setting.
+        String currentSetting = Settings.Secure.getString(
+                mContext.getContentResolver(), Settings.Secure.VOICE_INTERACTION_SERVICE);
+        if (currentSetting != null && !currentSetting.isEmpty()) {
+            mCurrentVoiceInteraction = ComponentName.unflattenFromString(currentSetting);
+        } else {
+            mCurrentVoiceInteraction = null;
+        }
+
+        ArraySet<ComponentName> interactorRecognizers = new ArraySet<>();
+
+        // Iterate through all the available interactors and load up their info to show
+        // in the preference.
+        int size = mAvailableVoiceInteractions.size();
+        for (int i = 0; i < size; i++) {
+            ResolveInfo resolveInfo = mAvailableVoiceInteractions.get(i);
+            VoiceInteractionServiceInfo info = new VoiceInteractionServiceInfo(
+                    mContext.getPackageManager(), resolveInfo.serviceInfo);
+            if (info.getParseError() != null) {
+                Log.w("VoiceInteractionService", "Error in VoiceInteractionService "
+                        + resolveInfo.serviceInfo.packageName + "/"
+                        + resolveInfo.serviceInfo.name + ": " + info.getParseError());
+                continue;
+            }
+            mAvailableInteractionInfos.add(new InteractionInfo(mContext.getPackageManager(), info));
+            if (info.getRecognitionService() != null) {
+                interactorRecognizers.add(new ComponentName(resolveInfo.serviceInfo.packageName,
+                        info.getRecognitionService()));
+            }
+        }
+        Collections.sort(mAvailableInteractionInfos);
+
+        // Get the currently selected recognizer from the secure setting.
+        currentSetting = Settings.Secure.getString(
+                mContext.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE);
+        if (currentSetting != null && !currentSetting.isEmpty()) {
+            mCurrentRecognizer = ComponentName.unflattenFromString(currentSetting);
+        } else {
+            mCurrentRecognizer = null;
+        }
+
+        // Iterate through all the available recognizers and load up their info to show
+        // in the preference.
+        size = mAvailableRecognition.size();
+        for (int i = 0; i < size; i++) {
+            ResolveInfo resolveInfo = mAvailableRecognition.get(i);
+            ComponentName comp = new ComponentName(resolveInfo.serviceInfo.packageName,
+                    resolveInfo.serviceInfo.name);
+            if (interactorRecognizers.contains(comp)) {
+                //continue;
+            }
+            ServiceInfo si = resolveInfo.serviceInfo;
+            XmlResourceParser parser = null;
+            String settingsActivity = null;
+            try {
+                parser = si.loadXmlMetaData(mContext.getPackageManager(),
+                        RecognitionService.SERVICE_META_DATA);
+                if (parser == null) {
+                    throw new XmlPullParserException("No " + RecognitionService.SERVICE_META_DATA +
+                            " meta-data for " + si.packageName);
+                }
+
+                Resources res = mContext.getPackageManager().getResourcesForApplication(
+                        si.applicationInfo);
+
+                AttributeSet attrs = Xml.asAttributeSet(parser);
+
+                int type;
+                while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+                        && type != XmlPullParser.START_TAG) {
+                }
+
+                String nodeName = parser.getName();
+                if (!"recognition-service".equals(nodeName)) {
+                    throw new XmlPullParserException(
+                            "Meta-data does not start with recognition-service tag");
+                }
+
+                TypedArray array = res.obtainAttributes(attrs,
+                        com.android.internal.R.styleable.RecognitionService);
+                settingsActivity = array.getString(
+                        com.android.internal.R.styleable.RecognitionService_settingsActivity);
+                array.recycle();
+            } catch (XmlPullParserException e) {
+                Log.e(TAG, "error parsing recognition service meta-data", e);
+            } catch (IOException e) {
+                Log.e(TAG, "error parsing recognition service meta-data", e);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "error parsing recognition service meta-data", e);
+            } finally {
+                if (parser != null) parser.close();
+            }
+            mAvailableRecognizerInfos.add(new RecognizerInfo(mContext.getPackageManager(),
+                    resolveInfo.serviceInfo, settingsActivity));
+        }
+        Collections.sort(mAvailableRecognizerInfos);
+    }
+}
diff --git a/src/com/android/settings/voice/VoiceInputPreference.java b/src/com/android/settings/voice/VoiceInputPreference.java
new file mode 100644 (file)
index 0000000..0ebffbb
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * 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.voice;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.preference.Preference;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Checkable;
+import android.widget.CompoundButton;
+import android.widget.RadioButton;
+
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+
+public final class VoiceInputPreference extends Preference {
+
+    private static final String TAG = "VoiceInputPreference";
+
+    private final CharSequence mLabel;
+
+    private final CharSequence mAppLabel;
+
+    private final CharSequence mAlertText;
+
+    private final ComponentName mSettingsComponent;
+
+    /**
+     * The shared radio button state, which button is checked etc.
+     */
+    private final RadioButtonGroupState mSharedState;
+
+    /**
+     * When true, the change callbacks on the radio button will not
+     * fire.
+     */
+    private volatile boolean mPreventRadioButtonCallbacks;
+
+    private View mSettingsIcon;
+    private RadioButton mRadioButton;
+
+    private final CompoundButton.OnCheckedChangeListener mRadioChangeListener =
+        new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                onRadioButtonClicked(buttonView, isChecked);
+            }
+        };
+
+    public VoiceInputPreference(Context context, VoiceInputHelper.BaseInfo info,
+            CharSequence summary, CharSequence alertText, RadioButtonGroupState state) {
+        super(context);
+        setLayoutResource(R.layout.preference_tts_engine);
+
+        mSharedState = state;
+        mLabel = info.label;
+        mAppLabel = info.appLabel;
+        mAlertText = alertText;
+        mSettingsComponent = info.settings;
+        mPreventRadioButtonCallbacks = false;
+
+        setKey(info.key);
+        setTitle(info.label);
+        setSummary(summary);
+    }
+
+    @Override
+    public View getView(View convertView, ViewGroup parent) {
+        if (mSharedState == null) {
+            throw new IllegalStateException("Call to getView() before a call to" +
+                    "setSharedState()");
+        }
+
+        View view = super.getView(convertView, parent);
+        final RadioButton rb = (RadioButton) view.findViewById(R.id.tts_engine_radiobutton);
+        rb.setOnCheckedChangeListener(mRadioChangeListener);
+
+        boolean isChecked = getKey().equals(mSharedState.getCurrentKey());
+        if (isChecked) {
+            mSharedState.setCurrentChecked(rb);
+        }
+
+        mPreventRadioButtonCallbacks = true;
+        rb.setChecked(isChecked);
+        mPreventRadioButtonCallbacks = false;
+
+        mRadioButton = rb;
+
+        View textLayout = view.findViewById(R.id.tts_engine_pref_text);
+        textLayout.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                onRadioButtonClicked(rb, !rb.isChecked());
+            }
+        });
+
+        mSettingsIcon = view.findViewById(R.id.tts_engine_settings);
+        mSettingsIcon.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(Intent.ACTION_MAIN);
+                intent.setComponent(mSettingsComponent);
+                getContext().startActivity(new Intent(intent));
+            }
+        });
+        updateCheckedState(isChecked);
+
+        return view;
+    }
+
+    private boolean shouldDisplayAlert() {
+        return mAlertText != null;
+    }
+
+    private void displayAlert(
+            final DialogInterface.OnClickListener positiveOnClickListener,
+            final DialogInterface.OnClickListener negativeOnClickListener) {
+        Log.i(TAG, "Displaying data alert for :" + getKey());
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+        String msg = String.format(getContext().getResources().getConfiguration().locale,
+                mAlertText.toString(), mAppLabel);
+        builder.setTitle(android.R.string.dialog_alert_title)
+                .setMessage(msg)
+                .setCancelable(true)
+                .setPositiveButton(android.R.string.ok, positiveOnClickListener)
+                .setNegativeButton(android.R.string.cancel, negativeOnClickListener)
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    @Override public void onCancel(DialogInterface dialog) {
+                        negativeOnClickListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
+                    }
+                });
+
+        AlertDialog dialog = builder.create();
+        dialog.show();
+    }
+
+    public void doClick() {
+        mRadioButton.performClick();
+    }
+
+    void updateCheckedState(boolean isChecked) {
+        if (mSettingsComponent != null) {
+            mSettingsIcon.setVisibility(View.VISIBLE);
+            if (isChecked) {
+                mSettingsIcon.setEnabled(true);
+                mSettingsIcon.setAlpha(1);
+            } else {
+                mSettingsIcon.setEnabled(false);
+                mSettingsIcon.setAlpha(Utils.DISABLED_ALPHA);
+            }
+        } else {
+            mSettingsIcon.setVisibility(View.GONE);
+        }
+    }
+
+    void onRadioButtonClicked(final CompoundButton buttonView, boolean isChecked) {
+        if (mPreventRadioButtonCallbacks) {
+            return;
+        }
+        if (mSharedState.getCurrentChecked() == buttonView) {
+            updateCheckedState(isChecked);
+            return;
+        }
+
+        if (isChecked) {
+            // Should we alert user? if that's true, delay making engine current one.
+            if (shouldDisplayAlert()) {
+                displayAlert(new DialogInterface.OnClickListener() {
+                                 @Override
+                                 public void onClick(DialogInterface dialog, int which) {
+                                     makeCurrentChecked(buttonView);
+                                 }
+                             }, new DialogInterface.OnClickListener() {
+                                 @Override
+                                 public void onClick(DialogInterface dialog, int which) {
+                                     // Undo the click.
+                                     buttonView.setChecked(false);
+                                 }
+                             }
+                );
+            } else {
+                // Privileged engine, set it current
+                makeCurrentChecked(buttonView);
+            }
+        } else {
+            updateCheckedState(isChecked);
+        }
+    }
+
+    void makeCurrentChecked(Checkable current) {
+        if (mSharedState.getCurrentChecked() != null) {
+            mSharedState.getCurrentChecked().setChecked(false);
+        }
+        mSharedState.setCurrentChecked(current);
+        mSharedState.setCurrentKey(getKey());
+        updateCheckedState(true);
+        callChangeListener(mSharedState.getCurrentKey());
+    }
+
+    /**
+     * Holds all state that is common to this group of radio buttons, such
+     * as the currently selected key and the currently checked compound button.
+     * (which corresponds to this key).
+     */
+    public interface RadioButtonGroupState {
+        String getCurrentKey();
+        Checkable getCurrentChecked();
+
+        void setCurrentKey(String key);
+        void setCurrentChecked(Checkable current);
+    }
+}
diff --git a/src/com/android/settings/voice/VoiceInputSettings.java b/src/com/android/settings/voice/VoiceInputSettings.java
new file mode 100644 (file)
index 0000000..309c6e9
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * 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.voice;
+
+import android.preference.Preference;
+import android.provider.Settings;
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.voice.VoiceInputPreference.RadioButtonGroupState;
+
+import android.os.Bundle;
+import android.preference.PreferenceCategory;
+import android.widget.Checkable;
+
+public class VoiceInputSettings extends SettingsPreferenceFragment implements
+        Preference.OnPreferenceClickListener, RadioButtonGroupState {
+
+    private static final String TAG = "VoiceInputSettings";
+    private static final boolean DBG = false;
+
+    /**
+     * Preference key for the engine selection preference.
+     */
+    private static final String KEY_SERVICE_PREFERENCE_SECTION =
+            "voice_service_preference_section";
+
+    private PreferenceCategory mServicePreferenceCategory;
+
+    private CharSequence mInteractorSummary;
+    private CharSequence mRecognizerSummary;
+    private CharSequence mInteractorWarning;
+
+    /**
+     * The currently selected engine.
+     */
+    private String mCurrentKey;
+
+    /**
+     * The engine checkbox that is currently checked. Saves us a bit of effort
+     * in deducing the right one from the currently selected engine.
+     */
+    private Checkable mCurrentChecked;
+
+    private VoiceInputHelper mHelper;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addPreferencesFromResource(R.xml.voice_input_settings);
+
+        mServicePreferenceCategory = (PreferenceCategory) findPreference(
+                KEY_SERVICE_PREFERENCE_SECTION);
+
+        mInteractorSummary = getActivity().getText(
+                R.string.voice_interactor_preference_summary);
+        mRecognizerSummary = getActivity().getText(
+                R.string.voice_recognizer_preference_summary);
+        mInteractorWarning = getActivity().getText(R.string.voice_interaction_security_warning);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        initSettings();
+    }
+
+    private void initSettings() {
+        mHelper = new VoiceInputHelper(getActivity());
+        mHelper.buildUi();
+
+        mServicePreferenceCategory.removeAll();
+
+        if (mHelper.mCurrentVoiceInteraction != null) {
+            mCurrentKey = mHelper.mCurrentVoiceInteraction.flattenToShortString();
+        } else if (mHelper.mCurrentRecognizer != null) {
+            mCurrentKey = mHelper.mCurrentRecognizer.flattenToShortString();
+        } else {
+            mCurrentKey = null;
+        }
+
+        for (int i=0; i<mHelper.mAvailableInteractionInfos.size(); i++) {
+            VoiceInputHelper.InteractionInfo info = mHelper.mAvailableInteractionInfos.get(i);
+            VoiceInputPreference pref = new VoiceInputPreference(getActivity(), info,
+                    mInteractorSummary, mInteractorWarning, this);
+            mServicePreferenceCategory.addPreference(pref);
+        }
+
+        for (int i=0; i<mHelper.mAvailableRecognizerInfos.size(); i++) {
+            VoiceInputHelper.RecognizerInfo info = mHelper.mAvailableRecognizerInfos.get(i);
+            VoiceInputPreference pref = new VoiceInputPreference(getActivity(), info,
+                    mRecognizerSummary, null, this);
+            mServicePreferenceCategory.addPreference(pref);
+        }
+    }
+
+    @Override
+    public Checkable getCurrentChecked() {
+        return mCurrentChecked;
+    }
+
+    @Override
+    public String getCurrentKey() {
+        return mCurrentKey;
+    }
+
+    @Override
+    public void setCurrentChecked(Checkable current) {
+        mCurrentChecked = current;
+    }
+
+    @Override
+    public void setCurrentKey(String key) {
+        mCurrentKey = key;
+        for (int i=0; i<mHelper.mAvailableInteractionInfos.size(); i++) {
+            VoiceInputHelper.InteractionInfo info = mHelper.mAvailableInteractionInfos.get(i);
+            if (info.key.equals(key)) {
+                // Put the new value back into secure settings.
+                Settings.Secure.putString(getActivity().getContentResolver(),
+                        Settings.Secure.VOICE_INTERACTION_SERVICE, key);
+                // Eventually we will require that an interactor always specify a recognizer
+                if (info.settings != null) {
+                    Settings.Secure.putString(getActivity().getContentResolver(),
+                            Settings.Secure.VOICE_RECOGNITION_SERVICE,
+                            info.settings.flattenToShortString());
+                }
+                return;
+            }
+        }
+
+        for (int i=0; i<mHelper.mAvailableRecognizerInfos.size(); i++) {
+            VoiceInputHelper.RecognizerInfo info = mHelper.mAvailableRecognizerInfos.get(i);
+            if (info.key.equals(key)) {
+                Settings.Secure.putString(getActivity().getContentResolver(),
+                        Settings.Secure.VOICE_INTERACTION_SERVICE, null);
+                Settings.Secure.putString(getActivity().getContentResolver(),
+                        Settings.Secure.VOICE_RECOGNITION_SERVICE, key);
+                return;
+            }
+        }
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        if (preference instanceof VoiceInputPreference) {
+            ((VoiceInputPreference)preference).doClick();
+        }
+        return true;
+    }
+}