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 (interactionServiceInfo.getParseError() != null) { 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 /** 150 * Base object used to represent {@link VoiceInteractionInfo} and {@link VoiceRecognitionInfo}. 151 */ 152 abstract static class VoiceInputInfo implements Comparable { 153 private final Context mContext; 154 private final ServiceInfo mServiceInfo; 155 VoiceInputInfo(Context context, ServiceInfo serviceInfo)156 VoiceInputInfo(Context context, ServiceInfo serviceInfo) { 157 mContext = context; 158 mServiceInfo = serviceInfo; 159 } 160 getContext()161 protected Context getContext() { 162 return mContext; 163 } 164 getServiceInfo()165 protected ServiceInfo getServiceInfo() { 166 return mServiceInfo; 167 } 168 169 @Override compareTo(Object o)170 public int compareTo(Object o) { 171 return getTag().toString().compareTo(((VoiceInputInfo) o).getTag().toString()); 172 } 173 174 /** 175 * Returns the {@link ComponentName} which represents the settings activity, if it exists. 176 */ 177 @Nullable getSettingsActivityComponentName()178 ComponentName getSettingsActivityComponentName() { 179 String activity = getSettingsActivity(); 180 return (activity != null) ? new ComponentName(mServiceInfo.packageName, activity) 181 : null; 182 } 183 184 /** Returns the package name for the service represented by this {@link VoiceInputInfo}. */ getPackageName()185 String getPackageName() { 186 return mServiceInfo.packageName; 187 } 188 189 /** 190 * Returns the component name for the service represented by this {@link VoiceInputInfo}. 191 */ getComponentName()192 ComponentName getComponentName() { 193 return new ComponentName(mServiceInfo.packageName, mServiceInfo.name); 194 } 195 196 /** 197 * Returns the label to describe the service represented by this {@link VoiceInputInfo}. 198 */ getLabel()199 abstract CharSequence getLabel(); 200 201 /** 202 * The string representation of the settings activity for the service represented by this 203 * {@link VoiceInputInfo}. 204 */ getSettingsActivity()205 protected abstract String getSettingsActivity(); 206 207 /** 208 * Returns a tag used to determine the sort order of the {@link VoiceInputInfo} instances. 209 */ getTag()210 protected CharSequence getTag() { 211 return mServiceInfo.loadLabel(mContext.getPackageManager()); 212 } 213 } 214 215 /** An object to represent {@link VoiceInteractionService} instances. */ 216 static class VoiceInteractionInfo extends VoiceInputInfo { 217 private final VoiceInteractionServiceInfo mInteractionServiceInfo; 218 VoiceInteractionInfo(Context context, VoiceInteractionServiceInfo info)219 VoiceInteractionInfo(Context context, VoiceInteractionServiceInfo info) { 220 super(context, info.getServiceInfo()); 221 222 mInteractionServiceInfo = info; 223 } 224 225 /** Returns the recognition service associated with this {@link VoiceInteractionService}. */ getRecognitionService()226 String getRecognitionService() { 227 return mInteractionServiceInfo.getRecognitionService(); 228 } 229 230 @Override getSettingsActivity()231 protected String getSettingsActivity() { 232 return mInteractionServiceInfo.getSettingsActivity(); 233 } 234 235 @Override getLabel()236 CharSequence getLabel() { 237 return getServiceInfo().applicationInfo.loadLabel(getContext().getPackageManager()); 238 } 239 } 240 241 /** An object to represent {@link RecognitionService} instances. */ 242 static class VoiceRecognitionInfo extends VoiceInputInfo { 243 VoiceRecognitionInfo(Context context, ServiceInfo serviceInfo)244 VoiceRecognitionInfo(Context context, ServiceInfo serviceInfo) { 245 super(context, serviceInfo); 246 } 247 248 @Override getSettingsActivity()249 protected String getSettingsActivity() { 250 return getServiceSettingsActivity(getServiceInfo()); 251 } 252 253 @Override getLabel()254 CharSequence getLabel() { 255 return getTag(); 256 } 257 getServiceSettingsActivity(ServiceInfo serviceInfo)258 private String getServiceSettingsActivity(ServiceInfo serviceInfo) { 259 XmlResourceParser parser = null; 260 String settingActivity = null; 261 try { 262 parser = serviceInfo.loadXmlMetaData(getContext().getPackageManager(), 263 RecognitionService.SERVICE_META_DATA); 264 if (parser == null) { 265 throw new XmlPullParserException( 266 "No " + RecognitionService.SERVICE_META_DATA + " meta-data for " 267 + serviceInfo.packageName); 268 } 269 270 Resources res = getContext().getPackageManager().getResourcesForApplication( 271 serviceInfo.applicationInfo); 272 273 AttributeSet attrs = Xml.asAttributeSet(parser); 274 275 int type; 276 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 277 && type != XmlPullParser.START_TAG) { 278 continue; 279 } 280 281 String nodeName = parser.getName(); 282 if (!"recognition-service".equals(nodeName)) { 283 throw new XmlPullParserException( 284 "Meta-data does not start with recognition-service tag"); 285 } 286 287 TypedArray array = res.obtainAttributes(attrs, 288 com.android.internal.R.styleable.RecognitionService); 289 settingActivity = array.getString( 290 com.android.internal.R.styleable.RecognitionService_settingsActivity); 291 array.recycle(); 292 } catch (XmlPullParserException e) { 293 LOG.e("error parsing recognition service meta-data", e); 294 } catch (IOException e) { 295 LOG.e("error parsing recognition service meta-data", e); 296 } catch (PackageManager.NameNotFoundException e) { 297 LOG.e("error parsing recognition service meta-data", e); 298 } finally { 299 if (parser != null) parser.close(); 300 } 301 302 return settingActivity; 303 } 304 } 305 } 306