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