Add TestApi to trigger the onDetect function of HotwordDetectionService

Bug: 184685043
Test: atest CtsVoiceInteractionTestCases
Test: atest CtsVoiceInteractionTestCases --instant
Change-Id: I531c1229de908c64e29f1976bd2fd1e70e545853
This commit is contained in:
lpeter
2021-04-13 02:17:46 +08:00
parent b1474958c7
commit afef0beebf
6 changed files with 222 additions and 0 deletions

View File

@@ -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 { package android.service.watchdog {
public abstract class ExplicitHealthCheckService extends android.app.Service { public abstract class ExplicitHealthCheckService extends android.app.Service {

View File

@@ -24,6 +24,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.annotation.RequiresPermission; import android.annotation.RequiresPermission;
import android.annotation.SystemApi; import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.ActivityThread; import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage; import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context; import android.content.Context;
@@ -49,6 +50,7 @@ import android.os.PersistableBundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.SharedMemory; import android.os.SharedMemory;
import android.service.voice.HotwordDetectionService.InitializationStatus; import android.service.voice.HotwordDetectionService.InitializationStatus;
import android.util.Log;
import android.util.Slog; import android.util.Slog;
import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IHotwordRecognitionStatusCallback;
@@ -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. * Gets the recognition modes supported by the associated keyphrase.
* *

View File

@@ -261,4 +261,11 @@ interface IVoiceInteractionManagerService {
in AudioFormat audioFormat, in AudioFormat audioFormat,
in PersistableBundle options, in PersistableBundle options,
in IMicrophoneHotwordDetectionVoiceInteractionCallback callback); in IMicrophoneHotwordDetectionVoiceInteractionCallback callback);
/**
* Test API to simulate to trigger hardware recognition event for test.
*/
void triggerHardwareRecognitionEventForTest(
in SoundTrigger.KeyphraseRecognitionEvent event,
in IHotwordRecognitionStatusCallback callback);
} }

View File

@@ -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, private void detectFromDspSource(SoundTrigger.KeyphraseRecognitionEvent recognitionEvent,
IHotwordRecognitionStatusCallback externalCallback) { IHotwordRecognitionStatusCallback externalCallback) {
if (DEBUG) { 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 * 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. * {@code sampleRate} Hz, using the format returned by DSP audio capture.

View File

@@ -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 --------------------------------// //----------------- Model management APIs --------------------------------//
@Override @Override

View File

@@ -40,6 +40,7 @@ import android.content.pm.IPackageManager;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.SoundTrigger;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@@ -493,6 +494,20 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
mHotwordDetectionConnection.stopListening(); 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( public IRecognitionStatusCallback createSoundTriggerCallbackLocked(
IHotwordRecognitionStatusCallback callback) { IHotwordRecognitionStatusCallback callback) {
if (DEBUG) { if (DEBUG) {