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

Commit dd100ed2 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Automerger Merge Worker
Browse files

Merge "Add TestApi to trigger the onDetect function of...

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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14161092

Change-Id: I41200d38286fde2633a9642004d451633f5f3a21
parents e5a3cb68 33466948
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -2346,6 +2346,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