diff options
6 files changed, 222 insertions, 0 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index e449728ca0a8..f3e0f314c46a 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2321,6 +2321,14 @@ package android.service.quicksettings { } +package android.service.voice { + + public class AlwaysOnHotwordDetector implements android.service.voice.HotwordDetector { + method @RequiresPermission(allOf={android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.CAPTURE_AUDIO_HOTWORD}) public void triggerHardwareRecognitionEventForTest(int, int, boolean, int, int, int, boolean, @NonNull android.media.AudioFormat, @Nullable byte[]); + } + +} + package android.service.watchdog { public abstract class ExplicitHealthCheckService extends android.app.Service { diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 8ca0e7ccff37..0b410a2cb619 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.app.ActivityThread; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -49,6 +50,7 @@ import android.os.PersistableBundle; import android.os.RemoteException; import android.os.SharedMemory; import android.service.voice.HotwordDetectionService.InitializationStatus; +import android.util.Log; import android.util.Slog; import com.android.internal.app.IHotwordRecognitionStatusCallback; @@ -628,6 +630,34 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { } /** + * Test API to simulate to trigger hardware recognition event for test. + * + * @hide + */ + @TestApi + @RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD}) + public void triggerHardwareRecognitionEventForTest(int status, int soundModelHandle, + boolean captureAvailable, int captureSession, int captureDelayMs, int capturePreambleMs, + boolean triggerInData, @NonNull AudioFormat captureFormat, @Nullable byte[] data) { + Log.d(TAG, "triggerHardwareRecognitionEventForTest()"); + synchronized (mLock) { + if (mAvailability == STATE_INVALID || mAvailability == STATE_ERROR) { + throw new IllegalStateException("triggerHardwareRecognitionEventForTest called on" + + " an invalid detector or error state"); + } + try { + mModelManagementService.triggerHardwareRecognitionEventForTest( + new KeyphraseRecognitionEvent(status, soundModelHandle, captureAvailable, + captureSession, captureDelayMs, capturePreambleMs, triggerInData, + captureFormat, data, null /* keyphraseExtras */), + mInternalCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** * Gets the recognition modes supported by the associated keyphrase. * * @see #RECOGNITION_MODE_USER_IDENTIFICATION diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 5a5e745c2426..dddc08a88062 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -261,4 +261,11 @@ interface IVoiceInteractionManagerService { in AudioFormat audioFormat, in PersistableBundle options, in IMicrophoneHotwordDetectionVoiceInteractionCallback callback); + + /** + * Test API to simulate to trigger hardware recognition event for test. + */ + void triggerHardwareRecognitionEventForTest( + in SoundTrigger.KeyphraseRecognitionEvent event, + in IHotwordRecognitionStatusCallback callback); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 6f701f7e3a36..a002bc506e16 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -270,6 +270,114 @@ final class HotwordDetectionConnection { } } + void triggerHardwareRecognitionEventForTestLocked( + SoundTrigger.KeyphraseRecognitionEvent event, + IHotwordRecognitionStatusCallback callback) { + if (DEBUG) { + Slog.d(TAG, "triggerHardwareRecognitionEventForTestLocked"); + } + detectFromDspSourceForTest(event, callback); + } + + private void detectFromDspSourceForTest(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, + IHotwordRecognitionStatusCallback externalCallback) { + if (DEBUG) { + Slog.d(TAG, "detectFromDspSourceForTest"); + } + + AudioRecord record = createFakeAudioRecord(); + if (record == null) { + Slog.d(TAG, "Failed to create fake audio record"); + return; + } + + Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe(); + if (clientPipe == null) { + Slog.d(TAG, "Failed to create pipe"); + return; + } + ParcelFileDescriptor audioSink = clientPipe.second; + ParcelFileDescriptor clientRead = clientPipe.first; + + record.startRecording(); + + mAudioCopyExecutor.execute(() -> { + try (OutputStream fos = + new ParcelFileDescriptor.AutoCloseOutputStream(audioSink)) { + + int remainToRead = 10240; + byte[] buffer = new byte[1024]; + while (remainToRead > 0) { + int bytesRead = record.read(buffer, 0, 1024); + if (DEBUG) { + Slog.d(TAG, "bytesRead = " + bytesRead); + } + if (bytesRead <= 0) { + break; + } + if (bytesRead > 8) { + System.arraycopy(new byte[] {'h', 'o', 't', 'w', 'o', 'r', 'd', '!'}, 0, + buffer, 0, 8); + } + + fos.write(buffer, 0, bytesRead); + remainToRead -= bytesRead; + } + } catch (IOException e) { + Slog.w(TAG, "Failed supplying audio data to validator", e); + } + }); + + Runnable cancellingJob = () -> { + Slog.d(TAG, "Timeout for getting callback from HotwordDetectionService"); + record.stop(); + record.release(); + bestEffortClose(audioSink); + bestEffortClose(clientRead); + }; + + ScheduledFuture<?> cancelingFuture = + mScheduledExecutorService.schedule( + cancellingJob, VALIDATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + + IDspHotwordDetectionCallback internalCallback = new IDspHotwordDetectionCallback.Stub() { + @Override + public void onDetected(HotwordDetectedResult result) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "onDetected"); + } + cancelingFuture.cancel(true); + record.stop(); + record.release(); + bestEffortClose(audioSink); + bestEffortClose(clientRead); + + externalCallback.onKeyphraseDetected(recognitionEvent); + } + + @Override + public void onRejected(HotwordRejectedResult result) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, "onRejected"); + } + cancelingFuture.cancel(true); + record.stop(); + record.release(); + bestEffortClose(audioSink); + bestEffortClose(clientRead); + + externalCallback.onRejected(result); + } + }; + + mRemoteHotwordDetectionService.run( + service -> service.detectFromDspSource( + clientRead, + recognitionEvent.getCaptureFormat(), + VALIDATION_TIMEOUT_MILLIS, + internalCallback)); + } + private void detectFromDspSource(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, IHotwordRecognitionStatusCallback externalCallback) { if (DEBUG) { @@ -456,6 +564,37 @@ final class HotwordDetectionConnection { } } + @Nullable + private AudioRecord createFakeAudioRecord() { + if (DEBUG) { + Slog.i(TAG, "#createFakeAudioRecord"); + } + try { + AudioRecord audioRecord = new AudioRecord.Builder() + .setAudioFormat(new AudioFormat.Builder() + .setSampleRate(32000) + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO).build()) + .setAudioAttributes(new AudioAttributes.Builder() + .setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD).build()) + .setBufferSizeInBytes( + AudioRecord.getMinBufferSize(32000, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT) * 2) + .build(); + + if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { + Slog.w(TAG, "Failed to initialize AudioRecord"); + audioRecord.release(); + return null; + } + return audioRecord; + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Failed to create AudioRecord", e); + } + return null; + } + /** * Returns the number of bytes required to store {@code bufferLengthSeconds} of audio sampled at * {@code sampleRate} Hz, using the format returned by DSP audio capture. diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 9aded899aaae..92cfe4980388 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -1130,6 +1130,29 @@ public class VoiceInteractionManagerService extends SystemService { } } + @Override + public void triggerHardwareRecognitionEventForTest( + SoundTrigger.KeyphraseRecognitionEvent event, + IHotwordRecognitionStatusCallback callback) + throws RemoteException { + enforceCallingPermission(Manifest.permission.RECORD_AUDIO); + enforceCallingPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD); + synchronized (this) { + enforceIsCurrentVoiceInteractionService(); + + if (mImpl == null) { + Slog.w(TAG, "triggerHardwareRecognitionEventForTest without running" + + " voice interaction service"); + return; + } + final long caller = Binder.clearCallingIdentity(); + try { + mImpl.triggerHardwareRecognitionEventForTestLocked(event, callback); + } finally { + Binder.restoreCallingIdentity(caller); + } + } + } //----------------- Model management APIs --------------------------------// @Override diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index 6922ccc03d0b..055284157457 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -40,6 +40,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.hardware.soundtrigger.IRecognitionStatusCallback; +import android.hardware.soundtrigger.SoundTrigger; import android.media.AudioFormat; import android.os.Bundle; import android.os.Handler; @@ -493,6 +494,20 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mHotwordDetectionConnection.stopListening(); } + public void triggerHardwareRecognitionEventForTestLocked( + SoundTrigger.KeyphraseRecognitionEvent event, + IHotwordRecognitionStatusCallback callback) { + if (DEBUG) { + Slog.d(TAG, "triggerHardwareRecognitionEventForTestLocked"); + } + if (mHotwordDetectionConnection == null) { + Slog.w(TAG, "triggerHardwareRecognitionEventForTestLocked() called but connection" + + " isn't established"); + return; + } + mHotwordDetectionConnection.triggerHardwareRecognitionEventForTestLocked(event, callback); + } + public IRecognitionStatusCallback createSoundTriggerCallbackLocked( IHotwordRecognitionStatusCallback callback) { if (DEBUG) { |