Home | History | Annotate | Download | only in soundtrigger
      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 
    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 
    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 
    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 
    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      */
    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      */
    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      */
    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
    350     public String toString() {
    351         return "KeyphraseEnrollmentInfo [Keyphrases=" + mKeyphrasePackageMap.toString()
    352                 + ", ParseError=" + mParseError + "]";
    353     }
    354 }
    355