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

Commit 33466948 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add TestApi to trigger the onDetect function of HotwordDetectionService" into sc-dev

parents fd1ba276 afef0bee
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -2341,6 +2341,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
@@ -284,6 +284,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) {
@@ -470,6 +578,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