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

Commit 9c9f5aa6 authored by lpeter's avatar lpeter
Browse files

Implement timeout mechanism when HotwordDetectionService initializes

Bug: 183684347
Test: atest CtsVoiceInteractionTestCases
Test: atest CtsVoiceInteractionTestCases --instant
Change-Id: I77d29491b055ee2a45aa088df4c1cd54e604c259
parent b9822aba
Loading
Loading
Loading
Loading
+9 −6
Original line number Diff line number Diff line
@@ -28,8 +28,10 @@ import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.media.AudioFormat;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -37,8 +39,6 @@ import android.os.RemoteException;
import android.os.SharedMemory;
import android.util.Log;

import com.android.internal.app.IHotwordRecognitionStatusCallback;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -58,6 +58,8 @@ public abstract class HotwordDetectionService extends Service {
    private static final boolean DBG = true;

    private static final long UPDATE_TIMEOUT_MILLIS = 5000;
    /** @hide */
    public static final String KEY_INITIALIZATION_STATUS = "initialization_status";

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
@@ -141,7 +143,7 @@ public abstract class HotwordDetectionService extends Service {

        @Override
        public void updateState(PersistableBundle options, SharedMemory sharedMemory,
                IHotwordRecognitionStatusCallback callback) throws RemoteException {
                IRemoteCallback callback) throws RemoteException {
            if (DBG) {
                Log.d(TAG, "#updateState");
            }
@@ -314,14 +316,15 @@ public abstract class HotwordDetectionService extends Service {
    }

    private void onUpdateStateInternal(@Nullable PersistableBundle options,
            @Nullable SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) {
        // TODO (b/183684347): Implement timeout case.
            @Nullable SharedMemory sharedMemory, IRemoteCallback callback) {
        IntConsumer intConsumer = null;
        if (callback != null) {
            intConsumer =
                    value -> {
                        try {
                            callback.onStatusReported(value);
                            Bundle status = new Bundle();
                            status.putInt(KEY_INITIALIZATION_STATUS, value);
                            callback.sendResult(status);
                        } catch (RemoteException e) {
                            throw e.rethrowFromSystemServer();
                        }
+2 −3
Original line number Diff line number Diff line
@@ -17,13 +17,12 @@
package android.service.voice;

import android.media.AudioFormat;
import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.SharedMemory;
import android.service.voice.IDspHotwordDetectionCallback;

import com.android.internal.app.IHotwordRecognitionStatusCallback;

/**
 * Provide the interface to communicate with hotword detection service.
 *
@@ -44,5 +43,5 @@ oneway interface IHotwordDetectionService {
        in IDspHotwordDetectionCallback callback);

    void updateState(in PersistableBundle options, in SharedMemory sharedMemory,
            in IHotwordRecognitionStatusCallback callback);
            in IRemoteCallback callback);
}
+73 −8
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.server.voiceinteraction;

import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL;
import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE;
import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_UNKNOWN;
import static android.service.voice.HotwordDetectionService.KEY_INITIALIZATION_STATUS;

import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -31,6 +33,8 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteException;
@@ -46,6 +50,7 @@ import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ServiceConnector;

import java.io.Closeable;
@@ -58,6 +63,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A class that provides the communication with the HotwordDetectionService.
@@ -75,11 +82,13 @@ final class HotwordDetectionConnection {
    private static final int MAX_STREAMING_SECONDS = 10;
    private static final int MICROPHONE_BUFFER_LENGTH_SECONDS = 8;
    private static final int HOTWORD_AUDIO_LENGTH_SECONDS = 3;
    private static final long MAX_UPDATE_TIMEOUT_MILLIS = 6000;

    private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool();
    // TODO: This may need to be a Handler(looper)
    private final ScheduledExecutorService mScheduledExecutorService =
            Executors.newSingleThreadScheduledExecutor();
    private final AtomicBoolean mUpdateStateFinish = new AtomicBoolean(false);

    final Object mLock;
    final ComponentName mDetectionComponentName;
@@ -107,16 +116,11 @@ final class HotwordDetectionConnection {
            @Override // from ServiceConnector.Impl
            protected void onServiceConnectionStatusChanged(IHotwordDetectionService service,
                    boolean connected) {
                if (DEBUG) {
                    Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected);
                }
                synchronized (mLock) {
                    mBound = connected;
                    if (connected) {
                        try {
                            service.updateState(options, sharedMemory, callback);
                        } catch (RemoteException e) {
                            // TODO: (b/181842909) Report an error to voice interactor
                            Slog.w(TAG, "Failed to updateState for HotwordDetectionService", e);
                        }
                    }
                }
            }

@@ -126,6 +130,67 @@ final class HotwordDetectionConnection {
            }
        };
        mRemoteHotwordDetectionService.connect();
        if (callback == null) {
            updateStateLocked(options, sharedMemory);
            return;
        }
        updateStateWithCallbackLocked(options, sharedMemory, callback);
    }

    private void updateStateWithCallbackLocked(PersistableBundle options,
            SharedMemory sharedMemory, IHotwordRecognitionStatusCallback callback) {
        if (DEBUG) {
            Slog.d(TAG, "updateStateWithCallbackLocked");
        }
        mRemoteHotwordDetectionService.postAsync(service -> {
            AndroidFuture<Void> future = new AndroidFuture<>();
            IRemoteCallback statusCallback = new IRemoteCallback.Stub() {
                @Override
                public void sendResult(Bundle bundle) throws RemoteException {
                    if (DEBUG) {
                        Slog.d(TAG, "updateState finish");
                    }
                    future.complete(null);
                    try {
                        if (mUpdateStateFinish.getAndSet(true)) {
                            Slog.w(TAG, "call callback after timeout");
                            return;
                        }
                        int status = bundle != null ? bundle.getInt(
                                KEY_INITIALIZATION_STATUS,
                                INITIALIZATION_STATUS_UNKNOWN)
                                : INITIALIZATION_STATUS_UNKNOWN;
                        callback.onStatusReported(status);
                    } catch (RemoteException e) {
                        Slog.w(TAG, "Failed to report initialization status: " + e);
                    }
                }
            };
            try {
                service.updateState(options, sharedMemory, statusCallback);
            } catch (RemoteException e) {
                // TODO: (b/181842909) Report an error to voice interactor
                Slog.w(TAG, "Failed to updateState for HotwordDetectionService", e);
            }
            return future;
        }).orTimeout(MAX_UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
                .whenComplete((res, err) -> {
                    if (err instanceof TimeoutException) {
                        Slog.w(TAG, "updateState timed out");
                        try {
                            if (mUpdateStateFinish.getAndSet(true)) {
                                return;
                            }
                            callback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN);
                        } catch (RemoteException e) {
                            Slog.w(TAG, "Failed to report initialization status: " + e);
                        }
                    } else if (err != null) {
                        Slog.w(TAG, "Failed to update state: " + err);
                    } else {
                        // NOTE: so far we don't need to take any action.
                    }
                });
    }

    private boolean isBound() {