| /* |
| * 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(); |
| } |
| } |
| } |
| } |