From 82ef1ef6db3adc7e3f1a009424fe35171c546f81 Mon Sep 17 00:00:00 2001 From: Aleksandar Kiridzic Date: Thu, 9 Jun 2022 13:54:59 +0100 Subject: [PATCH] speech: Add on-device speech recognition settings entry Currently under `Settings > System > Languages & input > Speech` there is a `Voice input` entry with a cog which starts the SSBG model manager settings activity. Under the same preference group another entry labeled `On-device recognition` is added which would open a similar model manager settings activity, but for the on-device recognizer. That settings activity is yet to be implemented. The new entry should appear only under the following conditions: - An on-device speech recognition service must be available in the system. - A speech recognition service with a proper settings activity meta-data must exist in the same package as the default on-device speech recognition service. Bug: 235457391 Test: Manual, already existing robotests Change-Id: I17208c8725500ccb3dd2fa51a12b003d32073c4e (cherry picked from commit 31473663f5c7ab072192a17aeb2a8189472548b7) --- res/values/strings.xml | 8 + res/xml/language_and_input.xml | 7 + .../language/LanguageAndInputSettings.java | 17 +- ...DeviceRecognitionPreferenceController.java | 133 +++++++++++++ .../settings/language/VoiceInputHelper.java | 175 ++++++++++++------ 5 files changed, 276 insertions(+), 64 deletions(-) create mode 100644 src/com/android/settings/language/OnDeviceRecognitionPreferenceController.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 63b634beab8..b7007c4b69c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6876,6 +6876,14 @@ behalf. It comes from the %s application. Enable the use of this service? + + + On-device recognition settings + + On-device recognition + + On-device speech recognition + Preferred engine diff --git a/res/xml/language_and_input.xml b/res/xml/language_and_input.xml index 770a862f3d1..64b5003bcef 100644 --- a/res/xml/language_and_input.xml +++ b/res/xml/language_and_input.xml @@ -63,6 +63,13 @@ android:title="@string/voice_input_settings_title" android:fragment="com.android.settings.language.DefaultVoiceInputPicker" /> + + speechCategoryChildren = new ArrayList<>( + List.of(defaultVoiceInputPreferenceController, ttsPreferenceController)); + + if (onDeviceRecognitionPreferenceController.isAvailable()) { + controllers.add(onDeviceRecognitionPreferenceController); + speechCategoryChildren.add(onDeviceRecognitionPreferenceController); + } + + controllers.add(new PreferenceCategoryController(context, KEY_SPEECH_CATEGORY) + .setChildren(speechCategoryChildren)); // Pointer final PointerSpeedController pointerController = new PointerSpeedController(context); diff --git a/src/com/android/settings/language/OnDeviceRecognitionPreferenceController.java b/src/com/android/settings/language/OnDeviceRecognitionPreferenceController.java new file mode 100644 index 00000000000..318663957c9 --- /dev/null +++ b/src/com/android/settings/language/OnDeviceRecognitionPreferenceController.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2022 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.language; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +import com.android.internal.R; +import com.android.settings.core.BasePreferenceController; + +import java.util.ArrayList; +import java.util.Optional; + +/** Controller of the On-device recognition preference. */ +public class OnDeviceRecognitionPreferenceController extends BasePreferenceController { + + private static final String TAG = "OnDeviceRecognitionPreferenceController"; + + private Optional mIntent; + + public OnDeviceRecognitionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + if (mIntent == null) { + mIntent = Optional.ofNullable(onDeviceRecognitionIntent()); + } + return mIntent.isPresent() + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mIntent != null && mIntent.isPresent()) { + preference.setIntent(mIntent.get()); + } + } + + /** + * Create an {@link Intent} for the activity in the default on-device recognizer service if + * there is a properly defined speech recognition xml meta-data for that service. + * + * @return {@link Intent} if the proper activity is fount, {@code null} otherwise. + */ + @Nullable + private Intent onDeviceRecognitionIntent() { + final String resString = mContext.getString( + R.string.config_defaultOnDeviceSpeechRecognitionService); + + if (resString == null) { + Log.v(TAG, "No on-device recognizer, intent not created."); + return null; + } + + final ComponentName defaultOnDeviceRecognizerComponentName = + ComponentName.unflattenFromString(resString); + + if (defaultOnDeviceRecognizerComponentName == null) { + Log.v(TAG, "Invalid on-device recognizer string format, intent not created."); + return null; + } + + final ArrayList validRecognitionServices = + VoiceInputHelper.validRecognitionServices(mContext); + + if (validRecognitionServices.isEmpty()) { + Log.v(TAG, "No speech recognition services" + + "with proper `recognition-service` meta-data found."); + return null; + } + + // Filter the recognizer services which are in the same package as the default on-device + // speech recognizer and have a settings activity defined in the meta-data. + final ArrayList validOnDeviceRecognitionServices = + new ArrayList<>(); + for (VoiceInputHelper.RecognizerInfo recognizerInfo: validRecognitionServices) { + if (!defaultOnDeviceRecognizerComponentName.getPackageName().equals( + recognizerInfo.mService.packageName)) { + Log.v(TAG, String.format("Recognition service not in the same package as the " + + "default on-device recognizer: %s.", + recognizerInfo.mComponentName.flattenToString())); + } else if (recognizerInfo.mSettings == null) { + Log.v(TAG, String.format("Recognition service with no settings activity: %s.", + recognizerInfo.mComponentName.flattenToString())); + } else { + validOnDeviceRecognitionServices.add(recognizerInfo); + Log.v(TAG, String.format("Recognition service in the same package as the default " + + "on-device recognizer with settings activity: %s.", + recognizerInfo.mSettings.flattenToString())); + } + } + + if (validOnDeviceRecognitionServices.isEmpty()) { + Log.v(TAG, "No speech recognition services with proper `recognition-service` " + + "meta-data found in the same package as the default on-device recognizer."); + return null; + } + + // Not more than one proper recognition services should be found in the same + // package as the default on-device recognizer. If that happens, + // the first one which passed the filter will be selected. + if (validOnDeviceRecognitionServices.size() > 1) { + Log.w(TAG, "More than one recognition services with proper `recognition-service` " + + "meta-data found in the same package as the default on-device recognizer."); + } + VoiceInputHelper.RecognizerInfo chosenRecognizer = validOnDeviceRecognitionServices.get(0); + + return new Intent(Intent.ACTION_MAIN).setComponent(chosenRecognizer.mSettings); + } +} diff --git a/src/com/android/settings/language/VoiceInputHelper.java b/src/com/android/settings/language/VoiceInputHelper.java index 7915ba479ec..289a2f91ec3 100644 --- a/src/com/android/settings/language/VoiceInputHelper.java +++ b/src/com/android/settings/language/VoiceInputHelper.java @@ -29,6 +29,7 @@ import android.provider.Settings; import android.speech.RecognitionService; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; @@ -44,12 +45,11 @@ public final class VoiceInputHelper { static final String TAG = "VoiceInputHelper"; final Context mContext; - final List mAvailableRecognition; - /** * Base info of the Voice Input provider. * * TODO: Remove this superclass as we only have 1 class now (RecognizerInfo). + * TODO: Group recognition service xml meta-data attributes in a single class. */ public static class BaseInfo implements Comparable { public final ServiceInfo mService; @@ -90,16 +90,12 @@ public final class VoiceInputHelper { } } - final ArrayList mAvailableRecognizerInfos = new ArrayList<>(); + ArrayList mAvailableRecognizerInfos = new ArrayList<>(); ComponentName mCurrentRecognizer; public VoiceInputHelper(Context context) { mContext = context; - - mAvailableRecognition = mContext.getPackageManager().queryIntentServices( - new Intent(RecognitionService.SERVICE_INTERFACE), - PackageManager.GET_META_DATA); } /** Draws the UI of the Voice Input picker page. */ @@ -113,63 +109,120 @@ public final class VoiceInputHelper { mCurrentRecognizer = null; } - // Iterate through all the available recognizers and load up their info to show - // in the preference. - int 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); - ServiceInfo si = resolveInfo.serviceInfo; - String settingsActivity = null; - // Always show in voice input settings unless specifically set to False. - boolean selectableAsDefault = true; - try (XmlResourceParser 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); - } + final ArrayList validRecognitionServices = + validRecognitionServices(mContext); - 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) { - // Intentionally do nothing. - } - - 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); - selectableAsDefault = array.getBoolean( - com.android.internal.R.styleable.RecognitionService_selectableAsDefault, - true); - 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); - } - // The current recognizer must always be shown in the settings, whatever its - // selectableAsDefault value is. - if (selectableAsDefault || comp.equals(mCurrentRecognizer)) { - mAvailableRecognizerInfos.add(new RecognizerInfo(mContext.getPackageManager(), - resolveInfo.serviceInfo, settingsActivity, selectableAsDefault)); + // Filter all recognizers which can be selected as default or are the current recognizer. + mAvailableRecognizerInfos = new ArrayList<>(); + for (RecognizerInfo recognizerInfo: validRecognitionServices) { + if (recognizerInfo.mSelectableAsDefault || new ComponentName( + recognizerInfo.mService.packageName, recognizerInfo.mService.name) + .equals(mCurrentRecognizer)) { + mAvailableRecognizerInfos.add(recognizerInfo); } } + Collections.sort(mAvailableRecognizerInfos); } + + /** + * Query all services with {@link RecognitionService#SERVICE_INTERFACE} intent. Filter only + * those which have proper xml meta-data which start with a `recognition-service` tag. + * Filtered services are sorted by their labels in the ascending order. + * + * @param context {@link Context} inside which the settings app is run. + * + * @return {@link ArrayList}<{@link RecognizerInfo}> + * containing info about the filtered speech recognition services. + */ + static ArrayList validRecognitionServices(Context context) { + final List resolvedRecognitionServices = + context.getPackageManager().queryIntentServices( + new Intent(RecognitionService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + + final ArrayList validRecognitionServices = new ArrayList<>(); + + for (ResolveInfo resolveInfo: resolvedRecognitionServices) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + + final Pair recognitionServiceAttributes = + parseRecognitionServiceXmlMetadata(context, serviceInfo); + + if (recognitionServiceAttributes != null) { + validRecognitionServices.add(new RecognizerInfo( + context.getPackageManager(), + serviceInfo, + recognitionServiceAttributes.first /* settingsActivity */, + recognitionServiceAttributes.second /* selectableAsDefault */)); + } + } + + return validRecognitionServices; + } + + /** + * Load recognition service's xml meta-data and parse it. Return the meta-data attributes, + * namely, `settingsActivity` {@link String} and `selectableAsDefault` {@link Boolean}. + * + *

