diff options
Diffstat (limited to 'core/java')
14 files changed, 795 insertions, 167 deletions
diff --git a/core/java/android/service/voice/DspInfo.java b/core/java/android/hardware/soundtrigger/DspInfo.java index 086230962cee..517159d35101 100644 --- a/core/java/android/service/voice/DspInfo.java +++ b/core/java/android/hardware/soundtrigger/DspInfo.java @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,13 @@ * limitations under the License. */ -package android.service.voice; +package android.hardware.soundtrigger; import java.util.UUID; /** * Properties of the DSP hardware on the device. + * * @hide */ public class DspInfo { diff --git a/core/java/android/hardware/soundtrigger/Keyphrase.aidl b/core/java/android/hardware/soundtrigger/Keyphrase.aidl new file mode 100644 index 000000000000..d9853a732096 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/Keyphrase.aidl @@ -0,0 +1,4 @@ +package android.hardware.soundtrigger; + +// @hide +parcelable Keyphrase;
\ No newline at end of file diff --git a/core/java/android/hardware/soundtrigger/Keyphrase.java b/core/java/android/hardware/soundtrigger/Keyphrase.java new file mode 100644 index 000000000000..42fd3502e126 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/Keyphrase.java @@ -0,0 +1,101 @@ +/** + * 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.os.Parcel; +import android.os.Parcelable; + +/** + * A Voice Keyphrase. + * + * @hide + */ +public class Keyphrase implements Parcelable { + /** A unique identifier for this keyphrase */ + public final int id; + /** A hint text to display corresponding to this keyphrase, e.g. "Hello There". */ + public final String hintText; + /** The locale of interest when using this Keyphrase. */ + public String locale; + + public static final Parcelable.Creator<Keyphrase> CREATOR + = new Parcelable.Creator<Keyphrase>() { + public Keyphrase createFromParcel(Parcel in) { + return Keyphrase.fromParcel(in); + } + + public Keyphrase[] newArray(int size) { + return new Keyphrase[size]; + } + }; + + private static Keyphrase fromParcel(Parcel in) { + return new Keyphrase(in.readInt(), in.readString(), in.readString()); + } + + public Keyphrase(int id, String hintText, String locale) { + this.id = id; + this.hintText = hintText; + this.locale = locale; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(id); + dest.writeString(hintText); + dest.writeString(locale); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((hintText == null) ? 0 : hintText.hashCode()); + result = prime * result + id; + result = prime * result + ((locale == null) ? 0 : locale.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Keyphrase other = (Keyphrase) obj; + if (hintText == null) { + if (other.hintText != null) + return false; + } else if (!hintText.equals(other.hintText)) + return false; + if (id != other.id) + return false; + if (locale == null) { + if (other.locale != null) + return false; + } else if (!locale.equals(other.locale)) + return false; + return true; + } +} diff --git a/core/java/android/service/voice/KeyphraseEnrollmentInfo.java b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java index ebe41ce43ef6..2f5de6ace761 100644 --- a/core/java/android/service/voice/KeyphraseEnrollmentInfo.java +++ b/core/java/android/hardware/soundtrigger/KeyphraseEnrollmentInfo.java @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.service.voice; +package android.hardware.soundtrigger; import android.Manifest; import android.content.Intent; @@ -24,6 +24,7 @@ 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.util.AttributeSet; import android.util.Slog; import android.util.Xml; @@ -34,7 +35,11 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.List; -/** @hide */ +/** + * Enrollment information about the different available keyphrases. + * + * @hide + */ public class KeyphraseEnrollmentInfo { private static final String TAG = "KeyphraseEnrollmentInfo"; /** @@ -53,10 +58,14 @@ public class KeyphraseEnrollmentInfo { public static final String ACTION_MANAGE_VOICE_KEYPHRASES = "com.android.intent.action.MANAGE_VOICE_KEYPHRASES"; /** - * Intent extra: The intent extra for un-enrolling a user for a particular keyphrase. + * 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_UNENROLL = - "com.android.intent.extra.VOICE_KEYPHRASE_UNENROLL"; + 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. */ @@ -68,7 +77,7 @@ public class KeyphraseEnrollmentInfo { public static final String EXTRA_VOICE_KEYPHRASE_LOCALE = "com.android.intent.extra.VOICE_KEYPHRASE_LOCALE"; - private KeyphraseInfo[] mKeyphrases; + private KeyphraseMetadata[] mKeyphrases; private String mEnrollmentPackage; private String mParseError; @@ -156,8 +165,8 @@ public class KeyphraseEnrollmentInfo { && !searchKeyphraseSupportedLocales.isEmpty()) { supportedLocales = searchKeyphraseSupportedLocales.split(","); } - mKeyphrases = new KeyphraseInfo[1]; - mKeyphrases[0] = new KeyphraseInfo( + mKeyphrases = new KeyphraseMetadata[1]; + mKeyphrases[0] = new KeyphraseMetadata( searchKeyphraseId, searchKeyphrase, supportedLocales); } else { mParseError = "searchKeyphraseId not specified in meta-data"; @@ -188,7 +197,7 @@ public class KeyphraseEnrollmentInfo { * @return An array of available keyphrases that can be enrolled on the system. * It may be null if no keyphrases can be enrolled. */ - public KeyphraseInfo[] getKeyphrases() { + public KeyphraseMetadata[] listKeyphraseMetadata() { return mKeyphrases; } @@ -196,51 +205,56 @@ public class KeyphraseEnrollmentInfo { * Returns an intent to launch an activity that manages the given keyphrase * for the locale. * - * @param enroll Indicates if the intent should enroll the user or un-enroll them. + * @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. + * This is a Java locale, for example "en_US". * @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(boolean enroll, String keyphrase, String locale) { + public Intent getManageKeyphraseIntent(int action, String keyphrase, String locale) { if (mEnrollmentPackage == null || mEnrollmentPackage.isEmpty()) { Slog.w(TAG, "No enrollment application exists"); return null; } - if (isKeyphraseEnrollmentSupported(keyphrase, locale)) { + if (getKeyphraseMetadata(keyphrase, locale) != null) { Intent intent = new Intent(ACTION_MANAGE_VOICE_KEYPHRASES) .setPackage(mEnrollmentPackage) .putExtra(EXTRA_VOICE_KEYPHRASE_HINT_TEXT, keyphrase) - .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale); - if (!enroll) intent.putExtra(EXTRA_VOICE_KEYPHRASE_UNENROLL, true); + .putExtra(EXTRA_VOICE_KEYPHRASE_LOCALE, locale) + .putExtra(EXTRA_VOICE_KEYPHRASE_ACTION, action); return intent; } return null; } /** - * Indicates if enrollment is supported for the given keyphrase & locale. + * 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 true, if an enrollment client supports the given keyphrase and the given locale. */ - public boolean isKeyphraseEnrollmentSupported(String keyphrase, String locale) { + public KeyphraseMetadata getKeyphraseMetadata(String keyphrase, String locale) { if (mKeyphrases == null || mKeyphrases.length == 0) { Slog.w(TAG, "Enrollment application doesn't support keyphrases"); - return false; + return null; } - for (KeyphraseInfo keyphraseInfo : mKeyphrases) { + for (KeyphraseMetadata keyphraseMetadata : mKeyphrases) { // Check if the given keyphrase is supported in the locale provided by // the enrollment application. - String supportedKeyphrase = keyphraseInfo.keyphrase; - if (supportedKeyphrase.equalsIgnoreCase(keyphrase) - && keyphraseInfo.supportedLocales.contains(locale)) { - return true; + if (keyphraseMetadata.supportsPhrase(keyphrase) + && keyphraseMetadata.supportsLocale(locale)) { + return keyphraseMetadata; } } - Slog.w(TAG, "Enrollment application doesn't support the given keyphrase"); - return false; + Slog.w(TAG, "Enrollment application doesn't support the given keyphrase/locale"); + return null; } } diff --git a/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java b/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java new file mode 100644 index 000000000000..03a49391ee55 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/KeyphraseMetadata.java @@ -0,0 +1,60 @@ +/** + * 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.util.ArraySet; + +/** + * A Voice Keyphrase metadata read from the enrollment application. + * + * @hide + */ +public class KeyphraseMetadata { + public final int id; + public final String keyphrase; + public final ArraySet<String> supportedLocales; + + public KeyphraseMetadata(int id, String keyphrase, String[] supportedLocales) { + this.id = id; + this.keyphrase = keyphrase; + this.supportedLocales = new ArraySet<String>(supportedLocales.length); + for (String locale : supportedLocales) { + this.supportedLocales.add(locale); + } + } + + @Override + public String toString() { + return "id=" + id + ", keyphrase=" + keyphrase + ", supported-locales=" + supportedLocales; + } + + /** + * @return Indicates if we support the given phrase. + */ + public boolean supportsPhrase(String phrase) { + // TODO(sansid): Come up with a scheme for custom keyphrases that should always match. + return keyphrase.equalsIgnoreCase(phrase); + } + + /** + * @return Indicates if we support the given locale. + */ + public boolean supportsLocale(String locale) { + // TODO(sansid): Come up with a scheme for keyphrases that are available in all locales. + return supportedLocales.contains(locale); + } +} diff --git a/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.aidl b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.aidl new file mode 100644 index 000000000000..39b33cc9cf74 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.aidl @@ -0,0 +1,4 @@ +package android.hardware.soundtrigger; + +// @hide +parcelable KeyphraseSoundModel;
\ No newline at end of file diff --git a/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.java b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.java new file mode 100644 index 000000000000..4ddba6af4c83 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/KeyphraseSoundModel.java @@ -0,0 +1,68 @@ +package android.hardware.soundtrigger; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.UUID; + +/** + * A KeyphraseSoundModel is a sound model capable of detecting voice keyphrases. + * It contains data needed by the hardware to detect a given number of key phrases + * and the list of corresponding {@link Keyphrase}s. + * + * @hide + */ +public class KeyphraseSoundModel implements Parcelable { + + /** Key phrases in this sound model */ + public final Keyphrase[] keyphrases; + public final byte[] data; + public final UUID uuid; + + public static final Parcelable.Creator<KeyphraseSoundModel> CREATOR + = new Parcelable.Creator<KeyphraseSoundModel>() { + public KeyphraseSoundModel createFromParcel(Parcel in) { + return KeyphraseSoundModel.fromParcel(in); + } + + public KeyphraseSoundModel[] newArray(int size) { + return new KeyphraseSoundModel[size]; + } + }; + + public KeyphraseSoundModel(UUID uuid, byte[] data,Keyphrase[] keyPhrases) { + this.uuid = uuid; + this.data = data; + this.keyphrases = keyPhrases; + } + + private static KeyphraseSoundModel fromParcel(Parcel in) { + UUID uuid = UUID.fromString(in.readString()); + int dataLength = in.readInt(); + byte[] data = null; + if (dataLength > 0) { + data = new byte[in.readInt()]; + in.readByteArray(data); + } + Keyphrase[] keyphrases = + (Keyphrase[]) in.readParcelableArray(Keyphrase.class.getClassLoader()); + return new KeyphraseSoundModel(uuid, data, keyphrases); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(uuid.toString()); + if (data != null) { + dest.writeInt(data.length); + dest.writeByteArray(data); + } else { + dest.writeInt(0); + } + dest.writeParcelableArray(keyphrases, 0); + } +} diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java index 7a4e5a5cd0df..1f48a92c42bb 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerHelper.java b/core/java/android/hardware/soundtrigger/SoundTriggerHelper.java new file mode 100644 index 000000000000..0be068dc8067 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/SoundTriggerHelper.java @@ -0,0 +1,217 @@ +/** + * 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.hardware.soundtrigger.SoundTrigger.ModuleProperties; +import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.ArrayList; + +/** + * Helper for {@link SoundTrigger} APIs. + * Currently this just acts as an abstraction over all SoundTrigger API calls. + * + * @hide + */ +public class SoundTriggerHelper implements SoundTrigger.StatusListener { + static final String TAG = "SoundTriggerHelper"; + // TODO: Remove this. + static final int TEMP_KEYPHRASE_ID = 1; + + /** + * Return codes for {@link #startRecognition(Keyphrase)}, {@link #stopRecognition(Keyphrase)} + * Note: Keep in sync with AlwaysOnKeyphraseInteractor.java + */ + public static final int STATUS_ERROR = Integer.MIN_VALUE; + public static final int STATUS_OK = 1; + + /** + * States for {@link Listener#onListeningStateChanged(int, int)}. + */ + public static final int STATE_STOPPED = 0; + public static final int STATE_STARTED = 1; + + private static final int INVALID_SOUND_MODEL_HANDLE = -1; + + /** The {@link DspInfo} for the system, or null if none exists. */ + public final DspInfo dspInfo; + + /** The properties for the DSP module */ + private final ModuleProperties mModuleProperties; + private final SoundTriggerModule mModule; + + private final SparseArray<Listener> mListeners; + + private int mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; + + /** + * The callback for sound trigger events. + */ + public interface Listener { + /** Called when the given keyphrase is spoken. */ + void onKeyphraseSpoken(); + + /** + * Called when the listening state for the given keyphrase changes. + * @param state Indicates the current state. + */ + void onListeningStateChanged(int state); + } + + public SoundTriggerHelper() { + ArrayList <ModuleProperties> modules = new ArrayList<>(); + int status = SoundTrigger.listModules(modules); + mListeners = new SparseArray<>(1); + if (status != SoundTrigger.STATUS_OK || modules.size() == 0) { + // TODO: Figure out how to handle errors in listing the modules here. + dspInfo = null; + mModuleProperties = null; + mModule = null; + } else { + // TODO: Figure out how to determine which module corresponds to the DSP hardware. + mModuleProperties = modules.get(0); + dspInfo = new DspInfo(mModuleProperties.uuid, mModuleProperties.implementor, + mModuleProperties.description, mModuleProperties.version, + mModuleProperties.powerConsumptionMw); + mModule = SoundTrigger.attachModule(mModuleProperties.id, this, null); + } + } + + /** + * @return True, if the given {@link Keyphrase} is supported on DSP. + */ + public boolean isKeyphraseSupported(Keyphrase keyphrase) { + // TODO: We also need to look into a SoundTrigger API that let's us + // query this. For now just return true. + return true; + } + + /** + * @return True, if the given {@link Keyphrase} has been enrolled. + */ + public boolean isKeyphraseEnrolled(Keyphrase keyphrase) { + // TODO: Query VoiceInteractionManagerService + // to list registered sound models. + return false; + } + + /** + * @return True, if a recognition for the given {@link Keyphrase} is active. + */ + public boolean isKeyphraseActive(Keyphrase keyphrase) { + // TODO: Check if the recognition for the keyphrase is currently active. + return false; + } + + /** + * Starts recognition for the given {@link Keyphrase}. + * + * @param keyphraseId The identifier of the keyphrase for which + * the recognition is to be started. + * @param listener The listener for the recognition events related to the given keyphrase. + * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. + */ + public int startRecognition(int keyphraseId, Listener listener) { + if (dspInfo == null || mModule == null) { + Slog.w(TAG, "Attempting startRecognition without the capability"); + return STATUS_ERROR; + } + + if (mListeners.get(keyphraseId) != listener) { + if (mCurrentSoundModelHandle != INVALID_SOUND_MODEL_HANDLE) { + Slog.w(TAG, "Canceling previous recognition"); + // TODO: Inspect the return codes here. + mModule.unloadSoundModel(mCurrentSoundModelHandle); + } + mListeners.get(keyphraseId).onListeningStateChanged(STATE_STOPPED); + } + + // Register the new listener. This replaces the old one. + // There can only be a maximum of one active listener for a keyphrase + // at any given time. + mListeners.put(keyphraseId, listener); + // TODO: Get the sound model for the given keyphrase here. + // mModule.loadSoundModel(model, soundModelHandle); + // mModule.startRecognition(soundModelHandle, data); + // mCurrentSoundModelHandle = soundModelHandle; + return STATUS_ERROR; + } + + /** + * Stops recognition for the given {@link Keyphrase} if a recognition is currently active. + * + * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. + */ + public int stopRecognition(int id, Listener listener) { + if (dspInfo == null || mModule == null) { + Slog.w(TAG, "Attempting stopRecognition without the capability"); + return STATUS_ERROR; + } + + if (mListeners.get(id) != listener) { + Slog.w(TAG, "Attempting stopRecognition for another recognition"); + return STATUS_ERROR; + } else { + // Stop recognition if it's the current one, ignore otherwise. + // TODO: Inspect the return codes here. + mModule.stopRecognition(mCurrentSoundModelHandle); + mModule.unloadSoundModel(mCurrentSoundModelHandle); + mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; + return STATUS_OK; + } + } + + //---- SoundTrigger.StatusListener methods + @Override + public void onRecognition(RecognitionEvent event) { + // Check which keyphrase triggered, and fire the appropriate event. + // TODO: Get the keyphrase out of the event and fire events on it. + // For now, as a nasty workaround, we fire all events to the listener for + // keyphrase with TEMP_KEYPHRASE_ID. + + switch (event.status) { + case SoundTrigger.RECOGNITION_STATUS_SUCCESS: + // TODO: The keyphrase should come from the recognition event + // as it may be for a different keyphrase than the current one. + if (mListeners.get(TEMP_KEYPHRASE_ID) != null) { + mListeners.get(TEMP_KEYPHRASE_ID).onKeyphraseSpoken(); + } + break; + case SoundTrigger.RECOGNITION_STATUS_ABORT: + // TODO: The keyphrase should come from the recognition event + // as it may be for a different keyphrase than the current one. + if (mListeners.get(TEMP_KEYPHRASE_ID) != null) { + mListeners.get(TEMP_KEYPHRASE_ID).onListeningStateChanged(STATE_STOPPED); + } + break; + case SoundTrigger.RECOGNITION_STATUS_FAILURE: + // TODO: The keyphrase should come from the recognition event + // as it may be for a different keyphrase than the current one. + if (mListeners.get(TEMP_KEYPHRASE_ID) != null) { + mListeners.get(TEMP_KEYPHRASE_ID).onListeningStateChanged(STATE_STOPPED); + } + break; + } + } + + @Override + public void onServiceDied() { + // TODO: Figure out how to restart the recognition here. + } +} diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java new file mode 100644 index 000000000000..67ce31e37d76 --- /dev/null +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -0,0 +1,270 @@ +/** + * 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.service.voice; + +import android.content.Intent; +import android.hardware.soundtrigger.Keyphrase; +import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; +import android.hardware.soundtrigger.KeyphraseMetadata; +import android.hardware.soundtrigger.SoundTriggerHelper; +import android.util.Slog; + +/** + * A class that lets a VoiceInteractionService implementation interact with + * always-on keyphrase detection APIs. + */ +public class AlwaysOnHotwordDetector { + //---- States of Keyphrase availability ----// + /** + * Indicates that the given keyphrase is not available on the system because of the + * hardware configuration. + */ + public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2; + /** + * Indicates that the given keyphrase is not supported. + */ + public static final int KEYPHRASE_UNSUPPORTED = -1; + /** + * Indicates that the given keyphrase is not enrolled. + */ + public static final int KEYPHRASE_UNENROLLED = 1; + /** + * Indicates that the given keyphrase is currently enrolled but not being actively listened for. + */ + public static final int KEYPHRASE_ENROLLED = 2; + + // Keyphrase management actions ----// + /** Indicates that we need to enroll. */ + public static final int MANAGE_ACTION_ENROLL = 0; + /** Indicates that we need to re-enroll. */ + public static final int MANAGE_ACTION_RE_ENROLL = 1; + /** Indicates that we need to un-enroll. */ + public static final int MANAGE_ACTION_UN_ENROLL = 2; + + /** + * Return codes for {@link #startRecognition()}, {@link #stopRecognition()} + */ + public static final int STATUS_ERROR = Integer.MIN_VALUE; + public static final int STATUS_OK = 1; + + //---- Keyphrase recognition status ----// + // TODO: Figure out if they are exclusive or should be flags instead? + public static final int RECOGNITION_NOT_AVAILABLE = -3; + public static final int RECOGNITION_NOT_REQUESTED = -2; + public static final int RECOGNITION_DISABLED_TEMPORARILY = -1; + public static final int RECOGNITION_REQUESTED = 1; + public static final int RECOGNITION_ACTIVE = 2; + static final String TAG = "AlwaysOnHotwordDetector"; + + private final String mText; + private final String mLocale; + private final Keyphrase mKeyphrase; + private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; + private final SoundTriggerHelper mSoundTriggerHelper; + private final SoundTriggerHelper.Listener mListener; + private final int mAvailability; + + private int mRecognitionState; + + /** + * Callbacks for always-on hotword detection. + */ + public interface Callback { + /** + * Called when the keyphrase is spoken. + * TODO: Add more data to the callback. + */ + void onDetected(); + /** + * Called when the detection for the associated keyphrase starts. + */ + void onDetectionStarted(); + /** + * Called when the detection for the associated keyphrase stops. + */ + void onDetectionStopped(); + } + + /** + * @param text The keyphrase text to get the detector for. + * @param locale The java locale for the detector. + * @param callback A non-null Callback for receiving the recognition events. + * + * @hide + */ + public AlwaysOnHotwordDetector(String text, String locale, Callback callback, + KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, + SoundTriggerHelper soundTriggerHelper) { + mText = text; + mLocale = locale; + mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo; + KeyphraseMetadata keyphraseMetadata = + mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale); + if (keyphraseMetadata != null) { + mKeyphrase = new Keyphrase(keyphraseMetadata.id, text, locale); + } else { + mKeyphrase = null; + } + mListener = new SoundTriggerListener(callback); + mSoundTriggerHelper = soundTriggerHelper; + mAvailability = getAvailabilityInternal(); + } + + /** + * Gets the state of always-on hotword detection for the given keyphrase and locale + * on this system. + * Availability implies that the hardware on this system is capable of listening for + * the given keyphrase or not. + * + * @return Indicates if always-on hotword detection is available for the given keyphrase. + * The return code is one of {@link #KEYPHRASE_HARDWARE_UNAVAILABLE}, + * {@link #KEYPHRASE_UNSUPPORTED}, {@link #KEYPHRASE_UNENROLLED} or + * {@link #KEYPHRASE_ENROLLED}. + */ + public int getAvailability() { + return mAvailability; + } + + /** + * Gets the status of the recognition. + * @return One of {@link #RECOGNITION_NOT_AVAILABLE}, {@link #RECOGNITION_NOT_REQUESTED}, + * {@link #RECOGNITION_DISABLED_TEMPORARILY} or {@link #RECOGNITION_ACTIVE}. + * @throws UnsupportedOperationException if the recognition isn't supported. + * Callers should check the availability by calling {@link #getAvailability()} + * before calling this method to avoid this exception. + */ + public int getRecognitionStatus() { + if (mAvailability != KEYPHRASE_ENROLLED) { + throw new UnsupportedOperationException( + "Recognition for the given keyphrase is not supported"); + } + + return mRecognitionState; + } + + /** + * Starts recognition for the associated keyphrase. + * + * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. + * @throws UnsupportedOperationException if the recognition isn't supported. + * Callers should check the availability by calling {@link #getAvailability()} + * before calling this method to avoid this exception. + */ + public int startRecognition() { + if (mAvailability != KEYPHRASE_ENROLLED) { + throw new UnsupportedOperationException( + "Recognition for the given keyphrase is not supported"); + } + + mRecognitionState = RECOGNITION_REQUESTED; + int code = mSoundTriggerHelper.startRecognition(mKeyphrase.id, mListener); + if (code != SoundTriggerHelper.STATUS_OK) { + Slog.w(TAG, "startRecognition() failed with error code " + code); + return STATUS_ERROR; + } else { + return STATUS_OK; + } + } + + /** + * Stops recognition for the associated keyphrase. + * + * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. + * @throws UnsupportedOperationException if the recognition isn't supported. + * Callers should check the availability by calling {@link #getAvailability()} + * before calling this method to avoid this exception. + */ + public int stopRecognition() { + if (mAvailability != KEYPHRASE_ENROLLED) { + throw new UnsupportedOperationException( + "Recognition for the given keyphrase is not supported"); + } + + mRecognitionState = RECOGNITION_NOT_REQUESTED; + int code = mSoundTriggerHelper.stopRecognition(mKeyphrase.id, mListener); + if (code != SoundTriggerHelper.STATUS_OK) { + Slog.w(TAG, "stopRecognition() failed with error code " + code); + return STATUS_ERROR; + } else { + return STATUS_OK; + } + } + + /** + * Gets an intent to manage the associated keyphrase. + * + * @param action The manage action that needs to be performed. + * One of {@link #MANAGE_ACTION_ENROLL}, {@link #MANAGE_ACTION_RE_ENROLL} or + * {@link #MANAGE_ACTION_UN_ENROLL}. + * @return An {@link Intent} to manage the given keyphrase. + * @throws UnsupportedOperationException if managing they keyphrase isn't supported. + * Callers should check the availability by calling {@link #getAvailability()} + * before calling this method to avoid this exception. + */ + public Intent getManageIntent(int action) { + if (mAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE + || mAvailability == KEYPHRASE_UNSUPPORTED) { + throw new UnsupportedOperationException( + "Managing the given keyphrase is not supported"); + } + if (action != MANAGE_ACTION_ENROLL + && action != MANAGE_ACTION_RE_ENROLL + && action != MANAGE_ACTION_UN_ENROLL) { + throw new IllegalArgumentException("Invalid action specified " + action); + } + + return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale); + } + + private int getAvailabilityInternal() { + if (mSoundTriggerHelper.dspInfo == null) { + return KEYPHRASE_HARDWARE_UNAVAILABLE; + } + if (mKeyphrase == null || !mSoundTriggerHelper.isKeyphraseSupported(mKeyphrase)) { + return KEYPHRASE_UNSUPPORTED; + } + if (!mSoundTriggerHelper.isKeyphraseEnrolled(mKeyphrase)) { + return KEYPHRASE_UNENROLLED; + } + return KEYPHRASE_ENROLLED; + } + + /** @hide */ + static final class SoundTriggerListener implements SoundTriggerHelper.Listener { + private final Callback mCallback; + + public SoundTriggerListener(Callback callback) { + this.mCallback = callback; + } + + @Override + public void onKeyphraseSpoken() { + Slog.i(TAG, "onKeyphraseSpoken"); + mCallback.onDetected(); + } + + @Override + public void onListeningStateChanged(int state) { + Slog.i(TAG, "onListeningStateChanged: state=" + state); + if (state == SoundTriggerHelper.STATE_STARTED) { + mCallback.onDetectionStarted(); + } else if (state == SoundTriggerHelper.STATE_STOPPED) { + mCallback.onDetectionStopped(); + } + } + } +} diff --git a/core/java/android/service/voice/KeyphraseInfo.java b/core/java/android/service/voice/KeyphraseInfo.java deleted file mode 100644 index d266e1a24471..000000000000 --- a/core/java/android/service/voice/KeyphraseInfo.java +++ /dev/null @@ -1,27 +0,0 @@ -package android.service.voice; - -import android.util.ArraySet; - -/** - * A Voice Keyphrase. - * @hide - */ -public class KeyphraseInfo { - public final int id; - public final String keyphrase; - public final ArraySet<String> supportedLocales; - - public KeyphraseInfo(int id, String keyphrase, String[] supportedLocales) { - this.id = id; - this.keyphrase = keyphrase; - this.supportedLocales = new ArraySet<String>(supportedLocales.length); - for (String locale : supportedLocales) { - this.supportedLocales.add(locale); - } - } - - @Override - public String toString() { - return "id=" + id + ", keyphrase=" + keyphrase + ", supported-locales=" + supportedLocales; - } -} diff --git a/core/java/android/service/voice/SoundTriggerManager.java b/core/java/android/service/voice/SoundTriggerManager.java deleted file mode 100644 index 2d049b9e8160..000000000000 --- a/core/java/android/service/voice/SoundTriggerManager.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.service.voice; - -import android.hardware.soundtrigger.SoundTrigger; -import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; - -import java.util.ArrayList; - -/** - * Manager for {@link SoundTrigger} APIs. - * Currently this just acts as an abstraction over all SoundTrigger API calls. - * @hide - */ -public class SoundTriggerManager { - /** The {@link DspInfo} for the system, or null if none exists. */ - public DspInfo dspInfo; - - public SoundTriggerManager() { - ArrayList <ModuleProperties> modules = new ArrayList<>(); - int status = SoundTrigger.listModules(modules); - if (status != SoundTrigger.STATUS_OK || modules.size() == 0) { - // TODO(sansid, elaurent): Figure out how to handle errors in listing the modules here. - dspInfo = null; - } else { - // TODO(sansid, elaurent): Figure out how to determine which module corresponds to the - // DSP hardware. - ModuleProperties properties = modules.get(0); - dspInfo = new DspInfo(properties.uuid, properties.implementor, properties.description, - properties.version, properties.powerConsumptionMw); - } - } - - /** - * @return True, if the keyphrase is supported on DSP for the given locale. - */ - public boolean isKeyphraseSupported(String keyphrase, String locale) { - // TODO(sansid): We also need to look into a SoundTrigger API that let's us - // query this. For now just return supported if there's a DSP available. - return dspInfo != null; - } - - /** - * @return True, if the keyphrase is has been enrolled for the given locale. - */ - public boolean isKeyphraseEnrolled(String keyphrase, String locale) { - // TODO(sansid, elaurent): Query SoundTrigger to list currently loaded sound models. - // They have been enrolled. - return false; - } - - /** - * @return True, if a recognition for the keyphrase is active for the given locale. - */ - public boolean isKeyphraseActive(String keyphrase, String locale) { - // TODO(sansid, elaurent): Check if the recognition for the keyphrase is currently active. - return false; - } -} diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index e0329f84564a..cf8d502dbea6 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -20,6 +20,8 @@ import android.annotation.SdkConstant; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; +import android.hardware.soundtrigger.SoundTriggerHelper; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; @@ -53,16 +55,6 @@ public class VoiceInteractionService extends Service { public static final String SERVICE_INTERFACE = "android.service.voice.VoiceInteractionService"; - // TODO(sansid): Unhide these. - /** @hide */ - public static final int KEYPHRASE_UNAVAILABLE = 0; - /** @hide */ - public static final int KEYPHRASE_UNENROLLED = 1; - /** @hide */ - public static final int KEYPHRASE_ENROLLED = 2; - /** @hide */ - public static final int KEYPHRASE_ACTIVE = 3; - /** * Name under which a VoiceInteractionService component publishes information about itself. * This meta-data should reference an XML resource containing a @@ -76,8 +68,8 @@ public class VoiceInteractionService extends Service { IVoiceInteractionManagerService mSystemService; - private SoundTriggerManager mSoundTriggerManager; private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; + private SoundTriggerHelper mSoundTriggerHelper; public void startSession(Bundle args) { try { @@ -92,7 +84,7 @@ public class VoiceInteractionService extends Service { mSystemService = IVoiceInteractionManagerService.Stub.asInterface( ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager()); - mSoundTriggerManager = new SoundTriggerManager(); + mSoundTriggerHelper = new SoundTriggerHelper(); } @Override @@ -104,34 +96,18 @@ public class VoiceInteractionService extends Service { } /** - * Gets the state of always-on hotword detection for the given keyphrase and locale - * on this system. - * Availability implies that the hardware on this system is capable of listening for - * the given keyphrase or not. - * The return code is one of {@link #KEYPHRASE_UNAVAILABLE}, {@link #KEYPHRASE_UNENROLLED} - * {@link #KEYPHRASE_ENROLLED} or {@link #KEYPHRASE_ACTIVE}. - * - * @param keyphrase The keyphrase whose availability is being checked. - * @param locale The locale for which the availability is being checked. - * @return Indicates if always-on hotword detection is available for the given keyphrase. - * TODO(sansid): Unhide this. - * @hide + * @param keyphrase The keyphrase that's being used, for example "Hello Android". + * @param locale The locale for which the enrollment needs to be performed. + * This is a Java locale, for example "en_US". + * @param callback The callback to notify of detection events. + * @return An always-on hotword detector for the given keyphrase and locale. */ - public final int getAlwaysOnKeyphraseAvailability(String keyphrase, String locale) { - // The available keyphrases is a combination of DSP availability and - // the keyphrases that have an enrollment application for them. - if (!mSoundTriggerManager.isKeyphraseSupported(keyphrase, locale) - || !mKeyphraseEnrollmentInfo.isKeyphraseEnrollmentSupported(keyphrase, locale)) { - return KEYPHRASE_UNAVAILABLE; - } - if (!mSoundTriggerManager.isKeyphraseEnrolled(keyphrase, locale)) { - return KEYPHRASE_UNENROLLED; - } - if (!mSoundTriggerManager.isKeyphraseActive(keyphrase, locale)) { - return KEYPHRASE_ENROLLED; - } else { - return KEYPHRASE_ACTIVE; - } + public final AlwaysOnHotwordDetector getAlwaysOnHotwordDetector( + String keyphrase, String locale, AlwaysOnHotwordDetector.Callback callback) { + // TODO: Cache instances and return the same one instead of creating a new interactor + // for the same keyphrase/locale combination. + return new AlwaysOnHotwordDetector(keyphrase, locale, callback, + mKeyphraseEnrollmentInfo, mSoundTriggerHelper); } /** diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 98e35ddf6cda..c78f770c5805 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -20,6 +20,7 @@ import android.content.Intent; import android.os.Bundle; import com.android.internal.app.IVoiceInteractor; +import android.hardware.soundtrigger.KeyphraseSoundModel; import android.service.voice.IVoiceInteractionService; import android.service.voice.IVoiceInteractionSession; @@ -29,4 +30,16 @@ interface IVoiceInteractionManagerService { IVoiceInteractor interactor); int startVoiceActivity(IBinder token, in Intent intent, String resolvedType); void finish(IBinder token); + + /** + * Lists the registered Sound models for keyphrase detection. + * May be null if no matching sound models exist. + * + * @param service The current voice interaction service. + */ + List<KeyphraseSoundModel> listRegisteredKeyphraseSoundModels(in IVoiceInteractionService service); + /** + * Updates the given keyphrase sound model. Adds the model if it doesn't exist currently. + */ + int updateKeyphraseSoundModel(in KeyphraseSoundModel model); } |
