summaryrefslogtreecommitdiffstats
path: root/src/com/android/settings/voice
diff options
context:
space:
mode:
authorDianne Hackborn <hackbod@google.com>2014-07-18 19:20:11 -0700
committerDianne Hackborn <hackbod@google.com>2014-07-21 20:14:43 -0700
commitff795ffba80e8a6455f27fab7ad0e8e14e2ec9a4 (patch)
tree9f6ce9f2f7d33865be69e451b9b383aa11590e74 /src/com/android/settings/voice
parentf6f1e2ba1382617cc4df76a004a0da4313a59129 (diff)
downloadpackages_apps_Settings-ff795ffba80e8a6455f27fab7ad0e8e14e2ec9a4.zip
packages_apps_Settings-ff795ffba80e8a6455f27fab7ad0e8e14e2ec9a4.tar.gz
packages_apps_Settings-ff795ffba80e8a6455f27fab7ad0e8e14e2ec9a4.tar.bz2
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
Diffstat (limited to 'src/com/android/settings/voice')
-rw-r--r--src/com/android/settings/voice/VoiceInputHelper.java211
-rw-r--r--src/com/android/settings/voice/VoiceInputPreference.java233
-rw-r--r--src/com/android/settings/voice/VoiceInputSettings.java163
3 files changed, 607 insertions, 0 deletions
diff --git a/src/com/android/settings/voice/VoiceInputHelper.java b/src/com/android/settings/voice/VoiceInputHelper.java
new file mode 100644
index 0000000..63b891a
--- /dev/null
+++ b/src/com/android/settings/voice/VoiceInputHelper.java
@@ -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
index 0000000..0ebffbb
--- /dev/null
+++ b/src/com/android/settings/voice/VoiceInputPreference.java
@@ -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
index 0000000..309c6e9
--- /dev/null
+++ b/src/com/android/settings/voice/VoiceInputSettings.java
@@ -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;
+ }
+}