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><{@link 56 * android.R.styleable#VoiceEnrollmentApplication 57 * voice-enrollment-application}></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