Unify voice interactor and recognizer settings.

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
This commit is contained in:
Dianne Hackborn
2014-07-18 19:20:11 -07:00
parent f6f1e2ba13
commit ff795ffba8
7 changed files with 670 additions and 373 deletions

View File

@@ -4061,24 +4061,28 @@
<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. -->
@@ -5462,6 +5466,7 @@
<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>
@@ -5474,7 +5479,6 @@
<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-->

View File

@@ -99,31 +99,11 @@
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

View File

@@ -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>

View File

@@ -16,65 +16,31 @@
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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}