Loading core/api/test-current.txt +8 −0 Original line number Diff line number Diff line Loading @@ -2341,6 +2341,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 { Loading core/java/android/service/voice/AlwaysOnHotwordDetector.java +30 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -627,6 +629,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. * Loading core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +7 −0 Original line number Diff line number Diff line Loading @@ -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); } services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +139 −0 Original line number Diff line number Diff line Loading @@ -284,6 +284,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) { Loading Loading @@ -470,6 +578,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. Loading services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +23 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading
core/api/test-current.txt +8 −0 Original line number Diff line number Diff line Loading @@ -2341,6 +2341,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 { Loading
core/java/android/service/voice/AlwaysOnHotwordDetector.java +30 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -627,6 +629,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. * Loading
core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +7 −0 Original line number Diff line number Diff line Loading @@ -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); }
services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +139 −0 Original line number Diff line number Diff line Loading @@ -284,6 +284,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) { Loading Loading @@ -470,6 +578,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. Loading
services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +23 −0 Original line number Diff line number Diff line Loading @@ -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 Loading