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

Commit afef0bee authored by lpeter's avatar lpeter
Browse files

Add TestApi to trigger the onDetect function of HotwordDetectionService

Bug: 184685043
Test: atest CtsVoiceInteractionTestCases
Test: atest CtsVoiceInteractionTestCases --instant
Change-Id: I531c1229de908c64e29f1976bd2fd1e70e545853
parent b1474958
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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 {

  public abstract class ExplicitHealthCheckService extends android.app.Service {
+30 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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.
     *
+7 −0
Original line number Diff line number Diff line
@@ -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);
}
+139 −0
Original line number Diff line number Diff line
@@ -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,
            IHotwordRecognitionStatusCallback externalCallback) {
        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
     * {@code sampleRate} Hz, using the format returned by DSP audio capture.
+23 −0
Original line number Diff line number Diff line
@@ -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