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

Commit b7acc5c2 authored by Nicholas Ambur's avatar Nicholas Ambur Committed by Android (Google) Code Review
Browse files

Merge changes from topic "multi-detector"

* changes:
  add equals for AlwaysOnHotwordDetector
  add support for multiple keyphrase detectors
parents d1687664 1b51fafb
Loading
Loading
Loading
Loading
+30 −8
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package android.service.voice;


import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.service.voice.VoiceInteractionService.MULTIPLE_ACTIVE_HOTWORD_DETECTORS;


import android.annotation.IntDef;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.NonNull;
@@ -51,6 +52,7 @@ import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.RemoteException;
import android.os.SharedMemory;
import android.os.SharedMemory;
import android.text.TextUtils;
import android.util.Log;
import android.util.Log;
import android.util.Slog;
import android.util.Slog;


@@ -67,6 +69,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.HashSet;
import java.util.List;
import java.util.List;
import java.util.Locale;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.Set;


/**
/**
@@ -1439,18 +1442,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.
     *
     *
@@ -1820,7 +1823,26 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector {
        }
        }
    }
    }


    @Override
    public boolean equals(Object obj) {
        if (CompatChanges.isChangeEnabled(MULTIPLE_ACTIVE_HOTWORD_DETECTORS)) {
            if (!(obj instanceof AlwaysOnHotwordDetector)) {
                return false;
            }
            AlwaysOnHotwordDetector other = (AlwaysOnHotwordDetector) obj;
            return TextUtils.equals(mText, other.mText) && mLocale.equals(other.mLocale);
        }

        return super.equals(obj);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mText, mLocale);
    }

    /** @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();
                });
            }
            }
        }
        }
    }
    }