diff options
Diffstat (limited to 'core/java/android')
9 files changed, 648 insertions, 41 deletions
diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java new file mode 100644 index 000000000000..e4eefc4e3a81 --- /dev/null +++ b/core/java/android/service/voice/AbstractHotwordDetector.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2021 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 static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.AudioFormat; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.app.IVoiceInteractionManagerService; + +/** Base implementation of {@link HotwordDetector}. */ +abstract class AbstractHotwordDetector implements HotwordDetector { + private static final String TAG = AbstractHotwordDetector.class.getSimpleName(); + private static final boolean DEBUG = false; + + private final IVoiceInteractionManagerService mManagerService; + private final Handler mHandler; + private final HotwordDetector.Callback mCallback; + + AbstractHotwordDetector( + IVoiceInteractionManagerService managerService, + HotwordDetector.Callback callback) { + mManagerService = managerService; + // TODO: this needs to be supplied from above + mHandler = new Handler(Looper.getMainLooper()); + mCallback = callback; + } + + /** + * Detect hotword from an externally supplied stream of data. + * + * @return a writeable file descriptor that clients can start writing data in the given format. + * In order to stop detection, clients can close the given stream. + */ + @Nullable + @Override + public boolean startRecognition( + @NonNull ParcelFileDescriptor audioStream, + @NonNull AudioFormat audioFormat, + @Nullable PersistableBundle options) { + if (DEBUG) { + Slog.i(TAG, "#recognizeHotword"); + } + + // TODO: consider closing existing session. + + try { + mManagerService.startListeningFromExternalSource( + audioStream, + audioFormat, + options, + new BinderCallback(mHandler, mCallback)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + return true; + } + + private static class BinderCallback + extends IMicrophoneHotwordDetectionVoiceInteractionCallback.Stub { + private final Handler mHandler; + // TODO: these need to be weak references. + private final HotwordDetector.Callback mCallback; + + BinderCallback(Handler handler, HotwordDetector.Callback callback) { + this.mHandler = handler; + this.mCallback = callback; + } + + /** TODO: onDetected */ + @Override + public void onDetected( + @Nullable HotwordDetectedResult hotwordDetectedResult, + @Nullable AudioFormat audioFormat, + @Nullable ParcelFileDescriptor audioStreamIgnored) { + mHandler.sendMessage(obtainMessage( + HotwordDetector.Callback::onDetected, + mCallback, + new AlwaysOnHotwordDetector.EventPayload(audioFormat, hotwordDetectedResult))); + } + } +} diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 94ca68ff6af8..73e0da16e049 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -68,7 +68,7 @@ import java.util.Locale; * mark and track it as such. */ @SystemApi -public class AlwaysOnHotwordDetector { +public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----// /** * Indicates that this hotword detector is no longer valid for any recognition @@ -459,7 +459,7 @@ public class AlwaysOnHotwordDetector { /** * Callbacks for always-on hotword detection. */ - public static abstract class Callback { + public abstract static class Callback implements HotwordDetector.Callback { /** * Updates the availability state of the active keyphrase and locale on every keyphrase @@ -547,11 +547,13 @@ public class AlwaysOnHotwordDetector { IVoiceInteractionManagerService modelManagementService, int targetSdkVersion, boolean supportHotwordDetectionService, @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { + super(modelManagementService, callback); + + mHandler = new MyHandler(); mText = text; mLocale = locale; mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo; mExternalCallback = callback; - mHandler = new MyHandler(); mInternalCallback = new SoundTriggerListener(mHandler); mModelManagementService = modelManagementService; mTargetSdkVersion = targetSdkVersion; @@ -705,6 +707,17 @@ public class AlwaysOnHotwordDetector { } /** + * Starts recognition for the associated keyphrase. + * + * @see #startRecognition(int) + */ + @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) + @Override + public boolean startRecognition() { + return startRecognition(0); + } + + /** * Stops recognition for the associated keyphrase. * Caller must be the active voice interaction service via * Settings.Secure.VOICE_INTERACTION_SERVICE. @@ -718,6 +731,7 @@ public class AlwaysOnHotwordDetector { * {@link VoiceInteractionService} hosting this detector has been shut down. */ @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) + @Override public boolean stopRecognition() { if (DBG) Slog.d(TAG, "stopRecognition()"); synchronized (mLock) { diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index db984c246b2f..fb731a094f90 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -20,6 +20,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa import android.annotation.CallSuper; import android.annotation.DurationMillisLong; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; @@ -36,6 +37,9 @@ import android.os.RemoteException; import android.os.SharedMemory; import android.util.Log; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Locale; /** @@ -51,6 +55,24 @@ public abstract class HotwordDetectionService extends Service { private static final boolean DBG = true; /** + * Source for the given audio stream. + * + * @hide + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + AUDIO_SOURCE_MICROPHONE, + AUDIO_SOURCE_EXTERNAL + }) + @interface AudioSource {} + + /** @hide */ + public static final int AUDIO_SOURCE_MICROPHONE = 1; + /** @hide */ + public static final int AUDIO_SOURCE_EXTERNAL = 2; + + /** * The {@link Intent} that must be declared as handled by the service. * To be supported, the service must also require the * {@link android.Manifest.permission#BIND_HOTWORD_DETECTION_SERVICE} permission so @@ -73,12 +95,12 @@ public abstract class HotwordDetectionService extends Service { if (DBG) { Log.d(TAG, "#detectFromDspSource"); } - mHandler.sendMessage(obtainMessage(HotwordDetectionService::onDetectFromDspSource, + mHandler.sendMessage(obtainMessage(HotwordDetectionService::onDetect, HotwordDetectionService.this, audioStream, audioFormat, timeoutMillis, - new DspHotwordDetectionCallback(callback))); + new Callback(callback))); } @Override @@ -92,6 +114,40 @@ public abstract class HotwordDetectionService extends Service { options, sharedMemory)); } + + @Override + public void detectFromMicrophoneSource( + ParcelFileDescriptor audioStream, + @AudioSource int audioSource, + AudioFormat audioFormat, + PersistableBundle options, + IDspHotwordDetectionCallback callback) + throws RemoteException { + if (DBG) { + Log.d(TAG, "#detectFromMicrophoneSource"); + } + switch (audioSource) { + case AUDIO_SOURCE_MICROPHONE: + mHandler.sendMessage(obtainMessage( + HotwordDetectionService::onDetect, + HotwordDetectionService.this, + audioStream, + audioFormat, + new Callback(callback))); + break; + case AUDIO_SOURCE_EXTERNAL: + mHandler.sendMessage(obtainMessage( + HotwordDetectionService::onDetect, + HotwordDetectionService.this, + audioStream, + audioFormat, + options, + new Callback(callback))); + break; + default: + Log.i(TAG, "Unsupported audio source " + audioSource); + } + } }; @CallSuper @@ -113,27 +169,30 @@ public abstract class HotwordDetectionService extends Service { } /** - * Detect the audio data generated from Dsp. - * - * <p>Note: the clients are supposed to call {@code close} on the input stream when they are - * done with the operation in order to free up resources. + * Called when the device hardware (such as a DSP) detected the hotword, to request second stage + * validation before handing over the audio to the {@link AlwaysOnHotwordDetector}. + * <p> + * After {@code callback} is invoked or {@code timeoutMillis} has passed, the system closes + * {@code audioStream} and invokes the appropriate {@link AlwaysOnHotwordDetector.Callback + * callback}. * * @param audioStream Stream containing audio bytes returned from DSP * @param audioFormat Format of the supplied audio * @param timeoutMillis Timeout in milliseconds for the operation to invoke the callback. If * the application fails to abide by the timeout, system will close the * microphone and cancel the operation. - * @param callback Use {@link HotwordDetectionService#DspHotwordDetectionCallback} to return - * the detected result. + * @param callback The callback to use for responding to the detection request. * * @hide */ @SystemApi - public void onDetectFromDspSource( + public void onDetect( @NonNull ParcelFileDescriptor audioStream, @NonNull AudioFormat audioFormat, @DurationMillisLong long timeoutMillis, - @NonNull DspHotwordDetectionCallback callback) { + @NonNull Callback callback) { + // TODO: Add a helpful error message. + throw new UnsupportedOperationException(); } /** @@ -154,38 +213,94 @@ public abstract class HotwordDetectionService extends Service { @SystemApi public void onUpdateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { + // TODO: Handle the unimplemented case by throwing? + } + + /** + * Called when the {@link VoiceInteractionService} requests that this service + * {@link HotwordDetector#startRecognition() start} hotword recognition on audio coming directly + * from the device microphone. + * <p> + * On such a request, the system streams mic audio to this service through {@code audioStream}. + * Audio is streamed until {@link HotwordDetector#stopRecognition()} is called, at which point + * the system closes {code audioStream}. + * <p> + * On successful detection of a hotword within {@code audioStream}, call + * {@link Callback#onDetected(HotwordDetectedResult)}. The system continues to stream audio + * through {@code audioStream}; {@code callback} is reusable. + * + * @param audioStream Stream containing audio bytes returned from a microphone + * @param audioFormat Format of the supplied audio + * @param callback The callback to use for responding to the detection request. + * {@link Callback#onRejected(HotwordRejectedResult) callback.onRejected} cannot be used here. + */ + public void onDetect( + @NonNull ParcelFileDescriptor audioStream, + @NonNull AudioFormat audioFormat, + @NonNull Callback callback) { + // TODO: Add a helpful error message. + throw new UnsupportedOperationException(); + } + + /** + * Called when the {@link VoiceInteractionService} requests that this service + * {@link HotwordDetector#startRecognition(ParcelFileDescriptor, AudioFormat, + * PersistableBundle)} run} hotword recognition on audio coming from an external connected + * microphone. + * <p> + * Upon invoking the {@code callback}, the system closes {@code audioStream} and sends the + * detection result to the {@link HotwordDetector.Callback hotword detector}. + * + * @param audioStream Stream containing audio bytes returned from a microphone + * @param audioFormat Format of the supplied audio + * @param options Options supporting detection, such as configuration specific to the source of + * the audio, provided through + * {@link HotwordDetector#startRecognition(ParcelFileDescriptor, AudioFormat, + * PersistableBundle)}. + * @param callback The callback to use for responding to the detection request. + */ + public void onDetect( + @NonNull ParcelFileDescriptor audioStream, + @NonNull AudioFormat audioFormat, + @Nullable PersistableBundle options, + @NonNull Callback callback) { + // TODO: Add a helpful error message. + throw new UnsupportedOperationException(); } /** - * Callback for returning the detected result. + * Callback for returning the detection result. * * @hide */ @SystemApi - public static final class DspHotwordDetectionCallback { + public static final class Callback { // TODO: need to make sure we don't store remote references, but not a high priority. private final IDspHotwordDetectionCallback mRemoteCallback; - private DspHotwordDetectionCallback(IDspHotwordDetectionCallback remoteCallback) { + private Callback(IDspHotwordDetectionCallback remoteCallback) { mRemoteCallback = remoteCallback; } /** * Called when the detected result is valid. */ - public void onDetected() { + public void onDetected(@Nullable HotwordDetectedResult hotwordDetectedResult) { try { - mRemoteCallback.onDetected(); + mRemoteCallback.onDetected(hotwordDetectedResult); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** - * Informs the {@link AlwaysOnHotwordDetector} that the keyphrase was not detected. + * Informs the {@link HotwordDetector} that the keyphrase was not detected. + * <p> + * This cannot not be used when recognition is done through + * {@link #onDetect(ParcelFileDescriptor, AudioFormat, Callback)}. * * @param result Info about the second stage detection result. This is provided to - * the {@link AlwaysOnHotwordDetector}. + * the {@link HotwordDetector}. */ public void onRejected(@Nullable HotwordRejectedResult result) { try { diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java index abf49b797da4..26491245914f 100644 --- a/core/java/android/service/voice/HotwordDetector.java +++ b/core/java/android/service/voice/HotwordDetector.java @@ -16,8 +16,17 @@ package android.service.voice; +import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; +import static android.Manifest.permission.RECORD_AUDIO; + import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.media.AudioFormat; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; /** * Basic functionality for hotword detectors. @@ -40,11 +49,100 @@ public interface HotwordDetector { int CONFIDENCE_LEVEL_HIGH = 3; /** @hide */ - @IntDef(prefix = { "CONFIDENCE_LEVEL_" }, value = { + @IntDef(prefix = {"CONFIDENCE_LEVEL_"}, value = { CONFIDENCE_LEVEL_NONE, CONFIDENCE_LEVEL_LOW, CONFIDENCE_LEVEL_MEDIUM, CONFIDENCE_LEVEL_HIGH }) - @interface HotwordConfidenceLevelValue {} + @interface HotwordConfidenceLevelValue { + } + + /** + * Starts hotword recognition. + * <p> + * On calling this, the system streams audio from the device microphone to this application's + * {@link HotwordDetectionService}. Audio is streamed until {@link #stopRecognition()} is + * called. + * <p> + * On detection of a hotword, + * {@link AlwaysOnHotwordDetector.Callback#onDetected(AlwaysOnHotwordDetector.EventPayload)} + * is called on the callback provided when creating this {@link HotwordDetector}. + * <p> + * There is a noticeable impact on battery while recognition is active, so make sure to call + * {@link #stopRecognition()} when detection isn't needed. + * <p> + * Calling this again while recognition is active does nothing. + * + * @return true if the request to start recognition succeeded + */ + @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) + boolean startRecognition(); + + /** + * Stops hotword recognition. + * + * @return true if the request to stop recognition succeeded + */ + boolean stopRecognition(); + + /** + * Starts hotword recognition on audio coming from an external connected microphone. + * <p> + * {@link #stopRecognition()} must be called before {@code audioStream} is closed. + * + * @param audioStream stream containing the audio bytes to run detection on + * @param audioFormat format of the encoded audio + * @param options options supporting detection, such as configuration specific to the + * source of the audio. This will be provided to the {@link HotwordDetectionService}. + * PersistableBundle does not allow any remotable objects or other contents that can be + * used to communicate with other processes. + * @return true if the request to start recognition succeeded + */ + boolean startRecognition( + @NonNull ParcelFileDescriptor audioStream, + @NonNull AudioFormat audioFormat, + @Nullable PersistableBundle options); + + /** + * The callback to notify of detection events. + */ + interface Callback { + + /** + * Called when the keyphrase is spoken. + * + * @param eventPayload Payload data for the detection event. + */ + // TODO: Consider creating a new EventPayload that the AOHD one subclasses. + void onDetected(@NonNull AlwaysOnHotwordDetector.EventPayload eventPayload); + + /** + * Called when the detection fails due to an error. + */ + void onError(); + + /** + * Called when the recognition is paused temporarily for some reason. + * This is an informational callback, and the clients shouldn't be doing anything here + * except showing an indication on their UI if they have to. + */ + void onRecognitionPaused(); + + /** + * Called when the recognition is resumed after it was temporarily paused. + * This is an informational callback, and the clients shouldn't be doing anything here + * except showing an indication on their UI if they have to. + */ + void onRecognitionResumed(); + + /** + * Called when the {@link HotwordDetectionService second stage detection} did not detect the + * keyphrase. + * + * @param result Info about the second stage detection result, provided by the + * {@link HotwordDetectionService}. + */ + void onRejected(@Nullable HotwordRejectedResult result); + } } diff --git a/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl b/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl index 6f641e1cd1e7..c6b10ff05b08 100644 --- a/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl +++ b/core/java/android/service/voice/IDspHotwordDetectionCallback.aidl @@ -16,6 +16,7 @@ package android.service.voice; +import android.service.voice.HotwordDetectedResult; import android.service.voice.HotwordRejectedResult; /** @@ -23,11 +24,14 @@ import android.service.voice.HotwordRejectedResult; * * @hide */ +// TODO: Rename this. oneway interface IDspHotwordDetectionCallback { + /** * Called when the detected result is valid. */ - void onDetected(); + void onDetected( + in HotwordDetectedResult hotwordDetectedResult); /** * Sends {@code result} to the HotwordDetector. diff --git a/core/java/android/service/voice/IHotwordDetectionService.aidl b/core/java/android/service/voice/IHotwordDetectionService.aidl index 0791f1ca49eb..cb140f9346fa 100644 --- a/core/java/android/service/voice/IHotwordDetectionService.aidl +++ b/core/java/android/service/voice/IHotwordDetectionService.aidl @@ -29,10 +29,17 @@ import android.service.voice.IDspHotwordDetectionCallback; */ oneway interface IHotwordDetectionService { void detectFromDspSource( - in ParcelFileDescriptor audioStream, - in AudioFormat audioFormat, - long timeoutMillis, - in IDspHotwordDetectionCallback callback); + in ParcelFileDescriptor audioStream, + in AudioFormat audioFormat, + long timeoutMillis, + in IDspHotwordDetectionCallback callback); + + void detectFromMicrophoneSource( + in ParcelFileDescriptor audioStream, + int audioSource, + in AudioFormat audioFormat, + in PersistableBundle options, + in IDspHotwordDetectionCallback callback); void updateState(in PersistableBundle options, in SharedMemory sharedMemory); } diff --git a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl new file mode 100644 index 000000000000..80f20fe405b1 --- /dev/null +++ b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 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.media.AudioFormat; +import android.service.voice.HotwordDetectedResult; + +/** + * Callback for returning the detected result from the HotwordDetectionService. + * + * @hide + */ +oneway interface IMicrophoneHotwordDetectionVoiceInteractionCallback { + + /** + * Called when the detected result is valid. + */ + void onDetected( + in HotwordDetectedResult hotwordDetectedResult, + in AudioFormat audioFormat, + in ParcelFileDescriptor audioStream); +} diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java new file mode 100644 index 000000000000..f49a9d45ae06 --- /dev/null +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2021 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 static android.Manifest.permission.RECORD_AUDIO; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.media.AudioFormat; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.SharedMemory; +import android.util.Slog; + +import com.android.internal.app.IVoiceInteractionManagerService; + +import java.io.PrintWriter; + +/** + * Manages hotword detection not relying on a specific hardware. + * + * <p>On devices where DSP is available it's strongly recommended to use + * {@link AlwaysOnHotwordDetector}. + * + * @hide + **/ +class SoftwareHotwordDetector extends AbstractHotwordDetector { + private static final String TAG = SoftwareHotwordDetector.class.getSimpleName(); + private static final boolean DEBUG = true; + + private final IVoiceInteractionManagerService mManagerService; + private final HotwordDetector.Callback mCallback; + private final AudioFormat mAudioFormat; + private final Handler mHandler; + private final Object mLock = new Object(); + + SoftwareHotwordDetector( + IVoiceInteractionManagerService managerService, + AudioFormat audioFormat, + PersistableBundle options, + SharedMemory sharedMemory, + HotwordDetector.Callback callback) { + super(managerService, callback); + + mManagerService = managerService; + mAudioFormat = audioFormat; + mCallback = callback; + mHandler = new Handler(Looper.getMainLooper()); + + try { + mManagerService.updateState(options, sharedMemory); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @RequiresPermission(RECORD_AUDIO) + @Override + public boolean startRecognition() { + if (DEBUG) { + Slog.i(TAG, "#startRecognition"); + } + + maybeCloseExistingSession(); + + try { + mManagerService.startListeningFromMic( + mAudioFormat, new BinderCallback(mHandler, mCallback)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + return true; + } + + /** TODO: stopRecognition */ + @RequiresPermission(RECORD_AUDIO) + @Override + public boolean stopRecognition() { + if (DEBUG) { + Slog.i(TAG, "#stopRecognition"); + } + + try { + mManagerService.stopListeningFromMic(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + return true; + } + + private void maybeCloseExistingSession() { + // TODO: needs to be synchronized. + // TODO: implement this + } + + private static class BinderCallback + extends IMicrophoneHotwordDetectionVoiceInteractionCallback.Stub { + private final Handler mHandler; + // TODO: this needs to be a weak reference. + private final HotwordDetector.Callback mCallback; + + BinderCallback(Handler handler, HotwordDetector.Callback callback) { + this.mHandler = handler; + this.mCallback = callback; + } + + /** TODO: onDetected */ + @Override + public void onDetected( + @Nullable HotwordDetectedResult hotwordDetectedResult, + @Nullable AudioFormat audioFormat, + @Nullable ParcelFileDescriptor audioStream) { + mHandler.sendMessage(obtainMessage( + HotwordDetector.Callback::onDetected, + mCallback, + new AlwaysOnHotwordDetector.EventPayload( + audioFormat, hotwordDetectedResult, audioStream))); + } + } + + /** @hide */ + public void dump(String prefix, PrintWriter pw) { + // TODO: implement this + } +} diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index cb3791d9986a..2a2522741955 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -29,6 +29,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; +import android.media.AudioFormat; import android.media.voice.KeyphraseModelManager; import android.os.Bundle; import android.os.Handler; @@ -134,6 +135,7 @@ public class VoiceInteractionService extends Service { private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; private AlwaysOnHotwordDetector mHotwordDetector; + private SoftwareHotwordDetector mSoftwareHotwordDetector; /** * Called when a user has activated an affordance to launch voice assist from the Keyguard. @@ -389,6 +391,60 @@ public class VoiceInteractionService extends Service { } /** + * Creates a {@link HotwordDetector} and initializes the application's + * {@link HotwordDetectionService} using {@code options} and {code sharedMemory}. + * + * <p>To be able to call this, you need to set android:hotwordDetectionService in the + * android.voice_interaction metadata file to a valid hotword detection service, and set + * android:isolatedProcess="true" in the hotword detection service's declaration. Otherwise, + * this throws an {@link IllegalStateException}. + * + * <p>This instance must be retained and used by the client. + * Calling this a second time invalidates the previously created hotword detector + * which can no longer be used to manage recognition. + * + * <p>Using this has a noticeable impact on battery, since the microphone is kept open + * for the lifetime of the recognition {@link HotwordDetector#startRecognition() session}. On + * devices where hardware filtering is available (such as through a DSP), it's highly + * recommended to use {@link #createAlwaysOnHotwordDetector} instead. + * + * @param audioFormat Format of the audio to be passed to {@link HotwordDetectionService}. + * @param options Application configuration data to be provided to the + * {@link HotwordDetectionService}. PersistableBundle does not allow any remotable objects or + * other contents that can be used to communicate with other processes. + * @param sharedMemory The unrestricted data blob to be provided to the + * {@link HotwordDetectionService}. Use this to provide hotword models or other such data to the + * sandboxed process. + * @param callback The callback to notify of detection events. + * @return A hotword detector for the given audio format. + * + * @see #createAlwaysOnHotwordDetector(String, Locale, PersistableBundle, SharedMemory, + * AlwaysOnHotwordDetector.Callback) + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.MANAGE_HOTWORD_DETECTION) + @NonNull + public final HotwordDetector createHotwordDetector( + @NonNull AudioFormat audioFormat, + @Nullable PersistableBundle options, + @Nullable SharedMemory sharedMemory, + @NonNull HotwordDetector.Callback callback) { + if (mSystemService == null) { + throw new IllegalStateException("Not available until onReady() is called"); + } + synchronized (mLock) { + // Allow only one concurrent recognition via the APIs. + safelyShutdownHotwordDetector(); + mSoftwareHotwordDetector = + new SoftwareHotwordDetector( + mSystemService, audioFormat, options, sharedMemory, callback); + } + return mSoftwareHotwordDetector; + } + + /** * Creates an {@link KeyphraseModelManager} to use for enrolling voice models outside of the * pre-bundled system voice models. * @hide @@ -431,24 +487,43 @@ public class VoiceInteractionService extends Service { private void safelyShutdownHotwordDetector() { synchronized (mLock) { - if (mHotwordDetector == null) { - return; - } + shutdownDspHotwordDetectorLocked(); + shutdownMicrophoneHotwordDetectorLocked(); + } + } - try { - mHotwordDetector.stopRecognition(); - } catch (Exception ex) { - // Ignore. - } + private void shutdownDspHotwordDetectorLocked() { + if (mHotwordDetector == null) { + return; + } - try { - mHotwordDetector.invalidate(); - } catch (Exception ex) { - // Ignore. - } + try { + mHotwordDetector.stopRecognition(); + } catch (Exception ex) { + // Ignore. + } - mHotwordDetector = null; + try { + mHotwordDetector.invalidate(); + } catch (Exception ex) { + // Ignore. } + + mHotwordDetector = null; + } + + private void shutdownMicrophoneHotwordDetectorLocked() { + if (mSoftwareHotwordDetector == null) { + return; + } + + try { + mSoftwareHotwordDetector.stopRecognition(); + } catch (Exception ex) { + // Ignore. + } + + mSoftwareHotwordDetector = null; } /** @@ -478,6 +553,13 @@ public class VoiceInteractionService extends Service { } else { mHotwordDetector.dump(" ", pw); } + + pw.println(" MicrophoneHotwordDetector"); + if (mSoftwareHotwordDetector == null) { + pw.println(" NULL"); + } else { + mSoftwareHotwordDetector.dump(" ", pw); + } } } } |
