1 /* 2 * Copyright (C) 2019 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.car.settings.applications.assist; 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.service.voice.VoiceInteractionService; 29 import android.service.voice.VoiceInteractionServiceInfo; 30 import android.speech.RecognitionService; 31 import android.util.AttributeSet; 32 import android.util.Xml; 33 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.collection.ArrayMap; 37 import androidx.collection.ArraySet; 38 39 import com.android.car.settings.common.Logger; 40 41 import org.xmlpull.v1.XmlPullParser; 42 import org.xmlpull.v1.XmlPullParserException; 43 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 51 /** 52 * Extracts the voice interaction services and voice recognition services and converts them into 53 * {@link VoiceInteractionInfo} instances and {@link VoiceRecognitionInfo} instances. 54 */ 55 public class VoiceInputInfoProvider { 56 57 private static final Logger LOG = new Logger(VoiceInputInfoProvider.class); 58 @VisibleForTesting 59 static final Intent VOICE_INTERACTION_SERVICE_TAG = new Intent( 60 VoiceInteractionService.SERVICE_INTERFACE); 61 @VisibleForTesting 62 static final Intent VOICE_RECOGNITION_SERVICE_TAG = new Intent( 63 RecognitionService.SERVICE_INTERFACE); 64 65 private final Context mContext; 66 private final Map<ComponentName, VoiceInputInfo> mComponentToInfoMap = new ArrayMap<>(); 67 private final List<VoiceInteractionInfo> mVoiceInteractionInfoList = new ArrayList<>(); 68 private final List<VoiceRecognitionInfo> mVoiceRecognitionInfoList = new ArrayList<>(); 69 private final Set<ComponentName> mRecognitionServiceNames = new ArraySet<>(); 70 VoiceInputInfoProvider(Context context)71 public VoiceInputInfoProvider(Context context) { 72 mContext = context; 73 74 loadVoiceInteractionServices(); 75 loadVoiceRecognitionServices(); 76 } 77 78 /** 79 * Gets the list of voice interaction services represented as {@link VoiceInteractionInfo} 80 * instances. 81 */ getVoiceInteractionInfoList()82 public List<VoiceInteractionInfo> getVoiceInteractionInfoList() { 83 return mVoiceInteractionInfoList; 84 } 85 86 /** 87 * Gets the list of voice recognition services represented as {@link VoiceRecognitionInfo} 88 * instances. 89 */ getVoiceRecognitionInfoList()90 public List<VoiceRecognitionInfo> getVoiceRecognitionInfoList() { 91 return mVoiceRecognitionInfoList; 92 } 93 94 /** 95 * Returns the appropriate {@link VoiceInteractionInfo} or {@link VoiceRecognitionInfo} based on 96 * the provided {@link ComponentName}. 97 * 98 * @return {@link VoiceInputInfo} if it exists for the component name, null otherwise. 99 */ 100 @Nullable getInfoForComponent(ComponentName key)101 public VoiceInputInfo getInfoForComponent(ComponentName key) { 102 return mComponentToInfoMap.getOrDefault(key, null); 103 } 104 loadVoiceInteractionServices()105 private void loadVoiceInteractionServices() { 106 List<ResolveInfo> mAvailableVoiceInteractionServices = 107 mContext.getPackageManager().queryIntentServices(VOICE_INTERACTION_SERVICE_TAG, 108 PackageManager.GET_META_DATA); 109 110 for (ResolveInfo resolveInfo : mAvailableVoiceInteractionServices) { 111 VoiceInteractionServiceInfo interactionServiceInfo = new VoiceInteractionServiceInfo( 112 mContext.getPackageManager(), resolveInfo.serviceInfo); 113 if (hasParseError(interactionServiceInfo)) { 114 LOG.w("Error in VoiceInteractionService " + resolveInfo.serviceInfo.packageName 115 + "/" + resolveInfo.serviceInfo.name + ": " 116 + interactionServiceInfo.getParseError()); 117 continue; 118 } 119 VoiceInteractionInfo voiceInteractionInfo = new VoiceInteractionInfo(mContext, 120 interactionServiceInfo); 121 mVoiceInteractionInfoList.add(voiceInteractionInfo); 122 if (interactionServiceInfo.getRecognitionService() != null) { 123 mRecognitionServiceNames.add(new ComponentName(resolveInfo.serviceInfo.packageName, 124 interactionServiceInfo.getRecognitionService())); 125 } 126 mComponentToInfoMap.put(new ComponentName(resolveInfo.serviceInfo.packageName, 127 resolveInfo.serviceInfo.name), voiceInteractionInfo); 128 } 129 Collections.sort(mVoiceInteractionInfoList); 130 } 131 loadVoiceRecognitionServices()132 private void loadVoiceRecognitionServices() { 133 List<ResolveInfo> mAvailableRecognitionServices = 134 mContext.getPackageManager().queryIntentServices(VOICE_RECOGNITION_SERVICE_TAG, 135 PackageManager.GET_META_DATA); 136 for (ResolveInfo resolveInfo : mAvailableRecognitionServices) { 137 ComponentName componentName = new ComponentName(resolveInfo.serviceInfo.packageName, 138 resolveInfo.serviceInfo.name); 139 140 VoiceRecognitionInfo voiceRecognitionInfo = new VoiceRecognitionInfo(mContext, 141 resolveInfo.serviceInfo); 142 mVoiceRecognitionInfoList.add(voiceRecognitionInfo); 143 mRecognitionServiceNames.add(componentName); 144 mComponentToInfoMap.put(componentName, voiceRecognitionInfo); 145 } 146 Collections.sort(mVoiceRecognitionInfoList); 147 } 148 149 @VisibleForTesting hasParseError(VoiceInteractionServiceInfo voiceInteractionServiceInfo)150 boolean hasParseError(VoiceInteractionServiceInfo voiceInteractionServiceInfo) { 151 return voiceInteractionServiceInfo.getParseError() != null; 152 } 153 154 /** 155 * Base object used to represent {@link VoiceInteractionInfo} and {@link VoiceRecognitionInfo}. 156 */ 157 abstract static class VoiceInputInfo implements Comparable { 158 private final Context mContext; 159 private final ServiceInfo mServiceInfo; 160 VoiceInputInfo(Context context, ServiceInfo serviceInfo)161 VoiceInputInfo(Context context, ServiceInfo serviceInfo) { 162 mContext = context; 163 mServiceInfo = serviceInfo; 164 } 165 getContext()166 protected Context getContext() { 167 return mContext; 168 } 169 getServiceInfo()170 protected ServiceInfo getServiceInfo() { 171 return mServiceInfo; 172 } 173 174 @Override compareTo(Object o)175 public int compareTo(Object o) { 176 return getTag().toString().compareTo(((VoiceInputInfo) o).getTag().toString()); 177 } 178 179 /** 180 * Returns the {@link ComponentName} which represents the settings activity, if it exists. 181 */ 182 @Nullable getSettingsActivityComponentName()183 ComponentName getSettingsActivityComponentName() { 184 String activity = getSettingsActivity(); 185 return (activity != null) ? new ComponentName(mServiceInfo.packageName, activity) 186 : null; 187 } 188 189 /** Returns the package name for the service represented by this {@link VoiceInputInfo}. */ getPackageName()190 String getPackageName() { 191 return mServiceInfo.packageName; 192 } 193 194 /** 195 * Returns the component name for the service represented by this {@link VoiceInputInfo}. 196 */ getComponentName()197 ComponentName getComponentName() { 198 return new ComponentName(mServiceInfo.packageName, mServiceInfo.name); 199 } 200 201 /** 202 * Returns the label to describe the service represented by this {@link VoiceInputInfo}. 203 */ getLabel()204 abstract CharSequence getLabel(); 205 206 /** 207 * The string representation of the settings activity for the service represented by this 208 * {@link VoiceInputInfo}. 209 */ getSettingsActivity()210 protected abstract String getSettingsActivity(); 211 212 /** 213 * Returns a tag used to determine the sort order of the {@link VoiceInputInfo} instances. 214 */ getTag()215 protected CharSequence getTag() { 216 return mServiceInfo.loadLabel(mContext.getPackageManager()); 217 } 218 } 219 220 /** An object to represent {@link VoiceInteractionService} instances. */ 221 static class VoiceInteractionInfo extends VoiceInputInfo { 222 private final VoiceInteractionServiceInfo mInteractionServiceInfo; 223 VoiceInteractionInfo(Context context, VoiceInteractionServiceInfo info)224 VoiceInteractionInfo(Context context, VoiceInteractionServiceInfo info) { 225 super(context, info.getServiceInfo()); 226 227 mInteractionServiceInfo = info; 228 } 229 230 /** Returns the recognition service associated with this {@link VoiceInteractionService}. */ getRecognitionService()231 String getRecognitionService() { 232 return mInteractionServiceInfo.getRecognitionService(); 233 } 234 235 @Override getSettingsActivity()236 protected String getSettingsActivity() { 237 return mInteractionServiceInfo.getSettingsActivity(); 238 } 239 240 @Override getLabel()241 CharSequence getLabel() { 242 return getServiceInfo().applicationInfo.loadLabel(getContext().getPackageManager()); 243 } 244 } 245 246 /** An object to represent {@link RecognitionService} instances. */ 247 static class VoiceRecognitionInfo extends VoiceInputInfo { 248 VoiceRecognitionInfo(Context context, ServiceInfo serviceInfo)249 VoiceRecognitionInfo(Context context, ServiceInfo serviceInfo) { 250 super(context, serviceInfo); 251 } 252 253 @Override getSettingsActivity()254 protected String getSettingsActivity() { 255 return getServiceSettingsActivity(getServiceInfo()); 256 } 257 258 @Override getLabel()259 CharSequence getLabel() { 260 return getTag(); 261 } 262 getServiceSettingsActivity(ServiceInfo serviceInfo)263 private String getServiceSettingsActivity(ServiceInfo serviceInfo) { 264 XmlResourceParser parser = null; 265 String settingActivity = null; 266 try { 267 parser = serviceInfo.loadXmlMetaData(getContext().getPackageManager(), 268 RecognitionService.SERVICE_META_DATA); 269 if (parser == null) { 270 throw new XmlPullParserException( 271 "No " + RecognitionService.SERVICE_META_DATA + " meta-data for " 272 + serviceInfo.packageName); 273 } 274 275 Resources res = getContext().getPackageManager().getResourcesForApplication( 276 serviceInfo.applicationInfo); 277 278 AttributeSet attrs = Xml.asAttributeSet(parser); 279 280 int type; 281 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 282 && type != XmlPullParser.START_TAG) { 283 continue; 284 } 285 286 String nodeName = parser.getName(); 287 if (!"recognition-service".equals(nodeName)) { 288 throw new XmlPullParserException( 289 "Meta-data does not start with recognition-service tag"); 290 } 291 292 TypedArray array = res.obtainAttributes(attrs, 293 com.android.internal.R.styleable.RecognitionService); 294 settingActivity = array.getString( 295 com.android.internal.R.styleable.RecognitionService_settingsActivity); 296 array.recycle(); 297 } catch (XmlPullParserException e) { 298 LOG.e("error parsing recognition service meta-data", e); 299 } catch (IOException e) { 300 LOG.e("error parsing recognition service meta-data", e); 301 } catch (PackageManager.NameNotFoundException e) { 302 LOG.e("error parsing recognition service meta-data", e); 303 } finally { 304 if (parser != null) parser.close(); 305 } 306 307 return settingActivity; 308 } 309 } 310 } 311