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