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

Commit 7398b8a5 authored by Ytai Ben-tsvi's avatar Ytai Ben-tsvi Committed by Automerger Merge Worker
Browse files

Merge "Fix concurrent capture handler" into tm-dev am: a18a8a12

parents 30241bbb a18a8a12
Loading
Loading
Loading
Loading
+95 −128
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.server.soundtrigger_middleware;

import android.annotation.NonNull;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger.ModelParameterRange;
import android.media.soundtrigger.PhraseRecognitionEvent;
import android.media.soundtrigger.PhraseSoundModel;
@@ -30,6 +29,7 @@ import android.media.soundtrigger.Status;
import android.os.IBinder;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
@@ -63,18 +63,24 @@ import java.util.concurrent.ConcurrentHashMap;
 */
public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal,
        ICaptureStateNotifier.Listener {
    private final @NonNull ISoundTriggerHal mDelegate;
    @NonNull private final ISoundTriggerHal mDelegate;
    private GlobalCallback mGlobalCallback;
    /**
     * This lock must be held to synchronize forward calls (start/stop/onCaptureStateChange) that
     * update the mActiveModels set and mCaptureState.
     * It must not be locked in HAL callbacks to avoid deadlocks.
     */
    @NonNull private final Object mStartStopLock = new Object();

    /**
     * Information about a model that is currently loaded. This is needed in order to be able to
     * send abort events to its designated callback.
     */
    private static class LoadedModel {
        final int type;
        final @NonNull ModelCallback callback;
        public final int type;
        @NonNull public final ModelCallback callback;

        private LoadedModel(int type, @NonNull ModelCallback callback) {
        LoadedModel(int type, @NonNull ModelCallback callback) {
            this.type = type;
            this.callback = callback;
        }
@@ -83,19 +89,19 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
    /**
     * This map holds the model type for every model that is loaded.
     */
    private final @NonNull Map<Integer, LoadedModel> mLoadedModels = new ConcurrentHashMap<>();
    @NonNull private final Map<Integer, LoadedModel> mLoadedModels = new ConcurrentHashMap<>();

    /**
     * A set of all models that are currently active.
     * We use this in order to know which models to stop in case of external capture.
     * Used as a lock to synchronize operations that effect activity.
     */
    private final @NonNull Set<Integer> mActiveModels = new HashSet<>();
    @NonNull private final Set<Integer> mActiveModels = new HashSet<>();

    /**
     * Notifier for changes in capture state.
     */
    private final @NonNull ICaptureStateNotifier mNotifier;
    @NonNull private final ICaptureStateNotifier mNotifier;

    /**
     * Whether capture is active.
@@ -106,10 +112,10 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
     * Since we're wrapping the death recipient, we need to keep a translation map for unlinking.
     * Key is the client recipient, value is the wrapper.
     */
    private final @NonNull Map<IBinder.DeathRecipient, IBinder.DeathRecipient>
    @NonNull private final Map<IBinder.DeathRecipient, IBinder.DeathRecipient>
            mDeathRecipientMap = new ConcurrentHashMap<>();

    private final @NonNull CallbackThread mCallbackThread = new CallbackThread();
    @NonNull private final CallbackThread mCallbackThread = new CallbackThread();

    public SoundTriggerHalConcurrentCaptureHandler(
            @NonNull ISoundTriggerHal delegate,
@@ -122,6 +128,7 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
    @Override
    public void startRecognition(int modelHandle, int deviceHandle, int ioHandle,
            RecognitionConfig config) {
        synchronized (mStartStopLock) {
            synchronized (mActiveModels) {
                if (mCaptureState) {
                    throw new RecoverableException(Status.RESOURCE_CONTENTION);
@@ -130,12 +137,19 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
                mActiveModels.add(modelHandle);
            }
        }
    }

    @Override
    public void stopRecognition(int modelHandle) {
        synchronized (mStartStopLock) {
            boolean wasActive;
            synchronized (mActiveModels) {
                wasActive = mActiveModels.remove(modelHandle);
            }
            if (wasActive) {
                // Must be done outside of the lock, since it may trigger synchronous callbacks.
                mDelegate.stopRecognition(modelHandle);
            mActiveModels.remove(modelHandle);
            }
        }
        // Block until all previous events are delivered. Since this is potentially blocking on
        // upward calls, it must be done outside the lock.
@@ -144,27 +158,38 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal

    @Override
    public void onCaptureStateChange(boolean active) {
        synchronized (mActiveModels) {
        synchronized (mStartStopLock) {
            if (active) {
                // Abort all active models. This must be done as one transaction to the event
                // thread, in order to be able to dedupe events before they are delivered.
                try (SafeCloseable ignored = mCallbackThread.stallReader()) {
                    for (int modelHandle : mActiveModels) {
                        mDelegate.stopRecognition(modelHandle);
                        LoadedModel model = mLoadedModels.get(modelHandle);
                        // An abort event must be the last one for its model.
                        mCallbackThread.pushWithDedupe(modelHandle, true,
                                () -> notifyAbort(modelHandle, model));
                    }
                }
                abortAllActiveModels();
            } else {
                if (mGlobalCallback != null) {
                    mGlobalCallback.onResourcesAvailable();
                }

            }
            mCaptureState = active;
        }
    }

    private void abortAllActiveModels() {
        while (true) {
            int toStop;
            synchronized (mActiveModels) {
                Iterator<Integer> iterator = mActiveModels.iterator();
                if (!iterator.hasNext()) {
                    return;
                }
                toStop = iterator.next();
                mActiveModels.remove(toStop);
            }
            // Invoke stop outside of the lock.
            mDelegate.stopRecognition(toStop);

            LoadedModel model = mLoadedModels.get(toStop);
            // Queue an abort event (no need to flush).
            mCallbackThread.push(() -> notifyAbort(toStop, model));
        }
    }

    @Override
    public int loadSoundModel(SoundModel soundModel, ModelCallback callback) {
        int handle = mDelegate.loadSoundModel(soundModel, new CallbackWrapper(callback));
@@ -188,23 +213,13 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal

    @Override
    public void registerCallback(GlobalCallback callback) {
        mGlobalCallback = new GlobalCallback() {
            @Override
            public void onResourcesAvailable() {
                mCallbackThread.push(callback::onResourcesAvailable);
            }
        };
        mGlobalCallback = () -> mCallbackThread.push(callback::onResourcesAvailable);
        mDelegate.registerCallback(mGlobalCallback);
    }

    @Override
    public void linkToDeath(IBinder.DeathRecipient recipient) {
        IBinder.DeathRecipient wrapper = new IBinder.DeathRecipient() {
            @Override
            public void binderDied() {
                mCallbackThread.push(() -> recipient.binderDied());
            }
        };
        IBinder.DeathRecipient wrapper = () -> mCallbackThread.push(recipient::binderDied);
        mDelegate.linkToDeath(wrapper);
        mDeathRecipientMap.put(recipient, wrapper);
    }
@@ -215,7 +230,7 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
    }

    private class CallbackWrapper implements ISoundTriggerHal.ModelCallback {
        private final @NonNull ISoundTriggerHal.ModelCallback mDelegateCallback;
        @NonNull private final ISoundTriggerHal.ModelCallback mDelegateCallback;

        private CallbackWrapper(@NonNull ModelCallback delegateCallback) {
            mDelegateCallback = delegateCallback;
@@ -223,19 +238,37 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal

        @Override
        public void recognitionCallback(int modelHandle, RecognitionEvent event) {
            // A recognition event must be the last one for its model, unless it is a forced one
            // (those leave the model active).
            mCallbackThread.pushWithDedupe(modelHandle, !event.recognitionStillActive,
            synchronized (mActiveModels) {
                if (!mActiveModels.contains(modelHandle)) {
                    // Discard the event.
                    return;
                }
                if (!event.recognitionStillActive) {
                    mActiveModels.remove(modelHandle);
                }
                // A recognition event must be the last one for its model, unless it indicates that
                // recognition is still active.
                mCallbackThread.push(
                        () -> mDelegateCallback.recognitionCallback(modelHandle, event));
            }
        }

        @Override
        public void phraseRecognitionCallback(int modelHandle, PhraseRecognitionEvent event) {
            // A recognition event must be the last one for its model, unless it is a forced one
            // (those leave the model active).
            mCallbackThread.pushWithDedupe(modelHandle, !event.common.recognitionStillActive,
            synchronized (mActiveModels) {
                if (!mActiveModels.contains(modelHandle)) {
                    // Discard the event.
                    return;
                }
                if (!event.common.recognitionStillActive) {
                    mActiveModels.remove(modelHandle);
                }
                // A recognition event must be the last one for its model, unless it indicates that
                // recognition is still active.
                mCallbackThread.push(
                        () -> mDelegateCallback.phraseRecognitionCallback(modelHandle, event));
            }
        }

        @Override
        public void modelUnloaded(int modelHandle) {
@@ -254,36 +287,12 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
     * <ul>
     * <li>Events are processed on a separate thread than the thread that pushed them, in the order
     * they were pushed.
     * <li>Events can be deduped upon entry to the queue. This is achieved as follows:
     * <ul>
     *     <li>Temporarily stall the reader via {@link #stallReader()}.
     *     <li>Within this scope, push as many events as needed via
     *     {@link #pushWithDedupe(int, boolean, Runnable)}.
     *     If an event with the same model handle as the one being pushed is already in the queue
     *     and has been marked as "lastForModel", the new event will be discarded before entering
     *     the queue.
     *     <li>Finally, un-stall the reader by existing the scope.
     *     <li>Events that do not require deduping can be pushed via {@link #push(Runnable)}.
     * </ul>
     * <li>Events can be flushed via {@link #flush()}. This will block until all events pushed prior
     * to this call have been fully processed.
     * </ul>
     */
    private static class CallbackThread {
        private static class Entry {
            final boolean lastForModel;
            final int modelHandle;
            final Runnable runnable;

            private Entry(boolean lastForModel, int modelHandle, Runnable runnable) {
                this.lastForModel = lastForModel;
                this.modelHandle = modelHandle;
                this.runnable = runnable;
            }
        }

        private boolean mStallReader = false;
        private final Queue<Entry> mList = new LinkedList<>();
        private final Queue<Runnable> mList = new LinkedList<>();
        private int mPushCount = 0;
        private int mProcessedCount = 0;

@@ -312,23 +321,11 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
         * @param runnable The runnable to push.
         */
        void push(Runnable runnable) {
            pushEntry(new Entry(false, 0, runnable), false);
            synchronized (mList) {
                mList.add(runnable);
                mPushCount++;
                mList.notifyAll();
            }


        /**
         * Push a new runnable to the queue, with deduping.
         * If an entry with the same model handle is already in the queue and was designated as
         * last for model, this one will be discarded.
         *
         * @param modelHandle The model handle, used for deduping purposes.
         * @param lastForModel If true, this entry will be considered the last one for this model
         *                     and any subsequence calls for this handle (whether lastForModel or
         *                     not) will be discarded while this entry is in the queue.
         * @param runnable    The runnable to push.
         */
        void pushWithDedupe(int modelHandle, boolean lastForModel, Runnable runnable) {
            pushEntry(new Entry(lastForModel, modelHandle, runnable), true);
        }

        /**
@@ -346,45 +343,15 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal
            }
        }

        /**
         * Creates a scope (using a try-with-resources block), within which events that are pushed
         * remain queued and processed. This is useful in order to utilize deduping.
         */
        SafeCloseable stallReader() {
            synchronized (mList) {
                mStallReader = true;
                return () -> {
                    synchronized (mList) {
                        mStallReader = false;
                        mList.notifyAll();
                    }
                };
            }
        }

        private void pushEntry(Entry entry, boolean dedupe) {
            synchronized (mList) {
                if (dedupe) {
                    for (Entry existing : mList) {
                        if (existing.lastForModel && existing.modelHandle == entry.modelHandle) {
                            return;
                        }
                    }
                }
                mList.add(entry);
                mPushCount++;
                mList.notifyAll();
            }
        }

        private Runnable pop() throws InterruptedException {
            synchronized (mList) {
                while (mStallReader || mList.isEmpty()) {
                while (mList.isEmpty()) {
                    mList.wait();
                }
                return mList.remove().runnable;
                return mList.remove();
            }
        }

    }

    /** Notify the client that recognition has been aborted. */
+15 −135
Original line number Diff line number Diff line
@@ -20,18 +20,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -56,32 +53,20 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

@RunWith(Parameterized.class)
public class SoundHw2CompatTest {
    @Parameterized.Parameter(0) public String mVersion;
    @Parameterized.Parameter(1) public boolean mSupportConcurrentCapture;
    @Parameterized.Parameter public String mVersion;

    private final Runnable mRebootRunnable = mock(Runnable.class);
    private ISoundTriggerHal mCanonical;
    private CaptureStateNotifier mCaptureStateNotifier;
    private android.hardware.soundtrigger.V2_0.ISoundTriggerHw mHalDriver;

    // We run the test once for every version of the underlying driver.
    @Parameterized.Parameters(name = "{0}, concurrent={1}")
    public static Iterable<Object[]> data() {
        List<Object[]> result = new LinkedList<>();

        for (String version : new String[]{"V2_0", "V2_1", "V2_2", "V2_3",}) {
            for (boolean concurrentCapture : new boolean[]{false, true}) {
                result.add(new Object[]{version, concurrentCapture});
            }
        }

        return result;
    @Parameterized.Parameters
    public static Object[] data() {
        return new String[]{"V2_0", "V2_1", "V2_2", "V2_3"};
    }

    @Before
@@ -139,7 +124,7 @@ public class SoundHw2CompatTest {
        when(mHalDriver.asBinder()).thenReturn(binder);

        android.hardware.soundtrigger.V2_3.Properties halProperties =
                TestUtil.createDefaultProperties_2_3(mSupportConcurrentCapture);
                TestUtil.createDefaultProperties_2_3();
        doAnswer(invocation -> {
            ((android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback) invocation.getArgument(
                    0)).onValues(0, halProperties.base);
@@ -156,10 +141,7 @@ public class SoundHw2CompatTest {
            }).when(driver).getProperties_2_3(any());
        }

        mCaptureStateNotifier = spy(new CaptureStateNotifier());

        mCanonical = SoundTriggerHw2Compat.create(mHalDriver, mRebootRunnable,
                mCaptureStateNotifier);
        mCanonical = SoundTriggerHw2Compat.create(mHalDriver, mRebootRunnable, null);

        // During initialization any method can be called, but after we're starting to enforce that
        // no additional methods are called.
@@ -171,7 +153,6 @@ public class SoundHw2CompatTest {
        mCanonical.detach();
        verifyNoMoreInteractions(mHalDriver);
        verifyNoMoreInteractions(mRebootRunnable);
        mCaptureStateNotifier.verifyNoMoreListeners();
    }

    @Test
@@ -194,12 +175,12 @@ public class SoundHw2CompatTest {
            // It is OK for the SUT to cache the properties, so the underlying method doesn't
            // need to be called every single time.
            verify(driver, atMost(1)).getProperties_2_3(any());
            TestUtil.validateDefaultProperties(properties, mSupportConcurrentCapture);
            TestUtil.validateDefaultProperties(properties);
        } else {
            // It is OK for the SUT to cache the properties, so the underlying method doesn't
            // need to be called every single time.
            verify(mHalDriver, atMost(1)).getProperties(any());
            TestUtil.validateDefaultProperties(properties, mSupportConcurrentCapture, 0, "");
            TestUtil.validateDefaultProperties(properties, 0, "");
        }
    }

@@ -291,7 +272,7 @@ public class SoundHw2CompatTest {

        ISoundTriggerHal.ModelCallback canonicalCallback = mock(
                ISoundTriggerHal.ModelCallback.class);
        final int maxModels = TestUtil.createDefaultProperties_2_0(false).maxSoundModels;
        final int maxModels = TestUtil.createDefaultProperties_2_0().maxSoundModels;
        int[] modelHandles = new int[maxModels];

        // Load as many models as we're allowed.
@@ -318,7 +299,7 @@ public class SoundHw2CompatTest {
        verify(globalCallback).onResourcesAvailable();
    }

    private int loadPhraseModel_2_0(ISoundTriggerHal.ModelCallback canonicalCallback)
    private void loadPhraseModel_2_0(ISoundTriggerHal.ModelCallback canonicalCallback)
            throws Exception {
        final int handle = 29;
        ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel>
@@ -345,10 +326,9 @@ public class SoundHw2CompatTest {

        TestUtil.validatePhraseSoundModel_2_0(modelCaptor.getValue());
        validateCallback_2_0(callbackCaptor.getValue(), canonicalCallback);
        return handle;
    }

    private int loadPhraseModel_2_1(ISoundTriggerHal.ModelCallback canonicalCallback)
    private void loadPhraseModel_2_1(ISoundTriggerHal.ModelCallback canonicalCallback)
            throws Exception {
        final android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver_2_1 =
                (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver;
@@ -380,14 +360,13 @@ public class SoundHw2CompatTest {

        TestUtil.validatePhraseSoundModel_2_1(model.get());
        validateCallback_2_1(callbackCaptor.getValue(), canonicalCallback);
        return handle;
    }

    public int loadPhraseModel(ISoundTriggerHal.ModelCallback canonicalCallback) throws Exception {
    public void loadPhraseModel(ISoundTriggerHal.ModelCallback canonicalCallback) throws Exception {
        if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) {
            return loadPhraseModel_2_1(canonicalCallback);
            loadPhraseModel_2_1(canonicalCallback);
        } else {
            return loadPhraseModel_2_0(canonicalCallback);
            loadPhraseModel_2_0(canonicalCallback);
        }
    }

@@ -483,80 +462,6 @@ public class SoundHw2CompatTest {
        startRecognition(handle, canonicalCallback);
    }

    @Test
    public void testConcurrentCaptureAbort() throws Exception {
        assumeFalse(mSupportConcurrentCapture);
        verify(mCaptureStateNotifier, atLeast(1)).registerListener(any());

        // Register global callback.
        ISoundTriggerHal.GlobalCallback globalCallback = mock(
                ISoundTriggerHal.GlobalCallback.class);
        mCanonical.registerCallback(globalCallback);

        // Load.
        ISoundTriggerHal.ModelCallback canonicalCallback = mock(
                ISoundTriggerHal.ModelCallback.class);
        final int handle = loadGenericModel(canonicalCallback);

        // Then start.
        startRecognition(handle, canonicalCallback);

        // Now activate external capture.
        mCaptureStateNotifier.setState(true);

        // Expect hardware to have been stopped.
        verify(mHalDriver).stopRecognition(handle);

        // Expect an abort event (async).
        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
                RecognitionEvent.class);
        mCanonical.flushCallbacks();
        verify(canonicalCallback).recognitionCallback(eq(handle), eventCaptor.capture());
        assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status);

        // Deactivate external capture.
        mCaptureStateNotifier.setState(false);

        // Expect a onResourcesAvailable().
        mCanonical.flushCallbacks();
        verify(globalCallback).onResourcesAvailable();
    }

    @Test
    public void testConcurrentCaptureReject() throws Exception {
        assumeFalse(mSupportConcurrentCapture);
        verify(mCaptureStateNotifier, atLeast(1)).registerListener(any());

        // Register global callback.
        ISoundTriggerHal.GlobalCallback globalCallback = mock(
                ISoundTriggerHal.GlobalCallback.class);
        mCanonical.registerCallback(globalCallback);

        // Load (this registers the callback).
        ISoundTriggerHal.ModelCallback canonicalCallback = mock(
                ISoundTriggerHal.ModelCallback.class);
        final int handle = loadGenericModel(canonicalCallback);

        // Report external capture active.
        mCaptureStateNotifier.setState(true);

        // Then start.
        RecognitionConfig config = TestUtil.createRecognitionConfig();
        try {
            mCanonical.startRecognition(handle, 203, 204, config);
            fail("Expected an exception");
        } catch (RecoverableException e) {
            assertEquals(Status.RESOURCE_CONTENTION, e.errorCode);
        }

        // Deactivate external capture.
        mCaptureStateNotifier.setState(false);

        // Expect a onResourcesAvailable().
        mCanonical.flushCallbacks();
        verify(globalCallback).onResourcesAvailable();
    }

    @Test
    public void testStopRecognition() throws Exception {
        mCanonical.stopRecognition(17);
@@ -675,7 +580,7 @@ public class SoundHw2CompatTest {
    }

    @Test
    public void testGlobalCallback() throws Exception {
    public void testGlobalCallback() {
        testGlobalCallback_2_0();
    }

@@ -803,29 +708,4 @@ public class SoundHw2CompatTest {
        verifyNoMoreInteractions(canonicalCallback);
        clearInvocations(canonicalCallback);
    }

    public static class CaptureStateNotifier implements ICaptureStateNotifier {
        private final List<Listener> mListeners = new LinkedList<>();

        @Override
        public boolean registerListener(Listener listener) {
            mListeners.add(listener);
            return false;
        }

        @Override
        public void unregisterListener(Listener listener) {
            mListeners.remove(listener);
        }

        public void setState(boolean state) {
            for (Listener listener : mListeners) {
                listener.onCaptureStateChange(state);
            }
        }

        public void verifyNoMoreListeners() {
            assertEquals(0, mListeners.size());
        }
    }
}
+313 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -134,7 +134,7 @@ public class SoundTriggerMiddlewareImplTest {
    public void setUp() throws Exception {
        clearInvocations(mHalDriver);
        clearInvocations(mAudioSessionProvider);
        when(mHalDriver.getProperties()).thenReturn(TestUtil.createDefaultProperties(false));
        when(mHalDriver.getProperties()).thenReturn(TestUtil.createDefaultProperties());
        mService = new SoundTriggerMiddlewareImpl(() -> mHalDriver, mAudioSessionProvider);
    }

@@ -156,7 +156,7 @@ public class SoundTriggerMiddlewareImplTest {
        assertEquals(1, allDescriptors.length);

        Properties properties = allDescriptors[0].properties;
        assertEquals(TestUtil.createDefaultProperties(false), properties);
        assertEquals(TestUtil.createDefaultProperties(), properties);
    }

    @Test
+11 −12

File changed.

Preview size limit exceeded, changes collapsed.