summaryrefslogtreecommitdiff
path: root/core/java/android
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/service/voice/AbstractHotwordDetector.java105
-rw-r--r--core/java/android/service/voice/AlwaysOnHotwordDetector.java20
-rw-r--r--core/java/android/service/voice/HotwordDetectionService.java149
-rw-r--r--core/java/android/service/voice/HotwordDetector.java102
-rw-r--r--core/java/android/service/voice/IDspHotwordDetectionCallback.aidl6
-rw-r--r--core/java/android/service/voice/IHotwordDetectionService.aidl15
-rw-r--r--core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl36
-rw-r--r--core/java/android/service/voice/SoftwareHotwordDetector.java146
-rw-r--r--core/java/android/service/voice/VoiceInteractionService.java110
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);
+ }
}
}
}