From 50261c6ae9d5fafb44de56aed6f028c4681d8a15 Mon Sep 17 00:00:00 2001 From: Ahaan Ugale Date: Mon, 22 Mar 2021 22:55:52 -0700 Subject: [PATCH] Unset default reco if selectableAsDefault=false. A non selectableAsDefault recognition service can still become the default if no other services are available. Bug: 180964085 Test: non selectableAsDefault reco is unset at boot and replaced Test: selectableAsDefault recos aren't affected Test: system default reco still works Change-Id: I429e6e457bc81ef0642ca90ccd9eb47eb10fbab8 --- .../RecognitionServiceInfo.java | 148 ++++++++++++++++++ .../VoiceInteractionManagerService.java | 62 ++++++-- 2 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 services/voiceinteraction/java/com/android/server/voiceinteraction/RecognitionServiceInfo.java diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/RecognitionServiceInfo.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/RecognitionServiceInfo.java new file mode 100644 index 0000000000000..05d4b9305d307 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/RecognitionServiceInfo.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2021 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.server.voiceinteraction; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +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.speech.RecognitionService; +import android.text.TextUtils; +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.List; + +// TODO: Move this class somewhere else, along with the default recognizer logic in +// VoiceInteractionManagerService. +// TODO: Use this class in com.android.settings.applications.assist.VoiceInputHelper. + +/** + * {@link ServiceInfo} and parsed metadata for a {@link RecognitionService}. + */ +class RecognitionServiceInfo { + private static final String TAG = "RecognitionServiceInfo"; + + private final String mParseError; + private final ServiceInfo mServiceInfo; + private final boolean mSelectableAsDefault; + + /** + * Queries the valid recognition services available for the user. + */ + static List getAvailableServices( + @NonNull Context context, @UserIdInt int user) { + List services = new ArrayList<>(); + + List resolveInfos = + context.getPackageManager().queryIntentServicesAsUser( + new Intent(RecognitionService.SERVICE_INTERFACE), + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, + user); + for (ResolveInfo resolveInfo : resolveInfos) { + RecognitionServiceInfo service = + parseInfo(context.getPackageManager(), resolveInfo.serviceInfo); + if (!TextUtils.isEmpty(service.mParseError)) { + Log.w(TAG, "Parse error in getAvailableServices: " + service.mParseError); + // We still use the recognizer to preserve pre-existing behavior. + } + services.add(service); + } + return services; + } + + /** + * Loads the service metadata published by the component. Success is indicated by {@link + * #getParseError()}. + * + * @param pm A PackageManager from which the XML can be loaded; usually the + * PackageManager from which {@code si} was originally retrieved. + * @param si The {@link android.speech.RecognitionService} info. + */ + static RecognitionServiceInfo parseInfo(@NonNull PackageManager pm, @NonNull ServiceInfo si) { + String parseError = ""; + boolean selectableAsDefault = true; // default + try (XmlResourceParser parser = si.loadXmlMetaData( + pm, + RecognitionService.SERVICE_META_DATA)) { + if (parser == null) { + parseError = "No " + RecognitionService.SERVICE_META_DATA + + " meta-data for " + si.packageName; + return new RecognitionServiceInfo(si, selectableAsDefault, parseError); + } + Resources res = pm.getResourcesForApplication(si.applicationInfo); + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type = 0; + while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { + type = parser.next(); + } + + String nodeName = parser.getName(); + if (!"recognition-service".equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with recognition-service tag"); + } + + TypedArray values = + res.obtainAttributes( + attrs, com.android.internal.R.styleable.RecognitionService); + selectableAsDefault = + values.getBoolean( + com.android.internal.R.styleable.RecognitionService_selectableAsDefault, + selectableAsDefault); + values.recycle(); + } catch (XmlPullParserException | IOException | PackageManager.NameNotFoundException e) { + parseError = "Error parsing recognition service meta-data: " + e; + } + return new RecognitionServiceInfo(si, selectableAsDefault, parseError); + } + + private RecognitionServiceInfo( + @NonNull ServiceInfo si, boolean selectableAsDefault, @NonNull String parseError) { + mServiceInfo = si; + mSelectableAsDefault = selectableAsDefault; + mParseError = parseError; + } + + @NonNull + public String getParseError() { + return mParseError; + } + + @NonNull + public ServiceInfo getServiceInfo() { + return mServiceInfo; + } + + public boolean isSelectableAsDefault() { + return mSelectableAsDefault; + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 29354eb0fd1f2..5e0d801f8c44b 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -71,7 +71,6 @@ import android.service.voice.VoiceInteractionManagerInternal; import android.service.voice.VoiceInteractionService; import android.service.voice.VoiceInteractionServiceInfo; import android.service.voice.VoiceInteractionSession; -import android.speech.RecognitionService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -103,6 +102,7 @@ import com.android.server.wm.ActivityTaskManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; @@ -404,9 +404,30 @@ public class VoiceInteractionManagerService extends SystemService { ComponentName curInteractor = !TextUtils.isEmpty(curInteractorStr) ? ComponentName.unflattenFromString(curInteractorStr) : null; try { - recognizerInfo = pm.getServiceInfo(curRecognizer, + recognizerInfo = pm.getServiceInfo( + curRecognizer, PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userHandle); + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.GET_META_DATA, + userHandle); + if (recognizerInfo != null) { + RecognitionServiceInfo rsi = + RecognitionServiceInfo.parseInfo( + mContext.getPackageManager(), recognizerInfo); + if (!TextUtils.isEmpty(rsi.getParseError())) { + Log.w(TAG, "Parse error in getAvailableServices: " + + rsi.getParseError()); + // We still use the recognizer to preserve pre-existing behavior. + } + if (!rsi.isSelectableAsDefault()) { + if (DEBUG) { + Slog.d(TAG, "Found non selectableAsDefault recognizer as" + + " default. Unsetting the default and looking for another" + + " one."); + } + recognizerInfo = null; + } + } if (curInteractor != null) { interactorInfo = pm.getServiceInfo(curInteractor, PackageManager.MATCH_DIRECT_BOOT_AWARE @@ -650,19 +671,23 @@ public class VoiceInteractionManagerService extends SystemService { prefPackage = getDefaultRecognizer(); } - List available = - mContext.getPackageManager().queryIntentServicesAsUser( - new Intent(RecognitionService.SERVICE_INTERFACE), - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userHandle); - int numAvailable = available.size(); - if (numAvailable == 0) { + List available = + RecognitionServiceInfo.getAvailableServices(mContext, userHandle); + if (available.size() == 0) { Slog.w(TAG, "no available voice recognition services found for user " + userHandle); return null; } else { + List nonSelectableAsDefault = + removeNonSelectableAsDefault(available); + if (available.size() == 0) { + Slog.w(TAG, "No selectableAsDefault recognition services found for user " + + userHandle + ". Falling back to non selectableAsDefault ones."); + available = nonSelectableAsDefault; + } + int numAvailable = available.size(); if (prefPackage != null) { - for (int i=0; i removeNonSelectableAsDefault( + List services) { + List nonSelectableAsDefault = new ArrayList<>(); + for (int i = services.size() - 1; i >= 0; i--) { + if (!services.get(i).isSelectableAsDefault()) { + nonSelectableAsDefault.add(services.remove(i)); + } + } + return nonSelectableAsDefault; + } + @Nullable public String getDefaultRecognizer() { String recognizer = mContext.getString(R.string.config_systemSpeechRecognizer);