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

Commit d4c79f5f authored by Nicholas Ambur's avatar Nicholas Ambur
Browse files

add support for multiple keyphrase detectors

This change allows for the active VoiceInteractionService to create any
number of unique DSP-based AlwaysOnHotwordDetectors in parallel. This
change also allows for a SoftwareHotwordDetector to be created alongside
the DSP-based detector(s).

When a implementation of VoiceInteractionService has a target SDK lower
than Android T SDK version, there is no behavior change, and the
application will only be able to use a single HotwordDetector at a given
time.

Test: atest VoiceInteractionSystemApiTest
Bug: 193232191
Change-Id: I4bc7211884d2d63dc7fcda059384f3a8233a41a8
parent ee2e9923
Loading
Loading
Loading
Loading
+9 −8
Original line number Original line Diff line number Diff line
@@ -1439,18 +1439,18 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
            mAvailability = STATE_INVALID;
            mAvailability = STATE_INVALID;
            mIsAvailabilityOverriddenByTestApi = false;
            mIsAvailabilityOverriddenByTestApi = false;
            notifyStateChangedLocked();
            notifyStateChangedLocked();

            if (mSupportHotwordDetectionService) {
                try {
                    mModelManagementService.shutdownHotwordDetectionService();
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
        }
        }
        super.destroy();
        super.destroy();
    }
    }


    /**
     * @hide
     */
    @Override
    public boolean isUsingHotwordDetectionService() {
        return mSupportHotwordDetectionService;
    }

    /**
    /**
     * Reloads the sound models from the service.
     * Reloads the sound models from the service.
     *
     *
@@ -1821,6 +1821,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
    }
    }


    /** @hide */
    /** @hide */
    @Override
    public void dump(String prefix, PrintWriter pw) {
    public void dump(String prefix, PrintWriter pw) {
        synchronized (mLock) {
        synchronized (mLock) {
            pw.print(prefix); pw.print("Text="); pw.println(mText);
            pw.print(prefix); pw.print("Text="); pw.println(mText);
+14 −0
Original line number Original line Diff line number Diff line
@@ -32,6 +32,8 @@ import android.os.PersistableBundle;
import android.os.SharedMemory;
import android.os.SharedMemory;
import android.util.AndroidException;
import android.util.AndroidException;


import java.io.PrintWriter;

/**
/**
 * Basic functionality for hotword detectors.
 * Basic functionality for hotword detectors.
 *
 *
@@ -171,6 +173,13 @@ public interface HotwordDetector {
        throw new UnsupportedOperationException("Not implemented. Must override in a subclass.");
        throw new UnsupportedOperationException("Not implemented. Must override in a subclass.");
    }
    }


    /**
     * @hide
     */
    default boolean isUsingHotwordDetectionService() {
        throw new UnsupportedOperationException("Not implemented. Must override in a subclass.");
    }

    /**
    /**
     * @hide
     * @hide
     */
     */
@@ -187,6 +196,11 @@ public interface HotwordDetector {
        }
        }
    }
    }


    /** @hide */
    default void dump(String prefix, PrintWriter pw) {
        throw new UnsupportedOperationException("Not implemented. Must override in a subclass.");
    }

    /**
    /**
     * The callback to notify of detection events.
     * The callback to notify of detection events.
     */
     */
+9 −6
Original line number Original line Diff line number Diff line
@@ -124,15 +124,17 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
            Log.i(TAG, "failed to stopRecognition in destroy", e);
            Log.i(TAG, "failed to stopRecognition in destroy", e);
        }
        }
        maybeCloseExistingSession();
        maybeCloseExistingSession();

        try {
            mManagerService.shutdownHotwordDetectionService();
        } catch (RemoteException ex) {
            ex.rethrowFromSystemServer();
        }
        super.destroy();
        super.destroy();
    }
    }


    /**
     * @hide
     */
    @Override
    public boolean isUsingHotwordDetectionService() {
        return true;
    }

    private void maybeCloseExistingSession() {
    private void maybeCloseExistingSession() {
        // TODO: needs to be synchronized.
        // TODO: needs to be synchronized.
        // TODO: implement this
        // TODO: implement this
@@ -240,6 +242,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector {
    }
    }


    /** @hide */
    /** @hide */
    @Override
    public void dump(String prefix, PrintWriter pw) {
    public void dump(String prefix, PrintWriter pw) {
        // TODO: implement this
        // TODO: implement this
    }
    }
+111 −50
Original line number Original line Diff line number Diff line
@@ -24,12 +24,16 @@ import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemApi;
import android.app.Service;
import android.app.Service;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.ComponentName;
import android.content.Context;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
import android.media.voice.KeyphraseModelManager;
import android.media.voice.KeyphraseModelManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Bundle;
import android.os.Handler;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder;
@@ -89,6 +93,37 @@ public class VoiceInteractionService extends Service {
     */
     */
    public static final String SERVICE_META_DATA = "android.voice_interaction";
    public static final String SERVICE_META_DATA = "android.voice_interaction";


    /**
     * For apps targeting Build.VERSION_CODES.TRAMISU and above, implementors of this
     * service can create multiple AlwaysOnHotwordDetector instances in parallel. They will
     * also e ale to create a single SoftwareHotwordDetector in parallel with any other
     * active AlwaysOnHotwordDetector instances.
     *
     * <p>Requirements when this change is enabled:
     * <ul>
     *     <li>
     *         Any number of AlwaysOnHotwordDetector instances can be created in parallel
     *         as long as they are unique to any other active AlwaysOnHotwordDetector.
     *     </li>
     *     <li>
     *         Only a single instance of SoftwareHotwordDetector can be active at a given
     *         time. It can be active at the same time as any number of
     *         AlwaysOnHotwordDetector instances.
     *     </li>
     *     <li>
     *         To release that reference and any resources associated with that reference,
     *         HotwordDetector#destroy() must be called. An attempt to create an
     *         HotwordDetector equal to an active HotwordDetector will be rejected
     *         until HotwordDetector#destroy() is called on the active instance.
     *     </li>
     * </ul>
     *
     * @hide
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
    static final long MULTIPLE_ACTIVE_HOTWORD_DETECTORS = 193232191L;

    IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() {
    IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() {
        @Override
        @Override
        public void ready() {
        public void ready() {
@@ -133,8 +168,7 @@ public class VoiceInteractionService extends Service {


    private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
    private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;


    private AlwaysOnHotwordDetector mHotwordDetector;
    private final Set<HotwordDetector> mActiveHotwordDetectors = new ArraySet<>();
    private SoftwareHotwordDetector mSoftwareHotwordDetector;


    /**
    /**
     * Called when a user has activated an affordance to launch voice assist from the Keyguard.
     * Called when a user has activated an affordance to launch voice assist from the Keyguard.
@@ -284,10 +318,12 @@ public class VoiceInteractionService extends Service {


    private void onSoundModelsChangedInternal() {
    private void onSoundModelsChangedInternal() {
        synchronized (this) {
        synchronized (this) {
            if (mHotwordDetector != null) {
            // TODO: Stop recognition if a sound model that was being recognized gets deleted.
            // TODO: Stop recognition if a sound model that was being recognized gets deleted.
                mHotwordDetector.onSoundModelsChanged();
            mActiveHotwordDetectors.forEach(detector -> {
                if (detector instanceof AlwaysOnHotwordDetector) {
                    ((AlwaysOnHotwordDetector) detector).onSoundModelsChanged();
                }
                }
            });
        }
        }
    }
    }


@@ -379,19 +415,31 @@ public class VoiceInteractionService extends Service {
            throw new IllegalStateException("Not available until onReady() is called");
            throw new IllegalStateException("Not available until onReady() is called");
        }
        }
        synchronized (mLock) {
        synchronized (mLock) {
            if (!CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) {
                // Allow only one concurrent recognition via the APIs.
                // Allow only one concurrent recognition via the APIs.
                safelyShutdownAllHotwordDetectors();
                safelyShutdownAllHotwordDetectors();
            }


            mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale,
            AlwaysOnHotwordDetector dspDetector = new AlwaysOnHotwordDetector(keyphrase, locale,
                    callback,
                    callback, mKeyphraseEnrollmentInfo, mSystemService,
                    mKeyphraseEnrollmentInfo, mSystemService,
                    getApplicationContext().getApplicationInfo().targetSdkVersion,
                    getApplicationContext().getApplicationInfo().targetSdkVersion,
                    supportHotwordDetectionService);
                    supportHotwordDetectionService);
            mHotwordDetector.registerOnDestroyListener((detector) -> onDspHotwordDetectorDestroyed(
            if (!mActiveHotwordDetectors.add(dspDetector)) {
                    (AlwaysOnHotwordDetector) detector));
                throw new IllegalArgumentException(
            mHotwordDetector.initialize(options, sharedMemory);
                        "the keyphrase=" + keyphrase + " and locale=" + locale
                                + " are already used by another always-on detector");
            }

            try {
                dspDetector.registerOnDestroyListener(this::onHotwordDetectorDestroyed);
                dspDetector.initialize(options, sharedMemory);
            } catch (Exception e) {
                mActiveHotwordDetectors.remove(dspDetector);
                dspDetector.destroy();
                throw e;
            }
            return dspDetector;
        }
        }
        return mHotwordDetector;
    }
    }


    /**
    /**
@@ -437,18 +485,34 @@ public class VoiceInteractionService extends Service {
            throw new IllegalStateException("Not available until onReady() is called");
            throw new IllegalStateException("Not available until onReady() is called");
        }
        }
        synchronized (mLock) {
        synchronized (mLock) {
            if (!CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) {
                // Allow only one concurrent recognition via the APIs.
                // Allow only one concurrent recognition via the APIs.
                safelyShutdownAllHotwordDetectors();
                safelyShutdownAllHotwordDetectors();
            } else {
                for (HotwordDetector detector : mActiveHotwordDetectors) {
                    if (detector instanceof SoftwareHotwordDetector) {
                        throw new IllegalArgumentException(
                                "There is already an active SoftwareHotwordDetector. "
                                        + "It must be destroyed to create a new one.");
                    }
                }
            }


            mSoftwareHotwordDetector =
            SoftwareHotwordDetector softwareHotwordDetector =
                    new SoftwareHotwordDetector(
                    new SoftwareHotwordDetector(
                            mSystemService, null, callback);
                            mSystemService, null, callback);
            mSoftwareHotwordDetector.registerOnDestroyListener(

                    (detector) -> onMicrophoneHotwordDetectorDestroyed(
            try {
                            (SoftwareHotwordDetector) detector));
                softwareHotwordDetector.registerOnDestroyListener(
            mSoftwareHotwordDetector.initialize(options, sharedMemory);
                        this::onHotwordDetectorDestroyed);
                softwareHotwordDetector.initialize(options, sharedMemory);
            } catch (Exception e) {
                mActiveHotwordDetectors.remove(softwareHotwordDetector);
                softwareHotwordDetector.destroy();
                throw e;
            }
            return softwareHotwordDetector;
        }
        }
        return mSoftwareHotwordDetector;
    }
    }


    /**
    /**
@@ -494,33 +558,34 @@ public class VoiceInteractionService extends Service {


    private void safelyShutdownAllHotwordDetectors() {
    private void safelyShutdownAllHotwordDetectors() {
        synchronized (mLock) {
        synchronized (mLock) {
            if (mHotwordDetector != null) {
            mActiveHotwordDetectors.forEach(detector -> {
                try {
                try {
                    mHotwordDetector.destroy();
                    detector.destroy();
                } catch (Exception ex) {
                } catch (Exception ex) {
                    Log.i(TAG, "exception destroying AlwaysOnHotwordDetector", ex);
                    Log.i(TAG, "exception destroying HotwordDetector", ex);
                }
                }
            }
            });

            if (mSoftwareHotwordDetector != null) {
                try {
                    mSoftwareHotwordDetector.destroy();
                } catch (Exception ex) {
                    Log.i(TAG, "exception destroying SoftwareHotwordDetector", ex);
        }
        }
    }
    }

    private void onHotwordDetectorDestroyed(@NonNull HotwordDetector detector) {
        synchronized (mLock) {
            mActiveHotwordDetectors.remove(detector);
            shutdownHotwordDetectionServiceIfRequiredLocked();
        }
        }
    }
    }


    private void onDspHotwordDetectorDestroyed(@NonNull AlwaysOnHotwordDetector detector) {
    private void shutdownHotwordDetectionServiceIfRequiredLocked() {
        synchronized (mLock) {
        for (HotwordDetector detector : mActiveHotwordDetectors) {
            mHotwordDetector = null;
            if (detector.isUsingHotwordDetectionService()) {
                return;
            }
            }
        }
        }


    private void onMicrophoneHotwordDetectorDestroyed(@NonNull SoftwareHotwordDetector detector) {
        try {
        synchronized (mLock) {
            mSystemService.shutdownHotwordDetectionService();
            mSoftwareHotwordDetector = null;
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
        }
    }
    }


@@ -545,18 +610,14 @@ public class VoiceInteractionService extends Service {
    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("VOICE INTERACTION");
        pw.println("VOICE INTERACTION");
        synchronized (mLock) {
        synchronized (mLock) {
            pw.println("  AlwaysOnHotwordDetector");
            pw.println("  HotwordDetector(s)");
            if (mHotwordDetector == null) {
            if (mActiveHotwordDetectors.size() == 0) {
                pw.println("    NULL");
            } else {
                mHotwordDetector.dump("    ", pw);
            }

            pw.println("  MicrophoneHotwordDetector");
            if (mSoftwareHotwordDetector == null) {
                pw.println("    NULL");
                pw.println("    NULL");
            } else {
            } else {
                mSoftwareHotwordDetector.dump("    ", pw);
                mActiveHotwordDetectors.forEach(detector -> {
                    detector.dump("    ", pw);
                    pw.println();
                });
            }
            }
        }
        }
    }
    }