Parsing fails if the meta-data for the given service is not found + * or the found meta-data does not start with a `recognition-service`.

+ * + * @param context {@link Context} inside which the settings app is run. + * @param serviceInfo {@link ServiceInfo} containing info + * about the speech recognition service in question. + * + * @return {@link Pair}<{@link String}, {@link Boolean}> containing `settingsActivity` + * and `selectableAsDefault` attributes if the parsing was successful, {@code null} otherwise. + */ + private static Pair parseRecognitionServiceXmlMetadata( + Context context, ServiceInfo serviceInfo) { + // Default recognition service attribute values. + // Every recognizer can be selected unless specified otherwise. + String settingsActivity; + boolean selectableAsDefault = true; + + // Parse xml meta-data. + try (XmlResourceParser parser = serviceInfo.loadXmlMetaData( + context.getPackageManager(), RecognitionService.SERVICE_META_DATA)) { + if (parser == null) { + throw new XmlPullParserException(String.format("No %s meta-data for %s package", + RecognitionService.SERVICE_META_DATA, serviceInfo.packageName)); + } + + final Resources res = context.getPackageManager().getResourcesForApplication( + serviceInfo.applicationInfo); + final AttributeSet attrs = Xml.asAttributeSet(parser); + + // Xml meta-data must start with a `recognition-service tag`. + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Intentionally do nothing. + } + + final String nodeName = parser.getName(); + if (!"recognition-service".equals(nodeName)) { + throw new XmlPullParserException(String.format( + "%s package meta-data does not start with a `recognition-service` tag", + serviceInfo.packageName)); + } + + final TypedArray array = res.obtainAttributes(attrs, + com.android.internal.R.styleable.RecognitionService); + settingsActivity = array.getString( + com.android.internal.R.styleable.RecognitionService_settingsActivity); + selectableAsDefault = array.getBoolean( + com.android.internal.R.styleable.RecognitionService_selectableAsDefault, + selectableAsDefault); + array.recycle(); + } catch (XmlPullParserException | IOException + | PackageManager.NameNotFoundException e) { + Log.e(TAG, String.format("Error parsing %s package recognition service meta-data", + serviceInfo.packageName), e); + return null; + } + + return Pair.create(settingsActivity, selectableAsDefault); + } }