1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.language; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.PackageManager; 23 import android.content.pm.ResolveInfo; 24 import android.content.pm.ServiceInfo; 25 import android.content.res.Resources; 26 import android.content.res.TypedArray; 27 import android.content.res.XmlResourceParser; 28 import android.provider.Settings; 29 import android.speech.RecognitionService; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.Pair; 33 import android.util.Xml; 34 35 import org.xmlpull.v1.XmlPullParser; 36 import org.xmlpull.v1.XmlPullParserException; 37 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.List; 42 43 /** Helper class of the Voice Input setting. */ 44 public final class VoiceInputHelper { 45 static final String TAG = "VoiceInputHelper"; 46 final Context mContext; 47 48 /** 49 * Base info of the Voice Input provider. 50 * 51 * TODO: Remove this superclass as we only have 1 class now (RecognizerInfo). 52 * TODO: Group recognition service xml meta-data attributes in a single class. 53 */ 54 public static class BaseInfo implements Comparable<BaseInfo> { 55 public final ServiceInfo mService; 56 public final ComponentName mComponentName; 57 public final String mKey; 58 public final ComponentName mSettings; 59 public final CharSequence mLabel; 60 public final String mLabelStr; 61 public final CharSequence mAppLabel; 62 BaseInfo(PackageManager pm, ServiceInfo service, String settings)63 public BaseInfo(PackageManager pm, ServiceInfo service, String settings) { 64 mService = service; 65 mComponentName = new ComponentName(service.packageName, service.name); 66 mKey = mComponentName.flattenToShortString(); 67 mSettings = settings != null 68 ? new ComponentName(service.packageName, settings) : null; 69 mLabel = service.loadLabel(pm); 70 mLabelStr = mLabel.toString(); 71 mAppLabel = service.applicationInfo.loadLabel(pm); 72 } 73 74 @Override compareTo(BaseInfo another)75 public int compareTo(BaseInfo another) { 76 return mLabelStr.compareTo(another.mLabelStr); 77 } 78 } 79 80 /** Info of the speech recognizer (i.e. recognition service). */ 81 public static class RecognizerInfo extends BaseInfo { 82 public final boolean mSelectableAsDefault; 83 RecognizerInfo(PackageManager pm, ServiceInfo serviceInfo, String settings, boolean selectableAsDefault)84 public RecognizerInfo(PackageManager pm, 85 ServiceInfo serviceInfo, 86 String settings, 87 boolean selectableAsDefault) { 88 super(pm, serviceInfo, settings); 89 this.mSelectableAsDefault = selectableAsDefault; 90 } 91 } 92 93 ArrayList<RecognizerInfo> mAvailableRecognizerInfos = new ArrayList<>(); 94 95 ComponentName mCurrentRecognizer; 96 VoiceInputHelper(Context context)97 public VoiceInputHelper(Context context) { 98 mContext = context; 99 } 100 101 /** Draws the UI of the Voice Input picker page. */ buildUi()102 public void buildUi() { 103 // Get the currently selected recognizer from the secure setting. 104 String currentSetting = Settings.Secure.getString( 105 mContext.getContentResolver(), Settings.Secure.VOICE_RECOGNITION_SERVICE); 106 if (currentSetting != null && !currentSetting.isEmpty()) { 107 mCurrentRecognizer = ComponentName.unflattenFromString(currentSetting); 108 } else { 109 mCurrentRecognizer = null; 110 } 111 112 final ArrayList<RecognizerInfo> validRecognitionServices = 113 validRecognitionServices(mContext); 114 115 // Filter all recognizers which can be selected as default or are the current recognizer. 116 mAvailableRecognizerInfos = new ArrayList<>(); 117 for (RecognizerInfo recognizerInfo: validRecognitionServices) { 118 if (recognizerInfo.mSelectableAsDefault || new ComponentName( 119 recognizerInfo.mService.packageName, recognizerInfo.mService.name) 120 .equals(mCurrentRecognizer)) { 121 mAvailableRecognizerInfos.add(recognizerInfo); 122 } 123 } 124 125 Collections.sort(mAvailableRecognizerInfos); 126 } 127 128 /** 129 * Query all services with {@link RecognitionService#SERVICE_INTERFACE} intent. Filter only 130 * those which have proper xml meta-data which start with a `recognition-service` tag. 131 * Filtered services are sorted by their labels in the ascending order. 132 * 133 * @param context {@link Context} inside which the settings app is run. 134 * 135 * @return {@link ArrayList}<{@link RecognizerInfo}> 136 * containing info about the filtered speech recognition services. 137 */ validRecognitionServices(Context context)138 static ArrayList<RecognizerInfo> validRecognitionServices(Context context) { 139 final List<ResolveInfo> resolvedRecognitionServices = 140 context.getPackageManager().queryIntentServices( 141 new Intent(RecognitionService.SERVICE_INTERFACE), 142 PackageManager.GET_META_DATA); 143 144 final ArrayList<RecognizerInfo> validRecognitionServices = new ArrayList<>(); 145 146 for (ResolveInfo resolveInfo: resolvedRecognitionServices) { 147 final ServiceInfo serviceInfo = resolveInfo.serviceInfo; 148 149 final Pair<String, Boolean> recognitionServiceAttributes = 150 parseRecognitionServiceXmlMetadata(context, serviceInfo); 151 152 if (recognitionServiceAttributes != null) { 153 validRecognitionServices.add(new RecognizerInfo( 154 context.getPackageManager(), 155 serviceInfo, 156 recognitionServiceAttributes.first /* settingsActivity */, 157 recognitionServiceAttributes.second /* selectableAsDefault */)); 158 } 159 } 160 161 return validRecognitionServices; 162 } 163 164 /** 165 * Load recognition service's xml meta-data and parse it. Return the meta-data attributes, 166 * namely, `settingsActivity` {@link String} and `selectableAsDefault` {@link Boolean}. 167 * 168 * <p>Parsing fails if the meta-data for the given service is not found 169 * or the found meta-data does not start with a `recognition-service`.</p> 170 * 171 * @param context {@link Context} inside which the settings app is run. 172 * @param serviceInfo {@link ServiceInfo} containing info 173 * about the speech recognition service in question. 174 * 175 * @return {@link Pair}<{@link String}, {@link Boolean}> containing `settingsActivity` 176 * and `selectableAsDefault` attributes if the parsing was successful, {@code null} otherwise. 177 */ parseRecognitionServiceXmlMetadata( Context context, ServiceInfo serviceInfo)178 private static Pair<String, Boolean> parseRecognitionServiceXmlMetadata( 179 Context context, ServiceInfo serviceInfo) { 180 // Default recognition service attribute values. 181 // Every recognizer can be selected unless specified otherwise. 182 String settingsActivity; 183 boolean selectableAsDefault = true; 184 185 // Parse xml meta-data. 186 try (XmlResourceParser parser = serviceInfo.loadXmlMetaData( 187 context.getPackageManager(), RecognitionService.SERVICE_META_DATA)) { 188 if (parser == null) { 189 throw new XmlPullParserException(String.format("No %s meta-data for %s package", 190 RecognitionService.SERVICE_META_DATA, serviceInfo.packageName)); 191 } 192 193 final Resources res = context.getPackageManager().getResourcesForApplication( 194 serviceInfo.applicationInfo); 195 final AttributeSet attrs = Xml.asAttributeSet(parser); 196 197 // Xml meta-data must start with a `recognition-service tag`. 198 int type; 199 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 200 && type != XmlPullParser.START_TAG) { 201 // Intentionally do nothing. 202 } 203 204 final String nodeName = parser.getName(); 205 if (!"recognition-service".equals(nodeName)) { 206 throw new XmlPullParserException(String.format( 207 "%s package meta-data does not start with a `recognition-service` tag", 208 serviceInfo.packageName)); 209 } 210 211 final TypedArray array = res.obtainAttributes(attrs, 212 com.android.internal.R.styleable.RecognitionService); 213 settingsActivity = array.getString( 214 com.android.internal.R.styleable.RecognitionService_settingsActivity); 215 selectableAsDefault = array.getBoolean( 216 com.android.internal.R.styleable.RecognitionService_selectableAsDefault, 217 selectableAsDefault); 218 array.recycle(); 219 } catch (XmlPullParserException | IOException 220 | PackageManager.NameNotFoundException e) { 221 Log.e(TAG, String.format("Error parsing %s package recognition service meta-data", 222 serviceInfo.packageName), e); 223 return null; 224 } 225 226 return Pair.create(settingsActivity, selectableAsDefault); 227 } 228 } 229