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