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 KeyphraseMetadata metadata = 134 getKeyphraseMetadataFromApplicationInfo(pm, ai, parseErrors); 135 if (metadata != null) { 136 mKeyphrasePackageMap.put(metadata, ai.packageName); 137 } 138 } catch (PackageManager.NameNotFoundException e) { 139 String error = "error parsing voice enrollment meta-data for " 140 + ri.activityInfo.packageName; 141 parseErrors.add(error + ": " + e); 142 Slog.w(TAG, error, e); 143 } 144 } 145 146 if (mKeyphrasePackageMap.isEmpty()) { 147 String error = "No suitable enrollment application found"; 148 parseErrors.add(error); 149 Slog.w(TAG, error); 150 mKeyphrases = null; 151 } else { 152 mKeyphrases = mKeyphrasePackageMap.keySet().toArray( 153 new KeyphraseMetadata[mKeyphrasePackageMap.size()]); 154 } 155 156 if (!parseErrors.isEmpty()) { 157 mParseError = TextUtils.join("\n", parseErrors); 158 } 159 } 160 getKeyphraseMetadataFromApplicationInfo(PackageManager pm, ApplicationInfo ai, List<String> parseErrors)161 private KeyphraseMetadata getKeyphraseMetadataFromApplicationInfo(PackageManager pm, 162 ApplicationInfo ai, List<String> parseErrors) { 163 XmlResourceParser parser = null; 164 String packageName = ai.packageName; 165 KeyphraseMetadata keyphraseMetadata = null; 166 try { 167 parser = ai.loadXmlMetaData(pm, VOICE_KEYPHRASE_META_DATA); 168 if (parser == null) { 169 String error = "No " + VOICE_KEYPHRASE_META_DATA + " meta-data for " + packageName; 170 parseErrors.add(error); 171 Slog.w(TAG, error); 172 return null; 173 } 174 175 Resources res = pm.getResourcesForApplication(ai); 176 AttributeSet attrs = Xml.asAttributeSet(parser); 177 178 int type; 179 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 180 && type != XmlPullParser.START_TAG) { 181 } 182 183 String nodeName = parser.getName(); 184 if (!"voice-enrollment-application".equals(nodeName)) { 185 String error = "Meta-data does not start with voice-enrollment-application tag for " 186 + packageName; 187 parseErrors.add(error); 188 Slog.w(TAG, error); 189 return null; 190 } 191 192 TypedArray array = res.obtainAttributes(attrs, 193 com.android.internal.R.styleable.VoiceEnrollmentApplication); 194 keyphraseMetadata = getKeyphraseFromTypedArray(array, packageName, parseErrors); 195 array.recycle(); 196 } catch (XmlPullParserException e) { 197 String error = "Error parsing keyphrase enrollment meta-data for " + packageName; 198 parseErrors.add(error + ": " + e); 199 Slog.w(TAG, error, e); 200 } catch (IOException e) { 201 String error = "Error parsing keyphrase enrollment meta-data for " + packageName; 202 parseErrors.add(error + ": " + e); 203 Slog.w(TAG, error, e); 204 } catch (PackageManager.NameNotFoundException e) { 205 String error = "Error parsing keyphrase enrollment meta-data for " + packageName; 206 parseErrors.add(error + ": " + e); 207 Slog.w(TAG, error, e); 208 } finally { 209 if (parser != null) parser.close(); 210 } 211 return keyphraseMetadata; 212 } 213 getKeyphraseFromTypedArray(TypedArray array, String packageName, List<String> parseErrors)214 private KeyphraseMetadata getKeyphraseFromTypedArray(TypedArray array, String packageName, 215 List<String> parseErrors) { 216 // Get the keyphrase ID. 217 int searchKeyphraseId = array.getInt( 218 com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphraseId, -1); 219 if (searchKeyphraseId <= 0) { 220 String error = "No valid searchKeyphraseId specified in meta-data for " + packageName; 221 parseErrors.add(error); 222 Slog.w(TAG, error); 223 return null; 224 } 225 226 // Get the keyphrase text. 227 String searchKeyphrase = array.getString( 228 com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphrase); 229 if (searchKeyphrase == null) { 230 String error = "No valid searchKeyphrase specified in meta-data for " + packageName; 231 parseErrors.add(error); 232 Slog.w(TAG, error); 233 return null; 234 } 235 236 // Get the supported locales. 237 String searchKeyphraseSupportedLocales = array.getString( 238 com.android.internal.R.styleable 239 .VoiceEnrollmentApplication_searchKeyphraseSupportedLocales); 240 if (searchKeyphraseSupportedLocales == null) { 241 String error = "No valid searchKeyphraseSupportedLocales specified in meta-data for " 242 + packageName; 243 parseErrors.add(error); 244 Slog.w(TAG, error); 245 return null; 246 } 247 ArraySet<Locale> locales = new ArraySet<>(); 248 // Try adding locales if the locale string is non-empty. 249 if (!TextUtils.isEmpty(searchKeyphraseSupportedLocales)) { 250 try { 251 String[] supportedLocalesDelimited = searchKeyphraseSupportedLocales.split(","); 252 for (int i = 0; i < supportedLocalesDelimited.length; i++) { 253 locales.add(Locale.forLanguageTag(supportedLocalesDelimited[i])); 254 } 255 } catch (Exception ex) { 256 // We catch a generic exception here because we don't want the system service 257 // to be affected by a malformed metadata because invalid locales were specified 258 // by the system application. 259 String error = "Error reading searchKeyphraseSupportedLocales from meta-data for " 260 + packageName; 261 parseErrors.add(error); 262 Slog.w(TAG, error); 263 return null; 264 } 265 } 266 267 // Get the supported recognition modes. 268 int recognitionModes = array.getInt(com.android.internal.R.styleable 269 .VoiceEnrollmentApplication_searchKeyphraseRecognitionFlags, -1); 270 if (recognitionModes < 0) { 271 String error = "No valid searchKeyphraseRecognitionFlags specified in meta-data for " 272 + packageName; 273 parseErrors.add(error); 274 Slog.w(TAG, error); 275 return null; 276 } 277 return new KeyphraseMetadata(searchKeyphraseId, searchKeyphrase, locales, recognitionModes); 278 } 279 getParseError()280 public String getParseError() { 281 return mParseError; 282 } 283 284 /** 285 * @return An array of available keyphrases that can be enrolled on the system. 286 * It may be null if no keyphrases can be enrolled. 287 */ listKeyphraseMetadata()288 public KeyphraseMetadata[] listKeyphraseMetadata() { 289 return mKeyphrases; 290 } 291 292 /** 293 * Returns an intent to launch an activity that manages the given keyphrase 294 * for the locale. 295 * 296 * @param action The enrollment related action that this intent is supposed to perform. 297 * This can be one of {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL}, 298 * {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL} 299 * or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL} 300 * @param keyphrase The keyphrase that the user needs to be enrolled to. 301 * @param locale The locale for which the enrollment needs to be performed. 302 * @return An {@link Intent} to manage the keyphrase. This can be null if managing the 303 * given keyphrase/locale combination isn't possible. 304 */ getManageKeyphraseIntent(int action, String keyphrase, Locale locale)305 public Intent getManageKeyphraseIntent(int action, String keyphrase, Locale locale) { 306 if (mKeyphrasePackageMap == null || mKeyphrasePackageMap.isEmpty()) { 307 Slog.w(TAG, "No enrollment application exists"); 308 return null; 309 } 310 311 KeyphraseMetadata keyphraseMetadata = getKeyphraseMetadata(keyphrase, locale); 312 if (keyphraseMetadata != null) { 313 Intent intent = new Intent(ACTION_MANAGE_VOICE_KEYPHRASES) 314 .setPackage(mKeyphrasePackageMap.get(keyphraseMetadata)) 315 .putExtra(EXTRA_VOICE_KEYPHRASE_HINT_TEXT, keyphrase) 316 .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale.toLanguageTag()) 317 .putExtra(EXTRA_VOICE_KEYPHRASE_ACTION, action); 318 return intent; 319 } 320 return null; 321 } 322 323 /** 324 * Gets the {@link KeyphraseMetadata} for the given keyphrase and locale, null if any metadata 325 * isn't available for the given combination. 326 * 327 * @param keyphrase The keyphrase that the user needs to be enrolled to. 328 * @param locale The locale for which the enrollment needs to be performed. 329 * This is a Java locale, for example "en_US". 330 * @return The metadata, if the enrollment client supports the given keyphrase 331 * and locale, null otherwise. 332 */ getKeyphraseMetadata(String keyphrase, Locale locale)333 public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, Locale locale) { 334 if (mKeyphrases != null && mKeyphrases.length > 0) { 335 for (KeyphraseMetadata keyphraseMetadata : mKeyphrases) { 336 // Check if the given keyphrase is supported in the locale provided by 337 // the enrollment application. 338 if (keyphraseMetadata.supportsPhrase(keyphrase) 339 && keyphraseMetadata.supportsLocale(locale)) { 340 return keyphraseMetadata; 341 } 342 } 343 } 344 Slog.w(TAG, "No enrollment application supports the given keyphrase/locale: '" 345 + keyphrase + "'/" + locale); 346 return null; 347 } 348 349 @Override toString()350 public String toString() { 351 return "KeyphraseEnrollmentInfo [Keyphrases=" + mKeyphrasePackageMap.toString() 352 + ", ParseError=" + mParseError + "]"; 353 } 354 } 355