Add support for onReject and onError for external hotword.

Bug: 230658902
Test: atest --no-bazel-mode CtsVoiceInteractionTestCases:HotwordDetectionServiceBasicTest
Change-Id: Ic845f06c2e3943f4968067dd23a0520aecce3973
This commit is contained in:
Sergey Volnov
2022-05-11 18:12:01 +01:00
parent 058f1d9eb1
commit be0edce59a
4 changed files with 101 additions and 41 deletions

View File

@@ -198,5 +198,16 @@ abstract class AbstractHotwordDetector implements HotwordDetector {
HotwordDetector.Callback::onError,
mCallback));
}
@Override
public void onRejected(@Nullable HotwordRejectedResult result) {
if (result == null) {
result = new HotwordRejectedResult.Builder().build();
}
mHandler.sendMessage(obtainMessage(
HotwordDetector.Callback::onRejected,
mCallback,
result));
}
}
}

View File

@@ -18,6 +18,7 @@ package android.service.voice;
import android.media.AudioFormat;
import android.service.voice.HotwordDetectedResult;
import android.service.voice.HotwordRejectedResult;
/**
* Callback for returning the detected result from the HotwordDetectionService.
@@ -38,4 +39,10 @@ oneway interface IMicrophoneHotwordDetectionVoiceInteractionCallback {
* Called when the detection fails due to an error.
*/
void onError();
/**
* Called when the detected result was not detected.
*/
void onRejected(
in HotwordRejectedResult hotwordRejectedResult);
}

View File

@@ -164,6 +164,17 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
HotwordDetector.Callback::onError,
mCallback));
}
@Override
public void onRejected(@Nullable HotwordRejectedResult result) {
if (result == null) {
result = new HotwordRejectedResult.Builder().build();
}
mHandler.sendMessage(obtainMessage(
HotwordDetector.Callback::onRejected,
mCallback,
result));
}
}
private static class InitializationStateListener

View File

@@ -119,6 +119,7 @@ final class HotwordDetectionConnection {
// TODO: These constants need to be refined.
private static final long VALIDATION_TIMEOUT_MILLIS = 4000;
private static final long MAX_UPDATE_TIMEOUT_MILLIS = 30000;
private static final long EXTERNAL_HOTWORD_CLEANUP_MILLIS = 2000;
private static final Duration MAX_UPDATE_TIMEOUT_DURATION =
Duration.ofMillis(MAX_UPDATE_TIMEOUT_MILLIS);
private static final long RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS = 60 * 60 * 1000; // 1 hour
@@ -854,6 +855,7 @@ final class HotwordDetectionConnection {
int bytesRead = source.read(buffer, 0, 1024);
if (bytesRead < 0) {
Slog.i(TAG, "Reached end of stream for external hotword");
break;
}
@@ -864,6 +866,12 @@ final class HotwordDetectionConnection {
}
} catch (IOException e) {
Slog.w(TAG, "Failed supplying audio data to validator", e);
try {
callback.onError();
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to report onError status: " + ex);
}
} finally {
synchronized (mLock) {
mCurrentAudioSink = null;
@@ -874,51 +882,68 @@ final class HotwordDetectionConnection {
// TODO: handle cancellations well
// TODO: what if we cancelled and started a new one?
mRemoteHotwordDetectionService.run(
service -> service.detectFromMicrophoneSource(
serviceAudioSource,
// TODO: consider making a proxy callback + copy of audio format
AUDIO_SOURCE_EXTERNAL,
audioFormat,
options,
new IDspHotwordDetectionCallback.Stub() {
@Override
public void onRejected(HotwordRejectedResult result)
throws RemoteException {
bestEffortClose(serviceAudioSink);
bestEffortClose(serviceAudioSource);
bestEffortClose(audioSource);
service -> {
service.detectFromMicrophoneSource(
serviceAudioSource,
// TODO: consider making a proxy callback + copy of audio format
AUDIO_SOURCE_EXTERNAL,
audioFormat,
options,
new IDspHotwordDetectionCallback.Stub() {
@Override
public void onRejected(HotwordRejectedResult result)
throws RemoteException {
mScheduledExecutorService.schedule(
() -> {
bestEffortClose(serviceAudioSink, audioSource);
},
EXTERNAL_HOTWORD_CLEANUP_MILLIS,
TimeUnit.MILLISECONDS);
if (mDebugHotwordLogging && result != null) {
Slog.i(TAG, "Egressed rejected result: " + result);
}
// TODO: Propagate the HotwordRejectedResult.
}
callback.onRejected(result);
@Override
public void onDetected(HotwordDetectedResult triggerResult)
throws RemoteException {
bestEffortClose(serviceAudioSink);
bestEffortClose(serviceAudioSource);
try {
enforcePermissionsForDataDelivery();
} catch (SecurityException e) {
bestEffortClose(audioSource);
callback.onError();
return;
}
callback.onDetected(triggerResult, null /* audioFormat */,
null /* audioStream */);
if (triggerResult != null) {
Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(
triggerResult) + " bits from hotword trusted process");
if (mDebugHotwordLogging) {
Slog.i(TAG, "Egressed detected result: " + triggerResult);
if (result != null) {
Slog.i(TAG, "Egressed 'hotword rejected result' "
+ "from hotword trusted process");
if (mDebugHotwordLogging) {
Slog.i(TAG, "Egressed detected result: " + result);
}
}
}
// TODO: Add a delay before closing.
bestEffortClose(audioSource);
}
}));
@Override
public void onDetected(HotwordDetectedResult triggerResult)
throws RemoteException {
mScheduledExecutorService.schedule(
() -> {
bestEffortClose(serviceAudioSink, audioSource);
},
EXTERNAL_HOTWORD_CLEANUP_MILLIS,
TimeUnit.MILLISECONDS);
try {
enforcePermissionsForDataDelivery();
} catch (SecurityException e) {
callback.onError();
return;
}
callback.onDetected(triggerResult, null /* audioFormat */,
null /* audioStream */);
if (triggerResult != null) {
Slog.i(TAG, "Egressed "
+ HotwordDetectedResult.getUsageSize(triggerResult)
+ " bits from hotword trusted process");
if (mDebugHotwordLogging) {
Slog.i(TAG,
"Egressed detected result: " + triggerResult);
}
}
}
});
// A copy of this has been created and passed to the hotword validator
bestEffortClose(serviceAudioSource);
});
}
private class ServiceConnectionFactory {
@@ -1118,6 +1143,12 @@ final class HotwordDetectionConnection {
});
}
private static void bestEffortClose(Closeable... closeables) {
for (Closeable closeable : closeables) {
bestEffortClose(closeable);
}
}
private static void bestEffortClose(Closeable closeable) {
try {
closeable.close();