• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2014 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 android.hardware.soundtrigger;
18 
19 import android.Manifest;
20 import android.content.Intent;
21 import android.content.pm.ApplicationInfo;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ResolveInfo;
24 import android.content.res.Resources;
25 import android.content.res.TypedArray;
26 import android.content.res.XmlResourceParser;
27 import android.service.voice.AlwaysOnHotwordDetector;
28 import android.text.TextUtils;
29 import android.util.ArraySet;
30 import android.util.AttributeSet;
31 import android.util.Slog;
32 import android.util.Xml;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.IOException;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.Map;
44 
45 /**
46  * Enrollment information about the different available keyphrases.
47  *
48  * @hide
49  */
50 public class KeyphraseEnrollmentInfo {
51     private static final String TAG = "KeyphraseEnrollmentInfo";
52     /**
53      * Name under which a Hotword enrollment component publishes information about itself.
54      * This meta-data should reference an XML resource containing a
55      * <code>&lt;{@link
56      * android.R.styleable#VoiceEnrollmentApplication
57      * voice-enrollment-application}&gt;</code> tag.
58      */
59     private static final String VOICE_KEYPHRASE_META_DATA = "android.voice_enrollment";
60     /**
61      * Activity Action: Show activity for managing the keyphrases for hotword detection.
62      * This needs to be defined by an activity that supports enrolling users for hotword/keyphrase
63      * detection.
64      */
65     public static final String ACTION_MANAGE_VOICE_KEYPHRASES =
66             "com.android.intent.action.MANAGE_VOICE_KEYPHRASES";
67     /**
68      * Intent extra: The intent extra for the specific manage action that needs to be performed.
69      * Possible values are {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
70      * {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
71      * or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}.
72      */
73     public static final String EXTRA_VOICE_KEYPHRASE_ACTION =
74             "com.android.intent.extra.VOICE_KEYPHRASE_ACTION";
75 
76     /**
77      * Intent extra: The hint text to be shown on the voice keyphrase management UI.
78      */
79     public static final String EXTRA_VOICE_KEYPHRASE_HINT_TEXT =
80             "com.android.intent.extra.VOICE_KEYPHRASE_HINT_TEXT";
81     /**
82      * Intent extra: The voice locale to use while managing the keyphrase.
83      * This is a BCP-47 language tag.
84      */
85     public static final String EXTRA_VOICE_KEYPHRASE_LOCALE =
86             "com.android.intent.extra.VOICE_KEYPHRASE_LOCALE";
87 
88     /**
89      * List of available keyphrases.
90      */
91     final private KeyphraseMetadata[] mKeyphrases;
92 
93     /**
94      * Map between KeyphraseMetadata and the package name of the enrollment app that provides it.
95      */
96     final private Map<KeyphraseMetadata, String> mKeyphrasePackageMap;
97 
98     private String mParseError;
99 
KeyphraseEnrollmentInfo(PackageManager pm)100     public KeyphraseEnrollmentInfo(PackageManager pm) {
101         // Find the apps that supports enrollment for hotword keyhphrases,
102         // Pick a privileged app and obtain the information about the supported keyphrases
103         // from its metadata.
104         List<ResolveInfo> ris = pm.queryIntentActivities(
105                 new Intent(ACTION_MANAGE_VOICE_KEYPHRASES), PackageManager.MATCH_DEFAULT_ONLY);
106         if (ris == null || ris.isEmpty()) {
107             // No application capable of enrolling for voice keyphrases is present.
108             mParseError = "No enrollment applications found";
109             mKeyphrasePackageMap = Collections.<KeyphraseMetadata, String>emptyMap();
110             mKeyphrases = null;
111             return;
112         }
113 
114         List<String> parseErrors = new LinkedList<String>();
115         mKeyphrasePackageMap = new HashMap<KeyphraseMetadata, String>();
116         for (ResolveInfo ri : ris) {
117             try {
118                 ApplicationInfo ai = pm.getApplicationInfo(
119                         ri.activityInfo.packageName, PackageManager.GET_META_DATA);
120                 if ((ai.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) == 0) {
121                     // The application isn't privileged (/system/priv-app).
122                     // The enrollment application needs to be a privileged system app.
123                     Slog.w(TAG, ai.packageName + "is not a privileged system app");
124                     continue;
125                 }
126                 if (!Manifest.permission.MANAGE_VOICE_KEYPHRASES.equals(ai.permission)) {
127                     // The application trying to manage keyphrases doesn't
128                     // require the MANAGE_VOICE_KEYPHRASES permission.
129                     Slog.w(TAG, ai.packageName + " does not require MANAGE_VOICE_KEYPHRASES");
130                     continue;
131                 }
132 
133                 mKeyphrasePackageMap.put(
134                         getKeyphraseMetadataFromApplicationInfo(pm, ai, parseErrors),
135                         ai.packageName);
136             } catch (PackageManager.NameNotFoundException e) {
137                 String error = "error parsing voice enrollment meta-data for "
138                         + ri.activityInfo.packageName;
139                 parseErrors.add(error + ": " + e);
140                 Slog.w(TAG, error, e);
141             }
142         }
143 
144         if (mKeyphrasePackageMap.isEmpty()) {
145             String error = "No suitable enrollment application found";
146             parseErrors.add(error);
147             Slog.w(TAG, error);
148             mKeyphrases = null;
149         } else {
150             mKeyphrases = mKeyphrasePackageMap.keySet().toArray(
151                     new KeyphraseMetadata[mKeyphrasePackageMap.size()]);
152         }
153 
154         if (!parseErrors.isEmpty()) {
155             mParseError = TextUtils.join("\n", parseErrors);
156         }
157     }
158 
getKeyphraseMetadataFromApplicationInfo(PackageManager pm, ApplicationInfo ai, List<String> parseErrors)159     private KeyphraseMetadata getKeyphraseMetadataFromApplicationInfo(PackageManager pm,
160             ApplicationInfo ai, List<String> parseErrors) {
161         XmlResourceParser parser = null;
162         String packageName = ai.packageName;
163         KeyphraseMetadata keyphraseMetadata = null;
164         try {
165             parser = ai.loadXmlMetaData(pm, VOICE_KEYPHRASE_META_DATA);
166             if (parser == null) {
167                 String error = "No " + VOICE_KEYPHRASE_META_DATA + " meta-data for " + packageName;
168                 parseErrors.add(error);
169                 Slog.w(TAG, error);
170                 return null;
171             }
172 
173             Resources res = pm.getResourcesForApplication(ai);
174             AttributeSet attrs = Xml.asAttributeSet(parser);
175 
176             int type;
177             while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
178                     && type != XmlPullParser.START_TAG) {
179             }
180 
181             String nodeName = parser.getName();
182             if (!"voice-enrollment-application".equals(nodeName)) {
183                 String error = "Meta-data does not start with voice-enrollment-application tag for "
184                         + packageName;
185                 parseErrors.add(error);
186                 Slog.w(TAG, error);
187                 return null;
188             }
189 
190             TypedArray array = res.obtainAttributes(attrs,
191                     com.android.internal.R.styleable.VoiceEnrollmentApplication);
192             keyphraseMetadata = getKeyphraseFromTypedArray(array, packageName, parseErrors);
193             array.recycle();
194         } catch (XmlPullParserException e) {
195             String error = "Error parsing keyphrase enrollment meta-data for " + packageName;
196             parseErrors.add(error + ": " + e);
197             Slog.w(TAG, error, e);
198         } catch (IOException e) {
199             String error = "Error parsing keyphrase enrollment meta-data for " + packageName;
200             parseErrors.add(error + ": " + e);
201             Slog.w(TAG, error, e);
202         } catch (PackageManager.NameNotFoundException e) {
203             String error = "Error parsing keyphrase enrollment meta-data for " + packageName;
204             parseErrors.add(error + ": " + e);
205             Slog.w(TAG, error, e);
206         } finally {
207             if (parser != null) parser.close();
208         }
209         return keyphraseMetadata;
210     }
211 
getKeyphraseFromTypedArray(TypedArray array, String packageName, List<String> parseErrors)212     private KeyphraseMetadata getKeyphraseFromTypedArray(TypedArray array, String packageName,
213             List<String> parseErrors) {
214         // Get the keyphrase ID.
215         int searchKeyphraseId = array.getInt(
216                 com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphraseId, -1);
217         if (searchKeyphraseId <= 0) {
218             String error = "No valid searchKeyphraseId specified in meta-data for " + packageName;
219             parseErrors.add(error);
220             Slog.w(TAG, error);
221             return null;
222         }
223 
224         // Get the keyphrase text.
225         String searchKeyphrase = array.getString(
226                 com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphrase);
227         if (searchKeyphrase == null) {
228             String error = "No valid searchKeyphrase specified in meta-data for " + packageName;
229             parseErrors.add(error);
230             Slog.w(TAG, error);
231             return null;
232         }
233 
234         // Get the supported locales.
235         String searchKeyphraseSupportedLocales = array.getString(
236                 com.android.internal.R.styleable
237                         .VoiceEnrollmentApplication_searchKeyphraseSupportedLocales);
238         if (searchKeyphraseSupportedLocales == null) {
239             String error = "No valid searchKeyphraseSupportedLocales specified in meta-data for "
240                     + packageName;
241             parseErrors.add(error);
242             Slog.w(TAG, error);
243             return null;
244         }
245         ArraySet<Locale> locales = new ArraySet<>();
246         // Try adding locales if the locale string is non-empty.
247         if (!TextUtils.isEmpty(searchKeyphraseSupportedLocales)) {
248             try {
249                 String[] supportedLocalesDelimited = searchKeyphraseSupportedLocales.split(",");
250                 for (int i = 0; i < supportedLocalesDelimited.length; i++) {
251                     locales.add(Locale.forLanguageTag(supportedLocalesDelimited[i]));
252                 }
253             } catch (Exception ex) {
254                 // We catch a generic exception here because we don't want the system service
255                 // to be affected by a malformed metadata because invalid locales were specified
256                 // by the system application.
257                 String error = "Error reading searchKeyphraseSupportedLocales from meta-data for "
258                         + packageName;
259                 parseErrors.add(error);
260                 Slog.w(TAG, error);
261                 return null;
262             }
263         }
264 
265         // Get the supported recognition modes.
266         int recognitionModes = array.getInt(com.android.internal.R.styleable
267                 .VoiceEnrollmentApplication_searchKeyphraseRecognitionFlags, -1);
268         if (recognitionModes < 0) {
269             String error = "No valid searchKeyphraseRecognitionFlags specified in meta-data for "
270                     + packageName;
271             parseErrors.add(error);
272             Slog.w(TAG, error);
273             return null;
274         }
275         return new KeyphraseMetadata(searchKeyphraseId, searchKeyphrase, locales, recognitionModes);
276     }
277 
getParseError()278     public String getParseError() {
279         return mParseError;
280     }
281 
282     /**
283      * @return An array of available keyphrases that can be enrolled on the system.
284      *         It may be null if no keyphrases can be enrolled.
285      */
listKeyphraseMetadata()286     public KeyphraseMetadata[] listKeyphraseMetadata() {
287         return mKeyphrases;
288     }
289 
290     /**
291      * Returns an intent to launch an activity that manages the given keyphrase
292      * for the locale.
293      *
294      * @param action The enrollment related action that this intent is supposed to perform.
295      *        This can be one of {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
296      *        {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
297      *        or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}
298      * @param keyphrase The keyphrase that the user needs to be enrolled to.
299      * @param locale The locale for which the enrollment needs to be performed.
300      * @return An {@link Intent} to manage the keyphrase. This can be null if managing the
301      *         given keyphrase/locale combination isn't possible.
302      */
getManageKeyphraseIntent(int action, String keyphrase, Locale locale)303     public Intent getManageKeyphraseIntent(int action, String keyphrase, Locale locale) {
304         if (mKeyphrasePackageMap == null || mKeyphrasePackageMap.isEmpty()) {
305             Slog.w(TAG, "No enrollment application exists");
306             return null;
307         }
308 
309         KeyphraseMetadata keyphraseMetadata = getKeyphraseMetadata(keyphrase, locale);
310         if (keyphraseMetadata != null) {
311             Intent intent = new Intent(ACTION_MANAGE_VOICE_KEYPHRASES)
312                     .setPackage(mKeyphrasePackageMap.get(keyphraseMetadata))
313                     .putExtra(EXTRA_VOICE_KEYPHRASE_HINT_TEXT, keyphrase)
314                     .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale.toLanguageTag())
315                     .putExtra(EXTRA_VOICE_KEYPHRASE_ACTION, action);
316             return intent;
317         }
318         return null;
319     }
320 
321     /**
322      * Gets the {@link KeyphraseMetadata} for the given keyphrase and locale, null if any metadata
323      * isn't available for the given combination.
324      *
325      * @param keyphrase The keyphrase that the user needs to be enrolled to.
326      * @param locale The locale for which the enrollment needs to be performed.
327      *        This is a Java locale, for example "en_US".
328      * @return The metadata, if the enrollment client supports the given keyphrase
329      *         and locale, null otherwise.
330      */
getKeyphraseMetadata(String keyphrase, Locale locale)331     public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, Locale locale) {
332         if (mKeyphrases != null && mKeyphrases.length > 0) {
333           for (KeyphraseMetadata keyphraseMetadata : mKeyphrases) {
334               // Check if the given keyphrase is supported in the locale provided by
335               // the enrollment application.
336               if (keyphraseMetadata.supportsPhrase(keyphrase)
337                       && keyphraseMetadata.supportsLocale(locale)) {
338                   return keyphraseMetadata;
339               }
340           }
341         }
342         Slog.w(TAG, "No Enrollment application supports the given keyphrase/locale");
343         return null;
344     }
345 
346     @Override
toString()347     public String toString() {
348         return "KeyphraseEnrollmentInfo [Keyphrases=" + mKeyphrasePackageMap.toString()
349                 + ", ParseError=" + mParseError + "]";
350     }
351 }
352