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:
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user