blob: e3bb589c9a19dbb706d01e0a50bb591252bbf65b [file] [log] [blame]
/*
* Copyright (C) 2020 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 java.util.Objects.requireNonNull;
import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.ContentCaptureOptions;
import android.content.Context;
import android.content.Intent;
import android.hardware.soundtrigger.SoundTrigger;
import android.media.AudioFormat;
import android.media.AudioSystem;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.util.Log;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.IContentCaptureManager;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
import java.util.function.IntConsumer;
/**
* Implemented by an application that wants to offer detection for hotword. The service can be used
* for both DSP and non-DSP detectors.
*
* The system will bind an application's {@link VoiceInteractionService} first. When {@link
* VoiceInteractionService#createHotwordDetector(PersistableBundle, SharedMemory,
* HotwordDetector.Callback)} or {@link VoiceInteractionService#createAlwaysOnHotwordDetector(
* String, Locale, PersistableBundle, SharedMemory, AlwaysOnHotwordDetector.Callback)} is called,
* the system will bind application's {@link HotwordDetectionService}. Either on a hardware
* trigger or on request from the {@link VoiceInteractionService}, the system calls into the
* {@link HotwordDetectionService} to request detection. The {@link HotwordDetectionService} then
* uses {@link Callback#onDetected(HotwordDetectedResult)} to inform the system that a relevant
* keyphrase was detected, or if applicable uses {@link Callback#onRejected(HotwordRejectedResult)}
* to inform the system that a keyphrase was not detected. The system then relays this result to
* the {@link VoiceInteractionService} through {@link HotwordDetector.Callback}.
*
* Note: Methods in this class may be called concurrently
*
* @hide
*/
@SystemApi
public abstract class HotwordDetectionService extends Service {
private static final String TAG = "HotwordDetectionService";
private static final boolean DBG = false;
private static final long UPDATE_TIMEOUT_MILLIS = 5000;
/** @hide */
public static final String KEY_INITIALIZATION_STATUS = "initialization_status";
/**
* The maximum number of initialization status for some application specific failed reasons.
*
* @hide
*/
public static final int MAXIMUM_NUMBER_OF_INITIALIZATION_STATUS_CUSTOM_ERROR = 2;
/**
* Indicates that the updated status is successful.
*/
public static final int INITIALIZATION_STATUS_SUCCESS = 0;
/**
* Indicates that the callback wasn’t invoked within the timeout.
* This is used by system.
*/
public static final int INITIALIZATION_STATUS_UNKNOWN = 100;
/**
* 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
* that other applications can not abuse it.
*/
@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_INTERFACE =
"android.service.voice.HotwordDetectionService";
@Nullable
private ContentCaptureManager mContentCaptureManager;
private final IHotwordDetectionService mInterface = new IHotwordDetectionService.Stub() {
@Override
public void detectFromDspSource(
SoundTrigger.KeyphraseRecognitionEvent event,
AudioFormat audioFormat,
long timeoutMillis,
IDspHotwordDetectionCallback callback)
throws RemoteException {
if (DBG) {
Log.d(TAG, "#detectFromDspSource");
}
HotwordDetectionService.this.onDetect(
new AlwaysOnHotwordDetector.EventPayload(
event.triggerInData, event.captureAvailable,
event.captureFormat, event.captureSession, event.data),
timeoutMillis,
new Callback(callback));
}
@Override
public void updateState(PersistableBundle options, SharedMemory sharedMemory,
IRemoteCallback callback) throws RemoteException {
Log.v(TAG, "#updateState" + (callback != null ? " with callback" : ""));
HotwordDetectionService.this.onUpdateStateInternal(
options,
sharedMemory,
callback);
}
@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:
HotwordDetectionService.this.onDetect(
new Callback(callback));
break;
case AUDIO_SOURCE_EXTERNAL:
HotwordDetectionService.this.onDetect(
audioStream,
audioFormat,
options,
new Callback(callback));
break;
default:
Log.i(TAG, "Unsupported audio source " + audioSource);
}
}
@Override
public void updateAudioFlinger(IBinder audioFlinger) {
AudioSystem.setAudioFlingerBinder(audioFlinger);
}
@Override
public void updateContentCaptureManager(IContentCaptureManager manager,
ContentCaptureOptions options) {
mContentCaptureManager = new ContentCaptureManager(
HotwordDetectionService.this, manager, options);
}
@Override
public void ping(IRemoteCallback callback) throws RemoteException {
callback.sendResult(null);
}
@Override
public void stopDetection() {
HotwordDetectionService.this.onStopDetection();
}
};
@Override
@Nullable
public final IBinder onBind(@NonNull Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return mInterface.asBinder();
}
Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": "
+ intent);
return null;
}
@Override
@SuppressLint("OnNameExpected")
public @Nullable Object getSystemService(@ServiceName @NonNull String name) {
if (Context.CONTENT_CAPTURE_MANAGER_SERVICE.equals(name)) {
return mContentCaptureManager;
} else {
return super.getSystemService(name);
}
}
/**
* Returns the maximum number of initialization status for some application specific failed
* reasons.
*
* Note: The value 0 is reserved for success.
*
* @hide
*/
@SystemApi
public static int getMaxCustomInitializationStatus() {
return MAXIMUM_NUMBER_OF_INITIALIZATION_STATUS_CUSTOM_ERROR;
}
/**
* 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, and invokes the
* appropriate {@link AlwaysOnHotwordDetector.Callback callback}.
*
* @param eventPayload Payload data for the hardware detection event. This may contain the
* trigger audio, if requested when calling
* {@link AlwaysOnHotwordDetector#startRecognition(int)}.
* @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 The callback to use for responding to the detection request.
*
* @hide
*/
@SystemApi
public void onDetect(
@NonNull AlwaysOnHotwordDetector.EventPayload eventPayload,
@DurationMillisLong long timeoutMillis,
@NonNull Callback callback) {
// TODO: Add a helpful error message.
throw new UnsupportedOperationException();
}
/**
* Called when the {@link VoiceInteractionService#createAlwaysOnHotwordDetector(String, Locale,
* PersistableBundle, SharedMemory, AlwaysOnHotwordDetector.Callback)} or
* {@link AlwaysOnHotwordDetector#updateState(PersistableBundle, SharedMemory)} requests an
* update of the hotword detection parameters.
*
* @param options Application configuration data to provide 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 provide to the
* {@link HotwordDetectionService}. Use this to provide the hotword models data or other
* such data to the trusted process.
* @param callbackTimeoutMillis Timeout in milliseconds for the operation to invoke the
* statusCallback.
* @param statusCallback Use this to return the updated result; the allowed values are
* {@link #INITIALIZATION_STATUS_SUCCESS}, 1<->{@link #getMaxCustomInitializationStatus()}.
* This is non-null only when the {@link HotwordDetectionService} is being initialized; and it
* is null if the state is updated after that.
*
* @hide
*/
@SystemApi
public void onUpdateState(
@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory,
@DurationMillisLong long callbackTimeoutMillis,
@Nullable IntConsumer statusCallback) {}
/**
* 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 successful detection of a hotword, call
* {@link Callback#onDetected(HotwordDetectedResult)}.
*
* @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 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();
}
private void onUpdateStateInternal(@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory, IRemoteCallback callback) {
IntConsumer intConsumer = null;
if (callback != null) {
intConsumer =
value -> {
if (value > getMaxCustomInitializationStatus()) {
throw new IllegalArgumentException(
"The initialization status is invalid for " + value);
}
try {
Bundle status = new Bundle();
status.putInt(KEY_INITIALIZATION_STATUS, value);
callback.sendResult(status);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
};
}
onUpdateState(options, sharedMemory, UPDATE_TIMEOUT_MILLIS, intConsumer);
}
/**
* Called when the {@link VoiceInteractionService}
* {@link HotwordDetector#stopRecognition() requests} that hotword recognition be stopped.
* <p>
* Any open {@link android.media.AudioRecord} should be closed here.
*/
public void onStopDetection() {
}
/**
* Callback for returning the detection result.
*
* @hide
*/
@SystemApi
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 Callback(IDspHotwordDetectionCallback remoteCallback) {
mRemoteCallback = remoteCallback;
}
/**
* Informs the {@link HotwordDetector} that the keyphrase was detected.
*
* @param result Info about the detection result. This is provided to the
* {@link HotwordDetector}.
*/
public void onDetected(@NonNull HotwordDetectedResult result) {
requireNonNull(result);
final PersistableBundle persistableBundle = result.getExtras();
if (!persistableBundle.isEmpty() && HotwordDetectedResult.getParcelableSize(
persistableBundle) > HotwordDetectedResult.getMaxBundleSize()) {
throw new IllegalArgumentException(
"The bundle size of result is larger than max bundle size ("
+ HotwordDetectedResult.getMaxBundleSize()
+ ") of HotwordDetectedResult");
}
try {
mRemoteCallback.onDetected(result);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* 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 HotwordDetector}.
*/
public void onRejected(@NonNull HotwordRejectedResult result) {
requireNonNull(result);
try {
mRemoteCallback.onRejected(result);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
}