content: /**
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.hardware.soundtrigger;
  
  import android.Manifest;
  import android.content.Intent;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.content.res.XmlResourceParser;
  import android.service.voice.AlwaysOnHotwordDetector;
  import android.text.TextUtils;
  import android.util.ArraySet;
  import android.util.AttributeSet;
  import android.util.Slog;
  import android.util.Xml;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  
  /**
   * Enrollment information about the different available keyphrases.
   *
   * @hide
   */
  public class KeyphraseEnrollmentInfo {
      private static final String TAG = "KeyphraseEnrollmentInfo";
      /**
       * Name under which a Hotword enrollment component publishes information about itself.
       * This meta-data should reference an XML resource containing a
       * <code>&lt;{@link
       * android.R.styleable#VoiceEnrollmentApplication
       * voice-enrollment-application}&gt;</code> tag.
       */
      private static final String VOICE_KEYPHRASE_META_DATA = "android.voice_enrollment";
      /**
       * Activity Action: Show activity for managing the keyphrases for hotword detection.
       * This needs to be defined by an activity that supports enrolling users for hotword/keyphrase
       * detection.
       */
      public static final String ACTION_MANAGE_VOICE_KEYPHRASES =
              "com.android.intent.action.MANAGE_VOICE_KEYPHRASES";
      /**
       * Intent extra: The intent extra for the specific manage action that needs to be performed.
       * Possible values are {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
       * {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
       * or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}.
       */
      public static final String EXTRA_VOICE_KEYPHRASE_ACTION =
              "com.android.intent.extra.VOICE_KEYPHRASE_ACTION";
  
      /**
       * Intent extra: The hint text to be shown on the voice keyphrase management UI.
       */
      public static final String EXTRA_VOICE_KEYPHRASE_HINT_TEXT =
              "com.android.intent.extra.VOICE_KEYPHRASE_HINT_TEXT";
      /**
       * Intent extra: The voice locale to use while managing the keyphrase.
       * This is a BCP-47 language tag.
       */
      public static final String EXTRA_VOICE_KEYPHRASE_LOCALE =
              "com.android.intent.extra.VOICE_KEYPHRASE_LOCALE";
  
      /**
       * List of available keyphrases.
       */
      final private KeyphraseMetadata[] mKeyphrases;
  
      /**
       * Map between KeyphraseMetadata and the package name of the enrollment app that provides it.
       */
      final private Map<KeyphraseMetadata, String> mKeyphrasePackageMap;
  
      private String mParseError;
  
      public KeyphraseEnrollmentInfo(PackageManager pm) {
          // Find the apps that supports enrollment for hotword keyhphrases,
          // Pick a privileged app and obtain the information about the supported keyphrases
          // from its metadata.
          List<ResolveInfo> ris = pm.queryIntentActivities(
                  new Intent(ACTION_MANAGE_VOICE_KEYPHRASES), PackageManager.MATCH_DEFAULT_ONLY);
          if (ris == null || ris.isEmpty()) {
              // No application capable of enrolling for voice keyphrases is present.
              mParseError = "No enrollment applications found";
              mKeyphrasePackageMap = Collections.<KeyphraseMetadata, String>emptyMap();
              mKeyphrases = null;
              return;
          }
  
          List<String> parseErrors = new LinkedList<String>();
          mKeyphrasePackageMap = new HashMap<KeyphraseMetadata, String>();
          for (ResolveInfo ri : ris) {
              try {
                  ApplicationInfo ai = pm.getApplicationInfo(
                          ri.activityInfo.packageName, PackageManager.GET_META_DATA);
                  if ((ai.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) == 0) {
                      // The application isn't privileged (/system/priv-app).
                      // The enrollment application needs to be a privileged system app.
                      Slog.w(TAG, ai.packageName + "is not a privileged system app");
                      continue;
                  }
                  if (!Manifest.permission.MANAGE_VOICE_KEYPHRASES.equals(ai.permission)) {
                      // The application trying to manage keyphrases doesn't
                      // require the MANAGE_VOICE_KEYPHRASES permission.
                      Slog.w(TAG, ai.packageName + " does not require MANAGE_VOICE_KEYPHRASES");
                      continue;
                  }
  
                  KeyphraseMetadata metadata =
                          getKeyphraseMetadataFromApplicationInfo(pm, ai, parseErrors);
                  if (metadata != null) {
                      mKeyphrasePackageMap.put(metadata, ai.packageName);
                  }
              } catch (PackageManager.NameNotFoundException e) {
                  String error = "error parsing voice enrollment meta-data for "
                          + ri.activityInfo.packageName;
                  parseErrors.add(error + ": " + e);
                  Slog.w(TAG, error, e);
              }
          }
  
          if (mKeyphrasePackageMap.isEmpty()) {
              String error = "No suitable enrollment application found";
              parseErrors.add(error);
              Slog.w(TAG, error);
              mKeyphrases = null;
          } else {
              mKeyphrases = mKeyphrasePackageMap.keySet().toArray(
                      new KeyphraseMetadata[mKeyphrasePackageMap.size()]);
          }
  
          if (!parseErrors.isEmpty()) {
              mParseError = TextUtils.join("\n", parseErrors);
          }
      }
  
      private KeyphraseMetadata getKeyphraseMetadataFromApplicationInfo(PackageManager pm,
              ApplicationInfo ai, List<String> parseErrors) {
          XmlResourceParser parser = null;
          String packageName = ai.packageName;
          KeyphraseMetadata keyphraseMetadata = null;
          try {
              parser = ai.loadXmlMetaData(pm, VOICE_KEYPHRASE_META_DATA);
              if (parser == null) {
                  String error = "No " + VOICE_KEYPHRASE_META_DATA + " meta-data for " + packageName;
                  parseErrors.add(error);
                  Slog.w(TAG, error);
                  return null;
              }
  
              Resources res = pm.getResourcesForApplication(ai);
              AttributeSet attrs = Xml.asAttributeSet(parser);
  
              int type;
              while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
                      && type != XmlPullParser.START_TAG) {
              }
  
              String nodeName = parser.getName();
              if (!"voice-enrollment-application".equals(nodeName)) {
                  String error = "Meta-data does not start with voice-enrollment-application tag for "
                          + packageName;
                  parseErrors.add(error);
                  Slog.w(TAG, error);
                  return null;
              }
  
              TypedArray array = res.obtainAttributes(attrs,
                      com.android.internal.R.styleable.VoiceEnrollmentApplication);
              keyphraseMetadata = getKeyphraseFromTypedArray(array, packageName, parseErrors);
              array.recycle();
          } catch (XmlPullParserException e) {
              String error = "Error parsing keyphrase enrollment meta-data for " + packageName;
              parseErrors.add(error + ": " + e);
              Slog.w(TAG, error, e);
          } catch (IOException e) {
              String error = "Error parsing keyphrase enrollment meta-data for " + packageName;
              parseErrors.add(error + ": " + e);
              Slog.w(TAG, error, e);
          } catch (PackageManager.NameNotFoundException e) {
              String error = "Error parsing keyphrase enrollment meta-data for " + packageName;
              parseErrors.add(error + ": " + e);
              Slog.w(TAG, error, e);
          } finally {
              if (parser != null) parser.close();
          }
          return keyphraseMetadata;
      }
  
      private KeyphraseMetadata getKeyphraseFromTypedArray(TypedArray array, String packageName,
              List<String> parseErrors) {
          // Get the keyphrase ID.
          int searchKeyphraseId = array.getInt(
                  com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphraseId, -1);
          if (searchKeyphraseId <= 0) {
              String error = "No valid searchKeyphraseId specified in meta-data for " + packageName;
              parseErrors.add(error);
              Slog.w(TAG, error);
              return null;
          }
  
          // Get the keyphrase text.
          String searchKeyphrase = array.getString(
                  com.android.internal.R.styleable.VoiceEnrollmentApplication_searchKeyphrase);
          if (searchKeyphrase == null) {
              String error = "No valid searchKeyphrase specified in meta-data for " + packageName;
              parseErrors.add(error);
              Slog.w(TAG, error);
              return null;
          }
  
          // Get the supported locales.
          String searchKeyphraseSupportedLocales = array.getString(
                  com.android.internal.R.styleable
                          .VoiceEnrollmentApplication_searchKeyphraseSupportedLocales);
          if (searchKeyphraseSupportedLocales == null) {
              String error = "No valid searchKeyphraseSupportedLocales specified in meta-data for "
                      + packageName;
              parseErrors.add(error);
              Slog.w(TAG, error);
              return null;
          }
          ArraySet<Locale> locales = new ArraySet<>();
          // Try adding locales if the locale string is non-empty.
          if (!TextUtils.isEmpty(searchKeyphraseSupportedLocales)) {
              try {
                  String[] supportedLocalesDelimited = searchKeyphraseSupportedLocales.split(",");
                  for (int i = 0; i < supportedLocalesDelimited.length; i++) {
                      locales.add(Locale.forLanguageTag(supportedLocalesDelimited[i]));
                  }
              } catch (Exception ex) {
                  // We catch a generic exception here because we don't want the system service
                  // to be affected by a malformed metadata because invalid locales were specified
                  // by the system application.
                  String error = "Error reading searchKeyphraseSupportedLocales from meta-data for "
                          + packageName;
                  parseErrors.add(error);
                  Slog.w(TAG, error);
                  return null;
              }
          }
  
          // Get the supported recognition modes.
          int recognitionModes = array.getInt(com.android.internal.R.styleable
                  .VoiceEnrollmentApplication_searchKeyphraseRecognitionFlags, -1);
          if (recognitionModes < 0) {
              String error = "No valid searchKeyphraseRecognitionFlags specified in meta-data for "
                      + packageName;
              parseErrors.add(error);
              Slog.w(TAG, error);
              return null;
          }
          return new KeyphraseMetadata(searchKeyphraseId, searchKeyphrase, locales, recognitionModes);
      }
  
      public String getParseError() {
          return mParseError;
      }
  
      /**
       * @return An array of available keyphrases that can be enrolled on the system.
       *         It may be null if no keyphrases can be enrolled.
       */
      public KeyphraseMetadata[] listKeyphraseMetadata() {
          return mKeyphrases;
      }
  
      /**
       * Returns an intent to launch an activity that manages the given keyphrase
       * for the locale.
       *
       * @param action The enrollment related action that this intent is supposed to perform.
       *        This can be one of {@link AlwaysOnHotwordDetector#MANAGE_ACTION_ENROLL},
       *        {@link AlwaysOnHotwordDetector#MANAGE_ACTION_RE_ENROLL}
       *        or {@link AlwaysOnHotwordDetector#MANAGE_ACTION_UN_ENROLL}
       * @param keyphrase The keyphrase that the user needs to be enrolled to.
       * @param locale The locale for which the enrollment needs to be performed.
       * @return An {@link Intent} to manage the keyphrase. This can be null if managing the
       *         given keyphrase/locale combination isn't possible.
       */
      public Intent getManageKeyphraseIntent(int action, String keyphrase, Locale locale) {
          if (mKeyphrasePackageMap == null || mKeyphrasePackageMap.isEmpty()) {
              Slog.w(TAG, "No enrollment application exists");
              return null;
          }
  
          KeyphraseMetadata keyphraseMetadata = getKeyphraseMetadata(keyphrase, locale);
          if (keyphraseMetadata != null) {
              Intent intent = new Intent(ACTION_MANAGE_VOICE_KEYPHRASES)
                      .setPackage(mKeyphrasePackageMap.get(keyphraseMetadata))
                      .putExtra(EXTRA_VOICE_KEYPHRASE_HINT_TEXT, keyphrase)
                      .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale.toLanguageTag())
                      .putExtra(EXTRA_VOICE_KEYPHRASE_ACTION, action);
              return intent;
          }
          return null;
      }
  
      /**
       * Gets the {@link KeyphraseMetadata} for the given keyphrase and locale, null if any metadata
       * isn't available for the given combination.
       *
       * @param keyphrase The keyphrase that the user needs to be enrolled to.
       * @param locale The locale for which the enrollment needs to be performed.
       *        This is a Java locale, for example "en_US".
       * @return The metadata, if the enrollment client supports the given keyphrase
       *         and locale, null otherwise.
       */
      public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, Locale locale) {
          if (mKeyphrases != null && mKeyphrases.length > 0) {
            for (KeyphraseMetadata keyphraseMetadata : mKeyphrases) {
                // Check if the given keyphrase is supported in the locale provided by
                // the enrollment application.
                if (keyphraseMetadata.supportsPhrase(keyphrase)
                        && keyphraseMetadata.supportsLocale(locale)) {
                    return keyphraseMetadata;
                }
            }
          }
          Slog.w(TAG, "No enrollment application supports the given keyphrase/locale: '"
                  + keyphrase + "'/" + locale);
          return null;
      }
  
      @Override
      public String toString() {
          return "KeyphraseEnrollmentInfo [Keyphrases=" + mKeyphrasePackageMap.toString()
                  + ", ParseError=" + mParseError + "]";
      }
  }
  