Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be0edce5 authored by Sergey Volnov's avatar Sergey Volnov
Browse files

Add support for onReject and onError for external hotword.

Bug: 230658902
Test: atest --no-bazel-mode CtsVoiceInteractionTestCases:HotwordDetectionServiceBasicTest
Change-Id: Ic845f06c2e3943f4968067dd23a0520aecce3973
parent 058f1d9e
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -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));
        }
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -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);
}
+11 −0
Original line number Diff line number Diff line
@@ -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
+73 −42
Original line number Diff line number Diff line
@@ -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,7 +882,8 @@ final class HotwordDetectionConnection {
        // TODO: handle cancellations well
        // TODO: what if we cancelled and started a new one?
        mRemoteHotwordDetectionService.run(
                service -> service.detectFromMicrophoneSource(
                service -> {
                    service.detectFromMicrophoneSource(
                            serviceAudioSource,
                            // TODO: consider making a proxy callback + copy of audio format
                            AUDIO_SOURCE_EXTERNAL,
@@ -884,41 +893,57 @@ final class HotwordDetectionConnection {
                                @Override
                                public void onRejected(HotwordRejectedResult result)
                                        throws RemoteException {
                                bestEffortClose(serviceAudioSink);
                                bestEffortClose(serviceAudioSource);
                                bestEffortClose(audioSource);
                                    mScheduledExecutorService.schedule(
                                            () -> {
                                                bestEffortClose(serviceAudioSink, audioSource);
                                            },
                                            EXTERNAL_HOTWORD_CLEANUP_MILLIS,
                                            TimeUnit.MILLISECONDS);

                                if (mDebugHotwordLogging && result != null) {
                                    Slog.i(TAG, "Egressed rejected result: " + result);
                                    callback.onRejected(result);

                                    if (result != null) {
                                        Slog.i(TAG, "Egressed 'hotword rejected result' "
                                                + "from hotword trusted process");
                                        if (mDebugHotwordLogging) {
                                            Slog.i(TAG, "Egressed detected result: " + result);
                                        }
                                    }
                                // TODO: Propagate the HotwordRejectedResult.
                                }

                                @Override
                                public void onDetected(HotwordDetectedResult triggerResult)
                                        throws RemoteException {
                                bestEffortClose(serviceAudioSink);
                                bestEffortClose(serviceAudioSource);
                                    mScheduledExecutorService.schedule(
                                            () -> {
                                                bestEffortClose(serviceAudioSink, audioSource);
                                            },
                                            EXTERNAL_HOTWORD_CLEANUP_MILLIS,
                                            TimeUnit.MILLISECONDS);

                                    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");
                                        Slog.i(TAG, "Egressed "
                                                + HotwordDetectedResult.getUsageSize(triggerResult)
                                                + " bits from hotword trusted process");
                                        if (mDebugHotwordLogging) {
                                        Slog.i(TAG, "Egressed detected result: " + triggerResult);
                                            Slog.i(TAG,
                                                    "Egressed detected result: " + triggerResult);
                                        }
                                    }
                                // TODO: Add a delay before closing.
                                bestEffortClose(audioSource);
                                }
                        }));
                            });

                    // 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();