diff options
author | Dianne Hackborn <hackbod@google.com> | 2014-07-18 19:20:11 -0700 |
---|---|---|
committer | Dianne Hackborn <hackbod@google.com> | 2014-07-21 20:14:43 -0700 |
commit | ff795ffba80e8a6455f27fab7ad0e8e14e2ec9a4 (patch) | |
tree | 9f6ce9f2f7d33865be69e451b9b383aa11590e74 /src/com/android/settings/voice | |
parent | f6f1e2ba1382617cc4df76a004a0da4313a59129 (diff) | |
download | packages_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.java | 211 | ||||
-rw-r--r-- | src/com/android/settings/voice/VoiceInputPreference.java | 233 | ||||
-rw-r--r-- | src/com/android/settings/voice/VoiceInputSettings.java | 163 |
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; + } +} |