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}&lt;{@link RecognizerInfo}&gt;
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}&lt;{@link String}, {@link Boolean}&gt;  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