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

Commit 6daae962 authored by Sandeep Siddhartha's avatar Sandeep Siddhartha
Browse files

AlwaysOnHotwordDetector needs to reflect enrollment changes

Add a callback for when any sound model change happens. This helps the VIS
to re-check the availability and either enroll the user, or start/stop recognition.

Also shut down any active recognition when VIS dies, or a different hotword detector instance is obtained from VIS.

Change-Id: I03f94e78c6ee307afe822a84aebc7e74c64de7b4
parent 70441467
Loading
Loading
Loading
Loading
+8 −5
Original line number Diff line number Diff line
@@ -27324,10 +27324,6 @@ package android.service.voice {
    method public int getSupportedRecognitionModes();
    method public int startRecognition(int);
    method public int stopRecognition();
    field public static final int KEYPHRASE_ENROLLED = 2; // 0x2
    field public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
    field public static final int KEYPHRASE_UNENROLLED = 1; // 0x1
    field public static final int KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
    field public static final int MANAGE_ACTION_ENROLL = 0; // 0x0
    field public static final int MANAGE_ACTION_RE_ENROLL = 1; // 0x1
    field public static final int MANAGE_ACTION_UN_ENROLL = 2; // 0x2
@@ -27335,6 +27331,11 @@ package android.service.voice {
    field public static final int RECOGNITION_FLAG_NONE = 0; // 0x0
    field public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 2; // 0x2
    field public static final int RECOGNITION_MODE_VOICE_TRIGGER = 1; // 0x1
    field public static final int STATE_HARDWARE_UNAVAILABLE = -2; // 0xfffffffe
    field public static final int STATE_INVALID = -3; // 0xfffffffd
    field public static final int STATE_KEYPHRASE_ENROLLED = 2; // 0x2
    field public static final int STATE_KEYPHRASE_UNENROLLED = 1; // 0x1
    field public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; // 0xffffffff
    field public static final int STATUS_ERROR = -2147483648; // 0x80000000
    field public static final int STATUS_OK = 0; // 0x0
  }
@@ -27346,10 +27347,12 @@ package android.service.voice {
  public class VoiceInteractionService extends android.app.Service {
    ctor public VoiceInteractionService();
    method public final android.service.voice.AlwaysOnHotwordDetector getAlwaysOnHotwordDetector(java.lang.String, java.lang.String, android.service.voice.AlwaysOnHotwordDetector.Callback);
    method public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(java.lang.String, java.lang.String, android.service.voice.AlwaysOnHotwordDetector.Callback);
    method public static boolean isActiveService(android.content.Context, android.content.ComponentName);
    method public android.os.IBinder onBind(android.content.Intent);
    method public void onReady();
    method public void onShutdown();
    method public void onSoundModelsChanged();
    method public void startSession(android.os.Bundle);
    field public static final java.lang.String SERVICE_INTERFACE = "android.service.voice.VoiceInteractionService";
    field public static final java.lang.String SERVICE_META_DATA = "android.voice_interaction";
+85 −37
Original line number Diff line number Diff line
@@ -41,26 +41,32 @@ import java.util.List;
 * always-on keyphrase detection APIs.
 */
public class AlwaysOnHotwordDetector {
    //---- States of Keyphrase availability ----//
    //---- States of Keyphrase availability. Return codes for getAvailability() ----//
    /**
     * Indicates that the given keyphrase is not available on the system because of the
     * hardware configuration.
     * Indicates that this hotword detector is no longer valid for any recognition
     * and should not be used anymore.
     */
    public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2;
    public static final int STATE_INVALID = -3;
    /**
     * Indicates that the given keyphrase is not supported.
     * Indicates that recognition for the given keyphrase is not available on the system
     * because of the hardware configuration.
     */
    public static final int KEYPHRASE_UNSUPPORTED = -1;
    public static final int STATE_HARDWARE_UNAVAILABLE = -2;
    /**
     * Indicates that recognition for the given keyphrase is not supported.
     */
    public static final int STATE_KEYPHRASE_UNSUPPORTED = -1;
    /**
     * Indicates that the given keyphrase is not enrolled.
     */
    public static final int KEYPHRASE_UNENROLLED = 1;
    public static final int STATE_KEYPHRASE_UNENROLLED = 1;
    /**
     * Indicates that the given keyphrase is currently enrolled but not being actively listened for.
     * Indicates that the given keyphrase is currently enrolled and it's possible to start
     * recognition for it.
     */
    public static final int KEYPHRASE_ENROLLED = 2;
    public static final int STATE_KEYPHRASE_ENROLLED = 2;

    // Keyphrase management actions ----//
    // Keyphrase management actions. Used in getManageIntent() ----//
    /** Indicates that we need to enroll. */
    public static final int MANAGE_ACTION_ENROLL = 0;
    /** Indicates that we need to re-enroll. */
@@ -83,7 +89,7 @@ public class AlwaysOnHotwordDetector {
     */
    public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1;

    //---- Recognition mode flags ----//
    //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----//
    // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags.

    /**
@@ -109,17 +115,20 @@ public class AlwaysOnHotwordDetector {
     * This may be null if this keyphrase isn't supported by the enrollment application.
     */
    private final KeyphraseMetadata mKeyphraseMetadata;
    /**
     * The sound model for the keyphrase, derived from the model management service
     * (IVoiceInteractionManagerService). May be null if the keyphrase isn't enrolled yet.
     */
    private final KeyphraseSoundModel mEnrolledSoundModel;
    private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
    private final IVoiceInteractionService mVoiceInteractionService;
    private final IVoiceInteractionManagerService mModelManagementService;
    private final SoundTriggerListener mInternalCallback;
    private final Callback mExternalCallback;
    private final boolean mDisabled;
    private final Object mLock = new Object();

    /**
     * The sound model for the keyphrase, derived from the model management service
     * (IVoiceInteractionManagerService). May be null if the keyphrase isn't enrolled yet.
     */
    private KeyphraseSoundModel mEnrolledSoundModel;
    private boolean mInvalidated;

    /**
     * Callbacks for always-on hotword detection.
@@ -151,6 +160,7 @@ public class AlwaysOnHotwordDetector {
            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
            IVoiceInteractionService voiceInteractionService,
            IVoiceInteractionManagerService modelManagementService) {
        mInvalidated = false;
        mText = text;
        mLocale = locale;
        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
@@ -160,28 +170,34 @@ public class AlwaysOnHotwordDetector {
        mVoiceInteractionService = voiceInteractionService;
        mModelManagementService = modelManagementService;
        if (mKeyphraseMetadata != null) {
            mEnrolledSoundModel = internalGetKeyphraseSoundModel(mKeyphraseMetadata.id);
        } else {
            mEnrolledSoundModel = null;
            mEnrolledSoundModel = internalGetKeyphraseSoundModelLocked(mKeyphraseMetadata.id);
        }
        int initialAvailability = internalGetAvailabilityLocked();
        mDisabled = (initialAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE)
                || (initialAvailability == KEYPHRASE_UNSUPPORTED);
        mDisabled = (initialAvailability == STATE_HARDWARE_UNAVAILABLE)
                || (initialAvailability == STATE_KEYPHRASE_UNSUPPORTED);
    }

    /**
     * Gets the state of always-on hotword detection for the given keyphrase and locale
     * on this system.
     * Availability implies that the hardware on this system is capable of listening for
     * the given keyphrase or not.
     * the given keyphrase or not. <p/>
     * If the return code is one of {@link #STATE_HARDWARE_UNAVAILABLE} or
     * {@link #STATE_KEYPHRASE_UNSUPPORTED}, no further interaction should be performed with this
     * detector. <br/>
     * If the state is {@link #STATE_KEYPHRASE_UNENROLLED} the caller may choose to begin
     * an enrollment flow for the keyphrase. <br/>
     * For {@value #STATE_KEYPHRASE_ENROLLED} a recognition can be started as desired. <br/>
     * If the return code is {@link #STATE_INVALID}, this detector is stale and must not be used.
     * A new detector should be obtained and used.
     *
     * @return Indicates if always-on hotword detection is available for the given keyphrase.
     *         The return code is one of {@link #KEYPHRASE_HARDWARE_UNAVAILABLE},
     *         {@link #KEYPHRASE_UNSUPPORTED}, {@link #KEYPHRASE_UNENROLLED} or
     *         {@link #KEYPHRASE_ENROLLED}.
     *         The return code is one of {@link #STATE_HARDWARE_UNAVAILABLE},
     *         {@link #STATE_KEYPHRASE_UNSUPPORTED}, {@link #STATE_KEYPHRASE_UNENROLLED},
     *         {@link #STATE_KEYPHRASE_ENROLLED}, or {@link #STATE_INVALID}.
     */
    public int getAvailability() {
        synchronized (this) {
        synchronized (mLock) {
            return internalGetAvailabilityLocked();
        }
    }
@@ -194,7 +210,7 @@ public class AlwaysOnHotwordDetector {
     *         before calling this method to avoid this exception.
     */
    public int getSupportedRecognitionModes() {
        synchronized (this) {
        synchronized (mLock) {
            return getSupportedRecognitionModesLocked();
        }
    }
@@ -220,13 +236,13 @@ public class AlwaysOnHotwordDetector {
     *         before calling this method to avoid this exception.
     */
    public int startRecognition(int recognitionFlags) {
        synchronized (this) {
        synchronized (mLock) {
            return startRecognitionLocked(recognitionFlags);
        }
    }

    private int startRecognitionLocked(int recognitionFlags) {
        if (internalGetAvailabilityLocked() != KEYPHRASE_ENROLLED) {
        if (internalGetAvailabilityLocked() != STATE_KEYPHRASE_ENROLLED) {
            throw new UnsupportedOperationException(
                    "Recognition for the given keyphrase is not supported");
        }
@@ -261,13 +277,13 @@ public class AlwaysOnHotwordDetector {
     *         before calling this method to avoid this exception.
     */
    public int stopRecognition() {
        synchronized (this) {
        synchronized (mLock) {
            return stopRecognitionLocked();
        }
    }

    private synchronized int stopRecognitionLocked() {
        if (internalGetAvailabilityLocked() != KEYPHRASE_ENROLLED) {
    private int stopRecognitionLocked() {
        if (internalGetAvailabilityLocked() != STATE_KEYPHRASE_ENROLLED) {
            throw new UnsupportedOperationException(
                    "Recognition for the given keyphrase is not supported");
        }
@@ -312,6 +328,10 @@ public class AlwaysOnHotwordDetector {
    }

    private int internalGetAvailabilityLocked() {
        if (mInvalidated) {
            return STATE_INVALID;
        }

        ModuleProperties dspModuleProperties = null;
        try {
            dspModuleProperties =
@@ -321,23 +341,51 @@ public class AlwaysOnHotwordDetector {
        }
        // No DSP available
        if (dspModuleProperties == null) {
            return KEYPHRASE_HARDWARE_UNAVAILABLE;
            return STATE_HARDWARE_UNAVAILABLE;
        }
        // No enrollment application supports this keyphrase/locale
        if (mKeyphraseMetadata == null) {
            return KEYPHRASE_UNSUPPORTED;
            return STATE_KEYPHRASE_UNSUPPORTED;
        }

        // This keyphrase hasn't been enrolled.
        if (mEnrolledSoundModel == null) {
            return KEYPHRASE_UNENROLLED;
            return STATE_KEYPHRASE_UNENROLLED;
        }
        return STATE_KEYPHRASE_ENROLLED;
    }

    /**
     * Invalidates this hotword detector so that any future calls to this result
     * in an IllegalStateException.
     *
     * @hide
     */
    void invalidate() {
        synchronized (mLock) {
            mInvalidated = true;
        }
    }

    /**
     * Reloads the sound models from the service.
     *
     * @hide
     */
    void onSoundModelsChanged() {
        synchronized (mLock) {
            // TODO: This should stop the recognition if it was using an enrolled sound model
            // that's no longer available.
            if (mKeyphraseMetadata != null) {
                mEnrolledSoundModel = internalGetKeyphraseSoundModelLocked(mKeyphraseMetadata.id);
            }
        }
        return KEYPHRASE_ENROLLED;
    }

    /**
     * @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
     */
    private KeyphraseSoundModel internalGetKeyphraseSoundModel(int keyphraseId) {
    private KeyphraseSoundModel internalGetKeyphraseSoundModelLocked(int keyphraseId) {
        List<KeyphraseSoundModel> soundModels;
        try {
            soundModels = mModelManagementService
+2 −0
Original line number Diff line number Diff line
@@ -21,4 +21,6 @@ package android.service.voice;
 */
oneway interface IVoiceInteractionService {
    void ready();
    void soundModelsChanged();
    void shutdown();
}
+87 −9
Original line number Diff line number Diff line
@@ -28,8 +28,8 @@ import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;

import android.provider.Settings;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IVoiceInteractionManagerService;

@@ -70,15 +70,27 @@ public class VoiceInteractionService extends Service {
        @Override public void ready() {
            mHandler.sendEmptyMessage(MSG_READY);
        }
        @Override public void shutdown() {
            mHandler.sendEmptyMessage(MSG_SHUTDOWN);
        }
        @Override public void soundModelsChanged() {
            mHandler.sendEmptyMessage(MSG_SOUND_MODELS_CHANGED);
        }
    };

    MyHandler mHandler;

    IVoiceInteractionManagerService mSystemService;

    private final Object mLock = new Object();

    private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;

    private AlwaysOnHotwordDetector mHotwordDetector;

    static final int MSG_READY = 1;
    static final int MSG_SHUTDOWN = 2;
    static final int MSG_SOUND_MODELS_CHANGED = 3;

    class MyHandler extends Handler {
        @Override
@@ -87,6 +99,12 @@ public class VoiceInteractionService extends Service {
                case MSG_READY:
                    onReady();
                    break;
                case MSG_SHUTDOWN:
                    onShutdownInternal();
                    break;
                case MSG_SOUND_MODELS_CHANGED:
                    onSoundModelsChangedInternal();
                    break;
                default:
                    super.handleMessage(msg);
            }
@@ -141,8 +159,9 @@ public class VoiceInteractionService extends Service {
    /**
     * Called during service initialization to tell you when the system is ready
     * to receive interaction from it. You should generally do initialization here
     * rather than in {@link #onCreate()}.  Methods such as {@link #startSession}
     * and {@link #getAlwaysOnHotwordDetector} will not be operational until this point.
     * rather than in {@link #onCreate()}. Methods such as {@link #startSession(Bundle)} and
     * {@link #createAlwaysOnHotwordDetector(String, String, android.service.voice.AlwaysOnHotwordDetector.Callback)}
     * will not be operational until this point.
     */
    public void onReady() {
        mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
@@ -150,23 +169,68 @@ public class VoiceInteractionService extends Service {
        mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager());
    }

    private void onShutdownInternal() {
        onShutdown();
        // Stop any active recognitions when shutting down.
        // This ensures that if implementations forget to stop any active recognition,
        // It's still guaranteed to have been stopped.
        // This helps with cases where the voice interaction implementation is changed
        // by the user.
        safelyShutdownHotwordDetector();
    }

    /**
     * Called during service de-initialization to tell you when the system is shutting the
     * service down.
     */
    public void onShutdown() {
    }

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

    /**
     * Called when the sound models available for recognition change.
     * This may be called if a new sound model is available or
     * an existing one is updated or removed.
     * Implementations must check the availability of the hotword detector if they own one
     * by calling {@link AlwaysOnHotwordDetector#getAvailability()} before calling into it.
     */
    public void onSoundModelsChanged() {
    }

    /**
     * Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale.
     * This instance must be retained and used by the client.
     * Calling this a second time invalidates the previously created hotword detector
     * which can no longer be used to manage recognition.
     *
     * @param keyphrase The keyphrase that's being used, for example "Hello Android".
     * @param locale The locale for which the enrollment needs to be performed.
     *        This is a Java locale, for example "en_US".
     * @param callback The callback to notify of detection events.
     * @return An always-on hotword detector for the given keyphrase and locale.
     */
    public final AlwaysOnHotwordDetector getAlwaysOnHotwordDetector(
    public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
            String keyphrase, String locale, AlwaysOnHotwordDetector.Callback callback) {
        if (mSystemService == null) {
            throw new IllegalStateException("Not available until onReady() is called");
        }
        // TODO: Cache instances and return the same one instead of creating a new interactor
        // for the same keyphrase/locale combination.
        return new AlwaysOnHotwordDetector(keyphrase, locale, callback,
        synchronized (mLock) {
            // Allow only one concurrent recognition via the APIs.
            safelyShutdownHotwordDetector();
            mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback,
                    mKeyphraseEnrollmentInfo, mInterface, mSystemService);
        }
        return mHotwordDetector;
    }

    /**
     * @return Details of keyphrases available for enrollment.
@@ -176,4 +240,18 @@ public class VoiceInteractionService extends Service {
    protected final KeyphraseEnrollmentInfo getKeyphraseEnrollmentInfo() {
        return mKeyphraseEnrollmentInfo;
    }

    private void safelyShutdownHotwordDetector() {
        try {
            synchronized (mLock) {
                if (mHotwordDetector != null) {
                    mHotwordDetector.stopRecognition();
                    mHotwordDetector.invalidate();
                    mHotwordDetector = null;
                }
            }
        } catch (Exception ex) {
            // Ignore.
        }
    }
}
+13 −10
Original line number Diff line number Diff line
@@ -290,20 +290,23 @@ public class VoiceInteractionManagerService extends SystemService {

                final long caller = Binder.clearCallingIdentity();
                try {
                    // If the keyphrases are not present in the model, delete the model.
                    boolean success = false;
                    if (model.keyphrases == null) {
                        if (mDbHelper.deleteKeyphraseSoundModel(model.uuid)) {
                            return SoundTriggerHelper.STATUS_OK;
                        // If the keyphrases are not present in the model, delete the model.
                        success = mDbHelper.deleteKeyphraseSoundModel(model.uuid);
                    } else {
                            return SoundTriggerHelper.STATUS_ERROR;
                        // Else update the model.
                        success = mDbHelper.addOrUpdateKeyphraseSoundModel(model);
                    }
                    if (success) {
                        // Notify the voice interaction service of a change in sound models.
                        if (mImpl != null && mImpl.mService != null) {
                            mImpl.notifySoundModelsChangedLocked();
                        }
                    } else {
                        if (mDbHelper.addOrUpdateKeyphraseSoundModel(model)) {
                        return SoundTriggerHelper.STATUS_OK;
                    } else {
                        return SoundTriggerHelper.STATUS_ERROR;
                    }
                    }
                } finally {
                    Binder.restoreCallingIdentity(caller);
                }
Loading