Implement the soundtrigger_middlewware service

This service is intended to replace:
frameworks/av/include/soundtrigger/ISoundTriggerHwService.h

This change only adds the replacement service, follow up
changes migrate the clients to use the new service and remove
the old one. The new service is feature-equivalent to the new
one, but offers the following advantages:
- AIDL interface (as opposed to hand-written parceling code).
- Pure Java implementation all the way to the HAL.
- Better documentation.
- Rigorous error handling.
- Unit tests.
- Reduced code complexity (less layers, better separation of
  concerns).
- Permission-based security model (as opposed to some baked-in
  assumptions about process affinity).

Change-Id: I79f4eff105d3e6245990be068b933d4d48c35a0d
Bug: 142070343
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 7b580c3..671b589 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4325,6 +4325,15 @@
     public static final String SOUND_TRIGGER_SERVICE = "soundtrigger";
 
     /**
+     * Use with {@link #getSystemService(String)} to access the
+     * {@link com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService}.
+     *
+     * @hide
+     * @see #getSystemService(String)
+     */
+    public static final String SOUND_TRIGGER_MIDDLEWARE_SERVICE = "soundtrigger_middleware";
+
+    /**
      * Official published name of the (internal) permission service.
      *
      * @see #getSystemService(String)
diff --git a/media/Android.bp b/media/Android.bp
index 20a9656..97d3138 100644
--- a/media/Android.bp
+++ b/media/Android.bp
@@ -141,6 +141,8 @@
         "java/android/media/soundtrigger_middleware/ISoundTriggerCallback.aidl",
         "java/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl",
         "java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl",
+        "java/android/media/soundtrigger_middleware/ModelParameter.aidl",
+        "java/android/media/soundtrigger_middleware/ModelParameterRange.aidl",
         "java/android/media/soundtrigger_middleware/Phrase.aidl",
         "java/android/media/soundtrigger_middleware/PhraseRecognitionEvent.aidl",
         "java/android/media/soundtrigger_middleware/PhraseRecognitionExtra.aidl",
diff --git a/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl
index 202595a..c4a5785 100644
--- a/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl
+++ b/media/java/android/media/soundtrigger_middleware/ISoundTriggerModule.aidl
@@ -15,6 +15,8 @@
  */
 package android.media.soundtrigger_middleware;
 
+import android.media.soundtrigger_middleware.ModelParameter;
+import android.media.soundtrigger_middleware.ModelParameterRange;
 import android.media.soundtrigger_middleware.SoundModel;
 import android.media.soundtrigger_middleware.PhraseSoundModel;
 import android.media.soundtrigger_middleware.RecognitionConfig;
@@ -97,6 +99,47 @@
     void forceRecognitionEvent(int modelHandle);
 
     /**
+     * Set a model specific parameter with the given value. This parameter
+     * will keep its value for the duration the model is loaded regardless of starting and stopping
+     * recognition. Once the model is unloaded, the value will be lost.
+     * It is expected to check if the handle supports the parameter via the
+     * queryModelParameterSupport API prior to calling this method.
+     *
+     * @param modelHandle The sound model handle indicating which model to modify parameters
+     * @param modelParam Parameter to set which will be validated against the
+     *                   ModelParameter type.
+     * @param value The value to set for the given model parameter
+     */
+    void setModelParameter(int modelHandle, ModelParameter modelParam, int value);
+
+    /**
+     * Get a model specific parameter. This parameter will keep its value
+     * for the duration the model is loaded regardless of starting and stopping recognition.
+     * Once the model is unloaded, the value will be lost. If the value is not set, a default
+     * value is returned. See ModelParameter for parameter default values.
+     * It is expected to check if the handle supports the parameter via the
+     * queryModelParameterSupport API prior to calling this method.
+     *
+     * @param modelHandle The sound model associated with given modelParam
+     * @param modelParam Parameter to set which will be validated against the
+     *                   ModelParameter type.
+     * @return Value set to the requested parameter.
+     */
+    int getModelParameter(int modelHandle, ModelParameter modelParam);
+
+    /**
+     * Determine if parameter control is supported for the given model handle, and its valid value
+     * range if it is.
+     *
+     * @param modelHandle The sound model handle indicating which model to query
+     * @param modelParam Parameter to set which will be validated against the
+     *                   ModelParameter type.
+     * @return If parameter is supported, the return value is its valid range, otherwise null.
+     */
+    @nullable ModelParameterRange queryModelParameterSupport(int modelHandle,
+                                                             ModelParameter modelParam);
+
+    /**
      * Detach from the module, releasing any active resources.
      * This will ensure the client callback is no longer called after this call returns.
      * All models must have been unloaded prior to calling this method.
diff --git a/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl b/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl
new file mode 100644
index 0000000..0993627
--- /dev/null
+++ b/media/java/android/media/soundtrigger_middleware/ModelParameter.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.soundtrigger_middleware;
+
+/**
+ * Model specific parameters to be used with parameter set and get APIs.
+ *
+ * {@hide}
+ */
+@Backing(type="int")
+enum ModelParameter {
+    /**
+     * Placeholder for invalid model parameter used for returning error or
+     * passing an invalid value.
+     */
+    INVALID = -1,
+
+    /**
+     * Controls the sensitivity threshold adjustment factor for a given model.
+     * Negative value corresponds to less sensitive model (high threshold) and
+     * a positive value corresponds to a more sensitive model (low threshold).
+     * Default value is 0.
+     */
+    THRESHOLD_FACTOR = 0,
+}
diff --git a/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl b/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl
new file mode 100644
index 0000000..d6948a8
--- /dev/null
+++ b/media/java/android/media/soundtrigger_middleware/ModelParameterRange.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.soundtrigger_middleware;
+
+/**
+ * Value range for a model parameter.
+ *
+ * {@hide}
+ */
+parcelable ModelParameterRange {
+    /** Minimum (inclusive) */
+    int minInclusive;
+    /** Maximum (inclusive) */
+    int maxInclusive;
+}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 203bc61..b7adfa4 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -116,6 +116,7 @@
         "android.hardware.oemlock-V1.0-java",
         "android.hardware.configstore-V1.0-java",
         "android.hardware.contexthub-V1.0-java",
