script-astra/Android/Sdk/sources/android-35/android/service/voice/HotwordDetectionService.java

461 lines
18 KiB
Java
Raw Normal View History

2025-01-20 15:15:20 +00:00
/*
* 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.FlaggedApi;
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.annotation.TestApi;
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.IBinder;
import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.speech.IRecognitionServiceManager;
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
implements SandboxedDetectionInitializer {
private static final String TAG = "HotwordDetectionService";
private static final boolean DBG = false;
private static final long UPDATE_TIMEOUT_MILLIS = 20000;
/**
* The PersistableBundle options key used in {@link #onDetect(ParcelFileDescriptor, AudioFormat,
* PersistableBundle, Callback)} to indicate whether the system will close the audio stream
* after {@code Callback} is invoked.
*/
@FlaggedApi(android.app.wearable.Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API)
public static final String KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK =
"android.service.voice.HotwordDetectionService."
+ "KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK";
/**
* Feature flag for Attention Service.
*
* @hide
*/
@TestApi
public static final boolean ENABLE_PROXIMITY_RESULT = true;
/**
* Indicates that the updated status is successful.
*
* @deprecated Replaced with
* {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_SUCCESS}
*/
@Deprecated
public static final int INITIALIZATION_STATUS_SUCCESS =
SandboxedDetectionInitializer.INITIALIZATION_STATUS_SUCCESS;
/**
* Indicates that the callback wasnt invoked within the timeout.
* This is used by system.
*
* @deprecated Replaced with
* {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_UNKNOWN}
*/
@Deprecated
public static final int INITIALIZATION_STATUS_UNKNOWN =
SandboxedDetectionInitializer.INITIALIZATION_STATUS_UNKNOWN;
/**
* 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;
@Nullable
private IRecognitionServiceManager mIRecognitionServiceManager;
private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.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.Builder(event).build(),
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 detectWithVisualSignals(
IDetectorSessionVisualQueryDetectionCallback callback) {
throw new UnsupportedOperationException("Not supported by HotwordDetectionService");
}
@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 updateRecognitionServiceManager(IRecognitionServiceManager manager) {
mIRecognitionServiceManager = manager;
}
@Override
public void ping(IRemoteCallback callback) throws RemoteException {
callback.sendResult(null);
}
@Override
public void stopDetection() {
HotwordDetectionService.this.onStopDetection();
}
@Override
public void registerRemoteStorageService(IDetectorSessionStorageService
detectorSessionStorageService) {
throw new UnsupportedOperationException("Hotword cannot access files from the disk.");
}
};
@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 if (Context.SPEECH_RECOGNITION_SERVICE.equals(name)
&& mIRecognitionServiceManager != null) {
return mIRecognitionServiceManager.asBinder();
} 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
* @deprecated Replaced with
* {@link SandboxedDetectionInitializer#getMaxCustomInitializationStatus()}
*/
@SystemApi
@Deprecated
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}.
*
* <p>When responding to a detection event, the
* {@link HotwordDetectedResult#getHotwordPhraseId()} must match a keyphrase ID listed
* in the eventPayload's
* {@link AlwaysOnHotwordDetector.EventPayload#getKeyphraseRecognitionExtras()} list. This is
* forcing the intention of the {@link HotwordDetectionService} to validate an event from the
* voice engine and not augment its result.
*
* @param eventPayload Payload data for the hardware detection event. This may contain the
* trigger audio, if requested when calling
* {@link AlwaysOnHotwordDetector#startRecognition(int)}.
* Each {@link AlwaysOnHotwordDetector} will be associated with at minimum a unique
* keyphrase ID indicated by
* {@link AlwaysOnHotwordDetector.EventPayload#getKeyphraseRecognitionExtras()}[0].
* Any extra
* {@link android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra}'s
* in the eventPayload represent additional phrases detected by the voice engine.
* @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.
*
* {@inheritDoc}
* @hide
*/
@Override
@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 will send the detection result to
* the {@link HotwordDetector}'s callback. If {@code
* options.getBoolean(KEY_SYSTEM_WILL_CLOSE_AUDIO_STREAM_AFTER_CALLBACK, true)} returns true,
* the system will also close the {@code audioStream} after {@code callback} is invoked.
*
* @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 =
SandboxedDetectionInitializer.createInitializationStatusConsumer(callback);
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: consider making the constructor a test api for testing purpose
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();
}
}
}
}