1 /*
2  * Copyright (C) 2021 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.server.voiceinteraction;
18 
19 import android.annotation.NonNull;
20 import android.annotation.UserIdInt;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.content.pm.ServiceInfo;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.content.res.XmlResourceParser;
29 import android.speech.RecognitionService;
30 import android.text.TextUtils;
31 import android.util.AttributeSet;
32 import android.util.Log;
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.List;
41 
42 // TODO: Move this class somewhere else, along with the default recognizer logic in
43 //  VoiceInteractionManagerService.
44 // TODO: Use this class in com.android.settings.applications.assist.VoiceInputHelper.
45 
46 /**
47  * {@link ServiceInfo} and parsed metadata for a {@link RecognitionService}.
48  */
49 class RecognitionServiceInfo {
50     private static final String TAG = "RecognitionServiceInfo";
51 
52     private final String mParseError;
53     private final ServiceInfo mServiceInfo;
54     private final boolean mSelectableAsDefault;
55 
56     /**
57      * Queries the valid recognition services available for the user.
58      */
getAvailableServices( @onNull Context context, @UserIdInt int user)59     static List<RecognitionServiceInfo> getAvailableServices(
60             @NonNull Context context, @UserIdInt int user) {
61         List<RecognitionServiceInfo> services = new ArrayList<>();
62 
63         List<ResolveInfo> resolveInfos =
64                 context.getPackageManager().queryIntentServicesAsUser(
65                         new Intent(RecognitionService.SERVICE_INTERFACE),
66                         PackageManager.MATCH_DIRECT_BOOT_AWARE
67                                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
68                         user);
69         for (ResolveInfo resolveInfo : resolveInfos) {
70             RecognitionServiceInfo service =
71                     parseInfo(context.getPackageManager(), resolveInfo.serviceInfo);
72             if (!TextUtils.isEmpty(service.mParseError)) {
73                 Log.w(TAG, "Parse error in getAvailableServices: " + service.mParseError);
74                 // We still use the recognizer to preserve pre-existing behavior.
75             }
76             services.add(service);
77         }
78         return services;
79     }
80 
81     /**
82      * Loads the service metadata published by the component. Success is indicated by {@link
83      * #getParseError()}.
84      *
85      * @param pm A PackageManager from which the XML can be loaded; usually the
86      *         PackageManager from which {@code si} was originally retrieved.
87      * @param si The {@link android.speech.RecognitionService} info.
88      */
parseInfo(@onNull PackageManager pm, @NonNull ServiceInfo si)89     static RecognitionServiceInfo parseInfo(@NonNull PackageManager pm, @NonNull ServiceInfo si) {
90         String parseError = "";
91         boolean selectableAsDefault = true; // default
92         try (XmlResourceParser parser = si.loadXmlMetaData(
93                 pm,
94                 RecognitionService.SERVICE_META_DATA)) {
95             if (parser == null) {
96                 parseError = "No " + RecognitionService.SERVICE_META_DATA
97                         + " meta-data for " + si.packageName;
98                 return new RecognitionServiceInfo(si, selectableAsDefault, parseError);
99             }
100             Resources res = pm.getResourcesForApplication(si.applicationInfo);
101             AttributeSet attrs = Xml.asAttributeSet(parser);
102 
103             int type = 0;
104             while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
105                 type = parser.next();
106             }
107 
108             String nodeName = parser.getName();
109             if (!"recognition-service".equals(nodeName)) {
110                 throw new XmlPullParserException(
111                         "Meta-data does not start with recognition-service tag");
112             }
113 
114             TypedArray values =
115                     res.obtainAttributes(
116                             attrs, com.android.internal.R.styleable.RecognitionService);
117             selectableAsDefault =
118                     values.getBoolean(
119                             com.android.internal.R.styleable.RecognitionService_selectableAsDefault,
120                             selectableAsDefault);
121             values.recycle();
122         } catch (XmlPullParserException | IOException | PackageManager.NameNotFoundException e) {
123             parseError = "Error parsing recognition service meta-data: " + e;
124         }
125         return new RecognitionServiceInfo(si, selectableAsDefault, parseError);
126     }
127 
RecognitionServiceInfo( @onNull ServiceInfo si, boolean selectableAsDefault, @NonNull String parseError)128     private RecognitionServiceInfo(
129             @NonNull ServiceInfo si, boolean selectableAsDefault, @NonNull String parseError) {
130         mServiceInfo = si;
131         mSelectableAsDefault = selectableAsDefault;
132         mParseError = parseError;
133     }
134 
135     @NonNull
getParseError()136     public String getParseError() {
137         return mParseError;
138     }
139 
140     @NonNull
getServiceInfo()141     public ServiceInfo getServiceInfo() {
142         return mServiceInfo;
143     }
144 
isSelectableAsDefault()145     public boolean isSelectableAsDefault() {
146         return mSelectableAsDefault;
147     }
148 }
149