+        "android.hardware.soundtrigger-V2.3-java",
         "android.hidl.manager-V1.2-java",
         "dnsresolver_aidl_interface-V2-java",
         "netd_event_listener_interface-java",
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java b/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java
new file mode 100644
index 0000000..3fa5230
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/AudioSessionProviderImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+/**
+ * An implementation of SoundTriggerMiddlewareImpl.AudioSessionProvider that ties to native
+ * AudioSystem module via JNI.
+ */
+class AudioSessionProviderImpl extends SoundTriggerMiddlewareImpl.AudioSessionProvider {
+    @Override
+    public native AudioSession acquireSession();
+
+    @Override
+    public native void releaseSession(int sessionHandle);
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
new file mode 100644
index 0000000..9b22f33
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.audio.common.V2_0.Uuid;
+import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback;
+import android.hardware.soundtrigger.V2_3.ISoundTriggerHw;
+import android.media.audio.common.AudioConfig;
+import android.media.audio.common.AudioOffloadInfo;
+import android.media.soundtrigger_middleware.ConfidenceLevel;
+import android.media.soundtrigger_middleware.ModelParameter;
+import android.media.soundtrigger_middleware.ModelParameterRange;
+import android.media.soundtrigger_middleware.Phrase;
+import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
+import android.media.soundtrigger_middleware.PhraseRecognitionExtra;
+import android.media.soundtrigger_middleware.PhraseSoundModel;
+import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.RecognitionEvent;
+import android.media.soundtrigger_middleware.RecognitionMode;
+import android.media.soundtrigger_middleware.RecognitionStatus;
+import android.media.soundtrigger_middleware.SoundModel;
+import android.media.soundtrigger_middleware.SoundModelType;
+import android.media.soundtrigger_middleware.SoundTriggerModuleProperties;
+import android.os.HidlMemoryUtil;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for type conversion between SoundTrigger HAL types and SoundTriggerMiddleware service
+ * types.
+ *
+ * @hide
+ */
+class ConversionUtil {
+    static @NonNull
+    SoundTriggerModuleProperties hidl2aidlProperties(
+            @NonNull ISoundTriggerHw.Properties hidlProperties) {
+        SoundTriggerModuleProperties aidlProperties = new SoundTriggerModuleProperties();
+        aidlProperties.implementor = hidlProperties.implementor;
+        aidlProperties.description = hidlProperties.description;
+        aidlProperties.version = hidlProperties.version;
+        aidlProperties.uuid = hidl2aidlUuid(hidlProperties.uuid);
+        aidlProperties.maxSoundModels = hidlProperties.maxSoundModels;
+        aidlProperties.maxKeyPhrases = hidlProperties.maxKeyPhrases;
+        aidlProperties.maxUsers = hidlProperties.maxUsers;
+        aidlProperties.recognitionModes = hidlProperties.recognitionModes;
+        aidlProperties.captureTransition = hidlProperties.captureTransition;
+        aidlProperties.maxBufferMs = hidlProperties.maxBufferMs;
+        aidlProperties.concurrentCapture = hidlProperties.concurrentCapture;
+        aidlProperties.triggerInEvent = hidlProperties.triggerInEvent;
+        aidlProperties.powerConsumptionMw = hidlProperties.powerConsumptionMw;
+        return aidlProperties;
+    }
+
+    static @NonNull
+    String hidl2aidlUuid(@NonNull Uuid hidlUuid) {
+        if (hidlUuid.node == null || hidlUuid.node.length != 6) {
+            throw new IllegalArgumentException("UUID.node must be of length 6.");
+        }
+        return String.format(UuidUtil.FORMAT,
+                hidlUuid.timeLow,
+                hidlUuid.timeMid,
+                hidlUuid.versionAndTimeHigh,
+                hidlUuid.variantAndClockSeqHigh,
+                hidlUuid.node[0],
+                hidlUuid.node[1],
+                hidlUuid.node[2],
+                hidlUuid.node[3],
+                hidlUuid.node[4],
+                hidlUuid.node[5]);
+    }
+
+    static @NonNull
+    Uuid aidl2hidlUuid(@NonNull String aidlUuid) {
+        Matcher matcher = UuidUtil.PATTERN.matcher(aidlUuid);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Illegal format for UUID: " + aidlUuid);
+        }
+        Uuid hidlUuid = new Uuid();
+        hidlUuid.timeLow = Integer.parseUnsignedInt(matcher.group(1), 16);
+        hidlUuid.timeMid = (short) Integer.parseUnsignedInt(matcher.group(2), 16);
+        hidlUuid.versionAndTimeHigh = (short) Integer.parseUnsignedInt(matcher.group(3), 16);
+        hidlUuid.variantAndClockSeqHigh = (short) Integer.parseUnsignedInt(matcher.group(4), 16);
+        hidlUuid.node = new byte[]{(byte) Integer.parseUnsignedInt(matcher.group(5), 16),
+                (byte) Integer.parseUnsignedInt(matcher.group(6), 16),
+                (byte) Integer.parseUnsignedInt(matcher.group(7), 16),
+                (byte) Integer.parseUnsignedInt(matcher.group(8), 16),
+                (byte) Integer.parseUnsignedInt(matcher.group(9), 16),
+                (byte) Integer.parseUnsignedInt(matcher.group(10), 16)};
+        return hidlUuid;
+    }
+
+    static int aidl2hidlSoundModelType(int aidlType) {
+        switch (aidlType) {
+            case SoundModelType.GENERIC:
+                return android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC;
+            case SoundModelType.KEYPHRASE:
+                return android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE;
+            default:
+                throw new IllegalArgumentException("Unknown sound model type: " + aidlType);
+        }
+    }
+
+    static int hidl2aidlSoundModelType(int hidlType) {
+        switch (hidlType) {
+            case android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC:
+                return SoundModelType.GENERIC;
+            case android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE:
+                return SoundModelType.KEYPHRASE;
+            default:
+                throw new IllegalArgumentException("Unknown sound model type: " + hidlType);
+        }
+    }
+
+    static @NonNull
+    ISoundTriggerHw.Phrase aidl2hidlPhrase(@NonNull Phrase aidlPhrase) {
+        ISoundTriggerHw.Phrase hidlPhrase = new ISoundTriggerHw.Phrase();
+        hidlPhrase.id = aidlPhrase.id;
+        hidlPhrase.recognitionModes = aidl2hidlRecognitionModes(aidlPhrase.recognitionModes);
+        for (int aidlUser : aidlPhrase.users) {
+            hidlPhrase.users.add(aidlUser);
+        }
+        hidlPhrase.locale = aidlPhrase.locale;
+        hidlPhrase.text = aidlPhrase.text;
+        return hidlPhrase;
+    }
+
+    static int aidl2hidlRecognitionModes(int aidlModes) {
+        int hidlModes = 0;
+
+        if ((aidlModes & RecognitionMode.VOICE_TRIGGER) != 0) {
+            hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER;
+        }
+        if ((aidlModes & RecognitionMode.USER_IDENTIFICATION) != 0) {
+            hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION;
+        }
+        if ((aidlModes & RecognitionMode.USER_AUTHENTICATION) != 0) {
+            hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION;
+        }
+        if ((aidlModes & RecognitionMode.GENERIC_TRIGGER) != 0) {
+            hidlModes |= android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER;
+        }
+        return hidlModes;
+    }
+
+    static int hidl2aidlRecognitionModes(int hidlModes) {
+        int aidlModes = 0;
+        if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER) != 0) {
+            aidlModes |= RecognitionMode.VOICE_TRIGGER;
+        }
+        if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION)
+                != 0) {
+            aidlModes |= RecognitionMode.USER_IDENTIFICATION;
+        }
+        if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION)
+                != 0) {
+            aidlModes |= RecognitionMode.USER_AUTHENTICATION;
+        }
+        if ((hidlModes & android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER) != 0) {
+            aidlModes |= RecognitionMode.GENERIC_TRIGGER;
+        }
+        return aidlModes;
+    }
+
+    static @NonNull
+    ISoundTriggerHw.SoundModel aidl2hidlSoundModel(@NonNull SoundModel aidlModel) {
+        ISoundTriggerHw.SoundModel hidlModel = new ISoundTriggerHw.SoundModel();
+        hidlModel.header.type = aidl2hidlSoundModelType(aidlModel.type);
+        hidlModel.header.uuid = aidl2hidlUuid(aidlModel.uuid);
+        hidlModel.header.vendorUuid = aidl2hidlUuid(aidlModel.vendorUuid);
+        hidlModel.data = HidlMemoryUtil.byteArrayToHidlMemory(aidlModel.data,
+                "SoundTrigger SoundModel");
+        return hidlModel;
+    }
+
+    static @NonNull
+    ISoundTriggerHw.PhraseSoundModel aidl2hidlPhraseSoundModel(
+            @NonNull PhraseSoundModel aidlModel) {
+        ISoundTriggerHw.PhraseSoundModel hidlModel = new ISoundTriggerHw.PhraseSoundModel();
+        hidlModel.common = aidl2hidlSoundModel(aidlModel.common);
+        for (Phrase aidlPhrase : aidlModel.phrases) {
+            hidlModel.phrases.add(aidl2hidlPhrase(aidlPhrase));
+        }
+        return hidlModel;
+    }
+
+    static @NonNull
+    ISoundTriggerHw.RecognitionConfig aidl2hidlRecognitionConfig(
+            @NonNull RecognitionConfig aidlConfig) {
+        ISoundTriggerHw.RecognitionConfig hidlConfig = new ISoundTriggerHw.RecognitionConfig();
+        hidlConfig.header.captureRequested = aidlConfig.captureRequested;
+        for (PhraseRecognitionExtra aidlPhraseExtra : aidlConfig.phraseRecognitionExtras) {
+            hidlConfig.header.phrases.add(aidl2hidlPhraseRecognitionExtra(aidlPhraseExtra));
+        }
+        hidlConfig.data = HidlMemoryUtil.byteArrayToHidlMemory(aidlConfig.data,
+                "SoundTrigger RecognitionConfig");
+        return hidlConfig;
+    }
+
+    static @NonNull
+    android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra aidl2hidlPhraseRecognitionExtra(
+            @NonNull PhraseRecognitionExtra aidlExtra) {
+        android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra hidlExtra =
+                new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra();
+        hidlExtra.id = aidlExtra.id;
+        hidlExtra.recognitionModes = aidl2hidlRecognitionModes(aidlExtra.recognitionModes);
+        hidlExtra.confidenceLevel = aidlExtra.confidenceLevel;
+        hidlExtra.levels.ensureCapacity(aidlExtra.levels.length);
+        for (ConfidenceLevel aidlLevel : aidlExtra.levels) {
+            hidlExtra.levels.add(aidl2hidlConfidenceLevel(aidlLevel));
+        }
+        return hidlExtra;
+    }
+
+    static @NonNull
+    PhraseRecognitionExtra hidl2aidlPhraseRecognitionExtra(
+            @NonNull android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra hidlExtra) {
+        PhraseRecognitionExtra aidlExtra = new PhraseRecognitionExtra();
+        aidlExtra.id = hidlExtra.id;
+        aidlExtra.recognitionModes = hidl2aidlRecognitionModes(hidlExtra.recognitionModes);
+        aidlExtra.confidenceLevel = hidlExtra.confidenceLevel;
+        aidlExtra.levels = new ConfidenceLevel[hidlExtra.levels.size()];
+        for (int i = 0; i < hidlExtra.levels.size(); ++i) {
+            aidlExtra.levels[i] = hidl2aidlConfidenceLevel(hidlExtra.levels.get(i));
+        }
+        return aidlExtra;
+    }
+
+    static @NonNull
+    android.hardware.soundtrigger.V2_0.ConfidenceLevel aidl2hidlConfidenceLevel(
+            @NonNull ConfidenceLevel aidlLevel) {
+        android.hardware.soundtrigger.V2_0.ConfidenceLevel hidlLevel =
+                new android.hardware.soundtrigger.V2_0.ConfidenceLevel();
+        hidlLevel.userId = aidlLevel.userId;
+        hidlLevel.levelPercent = aidlLevel.levelPercent;
+        return hidlLevel;
+    }
+
+    static @NonNull
+    ConfidenceLevel hidl2aidlConfidenceLevel(
+            @NonNull android.hardware.soundtrigger.V2_0.ConfidenceLevel hidlLevel) {
+        ConfidenceLevel aidlLevel = new ConfidenceLevel();
+        aidlLevel.userId = hidlLevel.userId;
+        aidlLevel.levelPercent = hidlLevel.levelPercent;
+        return aidlLevel;
+    }
+
+    static int hidl2aidlRecognitionStatus(int hidlStatus) {
+        switch (hidlStatus) {
+            case ISoundTriggerHwCallback.RecognitionStatus.SUCCESS:
+                return RecognitionStatus.SUCCESS;
+            case ISoundTriggerHwCallback.RecognitionStatus.ABORT:
+                return RecognitionStatus.ABORTED;
+            case ISoundTriggerHwCallback.RecognitionStatus.FAILURE:
+                return RecognitionStatus.FAILURE;
+            case 3: // This doesn't have a constant in HIDL.
+                return RecognitionStatus.FORCED;
+            default:
+                throw new IllegalArgumentException("Unknown recognition status: " + hidlStatus);
+        }
+    }
+
+    static @NonNull
+    RecognitionEvent hidl2aidlRecognitionEvent(@NonNull
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent hidlEvent) {
+        RecognitionEvent aidlEvent = new RecognitionEvent();
+        aidlEvent.status = hidl2aidlRecognitionStatus(hidlEvent.status);
+        aidlEvent.type = hidl2aidlSoundModelType(hidlEvent.type);
+        aidlEvent.captureAvailable = hidlEvent.captureAvailable;
+        // hidlEvent.captureSession is never a valid field.
+        aidlEvent.captureSession = -1;
+        aidlEvent.captureDelayMs = hidlEvent.captureDelayMs;
+        aidlEvent.capturePreambleMs = hidlEvent.capturePreambleMs;
+        aidlEvent.triggerInData = hidlEvent.triggerInData;
+        aidlEvent.audioConfig = hidl2aidlAudioConfig(hidlEvent.audioConfig);
+        aidlEvent.data = new byte[hidlEvent.data.size()];
+        for (int i = 0; i < aidlEvent.data.length; ++i) {
+            aidlEvent.data[i] = hidlEvent.data.get(i);
+        }
+        return aidlEvent;
+    }
+
+    static @NonNull
+    RecognitionEvent hidl2aidlRecognitionEvent(
+            @NonNull ISoundTriggerHwCallback.RecognitionEvent hidlEvent) {
+        RecognitionEvent aidlEvent = hidl2aidlRecognitionEvent(hidlEvent.header);
+        // Data needs to get overridden with 2.1 data.
+        aidlEvent.data = HidlMemoryUtil.hidlMemoryToByteArray(hidlEvent.data);
+        return aidlEvent;
+    }
+
+    static @NonNull
+    PhraseRecognitionEvent hidl2aidlPhraseRecognitionEvent(@NonNull
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent hidlEvent) {
+        PhraseRecognitionEvent aidlEvent = new PhraseRecognitionEvent();
+        aidlEvent.common = hidl2aidlRecognitionEvent(hidlEvent.common);
+        aidlEvent.phraseExtras = new PhraseRecognitionExtra[hidlEvent.phraseExtras.size()];
+        for (int i = 0; i < hidlEvent.phraseExtras.size(); ++i) {
+            aidlEvent.phraseExtras[i] = hidl2aidlPhraseRecognitionExtra(
+                    hidlEvent.phraseExtras.get(i));
+        }
+        return aidlEvent;
+    }
+
+    static @NonNull
+    PhraseRecognitionEvent hidl2aidlPhraseRecognitionEvent(
+            @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent hidlEvent) {
+        PhraseRecognitionEvent aidlEvent = new PhraseRecognitionEvent();
+        aidlEvent.common = hidl2aidlRecognitionEvent(hidlEvent.common);
+        aidlEvent.phraseExtras = new PhraseRecognitionExtra[hidlEvent.phraseExtras.size()];
+        for (int i = 0; i < hidlEvent.phraseExtras.size(); ++i) {
+            aidlEvent.phraseExtras[i] = hidl2aidlPhraseRecognitionExtra(
+                    hidlEvent.phraseExtras.get(i));
+        }
+        return aidlEvent;
+    }
+
+    static @NonNull
+    AudioConfig hidl2aidlAudioConfig(
+            @NonNull android.hardware.audio.common.V2_0.AudioConfig hidlConfig) {
+        AudioConfig aidlConfig = new AudioConfig();
+        // TODO(ytai): channelMask and format might need a more careful conversion to make sure the
+        //  constants match.
+        aidlConfig.sampleRateHz = hidlConfig.sampleRateHz;
+        aidlConfig.channelMask = hidlConfig.channelMask;
+        aidlConfig.format = hidlConfig.format;
+        aidlConfig.offloadInfo = hidl2aidlOffloadInfo(hidlConfig.offloadInfo);
+        aidlConfig.frameCount = hidlConfig.frameCount;
+        return aidlConfig;
+    }
+
+    static @NonNull
+    AudioOffloadInfo hidl2aidlOffloadInfo(
+            @NonNull android.hardware.audio.common.V2_0.AudioOffloadInfo hidlInfo) {
+        AudioOffloadInfo aidlInfo = new AudioOffloadInfo();
+        // TODO(ytai): channelMask, format, streamType and usage might need a more careful
+        //  conversion to make sure the constants match.
+        aidlInfo.sampleRateHz = hidlInfo.sampleRateHz;
+        aidlInfo.channelMask = hidlInfo.channelMask;
+        aidlInfo.format = hidlInfo.format;
+        aidlInfo.streamType = hidlInfo.streamType;
+        aidlInfo.bitRatePerSecond = hidlInfo.bitRatePerSecond;
+        aidlInfo.durationMicroseconds = hidlInfo.durationMicroseconds;
+        aidlInfo.hasVideo = hidlInfo.hasVideo;
+        aidlInfo.isStreaming = hidlInfo.isStreaming;
+        aidlInfo.bitWidth = hidlInfo.bitWidth;
+        aidlInfo.bufferSize = hidlInfo.bufferSize;
+        aidlInfo.usage = hidlInfo.usage;
+        return aidlInfo;
+    }
+
+    @Nullable
+    static ModelParameterRange hidl2aidlModelParameterRange(
+            android.hardware.soundtrigger.V2_3.ModelParameterRange hidlRange) {
+        if (hidlRange == null) {
+            return null;
+        }
+        ModelParameterRange aidlRange = new ModelParameterRange();
+        aidlRange.minInclusive = hidlRange.start;
+        aidlRange.maxInclusive = hidlRange.end;
+        return aidlRange;
+    }
+
+    static int aidl2hidlModelParameter(int aidlParam) {
+        switch (aidlParam) {
+            case ModelParameter.THRESHOLD_FACTOR:
+                return android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR;
+
+            default:
+                return android.hardware.soundtrigger.V2_3.ModelParameter.INVALID;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/HalException.java b/services/core/java/com/android/server/soundtrigger_middleware/HalException.java
new file mode 100644
index 0000000..8b3e708
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/HalException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+
+/**
+ * This exception represents a non-zero status code returned by a HAL invocation.
+ * Depending on the operation that threw the error, the integrity of the HAL implementation and the
+ * client's tolerance to error, this error may or may not be recoverable. The HAL itself is expected
+ * to retain the state it had prior to the invocation (so, unless the error is a result of a HAL
+ * bug, normal operation may resume).
+ * <p>
+ * The reason why this is a RuntimeException, even though the HAL interface allows returning them
+ * is because we expect none of them to actually occur as part of correct usage of the HAL.
+ *
+ * @hide
+ */
+public class HalException extends RuntimeException {
+    public final int errorCode;
+
+    public HalException(int errorCode, @NonNull String message) {
+        super(message);
+        this.errorCode = errorCode;
+    }
+
+    public HalException(int errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    @Override
+    public @NonNull String toString() {
+        return super.toString() + " (code " + errorCode + ")";
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java
new file mode 100644
index 0000000..f0a0d83
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/Hw2CompatUtil.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.os.HidlMemoryUtil;
+
+import java.util.ArrayList;
+
+/**
+ * Utilities for maintaining data compatibility between different minor versions of soundtrigger@2.x
+ * HAL.
+ * Note that some of these conversion utilities are destructive, i.e. mutate their input (for the
+ * sake of simplifying code and reducing copies).
+ */
+class Hw2CompatUtil {
+    static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel convertSoundModel_2_1_to_2_0(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel) {
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 = soundModel.header;
+        // Note: this mutates the input!
+        model_2_0.data = HidlMemoryUtil.hidlMemoryToByteList(soundModel.data);
+        return model_2_0;
+    }
+
+    static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent convertRecognitionEvent_2_0_to_2_1(
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event) {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 =
+                new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent();
+        event_2_1.header = event;
+        event_2_1.data = HidlMemoryUtil.byteListToHidlMemory(event_2_1.header.data,
+                "SoundTrigger RecognitionEvent");
+        // Note: this mutates the input!
+        event_2_1.header.data = new ArrayList<>();
+        return event_2_1;
+    }
+
+    static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent convertPhraseRecognitionEvent_2_0_to_2_1(
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event) {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent
+                event_2_1 =
+                new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent();
+        event_2_1.common = convertRecognitionEvent_2_0_to_2_1(event.common);
+        event_2_1.phraseExtras = event.phraseExtras;
+        return event_2_1;
+    }
+
+    static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel convertPhraseSoundModel_2_1_to_2_0(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel) {
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 =
+                new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel();
+        model_2_0.common = convertSoundModel_2_1_to_2_0(soundModel.common);
+        model_2_0.phrases = soundModel.phrases;
+        return model_2_0;
+    }
+
+    static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig convertRecognitionConfig_2_1_to_2_0(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config) {
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 =
+                config.header;
+        // Note: this mutates the input!
+        config_2_0.data = HidlMemoryUtil.hidlMemoryToByteList(config.data);
+        return config_2_0;
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java
new file mode 100644
index 0000000..81252c9
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/ISoundTriggerHw2.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.hardware.soundtrigger.V2_3.ISoundTriggerHw;
+import android.hardware.soundtrigger.V2_3.ModelParameterRange;
+import android.hidl.base.V1_0.IBase;
+import android.os.IHwBinder;
+
+/**
+ * This interface mimics android.hardware.soundtrigger.V2_x.ISoundTriggerHw and
+ * android.hardware.soundtrigger.V2_x.ISoundTriggerHwCallback, with a few key differences:
+ * <ul>
+ * <li>Methods in the original interface generally have a status return value and potentially a
+ * second return value which is the actual return value. This is reflected via a synchronous
+ * callback, which is not very pleasant to work with. This interface replaces that pattern with
+ * the convention that a HalException is thrown for non-OK status, and then we can use the
+ * return value for the actual return value.
+ * <li>This interface will always include all the methods from the latest 2.x version (and thus
+ * from every 2.x version) interface, with the convention that unsupported methods throw a
+ * {@link RecoverableException} with a
+ * {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED}
+ * code.
+ * <li>Cases where the original interface had multiple versions of a method representing the exact
+ * thing, or there exists a trivial conversion between the new and old version, this interface
+ * represents only the latest version, without any _version suffixes.
+ * <li>Removes some of the obscure IBinder methods.
+ * <li>No RemoteExceptions are specified. Some implementations of this interface may rethrow
+ * RemoteExceptions as RuntimeExceptions, some can guarantee handling them somehow and never throw
+ * them.
+ * <li>soundModelCallback has been removed, since nobody cares about it. Implementations are free
+ * to silently discard it.
+ * </ul>
+ * For cases where the client wants to explicitly handle specific versions of the underlying driver
+ * interface, they may call {@link #interfaceDescriptor()}.
+ * <p>
+ * <b>Note to maintainers</b>: This class must always be kept in sync with the latest 2.x version,
+ * so that clients have access to the entire functionality without having to burden themselves with
+ * compatibility, as much as possible.
+ */
+public interface ISoundTriggerHw2 {
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getProperties(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback
+     */
+    android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties();
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel,
+     * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int,
+     * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback)
+     */
+    int loadSoundModel(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
+            SoundTriggerHw2Compat.Callback callback, int cookie);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#loadPhraseSoundModel_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel,
+     * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int,
+     * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback)
+     */
+    int loadPhraseSoundModel(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
+            SoundTriggerHw2Compat.Callback callback, int cookie);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#unloadSoundModel(int)
+     */
+    void unloadSoundModel(int modelHandle);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopRecognition(int)
+     */
+    void stopRecognition(int modelHandle);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#stopAllRecognitions()
+     */
+    void stopAllRecognitions();
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#startRecognition_2_1(int,
+     * android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig,
+     * android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback, int)
+     */
+    void startRecognition(int modelHandle,
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config,
+            SoundTriggerHw2Compat.Callback callback, int cookie);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_2.ISoundTriggerHw#getModelState(int)
+     */
+    void getModelState(int modelHandle);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#getParameter(int, int,
+     * ISoundTriggerHw.getParameterCallback)
+     */
+    int getModelParameter(int modelHandle, int param);
+
+    /**
+     * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#setParameter(int, int, int)
+     */
+    void setModelParameter(int modelHandle, int param, int value);
+
+    /**
+     * @return null if not supported.
+     * @see android.hardware.soundtrigger.V2_3.ISoundTriggerHw#queryParameter(int, int,
+     * ISoundTriggerHw.queryParameterCallback)
+     */
+    ModelParameterRange queryParameter(int modelHandle, int param);
+
+    /**
+     * @see IHwBinder#linkToDeath(IHwBinder.DeathRecipient, long)
+     */
+    boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie);
+
+    /**
+     * @see IHwBinder#unlinkToDeath(IHwBinder.DeathRecipient)
+     */
+    boolean unlinkToDeath(IHwBinder.DeathRecipient recipient);
+
+    /**
+     * @see IBase#interfaceDescriptor()
+     */
+    String interfaceDescriptor() throws android.os.RemoteException;
+
+    interface Callback {
+        /**
+         * @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#recognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent,
+         * int)
+         */
+        void recognitionCallback(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event,
+                int cookie);
+
+        /**
+         * @see android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback#phraseRecognitionCallback_2_1(android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent,
+         * int)
+         */
+        void phraseRecognitionCallback(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
+                int cookie);
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java b/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java
new file mode 100644
index 0000000..e1fb226
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/InternalServerError.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+
+/**
+ * An internal server error.
+ * <p>
+ * This exception wraps any exception thrown from a service implementation, which is a result of a
+ * bug in the server implementation (or any of its dependencies).
+ * <p>
+ * Specifically, this type is excluded from the set of whitelisted exceptions that binder would
+ * tunnel to the client process, since these exceptions are ambiguous regarding whether the client
+ * had done something wrong or the server is buggy. For example, a client getting an
+ * IllegalArgumentException cannot easily determine whether they had provided illegal arguments to
+ * the method they were calling, or whether the method implementation provided illegal arguments to
+ * some method it was calling due to a bug.
+ *
+ * @hide
+ */
+public class InternalServerError extends RuntimeException {
+    public InternalServerError(@NonNull Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java b/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java
new file mode 100644
index 0000000..8361850
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/RecoverableException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+
+/**
+ * This exception represents a fault which:
+ * <ul>
+ * <li>Could not have been anticipated by a caller (i.e. is not a violation of any preconditions).
+ * <li>Is guaranteed to not have been caused any meaningful state change in the callee. The caller
+ *     may continue operation as if the call has never been made.
+ * </ul>
+ * <p>
+ * Some recoverable faults are permanent and some are transient / circumstantial, the specific error
+ * code can provide more information about the possible recovery options.
+ * <p>
+ * The reason why this is a RuntimeException is to allow it to go through interfaces defined by
+ * AIDL, which we have no control over.
+ *
+ * @hide
+ */
+public class RecoverableException extends RuntimeException {
+    public final int errorCode;
+
+    public RecoverableException(int errorCode, @NonNull String message) {
+        super(message);
+        this.errorCode = errorCode;
+    }
+
+    public RecoverableException(int errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    @Override
+    public @NonNull String toString() {
+        return super.toString() + " (code " + errorCode + ")";
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java
new file mode 100644
index 0000000..4a852c4
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.soundtrigger_middleware.Status;
+import android.os.IHwBinder;
+import android.os.RemoteException;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An implementation of {@link ISoundTriggerHw2}, on top of any
+ * android.hardware.soundtrigger.V2_x.ISoundTriggerHw implementation. This class hides away some of
+ * the details involved with retaining backward compatibility and adapts to the more pleasant syntax
+ * exposed by {@link ISoundTriggerHw2}, compared to the bare driver interface.
+ * <p>
+ * Exception handling:
+ * <ul>
+ * <li>All {@link RemoteException}s get rethrown as {@link RuntimeException}.
+ * <li>All HAL malfunctions get thrown as {@link HalException}.
+ * <li>All unsupported operations get thrown as {@link RecoverableException} with a
+ * {@link android.media.soundtrigger_middleware.Status#OPERATION_NOT_SUPPORTED}
+ * code.
+ * </ul>
+ */
+final class SoundTriggerHw2Compat implements ISoundTriggerHw2 {
+    private final @NonNull
+    IHwBinder mBinder;
+    private final @NonNull
+    android.hardware.soundtrigger.V2_0.ISoundTriggerHw mUnderlying_2_0;
+    private final @Nullable
+    android.hardware.soundtrigger.V2_1.ISoundTriggerHw mUnderlying_2_1;
+    private final @Nullable
+    android.hardware.soundtrigger.V2_2.ISoundTriggerHw mUnderlying_2_2;
+    private final @Nullable
+    android.hardware.soundtrigger.V2_3.ISoundTriggerHw mUnderlying_2_3;
+
+    public SoundTriggerHw2Compat(
+            @NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw underlying) {
+        this(underlying.asBinder());
+    }
+
+    public SoundTriggerHw2Compat(IHwBinder binder) {
+        Objects.requireNonNull(binder);
+
+        mBinder = binder;
+
+        // We want to share the proxy instances rather than create a separate proxy for every
+        // version, so we go down the versions in descending order to find the latest one supported,
+        // and then simply up-cast it to obtain all the versions that are earlier.
+
+        // Attempt 2.3
+        android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3 =
+                android.hardware.soundtrigger.V2_3.ISoundTriggerHw.asInterface(binder);
+        if (as2_3 != null) {
+            mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = as2_3;
+            return;
+        }
+
+        // Attempt 2.2
+        android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2 =
+                android.hardware.soundtrigger.V2_2.ISoundTriggerHw.asInterface(binder);
+        if (as2_2 != null) {
+            mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = as2_2;
+            mUnderlying_2_3 = null;
+            return;
+        }
+
+        // Attempt 2.1
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1 =
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHw.asInterface(binder);
+        if (as2_1 != null) {
+            mUnderlying_2_0 = mUnderlying_2_1 = as2_1;
+            mUnderlying_2_2 = mUnderlying_2_3 = null;
+            return;
+        }
+
+        // Attempt 2.0
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0 =
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHw.asInterface(binder);
+        if (as2_0 != null) {
+            mUnderlying_2_0 = as2_0;
+            mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = null;
+            return;
+        }
+
+        throw new RuntimeException("Binder doesn't support ISoundTriggerHw@2.0");
+    }
+
+    private static void handleHalStatus(int status, String methodName) {
+        if (status != 0) {
+            throw new HalException(status, methodName);
+        }
+    }
+
+    @Override
+    public android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties getProperties() {
+        try {
+            AtomicInteger retval = new AtomicInteger(-1);
+            AtomicReference<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Properties>
+                    properties =
+                    new AtomicReference<>();
+            as2_0().getProperties(
+                    (r, p) -> {
+                        retval.set(r);
+                        properties.set(p);
+                    });
+            handleHalStatus(retval.get(), "getProperties");
+            return properties.get();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public int loadSoundModel(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
+            Callback callback, int cookie) {
+        try {
+            AtomicInteger retval = new AtomicInteger(-1);
+            AtomicInteger handle = new AtomicInteger(0);
+            try {
+                as2_1().loadSoundModel_2_1(soundModel, new SoundTriggerCallback(callback), cookie,
+                        (r, h) -> {
+                            retval.set(r);
+                            handle.set(h);
+                        });
+            } catch (NotSupported e) {
+                // Fall-back to the 2.0 version:
+                return loadSoundModel_2_0(soundModel, callback, cookie);
+            }
+            handleHalStatus(retval.get(), "loadSoundModel_2_1");
+            return handle.get();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public int loadPhraseSoundModel(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
+            Callback callback, int cookie) {
+        try {
+            AtomicInteger retval = new AtomicInteger(-1);
+            AtomicInteger handle = new AtomicInteger(0);
+            try {
+                as2_1().loadPhraseSoundModel_2_1(soundModel, new SoundTriggerCallback(callback),
+                        cookie,
+                        (r, h) -> {
+                            retval.set(r);
+                            handle.set(h);
+                        });
+            } catch (NotSupported e) {
+                // Fall-back to the 2.0 version:
+                return loadPhraseSoundModel_2_0(soundModel, callback, cookie);
+            }
+            handleHalStatus(retval.get(), "loadSoundModel_2_1");
+            return handle.get();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public void unloadSoundModel(int modelHandle) {
+        try {
+            int retval = as2_0().unloadSoundModel(modelHandle);
+            handleHalStatus(retval, "unloadSoundModel");
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public void stopRecognition(int modelHandle) {
+        try {
+            int retval = as2_0().stopRecognition(modelHandle);
+            handleHalStatus(retval, "stopRecognition");
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+
+    }
+
+    @Override
+    public void stopAllRecognitions() {
+        try {
+            int retval = as2_0().stopAllRecognitions();
+            handleHalStatus(retval, "stopAllRecognitions");
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public void startRecognition(int modelHandle,
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config,
+            Callback callback, int cookie) {
+        try {
+            try {
+                int retval = as2_1().startRecognition_2_1(modelHandle, config,
+                        new SoundTriggerCallback(callback), cookie);
+                handleHalStatus(retval, "startRecognition_2_1");
+            } catch (NotSupported e) {
+                // Fall-back to the 2.0 version:
+                startRecognition_2_0(modelHandle, config, callback, cookie);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public void getModelState(int modelHandle) {
+        try {
+            int retval = as2_2().getModelState(modelHandle);
+            handleHalStatus(retval, "getModelState");
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        } catch (NotSupported e) {
+            throw e.throwAsRecoverableException();
+        }
+    }
+
+    @Override
+    public int getModelParameter(int modelHandle, int param) {
+        AtomicInteger status = new AtomicInteger(-1);
+        AtomicInteger value = new AtomicInteger(0);
+        try {
+            as2_3().getParameter(modelHandle, param,
+                    (s, v) -> {
+                        status.set(s);
+                        value.set(v);
+                    });
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        } catch (NotSupported e) {
+            throw e.throwAsRecoverableException();
+        }
+        handleHalStatus(status.get(), "getParameter");
+        return value.get();
+    }
+
+    @Override
+    public void setModelParameter(int modelHandle, int param, int value) {
+        try {
+            int retval = as2_3().setParameter(modelHandle, param, value);
+            handleHalStatus(retval, "setParameter");
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        } catch (NotSupported e) {
+            throw e.throwAsRecoverableException();
+        }
+    }
+
+    @Override
+    public android.hardware.soundtrigger.V2_3.ModelParameterRange queryParameter(int modelHandle,
+            int param) {
+        AtomicInteger status = new AtomicInteger(-1);
+        AtomicReference<android.hardware.soundtrigger.V2_3.OptionalModelParameterRange>
+                optionalRange =
+                new AtomicReference<>();
+        try {
+            as2_3().queryParameter(modelHandle, param,
+                    (s, r) -> {
+                        status.set(s);
+                        optionalRange.set(r);
+                    });
+        } catch (NotSupported e) {
+            // For older drivers, we consider no model parameter to be supported.
+            return null;
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+        handleHalStatus(status.get(), "queryParameter");
+        return (optionalRange.get().getDiscriminator()
+                == android.hardware.soundtrigger.V2_3.OptionalModelParameterRange.hidl_discriminator.range)
+                ?
+                optionalRange.get().range() : null;
+    }
+
+    @Override
+    public boolean linkToDeath(IHwBinder.DeathRecipient recipient, long cookie) {
+        return mBinder.linkToDeath(recipient, cookie);
+    }
+
+    @Override
+    public boolean unlinkToDeath(IHwBinder.DeathRecipient recipient) {
+        return mBinder.unlinkToDeath(recipient);
+    }
+
+    @Override
+    public String interfaceDescriptor() throws RemoteException {
+        return as2_0().interfaceDescriptor();
+    }
+
+    private int loadSoundModel_2_0(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
+            Callback callback, int cookie)
+            throws RemoteException {
+        // Convert the soundModel to V2.0.
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 =
+                Hw2CompatUtil.convertSoundModel_2_1_to_2_0(soundModel);
+
+        AtomicInteger retval = new AtomicInteger(-1);
+        AtomicInteger handle = new AtomicInteger(0);
+        as2_0().loadSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie, (r, h) -> {
+            retval.set(r);
+            handle.set(h);
+        });
+        handleHalStatus(retval.get(), "loadSoundModel");
+        return handle.get();
+    }
+
+    private int loadPhraseSoundModel_2_0(
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
+            Callback callback, int cookie)
+            throws RemoteException {
+        // Convert the soundModel to V2.0.
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 =
+                Hw2CompatUtil.convertPhraseSoundModel_2_1_to_2_0(soundModel);
+
+        AtomicInteger retval = new AtomicInteger(-1);
+        AtomicInteger handle = new AtomicInteger(0);
+        as2_0().loadPhraseSoundModel(model_2_0, new SoundTriggerCallback(callback), cookie,
+                (r, h) -> {
+                    retval.set(r);
+                    handle.set(h);
+                });
+        handleHalStatus(retval.get(), "loadSoundModel");
+        return handle.get();
+    }
+
+    private void startRecognition_2_0(int modelHandle,
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config,
+            Callback callback, int cookie)
+            throws RemoteException {
+
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 =
+                Hw2CompatUtil.convertRecognitionConfig_2_1_to_2_0(config);
+        int retval = as2_0().startRecognition(modelHandle, config_2_0,
+                new SoundTriggerCallback(callback), cookie);
+        handleHalStatus(retval, "startRecognition");
+    }
+
+    private @NonNull
+    android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0() {
+        return mUnderlying_2_0;
+    }
+
+    private @NonNull
+    android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1() throws NotSupported {
+        if (mUnderlying_2_1 == null) {
+            throw new NotSupported("Underlying driver version < 2.1");
+        }
+        return mUnderlying_2_1;
+    }
+
+    private @NonNull
+    android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2() throws NotSupported {
+        if (mUnderlying_2_2 == null) {
+            throw new NotSupported("Underlying driver version < 2.2");
+        }
+        return mUnderlying_2_2;
+    }
+
+    private @NonNull
+    android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3() throws NotSupported {
+        if (mUnderlying_2_3 == null) {
+            throw new NotSupported("Underlying driver version < 2.3");
+        }
+        return mUnderlying_2_3;
+    }
+
+    /**
+     * A checked exception representing the requested interface version not being supported.
+     * At the public interface layer, use {@link #throwAsRecoverableException()} to propagate it to
+     * the caller if the request cannot be fulfilled.
+     */
+    private static class NotSupported extends Exception {
+        NotSupported(String message) {
+            super(message);
+        }
+
+        /**
+         * Throw this as a recoverable exception.
+         *
+         * @return Never actually returns anything. Always throws. Used so that caller can write
+         * throw e.throwAsRecoverableException().
+         */
+        RecoverableException throwAsRecoverableException() {
+            throw new RecoverableException(Status.OPERATION_NOT_SUPPORTED, getMessage());
+        }
+    }
+
+    private static class SoundTriggerCallback extends
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.Stub {
+        private final @NonNull
+        Callback mDelegate;
+
+        private SoundTriggerCallback(
+                @NonNull Callback delegate) {
+            mDelegate = Objects.requireNonNull(delegate);
+        }
+
+        @Override
+        public void recognitionCallback_2_1(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event,
+                int cookie) {
+            mDelegate.recognitionCallback(event, cookie);
+        }
+
+        @Override
+        public void phraseRecognitionCallback_2_1(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
+                int cookie) {
+            mDelegate.phraseRecognitionCallback(event, cookie);
+        }
+
+        @Override
+        public void soundModelCallback_2_1(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent event,
+                int cookie) {
+            // Nobody cares.
+        }
+
+        @Override
+        public void recognitionCallback(
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event,
+                int cookie) {
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 =
+                    Hw2CompatUtil.convertRecognitionEvent_2_0_to_2_1(event);
+            mDelegate.recognitionCallback(event_2_1, cookie);
+        }
+
+        @Override
+        public void phraseRecognitionCallback(
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
+                int cookie) {
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent
+                    event_2_1 = Hw2CompatUtil.convertPhraseRecognitionEvent_2_0_to_2_1(event);
+            mDelegate.phraseRecognitionCallback(event_2_1, cookie);
+        }
+
+        @Override
+        public void soundModelCallback(
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent event,
+                int cookie) {
+            // Nobody cares.
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java
new file mode 100644
index 0000000..9d51b65
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImpl.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
+import android.media.soundtrigger_middleware.ISoundTriggerCallback;
+import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
+import android.media.soundtrigger_middleware.ISoundTriggerModule;
+import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is an implementation of the ISoundTriggerMiddlewareService interface.
+ * <p>
+ * <b>Important conventions:</b>
+ * <ul>
+ * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle invalid
+ * usage, and such usage will result in undefined behavior. If this service is to be offered to an
+ * untrusted client, it must be wrapped with input and state validation.
+ * <li>There is no binder instance associated with this implementation. Do not call asBinder().
+ * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal,
+ * recoverable faults. The error code would one of the
+ * {@link android.media.soundtrigger_middleware.Status}
+ * constants. Any other exception thrown should be regarded as a bug in the implementation or one
+ * of its dependencies (assuming correct usage).
+ * <li>The implementation is designed for testibility by featuring dependency injection (the
+ * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies on
+ * Android runtime.
+ * <li>The implementation is thread-safe.
+ * </ul>
+ *
+ * @hide
+ */
+public class SoundTriggerMiddlewareImpl implements ISoundTriggerMiddlewareService {
+    static private final String TAG = "SoundTriggerMiddlewareImpl";
+    private final SoundTriggerModule[] mModules;
+
+    /**
+     * Interface to the audio system, which can allocate capture session handles.
+     * SoundTrigger uses those sessions in order to associate a recognition session with an optional
+     * capture from the same device that triggered the recognition.
+     */
+    public static abstract class AudioSessionProvider {
+        public static final class AudioSession {
+            final int mSessionHandle;
+            final int mIoHandle;
+            final int mDeviceHandle;
+
+            AudioSession(int sessionHandle, int ioHandle, int deviceHandle) {
+                mSessionHandle = sessionHandle;
+                mIoHandle = ioHandle;
+                mDeviceHandle = deviceHandle;
+            }
+        }
+
+        public abstract AudioSession acquireSession();
+
+        public abstract void releaseSession(int sessionHandle);
+    }
+
+    /**
+     * Most generic constructor - gets an array of HAL driver instances.
+     */
+    public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw[] halServices,
+            @NonNull AudioSessionProvider audioSessionProvider) {
+        List<SoundTriggerModule> modules = new ArrayList<>(halServices.length);
+
+        for (int i = 0; i < halServices.length; ++i) {
+            ISoundTriggerHw service = halServices[i];
+            try {
+                modules.add(new SoundTriggerModule(service, audioSessionProvider));
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to a SoundTriggerModule instance", e);
+            }
+        }
+
+        mModules = modules.toArray(new SoundTriggerModule[modules.size()]);
+    }
+
+    /**
+     * Convenience constructor - gets a single HAL driver instance.
+     */
+    public SoundTriggerMiddlewareImpl(@NonNull ISoundTriggerHw halService,
+            @NonNull AudioSessionProvider audioSessionProvider) {
+        this(new ISoundTriggerHw[]{halService}, audioSessionProvider);
+    }
+
+    @Override
+    public @NonNull
+    SoundTriggerModuleDescriptor[] listModules() {
+        SoundTriggerModuleDescriptor[] result = new SoundTriggerModuleDescriptor[mModules.length];
+
+        for (int i = 0; i < mModules.length; ++i) {
+            SoundTriggerModuleDescriptor desc = new SoundTriggerModuleDescriptor();
+            desc.handle = i;
+            desc.properties = mModules[i].getProperties();
+            result[i] = desc;
+        }
+        return result;
+    }
+
+    @Override
+    public @NonNull
+    ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) {
+        return mModules[handle].attach(callback);
+    }
+
+    @Override
+    public void setExternalCaptureState(boolean active) {
+        for (SoundTriggerModule module : mModules) {
+            module.setExternalCaptureState(active);
+        }
+    }
+
+    @Override
+    public @NonNull
+    IBinder asBinder() {
+        throw new UnsupportedOperationException(
+                "This implementation is not inteded to be used directly with Binder.");
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
new file mode 100644
index 0000000..a7cfe10
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
@@ -0,0 +1,709 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
+import android.media.soundtrigger_middleware.ISoundTriggerCallback;
+import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
+import android.media.soundtrigger_middleware.ISoundTriggerModule;
+import android.media.soundtrigger_middleware.ModelParameterRange;
+import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
+import android.media.soundtrigger_middleware.PhraseSoundModel;
+import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.RecognitionEvent;
+import android.media.soundtrigger_middleware.RecognitionStatus;
+import android.media.soundtrigger_middleware.SoundModel;
+import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemService;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This is a wrapper around an {@link ISoundTriggerMiddlewareService} implementation, which exposes
+ * it as a Binder service and enforces permissions and correct usage by the client, as well as makes
+ * sure that exceptions representing a server malfunction do not get sent to the client.
+ * <p>
+ * This is intended to extract the non-business logic out of the underlying implementation and thus
+ * make it easier to maintain each one of those separate aspects. A design trade-off is being made
+ * here, in that this class would need to essentially eavesdrop on all the client-server
+ * communication and retain all state known to the client, while the client doesn't necessarily care
+ * about all of it, and while the server has its own representation of this information. However,
+ * in this case, this is a small amount of data, and the benefits in code elegance seem worth it.
+ * There is also some additional cost in employing a simplistic locking mechanism here, but
+ * following the same line of reasoning, the benefits in code simplicity outweigh it.
+ * <p>
+ * Every public method in this class, overriding an interface method, must follow the following
+ * pattern:
+ * <code><pre>
+ * @Override public T method(S arg) {
+ *     // Permission check.
+ *     checkPermissions();
+ *     // Input validation.
+ *     ValidationUtil.validateS(arg);
+ *     synchronized (this) {
+ *         // State validation.
+ *         if (...state is not valid for this call...) {
+ *             throw new IllegalStateException("State is invalid because...");
+ *         }
+ *         // From here on, every exception isn't client's fault.
+ *         try {
+ *             T result = mDelegate.method(arg);
+ *             // Update state.;
+ *             ...
+ *             return result;
+ *         } catch (Exception e) {
+ *             throw handleException(e);
+ *         }
+ *     }
+ * }
+ * </pre></code>
+ * Following this patterns ensures a consistent and rigorous handling of all aspects associated
+ * with client-server separation.
+ * <p>
+ * <b>Exception handling approach:</b><br>
+ * We make sure all client faults (permissions, argument and state validation) happen first, and
+ * would throw {@link SecurityException}, {@link IllegalArgumentException}/
+ * {@link NullPointerException} or {@link
+ * IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
+ * will get sent back to the client.<br>
+ * Once this is done, any subsequent fault is considered a server fault. Only {@link
+ * RecoverableException}s thrown by the implementation are special-cased: they would get sent back
+ * to the caller as a {@link ServiceSpecificException}, which is the behavior of Binder. Any other
+ * exception gets wrapped with a {@link InternalServerError}, which is specifically chosen as a type
+ * that <b>does NOT</b> get forwarded by binder. Those exceptions would be handled by a high-level
+ * exception handler on the server side, typically resulting in rebooting the server.
+ * <p>
+ * <b>Exposing this service as a System Service:</b><br>
+ * Insert this line into {@link com.android.server.SystemServer}:
+ * <code><pre>
+ * mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class);
+ * </pre></code>
+ *
+ * {@hide}
+ */
+public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub {
+    static private final String TAG = "SoundTriggerMiddlewareService";
+
+    final ISoundTriggerMiddlewareService mDelegate;
+    final Context mContext;
+    Set<Integer> mModuleHandles;
+
+    /**
+     * Constructor for internal use only. Could be exposed for testing purposes in the future.
+     * Users should access this class via {@link Lifecycle}.
+     */
+    private SoundTriggerMiddlewareService(
+            @NonNull ISoundTriggerMiddlewareService delegate, @NonNull Context context) {
+        mDelegate = delegate;
+        mContext = context;
+    }
+
+    /**
+     * Generic exception handling for exceptions thrown by the underlying implementation.
+     *
+     * Would throw any {@link RecoverableException} as a {@link ServiceSpecificException} (passed
+     * by Binder to the caller) and <i>any other</i> exception as {@link InternalServerError}
+     * (<b>not</b> passed by Binder to the caller).
+     * <p>
+     * Typical usage:
+     * <code><pre>
+     * try {
+     *     ... Do server operations ...
+     * } catch (Exception e) {
+     *     throw handleException(e);
+     * }
+     * </pre></code>
+     */
+    private static @NonNull
+    RuntimeException handleException(@NonNull Exception e) {
+        if (e instanceof RecoverableException) {
+            throw new ServiceSpecificException(((RecoverableException) e).errorCode,
+                    e.getMessage());
+        }
+        throw new InternalServerError(e);
+    }
+
+    @Override
+    public @NonNull
+    SoundTriggerModuleDescriptor[] listModules() {
+        // Permission check.
+        checkPermissions();
+        // Input validation (always valid).
+
+        synchronized (this) {
+            // State validation (always valid).
+
+            // From here on, every exception isn't client's fault.
+            try {
+                SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
+                mModuleHandles = new HashSet<>(result.length);
+                for (SoundTriggerModuleDescriptor desc : result) {
+                    mModuleHandles.add(desc.handle);
+                }
+                return result;
+            } catch (Exception e) {
+                throw handleException(e);
+            }
+        }
+    }
+
+    @Override
+    public @NonNull
+    ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback) {
+        // Permission check.
+        checkPermissions();
+        // Input validation.
+        Preconditions.checkNotNull(callback);
+        Preconditions.checkNotNull(callback.asBinder());
+
+        synchronized (this) {
+            // State validation.
+            if (mModuleHandles == null) {
+                throw new IllegalStateException(
+                        "Client must call listModules() prior to attaching.");
+            }
+            if (!mModuleHandles.contains(handle)) {
+                throw new IllegalArgumentException("Invalid handle: " + handle);
+            }
+
+            // From here on, every exception isn't client's fault.
+            try {
+                ModuleService moduleService = new ModuleService(callback);
+                moduleService.attach(mDelegate.attach(handle, moduleService));
+                return moduleService;
+            } catch (Exception e) {
+                throw handleException(e);
+            }
+        }
+    }
+
+    @Override
+    public void setExternalCaptureState(boolean active) {
+        // Permission check.
+        checkPreemptPermissions();
+        // Input validation (always valid).
+
+        synchronized (this) {
+            // State validation (always valid).
+
+            // From here on, every exception isn't client's fault.
+            try {
+                mDelegate.setExternalCaptureState(active);
+            } catch (Exception e) {
+                throw handleException(e);
+            }
+        }
+    }
+
+    /**
+     * Throws a {@link SecurityException} if caller doesn't have the right permissions to use this
+     * service.
+     */
+    private void checkPermissions() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.RECORD_AUDIO,
+                "Caller must have the android.permission.RECORD_AUDIO permission.");
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD,
+                "Caller must have the android.permission.CAPTURE_AUDIO_HOTWORD permission.");
+    }
+
+    /**
+     * Throws a {@link SecurityException} if caller doesn't have the right permissions to preempt
+     * active sound trigger sessions.
+     */
+    private void checkPreemptPermissions() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.PREEMPT_SOUND_TRIGGER,
+                "Caller must have the android.permission.PREEMPT_SOUND_TRIGGER permission.");
+    }
+
+    /** State of a sound model. */
+    static class ModelState {
+        /** Activity state of a sound model. */
+        enum Activity {
+            /** Model is loaded, recognition is inactive. */
+            LOADED,
+            /** Model is loaded, recognition is active. */
+            ACTIVE
+        }
+
+        /** Activity state. */
+        public Activity activityState = Activity.LOADED;
+
+        /**
+         * A map of known parameter support. A missing key means we don't know yet whether the
+         * parameter is supported. A null value means it is known to not be supported. A non-null
+         * value indicates the valid value range.
+         */
+        private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>();
+
+        /**
+         * Check that the given parameter is known to be supported for this model.
+         *
+         * @param modelParam The parameter key.
+         */
+        public void checkSupported(int modelParam) {
+            if (!parameterSupport.containsKey(modelParam)) {
+                throw new IllegalStateException("Parameter has not been checked for support.");
+            }
+            ModelParameterRange range = parameterSupport.get(modelParam);
+            if (range == null) {
+                throw new IllegalArgumentException("Paramater is not supported.");
+            }
+        }
+
+        /**
+         * Check that the given parameter is known to be supported for this model and that the given
+         * value is a valid value for it.
+         *
+         * @param modelParam The parameter key.
+         * @param value      The value.
+         */
+        public void checkSupported(int modelParam, int value) {
+            if (!parameterSupport.containsKey(modelParam)) {
+                throw new IllegalStateException("Parameter has not been checked for support.");
+            }
+            ModelParameterRange range = parameterSupport.get(modelParam);
+            if (range == null) {
+                throw new IllegalArgumentException("Paramater is not supported.");
+            }
+            Preconditions.checkArgumentInRange(value, range.minInclusive, range.maxInclusive,
+                    "value");
+        }
+
+        /**
+         * Update support state for the given parameter for this model.
+         *
+         * @param modelParam The parameter key.
+         * @param range      The parameter value range, or null if not supported.
+         */
+        public void updateParameterSupport(int modelParam, @Nullable ModelParameterRange range) {
+            parameterSupport.put(modelParam, range);
+        }
+    }
+
+    /**
+     * Entry-point to this module: exposes the module as a {@link SystemService}.
+     */
+    public static final class Lifecycle extends SystemService {
+        private SoundTriggerMiddlewareService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            ISoundTriggerHw[] services;
+            try {
+                services = new ISoundTriggerHw[]{ISoundTriggerHw.getService(true)};
+                Log.d(TAG, "Connected to default ISoundTriggerHw");
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to connect to default ISoundTriggerHw", e);
+                services = new ISoundTriggerHw[0];
+            }
+
+            mService = new SoundTriggerMiddlewareService(
+                    new SoundTriggerMiddlewareImpl(services, new AudioSessionProviderImpl()),
+                    getContext());
+            publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, mService);
+        }
+    }
+
+    /**
+     * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
+     * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
+     */
+    private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback,
+            DeathRecipient {
+        private final ISoundTriggerCallback mCallback;
+        private ISoundTriggerModule mDelegate;
+        private Map<Integer, ModelState> mLoadedModels = new HashMap<>();
+
+        ModuleService(@NonNull ISoundTriggerCallback callback) {
+            mCallback = callback;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+
+        void attach(@NonNull ISoundTriggerModule delegate) {
+            mDelegate = delegate;
+        }
+
+        @Override
+        public int loadModel(@NonNull SoundModel model) {
+            // Permission check.
+            checkPermissions();
+            // Input validation.
+            ValidationUtil.validateGenericModel(model);
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    int handle = mDelegate.loadModel(model);
+                    mLoadedModels.put(handle, new ModelState());
+                    return handle;
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public int loadPhraseModel(@NonNull PhraseSoundModel model) {
+            // Permission check.
+            checkPermissions();
+            // Input validation.
+            ValidationUtil.validatePhraseModel(model);
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    int handle = mDelegate.loadPhraseModel(model);
+                    mLoadedModels.put(handle, new ModelState());
+                    return handle;
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public void unloadModel(int modelHandle) {
+            // Permission check.
+            checkPermissions();
+            // Input validation (always valid).
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+                if (modelState.activityState != ModelState.Activity.LOADED) {
+                    throw new IllegalStateException("Model with handle: " + modelHandle
+                            + " has invalid state for unloading: " + modelState.activityState);
+                }
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    mDelegate.unloadModel(modelHandle);
+                    mLoadedModels.remove(modelHandle);
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
+            // Permission check.
+            checkPermissions();
+            // Input validation.
+            ValidationUtil.validateRecognitionConfig(config);
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+                if (modelState.activityState != ModelState.Activity.LOADED) {
+                    throw new IllegalStateException("Model with handle: " + modelHandle
+                            + " has invalid state for starting recognition: "
+                            + modelState.activityState);
+                }
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    mDelegate.startRecognition(modelHandle, config);
+                    modelState.activityState = ModelState.Activity.ACTIVE;
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public void stopRecognition(int modelHandle) {
+            // Permission check.
+            checkPermissions();
+            // Input validation (always valid).
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+                // stopRecognition is idempotent - no need to check model state.
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    mDelegate.stopRecognition(modelHandle);
+                    modelState.activityState = ModelState.Activity.LOADED;
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public void forceRecognitionEvent(int modelHandle) {
+            // Permission check.
+            checkPermissions();
+            // Input validation (always valid).
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+                // forceRecognitionEvent is idempotent - no need to check model state.
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    mDelegate.forceRecognitionEvent(modelHandle);
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public void setModelParameter(int modelHandle, int modelParam, int value) {
+            // Permission check.
+            checkPermissions();
+            // Input validation.
+            ValidationUtil.validateModelParameter(modelParam);
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+                modelState.checkSupported(modelParam, value);
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    mDelegate.setModelParameter(modelHandle, modelParam, value);
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public int getModelParameter(int modelHandle, int modelParam) {
+            // Permission check.
+            checkPermissions();
+            // Input validation.
+            ValidationUtil.validateModelParameter(modelParam);
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+                modelState.checkSupported(modelParam);
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    return mDelegate.getModelParameter(modelHandle, modelParam);
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        @Nullable
+        public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
+            // Permission check.
+            checkPermissions();
+            // Input validation.
+            ValidationUtil.validateModelParameter(modelParam);
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has been detached.");
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null) {
+                    throw new IllegalStateException("Invalid handle: " + modelHandle);
+                }
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    ModelParameterRange result = mDelegate.queryModelParameterSupport(modelHandle,
+                            modelParam);
+                    modelState.updateParameterSupport(modelParam, result);
+                    return result;
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        @Override
+        public void detach() {
+            // Permission check.
+            checkPermissions();
+            // Input validation (always valid).
+
+            synchronized (this) {
+                // State validation.
+                if (mDelegate == null) {
+                    throw new IllegalStateException("Module has already been detached.");
+                }
+                if (!mLoadedModels.isEmpty()) {
+                    throw new IllegalStateException("Cannot detach while models are loaded.");
+                }
+
+                // From here on, every exception isn't client's fault.
+                try {
+                    detachInternal();
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+
+        private void detachInternal() {
+            try {
+                mDelegate.detach();
+                mDelegate = null;
+                mCallback.asBinder().unlinkToDeath(this, 0);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+
+        ////////////////////////////////////////////////////////////////////////////////////////////
+        // Callbacks
+
+        @Override
+        public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) {
+            synchronized (this) {
+                if (event.status != RecognitionStatus.FORCED) {
+                    mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED;
+                }
+                try {
+                    mCallback.onRecognition(modelHandle, event);
+                } catch (RemoteException e) {
+                    // Dead client will be handled by binderDied() - no need to handle here.
+                    // In any case, client callbacks are considered best effort.
+                    Log.e(TAG, "Client callback execption.", e);
+                }
+            }
+        }
+
+        @Override
+        public void onPhraseRecognition(int modelHandle, @NonNull PhraseRecognitionEvent event) {
+            synchronized (this) {
+                if (event.common.status != RecognitionStatus.FORCED) {
+                    mLoadedModels.get(modelHandle).activityState = ModelState.Activity.LOADED;
+                }
+                try {
+                    mCallback.onPhraseRecognition(modelHandle, event);
+                } catch (RemoteException e) {
+                    // Dead client will be handled by binderDied() - no need to handle here.
+                    // In any case, client callbacks are considered best effort.
+                    Log.e(TAG, "Client callback execption.", e);
+                }
+            }
+        }
+
+        @Override
+        public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
+            synchronized (this) {
+                try {
+                    mCallback.onRecognitionAvailabilityChange(available);
+                } catch (RemoteException e) {
+                    // Dead client will be handled by binderDied() - no need to handle here.
+                    // In any case, client callbacks are considered best effort.
+                    Log.e(TAG, "Client callback execption.", e);
+                }
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            // This is called whenever our client process dies.
+            synchronized (this) {
+                try {
+                    // Gracefully stop all active recognitions and unload the models.
+                    for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
+                        if (entry.getValue().activityState == ModelState.Activity.ACTIVE) {
+                            mDelegate.stopRecognition(entry.getKey());
+                        }
+                        mDelegate.unloadModel(entry.getKey());
+                    }
+                    // Detach.
+                    detachInternal();
+                } catch (Exception e) {
+                    throw handleException(e);
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
new file mode 100644
index 0000000..3444be9
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback;
+import android.hardware.soundtrigger.V2_2.ISoundTriggerHw;
+import android.media.soundtrigger_middleware.ISoundTriggerCallback;
+import android.media.soundtrigger_middleware.ISoundTriggerModule;
+import android.media.soundtrigger_middleware.ModelParameterRange;
+import android.media.soundtrigger_middleware.PhraseSoundModel;
+import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.SoundModel;
+import android.media.soundtrigger_middleware.SoundModelType;
+import android.media.soundtrigger_middleware.SoundTriggerModuleProperties;
+import android.media.soundtrigger_middleware.Status;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This is an implementation of a single module of the ISoundTriggerMiddlewareService interface,
+ * exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate
+ * clients.
+ * <p>
+ * Typical usage is to query the module capabilities using {@link #getProperties()} and then to use
+ * the module through an {@link ISoundTriggerModule} instance, obtained via {@link
+ * #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared
+ * between sessions (i.e. cannot use a handle obtained from one session through another).
+ * <p>
+ * <b>Important conventions:</b>
+ * <ul>
+ * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle
+ * invalid usage, and such usage will result in undefined behavior. If this service is to be
+ * offered to an untrusted client, it must be wrapped with input and state validation.
+ * <li>The underlying driver is assumed to be correct. This implementation does not attempt to
+ * gracefully handle driver malfunction and such behavior will result in undefined behavior. If this
+ * service is to used with an untrusted driver, the driver must be wrapped with validation / error
+ * recovery code.
+ * <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not
+ * considered recoverable faults and should not occur in a properly functioning system.
+ * <li>There is no binder instance associated with this implementation. Do not call asBinder().
+ * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal,
+ * recoverable faults. The error code would one of the
+ * {@link android.media.soundtrigger_middleware.Status} constants. Any other exception
+ * thrown should be regarded as a bug in the implementation or one of its dependencies
+ * (assuming correct usage).
+ * <li>The implementation is designed for testibility by featuring dependency injection (the
+ * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies
+ * on Android runtime.
+ * <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry-
+ * points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance
+ * for their entire scope. Any other method can be assumed to be running with the lock already
+ * obtained, so no further locking should be done. While this is not necessarily the most efficient
+ * synchronization strategy, it is very easy to reason about and this code is likely not on any
+ * performance-critical
+ * path.
+ * </ul>
+ *
+ * @hide
+ */
+class SoundTriggerModule {
+    static private final String TAG = "SoundTriggerModule";
+    @NonNull private final ISoundTriggerHw2 mHalService;
+    @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider;
+    private final Set<Session> mActiveSessions = new HashSet<>();
+    private int mNumLoadedModels = 0;
+    private SoundTriggerModuleProperties mProperties = null;
+    private boolean mRecognitionAvailable;
+
+    /**
+     * Ctor.
+     *
+     * @param halService The underlying HAL driver.
+     */
+    SoundTriggerModule(@NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw halService,
+            @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) {
+        assert halService != null;
+        mHalService = new SoundTriggerHw2Compat(halService);
+        mAudioSessionProvider = audioSessionProvider;
+        mProperties = ConversionUtil.hidl2aidlProperties(mHalService.getProperties());
+
+        // We conservatively assume that external capture is active until explicitly told otherwise.
+        mRecognitionAvailable = mProperties.concurrentCapture;
+    }
+
+    /**
+     * Establish a client session with this module.
+     *
+     * This module may be shared by multiple clients, each will get its own session. While resources
+     * are shared between the clients, each session has its own state and data should not be shared
+     * across sessions.
+     *
+     * @param callback The client callback, which will be used for all messages. This is a oneway
+     *                 callback, so will never block, throw an unchecked exception or return a
+     *                 value.
+     * @return The interface through which this module can be controlled.
+     */
+    synchronized @NonNull
+    Session attach(@NonNull ISoundTriggerCallback callback) {
+        Log.d(TAG, "attach()");
+        Session session = new Session(callback);
+        mActiveSessions.add(session);
+        return session;
+    }
+
+    /**
+     * Query the module's properties.
+     *
+     * @return The properties structure.
+     */
+    synchronized @NonNull
+    SoundTriggerModuleProperties getProperties() {
+        return mProperties;
+    }
+
+    /**
+     * Notify the module that external capture has started / finished, using the same input device
+     * used for recognition.
+     * If the underlying driver does not support recognition while capturing, capture will be
+     * aborted, and the recognition callback will receive and abort event. In addition, all active
+     * clients will be notified of the change in state.
+     *
+     * @param active true iff external capture is active.
+     */
+    synchronized void setExternalCaptureState(boolean active) {
+        Log.d(TAG, String.format("setExternalCaptureState(active=%b)", active));
+        if (mProperties.concurrentCapture) {
+            // If we support concurrent capture, we don't care about any of this.
+            return;
+        }
+        mRecognitionAvailable = !active;
+        if (!mRecognitionAvailable) {
+            // Our module does not support recognition while a capture is active -
+            // need to abort all active recognitions.
+            for (Session session : mActiveSessions) {
+                session.abortActiveRecognitions();
+            }
+        }
+        for (Session session : mActiveSessions) {
+            session.notifyRecognitionAvailability();
+        }
+    }
+
+    /**
+     * Remove session from the list of active sessions.
+     *
+     * @param session The session to remove.
+     */
+    private void removeSession(@NonNull Session session) {
+        mActiveSessions.remove(session);
+    }
+
+    /** State of a single sound model. */
+    private enum ModelState {
+        /** Initial state, until load() is called. */
+        INIT,
+        /** Model is loaded, but recognition is not active. */
+        LOADED,
+        /** Model is loaded and recognition is active. */
+        ACTIVE
+    }
+
+    /**
+     * A single client session with this module.
+     *
+     * This is the main interface used to interact with this module.
+     */
+    private class Session implements ISoundTriggerModule {
+        private ISoundTriggerCallback mCallback;
+        private Map<Integer, Model> mLoadedModels = new HashMap<>();
+
+        /**
+         * Ctor.
+         *
+         * @param callback The client callback interface.
+         */
+        private Session(@NonNull ISoundTriggerCallback callback) {
+            mCallback = callback;
+            notifyRecognitionAvailability();
+        }
+
+        @Override
+        public void detach() {
+            Log.d(TAG, "detach()");
+            synchronized (SoundTriggerModule.this) {
+                removeSession(this);
+            }
+        }
+
+        @Override
+        public int loadModel(@NonNull SoundModel model) {
+            Log.d(TAG, String.format("loadModel(model=%s)", model));
+            synchronized (SoundTriggerModule.this) {
+                if (mNumLoadedModels == mProperties.maxSoundModels) {
+                    throw new RecoverableException(Status.RESOURCE_CONTENTION,
+                            "Maximum number of models loaded.");
+                }
+                Model loadedModel = new Model();
+                int result = loadedModel.load(model);
+                ++mNumLoadedModels;
+                return result;
+            }
+        }
+
+        @Override
+        public int loadPhraseModel(@NonNull PhraseSoundModel model) {
+            Log.d(TAG, String.format("loadPhraseModel(model=%s)", model));
+            synchronized (SoundTriggerModule.this) {
+                if (mNumLoadedModels == mProperties.maxSoundModels) {
+                    throw new RecoverableException(Status.RESOURCE_CONTENTION,
+                            "Maximum number of models loaded.");
+                }
+                Model loadedModel = new Model();
+                int result = loadedModel.load(model);
+                ++mNumLoadedModels;
+                Log.d(TAG, String.format("loadPhraseModel()->%d", result));
+                return result;
+            }
+        }
+
+        @Override
+        public void unloadModel(int modelHandle) {
+            Log.d(TAG, String.format("unloadModel(handle=%d)", modelHandle));
+            synchronized (SoundTriggerModule.this) {
+                mLoadedModels.get(modelHandle).unload();
+                --mNumLoadedModels;
+            }
+        }
+
+        @Override
+        public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
+            Log.d(TAG,
+                    String.format("startRecognition(handle=%d, config=%s)", modelHandle, config));
+            synchronized (SoundTriggerModule.this) {
+                mLoadedModels.get(modelHandle).startRecognition(config);
+            }
+        }
+
+        @Override
+        public void stopRecognition(int modelHandle) {
+            Log.d(TAG, String.format("stopRecognition(handle=%d)", modelHandle));
+            synchronized (SoundTriggerModule.this) {
+                mLoadedModels.get(modelHandle).stopRecognition();
+            }
+        }
+
+        @Override
+        public void forceRecognitionEvent(int modelHandle) {
+            Log.d(TAG, String.format("forceRecognitionEvent(handle=%d)", modelHandle));
+            synchronized (SoundTriggerModule.this) {
+                mLoadedModels.get(modelHandle).forceRecognitionEvent();
+            }
+        }
+
+        @Override
+        public void setModelParameter(int modelHandle, int modelParam, int value)
+                throws RemoteException {
+            Log.d(TAG,
+                    String.format("setModelParameter(handle=%d, param=%d, value=%d)", modelHandle,
+                            modelParam, value));
+            synchronized (SoundTriggerModule.this) {
+                mLoadedModels.get(modelHandle).setParameter(modelParam, value);
+            }
+        }
+
+        @Override
+        public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
+            Log.d(TAG, String.format("getModelParameter(handle=%d, param=%d)", modelHandle,
+                    modelParam));
+            synchronized (SoundTriggerModule.this) {
+                return mLoadedModels.get(modelHandle).getParameter(modelParam);
+            }
+        }
+
+        @Override
+        @Nullable
+        public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
+            Log.d(TAG, String.format("queryModelParameterSupport(handle=%d, param=%d)", modelHandle,
+                    modelParam));
+            synchronized (SoundTriggerModule.this) {
+                return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam);
+            }
+        }
+
+        /**
+         * Abort all currently active recognitions.
+         */
+        private void abortActiveRecognitions() {
+            for (Model model : mLoadedModels.values()) {
+                model.abortActiveRecognition();
+            }
+        }
+
+        private void notifyRecognitionAvailability() {
+            try {
+                mCallback.onRecognitionAvailabilityChange(mRecognitionAvailable);
+            } catch (RemoteException e) {
+                // Dead client will be handled by binderDied() - no need to handle here.
+                // In any case, client callbacks are considered best effort.
+                Log.e(TAG, "Client callback execption.", e);
+            }
+        }
+
+        @Override
+        public @NonNull
+        IBinder asBinder() {
+            throw new UnsupportedOperationException(
+                    "This implementation is not intended to be used directly with Binder.");
+        }
+
+        /**
+         * A single sound model in the system.
+         *
+         * All model-based operations are delegated to this class and implemented here.
+         */
+        private class Model implements ISoundTriggerHw2.Callback {
+            public int mHandle;
+            private ModelState mState = ModelState.INIT;
+            private int mModelType = SoundModelType.UNKNOWN;
+            private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession;
+
+            private @NonNull
+            ModelState getState() {
+                return mState;
+            }
+
+            private void setState(@NonNull ModelState state) {
+                mState = state;
+                SoundTriggerModule.this.notifyAll();
+            }
+
+            private void waitStateChange() throws InterruptedException {
+                SoundTriggerModule.this.wait();
+            }
+
+            private int load(@NonNull SoundModel model) {
+                mModelType = model.type;
+                ISoundTriggerHw.SoundModel hidlModel = ConversionUtil.aidl2hidlSoundModel(model);
+
+                mSession = mAudioSessionProvider.acquireSession();
+                try {
+                    mHandle = mHalService.loadSoundModel(hidlModel, this, 0);
+                } catch (Exception e) {
+                    mAudioSessionProvider.releaseSession(mSession.mSessionHandle);
+                    throw e;
+                }
+
+                setState(ModelState.LOADED);
+                mLoadedModels.put(mHandle, this);
+                return mHandle;
+            }
+
+            private int load(@NonNull PhraseSoundModel model) {
+                mModelType = model.common.type;
+                ISoundTriggerHw.PhraseSoundModel hidlModel =
+                        ConversionUtil.aidl2hidlPhraseSoundModel(model);
+
+                mSession = mAudioSessionProvider.acquireSession();
+                try {
+                    mHandle = mHalService.loadPhraseSoundModel(hidlModel, this, 0);
+                } catch (Exception e) {
+                    mAudioSessionProvider.releaseSession(mSession.mSessionHandle);
+                    throw e;
+                }
+
+                setState(ModelState.LOADED);
+                mLoadedModels.put(mHandle, this);
+                return mHandle;
+            }
+
+            private void unload() {
+                mAudioSessionProvider.releaseSession(mSession.mSessionHandle);
+                mHalService.unloadSoundModel(mHandle);
+                mLoadedModels.remove(mHandle);
+            }
+
+            private void startRecognition(@NonNull RecognitionConfig config) {
+                if (!mRecognitionAvailable) {
+                    // Recognition is unavailable - send an abort event immediately.
+                    notifyAbort();
+                    return;
+                }
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig hidlConfig =
+                        ConversionUtil.aidl2hidlRecognitionConfig(config);
+                hidlConfig.header.captureDevice = mSession.mDeviceHandle;
+                hidlConfig.header.captureHandle = mSession.mIoHandle;
+                mHalService.startRecognition(mHandle, hidlConfig, this, 0);
+                setState(ModelState.ACTIVE);
+            }
+
+            private void stopRecognition() {
+                if (getState() == ModelState.LOADED) {
+                    // This call is idempotent in order to avoid races.
+                    return;
+                }
+                mHalService.stopRecognition(mHandle);
+                setState(ModelState.LOADED);
+            }
+
+            /** Request a forced recognition event. Will do nothing if recognition is inactive. */
+            private void forceRecognitionEvent() {
+                if (getState() != ModelState.ACTIVE) {
+                    // This call is idempotent in order to avoid races.
+                    return;
+                }
+                mHalService.getModelState(mHandle);
+            }
+
+
+            private void setParameter(int modelParam, int value) {
+                mHalService.setModelParameter(mHandle,
+                        ConversionUtil.aidl2hidlModelParameter(modelParam), value);
+            }
+
+            private int getParameter(int modelParam) {
+                return mHalService.getModelParameter(mHandle,
+                        ConversionUtil.aidl2hidlModelParameter(modelParam));
+            }
+
+            @Nullable
+            private ModelParameterRange queryModelParameterSupport(int modelParam) {
+                return ConversionUtil.hidl2aidlModelParameterRange(
+                        mHalService.queryParameter(mHandle,
+                                ConversionUtil.aidl2hidlModelParameter(modelParam)));
+            }
+
+            /** Abort the recognition, if active. */
+            private void abortActiveRecognition() {
+                // If we're inactive, do nothing.
+                if (getState() != ModelState.ACTIVE) {
+                    return;
+                }
+                // Stop recognition.
+                stopRecognition();
+
+                // Notify the client that recognition has been aborted.
+                notifyAbort();
+            }
+
+            /** Notify the client that recognition has been aborted. */
+            private void notifyAbort() {
+                try {
+                    switch (mModelType) {
+                        case SoundModelType.GENERIC: {
+                            android.media.soundtrigger_middleware.RecognitionEvent event =
+                                    new android.media.soundtrigger_middleware.RecognitionEvent();
+                            event.status =
+                                    android.media.soundtrigger_middleware.RecognitionStatus.ABORTED;
+                            mCallback.onRecognition(mHandle, event);
+                        }
+                        break;
+
+                        case SoundModelType.KEYPHRASE: {
+                            android.media.soundtrigger_middleware.PhraseRecognitionEvent event =
+                                    new android.media.soundtrigger_middleware.PhraseRecognitionEvent();
+                            event.common =
+                                    new android.media.soundtrigger_middleware.RecognitionEvent();
+                            event.common.status =
+                                    android.media.soundtrigger_middleware.RecognitionStatus.ABORTED;
+                            mCallback.onPhraseRecognition(mHandle, event);
+                        }
+                        break;
+
+                        default:
+                            Log.e(TAG, "Unknown model type: " + mModelType);
+
+                    }
+                } catch (RemoteException e) {
+                    // Dead client will be handled by binderDied() - no need to handle here.
+                    // In any case, client callbacks are considered best effort.
+                    Log.e(TAG, "Client callback execption.", e);
+                }
+            }
+
+            @Override
+            public void recognitionCallback(
+                    @NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent,
+                    int cookie) {
+                Log.d(TAG, String.format("recognitionCallback_2_1(event=%s, cookie=%d)",
+                        recognitionEvent, cookie));
+                synchronized (SoundTriggerModule.this) {
+                    android.media.soundtrigger_middleware.RecognitionEvent aidlEvent =
+                            ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent);
+                    aidlEvent.captureSession = mSession.mSessionHandle;
+                    try {
+                        mCallback.onRecognition(mHandle, aidlEvent);
+                    } catch (RemoteException e) {
+                        // Dead client will be handled by binderDied() - no need to handle here.
+                        // In any case, client callbacks are considered best effort.
+                        Log.e(TAG, "Client callback execption.", e);
+                    }
+                    if (aidlEvent.status
+                            != android.media.soundtrigger_middleware.RecognitionStatus.FORCED) {
+                        setState(ModelState.LOADED);
+                    }
+                }
+            }
+
+            @Override
+            public void phraseRecognitionCallback(
+                    @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent,
+                    int cookie) {
+                Log.d(TAG, String.format("phraseRecognitionCallback_2_1(event=%s, cookie=%d)",
+                        phraseRecognitionEvent, cookie));
+                synchronized (SoundTriggerModule.this) {
+                    android.media.soundtrigger_middleware.PhraseRecognitionEvent aidlEvent =
+                            ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent);
+                    aidlEvent.common.captureSession = mSession.mSessionHandle;
+                    try {
+                        mCallback.onPhraseRecognition(mHandle, aidlEvent);
+                    } catch (RemoteException e) {
+                        // Dead client will be handled by binderDied() - no need to handle here.
+                        // In any case, client callbacks are considered best effort.
+                        Log.e(TAG, "Client callback execption.", e);
+                    }
+                    if (aidlEvent.common.status
+                            != android.media.soundtrigger_middleware.RecognitionStatus.FORCED) {
+                        setState(ModelState.LOADED);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING b/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING
new file mode 100644
index 0000000..9ed894b
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.soundtrigger_middleware"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java
new file mode 100644
index 0000000..80f69d0
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/UuidUtil.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for representing UUIDs as strings.
+ *
+ * @hide
+ */
+public class UuidUtil {
+    /**
+     * Regex pattern that can be used to validate / extract the various fields of a string-formatted
+     * UUID.
+     */
+    static final Pattern PATTERN = Pattern.compile("^([a-fA-F0-9]{8})-" +
+            "([a-fA-F0-9]{4})-" +
+            "([a-fA-F0-9]{4})-" +
+            "([a-fA-F0-9]{4})-" +
+            "([a-fA-F0-9]{2})" +
+            "([a-fA-F0-9]{2})" +
+            "([a-fA-F0-9]{2})" +
+            "([a-fA-F0-9]{2})" +
+            "([a-fA-F0-9]{2})" +
+            "([a-fA-F0-9]{2})$");
+
+    /** Printf-style pattern for formatting the various fields of a UUID as a string. */
+    static final String FORMAT = "%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x";
+}
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java
new file mode 100644
index 0000000..4898e6b
--- /dev/null
+++ b/services/core/java/com/android/server/soundtrigger_middleware/ValidationUtil.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import android.annotation.Nullable;
+import android.media.soundtrigger_middleware.ConfidenceLevel;
+import android.media.soundtrigger_middleware.ModelParameter;
+import android.media.soundtrigger_middleware.Phrase;
+import android.media.soundtrigger_middleware.PhraseRecognitionExtra;
+import android.media.soundtrigger_middleware.PhraseSoundModel;
+import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.RecognitionMode;
+import android.media.soundtrigger_middleware.SoundModel;
+import android.media.soundtrigger_middleware.SoundModelType;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for asserting the validity of various data types used by this module.
+ * Each of the methods below would throw an {@link IllegalArgumentException} if its input is
+ * invalid. The input's validity is determined irrespective of any context. In cases where the valid
+ * value space is further limited by state, it is the caller's responsibility to assert.
+ *
+ * @hide
+ */
+public class ValidationUtil {
+    static void validateUuid(@Nullable String uuid) {
+        Preconditions.checkNotNull(uuid);
+        Matcher matcher = UuidUtil.PATTERN.matcher(uuid);
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException(
+                    "Illegal format for UUID: " + uuid);
+        }
+    }
+
+    static void validateGenericModel(@Nullable SoundModel model) {
+        validateModel(model, SoundModelType.GENERIC);
+    }
+
+    static void validateModel(@Nullable SoundModel model, int expectedType) {
+        Preconditions.checkNotNull(model);
+        if (model.type != expectedType) {
+            throw new IllegalArgumentException("Invalid type");
+        }
+        validateUuid(model.uuid);
+        validateUuid(model.vendorUuid);
+        Preconditions.checkNotNull(model.data);
+    }
+
+    static void validatePhraseModel(@Nullable PhraseSoundModel model) {
+        Preconditions.checkNotNull(model);
+        validateModel(model.common, SoundModelType.KEYPHRASE);
+        Preconditions.checkNotNull(model.phrases);
+        for (Phrase phrase : model.phrases) {
+            Preconditions.checkNotNull(phrase);
+            if ((phrase.recognitionModes & ~(RecognitionMode.VOICE_TRIGGER
+                    | RecognitionMode.USER_IDENTIFICATION | RecognitionMode.USER_AUTHENTICATION
+                    | RecognitionMode.GENERIC_TRIGGER)) != 0) {
+                throw new IllegalArgumentException("Invalid recognitionModes");
+            }
+            Preconditions.checkNotNull(phrase.users);
+            Preconditions.checkNotNull(phrase.locale);
+            Preconditions.checkNotNull(phrase.text);
+        }
+    }
+
+    static void validateRecognitionConfig(@Nullable RecognitionConfig config) {
+        Preconditions.checkNotNull(config);
+        Preconditions.checkNotNull(config.phraseRecognitionExtras);
+        for (PhraseRecognitionExtra extra : config.phraseRecognitionExtras) {
+            Preconditions.checkNotNull(extra);
+            if ((extra.recognitionModes & ~(RecognitionMode.VOICE_TRIGGER
+                    | RecognitionMode.USER_IDENTIFICATION | RecognitionMode.USER_AUTHENTICATION
+                    | RecognitionMode.GENERIC_TRIGGER)) != 0) {
+                throw new IllegalArgumentException("Invalid recognitionModes");
+            }
+            if (extra.confidenceLevel < 0 || extra.confidenceLevel > 100) {
+                throw new IllegalArgumentException("Invalid confidenceLevel");
+            }
+            Preconditions.checkNotNull(extra.levels);
+            for (ConfidenceLevel level : extra.levels) {
+                Preconditions.checkNotNull(level);
+                if (level.levelPercent < 0 || level.levelPercent > 100) {
+                    throw new IllegalArgumentException("Invalid confidenceLevel");
+                }
+            }
+        }
+        Preconditions.checkNotNull(config.data);
+    }
+
+    static void validateModelParameter(int modelParam) {
+        switch (modelParam) {
+            case ModelParameter.THRESHOLD_FACTOR:
+                return;
+
+            default:
+                throw new IllegalArgumentException("Invalid model parameter");
+        }
+    }
+}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index fd8094c..a34b7fd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -35,6 +35,7 @@
         "com_android_server_power_PowerManagerService.cpp",
         "com_android_server_security_VerityUtils.cpp",
         "com_android_server_SerialService.cpp",
+        "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp",
         "com_android_server_storage_AppFuseBridge.cpp",
         "com_android_server_SystemServer.cpp",
         "com_android_server_TestNetworkService.cpp",
diff --git a/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp b/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp
new file mode 100644
index 0000000..774534f
--- /dev/null
+++ b/services/core/jni/com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sstream>
+
+#include "core_jni_helpers.h"
+#include <media/AudioSystem.h>
+
+namespace android {
+
+namespace {
+
+#define PACKAGE "com/android/server/soundtrigger_middleware"
+#define CLASSNAME PACKAGE "/AudioSessionProviderImpl"
+#define SESSION_CLASSNAME PACKAGE "/SoundTriggerMiddlewareImpl$AudioSessionProvider$AudioSession"
+
+jobject acquireAudioSession(
+        JNIEnv* env,
+        jobject clazz) {
+
+    audio_session_t session;
+    audio_io_handle_t ioHandle;
+    audio_devices_t device;
+
+    status_t status = AudioSystem::acquireSoundTriggerSession(&session,
+                                                              &ioHandle,
+                                                              &device);
+    if (status != 0) {
+        std::ostringstream message;
+        message
+                << "AudioSystem::acquireSoundTriggerSession returned an error code: "
+                << status;
+        env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"),
+                      message.str().c_str());
+        return nullptr;
+    }
+
+    jclass cls = FindClassOrDie(env, SESSION_CLASSNAME);
+    jmethodID ctor = GetMethodIDOrDie(env, cls, "<init>", "(III)V");
+    return env->NewObject(cls,
+                          ctor,
+                          static_cast<int>(session),
+                          static_cast<int>(ioHandle),
+                          static_cast<int>(device));
+}
+
+void releaseAudioSession(JNIEnv* env, jobject clazz, jint handle) {
+    status_t status =
+            AudioSystem::releaseSoundTriggerSession(static_cast<audio_session_t>(handle));
+
+    if (status != 0) {
+        std::ostringstream message;
+        message
+                << "AudioSystem::releaseAudioSystemSession returned an error code: "
+                << status;
+        env->ThrowNew(FindClassOrDie(env, "java/lang/RuntimeException"),
+                      message.str().c_str());
+    }
+}
+
+const JNINativeMethod g_methods[] = {
+        {"acquireSession", "()L" SESSION_CLASSNAME ";",
+         reinterpret_cast<void*>(acquireAudioSession)},
+        {"releaseSession", "(I)V",
+         reinterpret_cast<void*>(releaseAudioSession)},
+};
+
+}  // namespace
+
+int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(
+        JNIEnv* env) {
+    return RegisterMethodsOrDie(env,
+                                CLASSNAME,
+                                g_methods,
+                                NELEM(g_methods));
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 692c9d2..165edf1 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -56,6 +56,8 @@
 int register_android_server_security_VerityUtils(JNIEnv* env);
 int register_android_server_am_AppCompactor(JNIEnv* env);
 int register_android_server_am_LowMemDetector(JNIEnv* env);
+int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(
+        JNIEnv* env);
 };
 
 using namespace android;
@@ -105,5 +107,7 @@
     register_android_server_security_VerityUtils(env);
     register_android_server_am_AppCompactor(env);
     register_android_server_am_LowMemDetector(env);
+    register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(
+            env);
     return JNI_VERSION_1_4;
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index b6e501a..7c43972 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -147,6 +147,7 @@
 import com.android.server.security.KeyChainSystemService;
 import com.android.server.signedconfig.SignedConfigService;
 import com.android.server.soundtrigger.SoundTriggerService;
+import com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService;
 import com.android.server.statusbar.StatusBarManagerService;
 import com.android.server.storage.DeviceStorageMonitorService;
 import com.android.server.telecom.TelecomLoaderService;
@@ -1544,6 +1545,10 @@
             }
             t.traceEnd();
 
+            t.traceBegin("StartSoundTriggerMiddlewareService");
+            mSystemServiceManager.startService(SoundTriggerMiddlewareService.Lifecycle.class);
+            t.traceEnd();
+
             if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_BROADCAST_RADIO)) {
                 t.traceBegin("StartBroadcastRadioService");
                 mSystemServiceManager.startService(BroadcastRadioService.class);
diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java
new file mode 100644
index 0000000..5a2ce45
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/ConversionUtilTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import static org.junit.Assert.assertEquals;
+
+import android.hardware.audio.common.V2_0.Uuid;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ConversionUtilTest {
+    private static final String TAG = "ConversionUtilTest";
+
+    @Test
+    public void testUuidRoundTrip() {
+        Uuid hidl = new Uuid();
+        hidl.timeLow = 0xFEDCBA98;
+        hidl.timeMid = (short) 0xEDCB;
+        hidl.versionAndTimeHigh = (short) 0xDCBA;
+        hidl.variantAndClockSeqHigh = (short) 0xCBA9;
+        hidl.node = new byte[] { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 };
+
+        String aidl = ConversionUtil.hidl2aidlUuid(hidl);
+        assertEquals("fedcba98-edcb-dcba-cba9-111213141516", aidl);
+
+        Uuid reconstructed = ConversionUtil.aidl2hidlUuid(aidl);
+        assertEquals(hidl, reconstructed);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java
new file mode 100644
index 0000000..82f32f8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java
@@ -0,0 +1,1306 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.soundtrigger_middleware;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.audio.common.V2_0.AudioConfig;
+import android.hardware.audio.common.V2_0.Uuid;
+import android.hardware.soundtrigger.V2_3.OptionalModelParameterRange;
+import android.media.audio.common.AudioChannelMask;
+import android.media.audio.common.AudioFormat;
+import android.media.soundtrigger_middleware.ConfidenceLevel;
+import android.media.soundtrigger_middleware.ISoundTriggerCallback;
+import android.media.soundtrigger_middleware.ISoundTriggerModule;
+import android.media.soundtrigger_middleware.ModelParameter;
+import android.media.soundtrigger_middleware.ModelParameterRange;
+import android.media.soundtrigger_middleware.Phrase;
+import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
+import android.media.soundtrigger_middleware.PhraseRecognitionExtra;
+import android.media.soundtrigger_middleware.PhraseSoundModel;
+import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.RecognitionEvent;
+import android.media.soundtrigger_middleware.RecognitionMode;
+import android.media.soundtrigger_middleware.RecognitionStatus;
+import android.media.soundtrigger_middleware.SoundModel;
+import android.media.soundtrigger_middleware.SoundModelType;
+import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
+import android.media.soundtrigger_middleware.SoundTriggerModuleProperties;
+import android.os.HidlMemoryUtil;
+import android.os.HwParcel;
+import android.os.IHwBinder;
+import android.os.IHwInterface;
+import android.os.RemoteException;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+@RunWith(Parameterized.class)
+public class SoundTriggerMiddlewareImplTest {
+    private static final String TAG = "SoundTriggerMiddlewareImplTest";
+
+    // We run the test once for every version of the underlying driver.
+    @Parameterized.Parameters
+    public static Object[] data() {
+        return new Object[]{
+                mock(android.hardware.soundtrigger.V2_0.ISoundTriggerHw.class),
+                mock(android.hardware.soundtrigger.V2_1.ISoundTriggerHw.class),
+                mock(android.hardware.soundtrigger.V2_2.ISoundTriggerHw.class),
+                mock(android.hardware.soundtrigger.V2_3.ISoundTriggerHw.class),
+        };
+    }
+
+    @Mock
+    @Parameterized.Parameter
+    public android.hardware.soundtrigger.V2_0.ISoundTriggerHw mHalDriver;
+
+    @Mock
+    private SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider = mock(
+            SoundTriggerMiddlewareImpl.AudioSessionProvider.class);
+
+    private SoundTriggerMiddlewareImpl mService;
+
+    private static ISoundTriggerCallback createCallbackMock() {
+        return mock(ISoundTriggerCallback.Stub.class, Mockito.CALLS_REAL_METHODS);
+    }
+
+    private static SoundModel createGenericSoundModel() {
+        return createSoundModel(SoundModelType.GENERIC);
+    }
+
+    private static SoundModel createSoundModel(int type) {
+        SoundModel model = new SoundModel();
+        model.type = type;
+        model.uuid = "12345678-2345-3456-4567-abcdef987654";
+        model.vendorUuid = "87654321-5432-6543-7654-456789fedcba";
+        model.data = new byte[]{91, 92, 93, 94, 95};
+        return model;
+    }
+
+    private static PhraseSoundModel createPhraseSoundModel() {
+        PhraseSoundModel model = new PhraseSoundModel();
+        model.common = createSoundModel(SoundModelType.KEYPHRASE);
+        model.phrases = new Phrase[1];
+        model.phrases[0] = new Phrase();
+        model.phrases[0].id = 123;
+        model.phrases[0].users = new int[]{5, 6, 7};
+        model.phrases[0].locale = "locale";
+        model.phrases[0].text = "text";
+        model.phrases[0].recognitionModes =
+                RecognitionMode.USER_AUTHENTICATION | RecognitionMode.USER_IDENTIFICATION;
+        return model;
+    }
+
+    private static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties createDefaultProperties(
+            boolean supportConcurrentCapture) {
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties =
+                new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties();
+        properties.implementor = "implementor";
+        properties.description = "description";
+        properties.version = 123;
+        properties.uuid = new Uuid();
+        properties.uuid.timeLow = 1;
+        properties.uuid.timeMid = 2;
+        properties.uuid.versionAndTimeHigh = 3;
+        properties.uuid.variantAndClockSeqHigh = 4;
+        properties.uuid.node = new byte[]{5, 6, 7, 8, 9, 10};
+
+        properties.maxSoundModels = 456;
+        properties.maxKeyPhrases = 567;
+        properties.maxUsers = 678;
+        properties.recognitionModes = 789;
+        properties.captureTransition = true;
+        properties.maxBufferMs = 321;
+        properties.concurrentCapture = supportConcurrentCapture;
+        properties.triggerInEvent = true;
+        properties.powerConsumptionMw = 432;
+        return properties;
+    }
+
+    private static void validateDefaultProperties(SoundTriggerModuleProperties properties,
+            boolean supportConcurrentCapture) {
+        assertEquals("implementor", properties.implementor);
+        assertEquals("description", properties.description);
+        assertEquals(123, properties.version);
+        assertEquals("00000001-0002-0003-0004-05060708090a", properties.uuid);
+        assertEquals(456, properties.maxSoundModels);
+        assertEquals(567, properties.maxKeyPhrases);
+        assertEquals(678, properties.maxUsers);
+        assertEquals(789, properties.recognitionModes);
+        assertTrue(properties.captureTransition);
+        assertEquals(321, properties.maxBufferMs);
+        assertEquals(supportConcurrentCapture, properties.concurrentCapture);
+        assertTrue(properties.triggerInEvent);
+        assertEquals(432, properties.powerConsumptionMw);
+    }
+
+
+    private static android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent createRecognitionEvent_2_0(
+            int hwHandle,
+            int status) {
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent halEvent =
+                new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent();
+        halEvent.status = status;
+        halEvent.type = SoundModelType.GENERIC;
+        halEvent.model = hwHandle;
+        halEvent.captureAvailable = true;
+        // This field is ignored.
+        halEvent.captureSession = 123;
+        halEvent.captureDelayMs = 234;
+        halEvent.capturePreambleMs = 345;
+        halEvent.triggerInData = true;
+        halEvent.audioConfig = new AudioConfig();
+        halEvent.audioConfig.sampleRateHz = 456;
+        halEvent.audioConfig.channelMask = AudioChannelMask.IN_LEFT;
+        halEvent.audioConfig.format = AudioFormat.MP3;
+        // hwEvent.audioConfig.offloadInfo is irrelevant.
+        halEvent.data.add((byte) 31);
+        halEvent.data.add((byte) 32);
+        halEvent.data.add((byte) 33);
+        return halEvent;
+    }
+
+    private static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent createRecognitionEvent_2_1(
+            int hwHandle,
+            int status) {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent halEvent =
+                new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent();
+        halEvent.header = createRecognitionEvent_2_0(hwHandle, status);
+        halEvent.header.data.clear();
+        halEvent.data = HidlMemoryUtil.byteArrayToHidlMemory(new byte[]{31, 32, 33});
+        return halEvent;
+    }
+
+    private static void validateRecognitionEvent(RecognitionEvent event, int status) {
+        assertEquals(status, event.status);
+        assertEquals(SoundModelType.GENERIC, event.type);
+        assertTrue(event.captureAvailable);
+        assertEquals(101, event.captureSession);
+        assertEquals(234, event.captureDelayMs);
+        assertEquals(345, event.capturePreambleMs);
+        assertTrue(event.triggerInData);
+        assertEquals(456, event.audioConfig.sampleRateHz);
+        assertEquals(AudioChannelMask.IN_LEFT, event.audioConfig.channelMask);
+        assertEquals(AudioFormat.MP3, event.audioConfig.format);
+    }
+
+    private static android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent createPhraseRecognitionEvent_2_0(
+            int hwHandle, int status) {
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent halEvent =
+                new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent();
+        halEvent.common = createRecognitionEvent_2_0(hwHandle, status);
+
+        android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halExtra =
+                new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra();
+        halExtra.id = 123;
+        halExtra.confidenceLevel = 52;
+        halExtra.recognitionModes = android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER
+                | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER;
+        android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel =
+                new android.hardware.soundtrigger.V2_0.ConfidenceLevel();
+        halLevel.userId = 31;
+        halLevel.levelPercent = 43;
+        halExtra.levels.add(halLevel);
+        halEvent.phraseExtras.add(halExtra);
+        return halEvent;
+    }
+
+    private static android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent createPhraseRecognitionEvent_2_1(
+            int hwHandle, int status) {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent halEvent =
+                new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent();
+        halEvent.common = createRecognitionEvent_2_1(hwHandle, status);
+
+        android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halExtra =
+                new android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra();
+        halExtra.id = 123;
+        halExtra.confidenceLevel = 52;
+        halExtra.recognitionModes = android.hardware.soundtrigger.V2_0.RecognitionMode.VOICE_TRIGGER
+                | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER;
+        android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel =
+                new android.hardware.soundtrigger.V2_0.ConfidenceLevel();
+        halLevel.userId = 31;
+        halLevel.levelPercent = 43;
+        halExtra.levels.add(halLevel);
+        halEvent.phraseExtras.add(halExtra);
+        return halEvent;
+    }
+
+    private static void validatePhraseRecognitionEvent(PhraseRecognitionEvent event, int status) {
+        validateRecognitionEvent(event.common, status);
+
+        assertEquals(1, event.phraseExtras.length);
+        assertEquals(123, event.phraseExtras[0].id);
+        assertEquals(52, event.phraseExtras[0].confidenceLevel);
+        assertEquals(RecognitionMode.VOICE_TRIGGER | RecognitionMode.GENERIC_TRIGGER,
+                event.phraseExtras[0].recognitionModes);
+        assertEquals(1, event.phraseExtras[0].levels.length);
+        assertEquals(31, event.phraseExtras[0].levels[0].userId);
+        assertEquals(43, event.phraseExtras[0].levels[0].levelPercent);
+    }
+
+    private void initService(boolean supportConcurrentCapture) throws RemoteException {
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties =
+                    createDefaultProperties(
+                            supportConcurrentCapture);
+            ((android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback) invocation.getArgument(
+                    0)).onValues(0,
+                    properties);
+            return null;
+        }).when(mHalDriver).getProperties(any());
+        mService = new SoundTriggerMiddlewareImpl(mHalDriver, mAudioSessionProvider);
+    }
+
+    private int loadGenericModel_2_0(ISoundTriggerModule module, int hwHandle)
+            throws RemoteException {
+        SoundModel model = createGenericSoundModel();
+        ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel> modelCaptor =
+                ArgumentCaptor.forClass(
+                        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel.class);
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback =
+                    invocation.getArgument(1);
+            int callbackCookie = invocation.getArgument(2);
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHw.loadSoundModelCallback
+                    resultCallback = invocation.getArgument(3);
+
+            // This is the return of this method.
+            resultCallback.onValues(0, hwHandle);
+
+            // This is the async mCallback that comes after.
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent modelEvent =
+                    new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent();
+            modelEvent.status =
+                    android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.SoundModelStatus.UPDATED;
+            modelEvent.model = hwHandle;
+            callback.soundModelCallback(modelEvent, callbackCookie);
+            return null;
+        }).when(mHalDriver).loadSoundModel(modelCaptor.capture(), any(), anyInt(), any());
+
+        when(mAudioSessionProvider.acquireSession()).thenReturn(
+                new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103));
+
+        int handle = module.loadModel(model);
+        verify(mHalDriver).loadSoundModel(any(), any(), anyInt(), any());
+        verify(mAudioSessionProvider).acquireSession();
+
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel hidlModel =
+                modelCaptor.getValue();
+        assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC,
+                hidlModel.type);
+        assertEquals(model.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.uuid));
+        assertEquals(model.vendorUuid, ConversionUtil.hidl2aidlUuid(hidlModel.vendorUuid));
+        assertArrayEquals(new Byte[]{91, 92, 93, 94, 95}, hidlModel.data.toArray());
+
+        return handle;
+    }
+
+    private int loadGenericModel_2_1(ISoundTriggerModule module, int hwHandle)
+            throws RemoteException {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver;
+        SoundModel model = createGenericSoundModel();
+        ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel> modelCaptor =
+                ArgumentCaptor.forClass(
+                        android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel.class);
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback callback =
+                    invocation.getArgument(1);
+            int callbackCookie = invocation.getArgument(2);
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadSoundModel_2_1Callback
+                    resultCallback = invocation.getArgument(3);
+
+            // This is the return of this method.
+            resultCallback.onValues(0, hwHandle);
+
+            // This is the async mCallback that comes after.
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent modelEvent =
+                    new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent();
+            modelEvent.header.status =
+                    android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.SoundModelStatus.UPDATED;
+            modelEvent.header.model = hwHandle;
+            callback.soundModelCallback_2_1(modelEvent, callbackCookie);
+            return null;
+        }).when(driver).loadSoundModel_2_1(modelCaptor.capture(), any(), anyInt(), any());
+
+        when(mAudioSessionProvider.acquireSession()).thenReturn(
+                new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103));
+
+        int handle = module.loadModel(model);
+        verify(driver).loadSoundModel_2_1(any(), any(), anyInt(), any());
+        verify(mAudioSessionProvider).acquireSession();
+
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel hidlModel =
+                modelCaptor.getValue();
+        assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.GENERIC,
+                hidlModel.header.type);
+        assertEquals(model.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.header.uuid));
+        assertEquals(model.vendorUuid, ConversionUtil.hidl2aidlUuid(hidlModel.header.vendorUuid));
+        assertArrayEquals(new byte[]{91, 92, 93, 94, 95},
+                HidlMemoryUtil.hidlMemoryToByteArray(hidlModel.data));
+
+        return handle;
+    }
+
+    private int loadGenericModel(ISoundTriggerModule module, int hwHandle) throws RemoteException {
+        if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) {
+            return loadGenericModel_2_1(module, hwHandle);
+        } else {
+            return loadGenericModel_2_0(module, hwHandle);
+        }
+    }
+
+    private int loadPhraseModel_2_0(ISoundTriggerModule module, int hwHandle)
+            throws RemoteException {
+        PhraseSoundModel model = createPhraseSoundModel();
+        ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel>
+                modelCaptor = ArgumentCaptor.forClass(
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel.class);
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback =
+                    invocation.getArgument(
+                            1);
+            int callbackCookie = invocation.getArgument(2);
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHw.loadPhraseSoundModelCallback
+                    resultCallback =
+                    invocation.getArgument(
+                            3);
+
+            // This is the return of this method.
+            resultCallback.onValues(0, hwHandle);
+
+            // This is the async mCallback that comes after.
+            android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent modelEvent =
+                    new android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent();
+            modelEvent.status =
+                    android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.SoundModelStatus.UPDATED;
+            modelEvent.model = hwHandle;
+            callback.soundModelCallback(modelEvent, callbackCookie);
+            return null;
+        }).when(mHalDriver).loadPhraseSoundModel(modelCaptor.capture(), any(), anyInt(), any());
+
+        when(mAudioSessionProvider.acquireSession()).thenReturn(
+                new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103));
+
+        int handle = module.loadPhraseModel(model);
+        verify(mHalDriver).loadPhraseSoundModel(any(), any(), anyInt(), any());
+        verify(mAudioSessionProvider).acquireSession();
+
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel hidlModel =
+                modelCaptor.getValue();
+
+        // Validate common part.
+        assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE,
+                hidlModel.common.type);
+        assertEquals(model.common.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.common.uuid));
+        assertEquals(model.common.vendorUuid,
+                ConversionUtil.hidl2aidlUuid(hidlModel.common.vendorUuid));
+        assertArrayEquals(new Byte[]{91, 92, 93, 94, 95}, hidlModel.common.data.toArray());
+
+        // Validate phrase part.
+        assertEquals(1, hidlModel.phrases.size());
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Phrase hidlPhrase =
+                hidlModel.phrases.get(0);
+        assertEquals(123, hidlPhrase.id);
+        assertArrayEquals(new Integer[]{5, 6, 7}, hidlPhrase.users.toArray());
+        assertEquals("locale", hidlPhrase.locale);
+        assertEquals("text", hidlPhrase.text);
+        assertEquals(android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION
+                        | android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION,
+                hidlPhrase.recognitionModes);
+
+        return handle;
+    }
+
+    private int loadPhraseModel_2_1(ISoundTriggerModule module, int hwHandle)
+            throws RemoteException {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver;
+
+        PhraseSoundModel model = createPhraseSoundModel();
+        ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel>
+                modelCaptor = ArgumentCaptor.forClass(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel.class);
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback callback =
+                    invocation.getArgument(
+                            1);
+            int callbackCookie = invocation.getArgument(2);
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHw.loadPhraseSoundModel_2_1Callback
+                    resultCallback =
+                    invocation.getArgument(
+                            3);
+
+            // This is the return of this method.
+            resultCallback.onValues(0, hwHandle);
+
+            // This is the async mCallback that comes after.
+            android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent modelEvent =
+                    new android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent();
+            modelEvent.header.status =
+                    android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.SoundModelStatus.UPDATED;
+            modelEvent.header.model = hwHandle;
+            callback.soundModelCallback_2_1(modelEvent, callbackCookie);
+            return null;
+        }).when(driver).loadPhraseSoundModel_2_1(modelCaptor.capture(), any(), anyInt(), any());
+
+        when(mAudioSessionProvider.acquireSession()).thenReturn(
+                new SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession(101, 102, 103));
+
+        int handle = module.loadPhraseModel(model);
+        verify(driver).loadPhraseSoundModel_2_1(any(), any(), anyInt(), any());
+        verify(mAudioSessionProvider).acquireSession();
+
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel hidlModel =
+                modelCaptor.getValue();
+
+        // Validate common part.
+        assertEquals(android.hardware.soundtrigger.V2_0.SoundModelType.KEYPHRASE,
+                hidlModel.common.header.type);
+        assertEquals(model.common.uuid, ConversionUtil.hidl2aidlUuid(hidlModel.common.header.uuid));
+        assertEquals(model.common.vendorUuid,
+                ConversionUtil.hidl2aidlUuid(hidlModel.common.header.vendorUuid));
+        assertArrayEquals(new byte[]{91, 92, 93, 94, 95},
+                HidlMemoryUtil.hidlMemoryToByteArray(hidlModel.common.data));
+
+        // Validate phrase part.
+        assertEquals(1, hidlModel.phrases.size());
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw.Phrase hidlPhrase =
+                hidlModel.phrases.get(0);
+        assertEquals(123, hidlPhrase.id);
+        assertArrayEquals(new Integer[]{5, 6, 7}, hidlPhrase.users.toArray());
+        assertEquals("locale", hidlPhrase.locale);
+        assertEquals("text", hidlPhrase.text);
+        assertEquals(android.hardware.soundtrigger.V2_0.RecognitionMode.USER_AUTHENTICATION
+                        | android.hardware.soundtrigger.V2_0.RecognitionMode.USER_IDENTIFICATION,
+                hidlPhrase.recognitionModes);
+
+        return handle;
+    }
+
+    private int loadPhraseModel(ISoundTriggerModule module, int hwHandle) throws RemoteException {
+        if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) {
+            return loadPhraseModel_2_1(module, hwHandle);
+        } else {
+            return loadPhraseModel_2_0(module, hwHandle);
+        }
+    }
+
+    private void unloadModel(ISoundTriggerModule module, int handle, int hwHandle)
+            throws RemoteException {
+        module.unloadModel(handle);
+        verify(mHalDriver).unloadSoundModel(hwHandle);
+        verify(mAudioSessionProvider).releaseSession(101);
+    }
+
+    private SoundTriggerHwCallback startRecognition_2_0(ISoundTriggerModule module, int handle,
+            int hwHandle) throws RemoteException {
+        ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig>
+                configCaptor = ArgumentCaptor.forClass(
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig.class);
+        ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback> callbackCaptor =
+                ArgumentCaptor.forClass(
+                        android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.class);
+        ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        when(mHalDriver.startRecognition(eq(hwHandle), configCaptor.capture(),
+                callbackCaptor.capture(), cookieCaptor.capture())).thenReturn(0);
+
+        RecognitionConfig config = createRecognitionConfig();
+
+        module.startRecognition(handle, config);
+        verify(mHalDriver).startRecognition(eq(hwHandle), any(), any(), anyInt());
+
+        android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig halConfig =
+                configCaptor.getValue();
+        assertTrue(halConfig.captureRequested);
+        assertEquals(102, halConfig.captureHandle);
+        assertEquals(103, halConfig.captureDevice);
+        assertEquals(1, halConfig.phrases.size());
+        android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halPhraseExtra =
+                halConfig.phrases.get(0);
+        assertEquals(123, halPhraseExtra.id);
+        assertEquals(4, halPhraseExtra.confidenceLevel);
+        assertEquals(5, halPhraseExtra.recognitionModes);
+        assertEquals(1, halPhraseExtra.levels.size());
+        android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = halPhraseExtra.levels.get(0);
+        assertEquals(234, halLevel.userId);
+        assertEquals(34, halLevel.levelPercent);
+        assertArrayEquals(new Byte[]{5, 4, 3, 2, 1}, halConfig.data.toArray());
+
+        return new SoundTriggerHwCallback(callbackCaptor.getValue(), cookieCaptor.getValue());
+    }
+
+    private SoundTriggerHwCallback startRecognition_2_1(ISoundTriggerModule module, int handle,
+            int hwHandle) throws RemoteException {
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver;
+
+        ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig>
+                configCaptor = ArgumentCaptor.forClass(
+                android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig.class);
+        ArgumentCaptor<android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback> callbackCaptor =
+                ArgumentCaptor.forClass(
+                        android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.class);
+        ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class);
+
+        when(driver.startRecognition_2_1(eq(hwHandle), configCaptor.capture(),
+                callbackCaptor.capture(), cookieCaptor.capture())).thenReturn(0);
+
+        RecognitionConfig config = createRecognitionConfig();
+
+        module.startRecognition(handle, config);
+        verify(driver).startRecognition_2_1(eq(hwHandle), any(), any(), anyInt());
+
+        android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig halConfig =
+                configCaptor.getValue();
+        assertTrue(halConfig.header.captureRequested);
+        assertEquals(102, halConfig.header.captureHandle);
+        assertEquals(103, halConfig.header.captureDevice);
+        assertEquals(1, halConfig.header.phrases.size());
+        android.hardware.soundtrigger.V2_0.PhraseRecognitionExtra halPhraseExtra =
+                halConfig.header.phrases.get(0);
+        assertEquals(123, halPhraseExtra.id);
+        assertEquals(4, halPhraseExtra.confidenceLevel);
+        assertEquals(5, halPhraseExtra.recognitionModes);
+        assertEquals(1, halPhraseExtra.levels.size());
+        android.hardware.soundtrigger.V2_0.ConfidenceLevel halLevel = halPhraseExtra.levels.get(0);
+        assertEquals(234, halLevel.userId);
+        assertEquals(34, halLevel.levelPercent);
+        assertArrayEquals(new byte[]{5, 4, 3, 2, 1},
+                HidlMemoryUtil.hidlMemoryToByteArray(halConfig.data));
+
+        return new SoundTriggerHwCallback(callbackCaptor.getValue(), cookieCaptor.getValue());
+    }
+
+    private SoundTriggerHwCallback startRecognition(ISoundTriggerModule module, int handle,
+            int hwHandle) throws RemoteException {
+        if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) {
+            return startRecognition_2_1(module, handle, hwHandle);
+        } else {
+            return startRecognition_2_0(module, handle, hwHandle);
+        }
+    }
+
+    private RecognitionConfig createRecognitionConfig() {
+        RecognitionConfig config = new RecognitionConfig();
+        config.captureRequested = true;
+        config.phraseRecognitionExtras = new PhraseRecognitionExtra[]{new PhraseRecognitionExtra()};
+        config.phraseRecognitionExtras[0].id = 123;
+        config.phraseRecognitionExtras[0].confidenceLevel = 4;
+        config.phraseRecognitionExtras[0].recognitionModes = 5;
+        config.phraseRecognitionExtras[0].levels = new ConfidenceLevel[]{new ConfidenceLevel()};
+        config.phraseRecognitionExtras[0].levels[0].userId = 234;
+        config.phraseRecognitionExtras[0].levels[0].levelPercent = 34;
+        config.data = new byte[]{5, 4, 3, 2, 1};
+        return config;
+    }
+
+    private void stopRecognition(ISoundTriggerModule module, int handle, int hwHandle)
+            throws RemoteException {
+        when(mHalDriver.stopRecognition(hwHandle)).thenReturn(0);
+        module.stopRecognition(handle);
+        verify(mHalDriver).stopRecognition(hwHandle);
+    }
+
+    private void verifyNotStartRecognition() throws RemoteException {
+        verify(mHalDriver, never()).startRecognition(anyInt(), any(), any(), anyInt());
+        if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) {
+            verify((android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver,
+                    never()).startRecognition_2_1(anyInt(), any(), any(), anyInt());
+        }
+    }
+
+
+    @Before
+    public void setUp() throws Exception {
+        clearInvocations(mHalDriver);
+        clearInvocations(mAudioSessionProvider);
+
+        // This binder is associated with the mock, so it can be cast to either version of the
+        // HAL interface.
+        final IHwBinder binder = new IHwBinder() {
+            @Override
+            public void transact(int code, HwParcel request, HwParcel reply, int flags)
+                    throws RemoteException {
+                // This is a little hacky, but a very easy way to gracefully reject a request for
+                // an unsupported interface (after queryLocalInterface() returns null, the client
+                // will attempt a remote transaction to obtain the interface. RemoteException will
+                // cause it to give up).
+                throw new RemoteException();
+            }
+
+            @Override
+            public IHwInterface queryLocalInterface(String descriptor) {
+                if (descriptor.equals("android.hardware.soundtrigger@2.0::ISoundTriggerHw")
+                        || descriptor.equals("android.hardware.soundtrigger@2.1::ISoundTriggerHw")
+                        && mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw
+                        || descriptor.equals("android.hardware.soundtrigger@2.2::ISoundTriggerHw")
+                        && mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw
+                        || descriptor.equals("android.hardware.soundtrigger@2.3::ISoundTriggerHw")
+                        && mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw) {
+                    return mHalDriver;
+                }
+                return null;
+            }
+
+            @Override
+            public boolean linkToDeath(DeathRecipient recipient, long cookie) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public boolean unlinkToDeath(DeathRecipient recipient) {
+                throw new UnsupportedOperationException();
+            }
+        };
+
+        when(mHalDriver.asBinder()).thenReturn(binder);
+    }
+
+    @Test
+    public void testSetUpAndTearDown() {
+    }
+
+    @Test
+    public void testListModules() throws Exception {
+        initService(true);
+        // Note: input and output properties are NOT the same type, even though they are in any way
+        // equivalent. One is a type that's exposed by the HAL and one is a type that's exposed by
+        // the service. The service actually performs a (trivial) conversion between the two.
+        SoundTriggerModuleDescriptor[] allDescriptors = mService.listModules();
+        assertEquals(1, allDescriptors.length);
+
+        SoundTriggerModuleProperties properties = allDescriptors[0].properties;
+
+        validateDefaultProperties(properties, true);
+    }
+
+    @Test
+    public void testAttachDetach() throws Exception {
+        // Normal attachment / detachment.
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(true);
+        assertNotNull(module);
+        module.detach();
+    }
+
+    @Test
+    public void testAttachDetachNotAvailable() throws Exception {
+        // Attachment / detachment during external capture, with a module not supporting concurrent
+        // capture.
+        initService(false);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(false);
+        assertNotNull(module);
+        module.detach();
+    }
+
+    @Test
+    public void testAttachDetachAvailable() throws Exception {
+        // Attachment / detachment during external capture, with a module supporting concurrent
+        // capture.
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(true);
+        assertNotNull(module);
+        module.detach();
+    }
+
+    @Test
+    public void testLoadUnloadModel() throws Exception {
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        final int hwHandle = 7;
+        int handle = loadGenericModel(module, hwHandle);
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testLoadUnloadPhraseModel() throws Exception {
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        final int hwHandle = 73;
+        int handle = loadPhraseModel(module, hwHandle);
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testStartStopRecognition() throws Exception {
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        // Load the model.
+        final int hwHandle = 7;
+        int handle = loadGenericModel(module, hwHandle);
+
+        // Initiate a recognition.
+        startRecognition(module, handle, hwHandle);
+
+        // Stop the recognition.
+        stopRecognition(module, handle, hwHandle);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testStartStopPhraseRecognition() throws Exception {
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        // Load the model.
+        final int hwHandle = 67;
+        int handle = loadPhraseModel(module, hwHandle);
+
+        // Initiate a recognition.
+        startRecognition(module, handle, hwHandle);
+
+        // Stop the recognition.
+        stopRecognition(module, handle, hwHandle);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testRecognition() throws Exception {
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        // Load the model.
+        final int hwHandle = 7;
+        int handle = loadGenericModel(module, hwHandle);
+
+        // Initiate a recognition.
+        SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle);
+
+        // Signal a capture from the driver.
+        hwCallback.sendRecognitionEvent(hwHandle,
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionStatus.SUCCESS);
+
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        verify(callback).onRecognition(eq(handle), eventCaptor.capture());
+
+        // Validate the event.
+        validateRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.SUCCESS);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testPhraseRecognition() throws Exception {
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        // Load the model.
+        final int hwHandle = 7;
+        int handle = loadPhraseModel(module, hwHandle);
+
+        // Initiate a recognition.
+        SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle);
+
+        // Signal a capture from the driver.
+        hwCallback.sendPhraseRecognitionEvent(hwHandle,
+                android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionStatus.SUCCESS);
+
+        ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                PhraseRecognitionEvent.class);
+        verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture());
+
+        // Validate the event.
+        validatePhraseRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.SUCCESS);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testForceRecognition() throws Exception {
+        if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw)) {
+            return;
+        }
+
+        android.hardware.soundtrigger.V2_2.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_2.ISoundTriggerHw) mHalDriver;
+
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        // Load the model.
+        final int hwHandle = 17;
+        int handle = loadGenericModel(module, hwHandle);
+
+        // Initiate a recognition.
+        SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle);
+
+        // Force a trigger.
+        module.forceRecognitionEvent(handle);
+        verify(driver).getModelState(hwHandle);
+
+        // Signal a capture from the driver.
+        // '3' means 'forced', there's no constant for that in the HAL.
+        hwCallback.sendRecognitionEvent(hwHandle, 3);
+
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        verify(callback).onRecognition(eq(handle), eventCaptor.capture());
+
+        // Validate the event.
+        validateRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.FORCED);
+
+        // Stop the recognition.
+        stopRecognition(module, handle, hwHandle);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testForcePhraseRecognition() throws Exception {
+        if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_2.ISoundTriggerHw)) {
+            return;
+        }
+
+        android.hardware.soundtrigger.V2_2.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_2.ISoundTriggerHw) mHalDriver;
+
+        initService(true);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+
+        // Load the model.
+        final int hwHandle = 17;
+        int handle = loadPhraseModel(module, hwHandle);
+
+        // Initiate a recognition.
+        SoundTriggerHwCallback hwCallback = startRecognition(module, handle, hwHandle);
+
+        // Force a trigger.
+        module.forceRecognitionEvent(handle);
+        verify(driver).getModelState(hwHandle);
+
+        // Signal a capture from the driver.
+        // '3' means 'forced', there's no constant for that in the HAL.
+        hwCallback.sendPhraseRecognitionEvent(hwHandle, 3);
+
+        ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                PhraseRecognitionEvent.class);
+        verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture());
+
+        // Validate the event.
+        validatePhraseRecognitionEvent(eventCaptor.getValue(), RecognitionStatus.FORCED);
+
+        // Stop the recognition.
+        stopRecognition(module, handle, hwHandle);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testAbortRecognition() throws Exception {
+        // Make sure the HAL doesn't support concurrent capture.
+        initService(false);
+        mService.setExternalCaptureState(false);
+
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(true);
+
+        // Load the model.
+        final int hwHandle = 11;
+        int handle = loadGenericModel(module, hwHandle);
+
+        // Initiate a recognition.
+        startRecognition(module, handle, hwHandle);
+
+        // Abort.
+        mService.setExternalCaptureState(true);
+
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        verify(callback).onRecognition(eq(handle), eventCaptor.capture());
+
+        // Validate the event.
+        assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status);
+
+        // Make sure we are notified of the lost availability.
+        verify(callback).onRecognitionAvailabilityChange(false);
+
+        // Attempt to start a new recognition - should get an abort event immediately, without
+        // involving the HAL.
+        clearInvocations(callback);
+        clearInvocations(mHalDriver);
+        module.startRecognition(handle, createRecognitionConfig());
+        verify(callback).onRecognition(eq(handle), eventCaptor.capture());
+        assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status);
+        verifyNotStartRecognition();
+
+        // Now enable it and make sure we are notified.
+        mService.setExternalCaptureState(false);
+        verify(callback).onRecognitionAvailabilityChange(true);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testAbortPhraseRecognition() throws Exception {
+        // Make sure the HAL doesn't support concurrent capture.
+        initService(false);
+        mService.setExternalCaptureState(false);
+
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(true);
+
+        // Load the model.
+        final int hwHandle = 11;
+        int handle = loadPhraseModel(module, hwHandle);
+
+        // Initiate a recognition.
+        startRecognition(module, handle, hwHandle);
+
+        // Abort.
+        mService.setExternalCaptureState(true);
+
+        ArgumentCaptor<PhraseRecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                PhraseRecognitionEvent.class);
+        verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture());
+
+        // Validate the event.
+        assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().common.status);
+
+        // Make sure we are notified of the lost availability.
+        verify(callback).onRecognitionAvailabilityChange(false);
+
+        // Attempt to start a new recognition - should get an abort event immediately, without
+        // involving the HAL.
+        clearInvocations(callback);
+        clearInvocations(mHalDriver);
+        module.startRecognition(handle, createRecognitionConfig());
+        verify(callback).onPhraseRecognition(eq(handle), eventCaptor.capture());
+        assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().common.status);
+        verifyNotStartRecognition();
+
+        // Now enable it and make sure we are notified.
+        mService.setExternalCaptureState(false);
+        verify(callback).onRecognitionAvailabilityChange(true);
+
+        // Unload the model.
+        unloadModel(module, handle, hwHandle);
+        module.detach();
+    }
+
+    @Test
+    public void testNotAbortRecognitionConcurrent() throws Exception {
+        // Make sure the HAL supports concurrent capture.
+        initService(true);
+
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(true);
+        clearInvocations(callback);
+
+        // Load the model.
+        final int hwHandle = 13;
+        int handle = loadGenericModel(module, hwHandle);
+
+        // Initiate a recognition.
+        startRecognition(module, handle, hwHandle);
+
+        // Signal concurrent capture. Shouldn't abort.
+        mService.setExternalCaptureState(true);
+        verify(callback, never()).onRecognition(anyInt(), any());
+        verify(callback, never()).onRecognitionAvailabilityChange(anyBoolean());
+
+        // Stop the recognition.
+        stopRecognition(module, handle, hwHandle);
+
+        // Initiating a new one should work fine.
+        clearInvocations(mHalDriver);
+        startRecognition(module, handle, hwHandle);
+        verify(callback, never()).onRecognition(anyInt(), any());
+        stopRecognition(module, handle, hwHandle);
+
+        // Unload the model.
+        module.unloadModel(handle);
+        module.detach();
+    }
+
+    @Test
+    public void testNotAbortPhraseRecognitionConcurrent() throws Exception {
+        // Make sure the HAL supports concurrent capture.
+        initService(true);
+
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        verify(callback).onRecognitionAvailabilityChange(true);
+        clearInvocations(callback);
+
+        // Load the model.
+        final int hwHandle = 13;
+        int handle = loadPhraseModel(module, hwHandle);
+
+        // Initiate a recognition.
+        startRecognition(module, handle, hwHandle);
+
+        // Signal concurrent capture. Shouldn't abort.
+        mService.setExternalCaptureState(true);
+        verify(callback, never()).onPhraseRecognition(anyInt(), any());
+        verify(callback, never()).onRecognitionAvailabilityChange(anyBoolean());
+
+        // Stop the recognition.
+        stopRecognition(module, handle, hwHandle);
+
+        // Initiating a new one should work fine.
+        clearInvocations(mHalDriver);
+        startRecognition(module, handle, hwHandle);
+        verify(callback, never()).onRecognition(anyInt(), any());
+        stopRecognition(module, handle, hwHandle);
+
+        // Unload the model.
+        module.unloadModel(handle);
+        module.detach();
+    }
+
+    @Test
+    public void testParameterSupported() throws Exception {
+        if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) {
+            return;
+        }
+
+        android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver;
+
+        initService(false);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        final int hwHandle = 12;
+        int modelHandle = loadGenericModel(module, hwHandle);
+
+        doAnswer((Answer<Void>) invocation -> {
+            android.hardware.soundtrigger.V2_3.ISoundTriggerHw.queryParameterCallback
+                    resultCallback = invocation.getArgument(2);
+            android.hardware.soundtrigger.V2_3.ModelParameterRange range =
+                    new android.hardware.soundtrigger.V2_3.ModelParameterRange();
+            range.start = 23;
+            range.end = 45;
+            OptionalModelParameterRange optionalRange = new OptionalModelParameterRange();
+            optionalRange.range(range);
+            resultCallback.onValues(0, optionalRange);
+            return null;
+        }).when(driver).queryParameter(eq(hwHandle),
+                eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any());
+
+        ModelParameterRange range = module.queryModelParameterSupport(modelHandle,
+                ModelParameter.THRESHOLD_FACTOR);
+
+        verify(driver).queryParameter(eq(hwHandle),
+                eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any());
+
+        assertEquals(23, range.minInclusive);
+        assertEquals(45, range.maxInclusive);
+    }
+
+    @Test
+    public void testParameterNotSupportedOld() throws Exception {
+        if (mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw) {
+            return;
+        }
+
+        initService(false);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        final int hwHandle = 13;
+        int modelHandle = loadGenericModel(module, hwHandle);
+
+        ModelParameterRange range = module.queryModelParameterSupport(modelHandle,
+                ModelParameter.THRESHOLD_FACTOR);
+
+        assertNull(range);
+    }
+
+    @Test
+    public void testParameterNotSupported() throws Exception {
+        if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) {
+            return;
+        }
+
+        android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver;
+
+        initService(false);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        final int hwHandle = 13;
+        int modelHandle = loadGenericModel(module, hwHandle);
+
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_3.ISoundTriggerHw.queryParameterCallback
+                    resultCallback = invocation.getArgument(2);
+            // This is the return of this method.
+            resultCallback.onValues(0, new OptionalModelParameterRange());
+            return null;
+        }).when(driver).queryParameter(eq(hwHandle),
+                eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any());
+
+        ModelParameterRange range = module.queryModelParameterSupport(modelHandle,
+                ModelParameter.THRESHOLD_FACTOR);
+
+        verify(driver).queryParameter(eq(hwHandle),
+                eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any());
+
+        assertNull(range);
+    }
+
+    @Test
+    public void testGetParameter() throws Exception {
+        if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) {
+            return;
+        }
+
+        android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver;
+
+        initService(false);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        final int hwHandle = 14;
+        int modelHandle = loadGenericModel(module, hwHandle);
+
+        doAnswer(invocation -> {
+            android.hardware.soundtrigger.V2_3.ISoundTriggerHw.getParameterCallback
+                    resultCallback = invocation.getArgument(2);
+            // This is the return of this method.
+            resultCallback.onValues(0, 234);
+            return null;
+        }).when(driver).getParameter(eq(hwHandle),
+                eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any());
+
+        int value = module.getModelParameter(modelHandle, ModelParameter.THRESHOLD_FACTOR);
+
+        verify(driver).getParameter(eq(hwHandle),
+                eq(android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR), any());
+
+        assertEquals(234, value);
+    }
+
+    @Test
+    public void testSetParameter() throws Exception {
+        if (!(mHalDriver instanceof android.hardware.soundtrigger.V2_3.ISoundTriggerHw)) {
+            return;
+        }
+
+        android.hardware.soundtrigger.V2_3.ISoundTriggerHw driver =
+                (android.hardware.soundtrigger.V2_3.ISoundTriggerHw) mHalDriver;
+
+        initService(false);
+        ISoundTriggerCallback callback = createCallbackMock();
+        ISoundTriggerModule module = mService.attach(0, callback);
+        final int hwHandle = 17;
+        int modelHandle = loadGenericModel(module, hwHandle);
+
+        when(driver.setParameter(hwHandle,
+                android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR,
+                456)).thenReturn(0);
+
+        module.setModelParameter(modelHandle, ModelParameter.THRESHOLD_FACTOR, 456);
+
+        verify(driver).setParameter(hwHandle,
+                android.hardware.soundtrigger.V2_3.ModelParameter.THRESHOLD_FACTOR, 456);
+    }
+
+    private static class SoundTriggerHwCallback {
+        private final android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback mCallback;
+        private final int mCookie;
+
+        SoundTriggerHwCallback(android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback callback,
+                int cookie) {
+            mCallback = callback;
+            mCookie = cookie;
+        }
+
+        private void sendRecognitionEvent(int hwHandle, int status) throws RemoteException {
+            if (mCallback instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) {
+                ((android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) mCallback).recognitionCallback_2_1(
+                        createRecognitionEvent_2_1(hwHandle, status), mCookie);
+            } else {
+                mCallback.recognitionCallback(createRecognitionEvent_2_0(hwHandle, status),
+                        mCookie);
+            }
+        }
+
+        private void sendPhraseRecognitionEvent(int hwHandle, int status) throws RemoteException {
+            if (mCallback instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) {
+                ((android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback) mCallback).phraseRecognitionCallback_2_1(
+                        createPhraseRecognitionEvent_2_1(hwHandle, status), mCookie);
+            } else {
+                mCallback.phraseRecognitionCallback(
+                        createPhraseRecognitionEvent_2_0(hwHandle, status), mCookie);
+            }
+        }
+    }
+}