1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.hardware.soundtrigger;
18
19import static org.mockito.Matchers.any;
20import static org.mockito.Matchers.eq;
21import static org.mockito.Mockito.reset;
22import static org.mockito.Mockito.spy;
23import static org.mockito.Mockito.timeout;
24import static org.mockito.Mockito.verify;
25
26import android.content.Context;
27import android.hardware.soundtrigger.SoundTrigger.GenericRecognitionEvent;
28import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
29import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent;
30import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
31import android.media.soundtrigger.SoundTriggerManager;
32import android.os.ParcelUuid;
33import android.os.ServiceManager;
34import android.test.AndroidTestCase;
35import android.test.suitebuilder.annotation.LargeTest;
36import android.test.suitebuilder.annotation.SmallTest;
37
38import com.android.internal.app.ISoundTriggerService;
39
40import java.io.DataOutputStream;
41import java.net.InetAddress;
42import java.net.Socket;
43import java.util.ArrayList;
44import java.util.HashSet;
45import java.util.Random;
46import java.util.UUID;
47
48import org.mockito.MockitoAnnotations;
49
50public class GenericSoundModelTest extends AndroidTestCase {
51    static final int MSG_DETECTION_ERROR = -1;
52    static final int MSG_DETECTION_RESUME = 0;
53    static final int MSG_DETECTION_PAUSE = 1;
54    static final int MSG_KEYPHRASE_TRIGGER = 2;
55    static final int MSG_GENERIC_TRIGGER = 4;
56
57    private Random random = new Random();
58    private HashSet<UUID> loadedModelUuids;
59    private ISoundTriggerService soundTriggerService;
60    private SoundTriggerManager soundTriggerManager;
61
62    @Override
63    public void setUp() throws Exception {
64        super.setUp();
65        MockitoAnnotations.initMocks(this);
66
67        Context context = getContext();
68        soundTriggerService = ISoundTriggerService.Stub.asInterface(
69                ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE));
70        soundTriggerManager = (SoundTriggerManager) context.getSystemService(
71                Context.SOUND_TRIGGER_SERVICE);
72
73        loadedModelUuids = new HashSet<UUID>();
74    }
75
76    @Override
77    public void tearDown() throws Exception {
78        for (UUID modelUuid : loadedModelUuids) {
79            soundTriggerService.deleteSoundModel(new ParcelUuid(modelUuid));
80        }
81        super.tearDown();
82    }
83
84    GenericSoundModel new_sound_model() {
85        // Create sound model
86        byte[] data = new byte[1024];
87        random.nextBytes(data);
88        UUID modelUuid = UUID.randomUUID();
89        UUID mVendorUuid = UUID.randomUUID();
90        return new GenericSoundModel(modelUuid, mVendorUuid, data);
91    }
92
93    @SmallTest
94    public void testUpdateGenericSoundModel() throws Exception {
95        GenericSoundModel model = new_sound_model();
96
97        // Update sound model
98        soundTriggerService.updateSoundModel(model);
99        loadedModelUuids.add(model.uuid);
100
101        // Confirm it was updated
102        GenericSoundModel returnedModel =
103                soundTriggerService.getSoundModel(new ParcelUuid(model.uuid));
104        assertEquals(model, returnedModel);
105    }
106
107    @SmallTest
108    public void testDeleteGenericSoundModel() throws Exception {
109        GenericSoundModel model = new_sound_model();
110
111        // Update sound model
112        soundTriggerService.updateSoundModel(model);
113        loadedModelUuids.add(model.uuid);
114
115        // Delete sound model
116        soundTriggerService.deleteSoundModel(new ParcelUuid(model.uuid));
117        loadedModelUuids.remove(model.uuid);
118
119        // Confirm it was deleted
120        GenericSoundModel returnedModel =
121                soundTriggerService.getSoundModel(new ParcelUuid(model.uuid));
122        assertEquals(null, returnedModel);
123    }
124
125    @LargeTest
126    public void testStartStopGenericSoundModel() throws Exception {
127        GenericSoundModel model = new_sound_model();
128
129        boolean captureTriggerAudio = true;
130        boolean allowMultipleTriggers = true;
131        RecognitionConfig config = new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
132                null, null);
133        TestRecognitionStatusCallback spyCallback = spy(new TestRecognitionStatusCallback());
134
135        // Update and start sound model recognition
136        soundTriggerService.updateSoundModel(model);
137        loadedModelUuids.add(model.uuid);
138        int r = soundTriggerService.startRecognition(new ParcelUuid(model.uuid), spyCallback,
139                config);
140        assertEquals("Could Not Start Recognition with code: " + r,
141                android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
142
143        // Stop recognition
144        r = soundTriggerService.stopRecognition(new ParcelUuid(model.uuid), spyCallback);
145        assertEquals("Could Not Stop Recognition with code: " + r,
146                android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
147    }
148
149    @LargeTest
150    public void testTriggerGenericSoundModel() throws Exception {
151        GenericSoundModel model = new_sound_model();
152
153        boolean captureTriggerAudio = true;
154        boolean allowMultipleTriggers = true;
155        RecognitionConfig config = new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
156                null, null);
157        TestRecognitionStatusCallback spyCallback = spy(new TestRecognitionStatusCallback());
158
159        // Update and start sound model
160        soundTriggerService.updateSoundModel(model);
161        loadedModelUuids.add(model.uuid);
162        soundTriggerService.startRecognition(new ParcelUuid(model.uuid), spyCallback, config);
163
164        // Send trigger to stub HAL
165        Socket socket = new Socket(InetAddress.getLocalHost(), 14035);
166        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
167        out.writeBytes("trig " + model.uuid.toString() + "\r\n");
168        out.flush();
169        socket.close();
170
171        // Verify trigger was received
172        verify(spyCallback, timeout(100)).onGenericSoundTriggerDetected(any());
173    }
174
175    /**
176     * Tests a more complicated pattern of loading, unloading, triggering, starting and stopping
177     * recognition. Intended to find unexpected errors that occur in unexpected states.
178     */
179    @LargeTest
180    public void testFuzzGenericSoundModel() throws Exception {
181        int numModels = 2;
182
183        final int STATUS_UNLOADED = 0;
184        final int STATUS_LOADED = 1;
185        final int STATUS_STARTED = 2;
186
187        class ModelInfo {
188            int status;
189            GenericSoundModel model;
190
191            public ModelInfo(GenericSoundModel model, int status) {
192                this.status = status;
193                this.model = model;
194            }
195        }
196
197        Random predictableRandom = new Random(100);
198
199        ArrayList modelInfos = new ArrayList<ModelInfo>();
200        for(int i=0; i<numModels; i++) {
201            // Create sound model
202            byte[] data = new byte[1024];
203            predictableRandom.nextBytes(data);
204            UUID modelUuid = UUID.randomUUID();
205            UUID mVendorUuid = UUID.randomUUID();
206            GenericSoundModel model = new GenericSoundModel(modelUuid, mVendorUuid, data);
207            ModelInfo modelInfo = new ModelInfo(model, STATUS_UNLOADED);
208            modelInfos.add(modelInfo);
209        }
210
211        boolean captureTriggerAudio = true;
212        boolean allowMultipleTriggers = true;
213        RecognitionConfig config = new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
214                null, null);
215        TestRecognitionStatusCallback spyCallback = spy(new TestRecognitionStatusCallback());
216
217
218        int numOperationsToRun = 100;
219        for(int i=0; i<numOperationsToRun; i++) {
220            // Select a random model
221            int modelInfoIndex = predictableRandom.nextInt(modelInfos.size());
222            ModelInfo modelInfo = (ModelInfo) modelInfos.get(modelInfoIndex);
223
224            // Perform a random operation
225            int operation = predictableRandom.nextInt(5);
226
227            if (operation == 0 && modelInfo.status == STATUS_UNLOADED) {
228                // Update and start sound model
229                soundTriggerService.updateSoundModel(modelInfo.model);
230                loadedModelUuids.add(modelInfo.model.uuid);
231                modelInfo.status = STATUS_LOADED;
232            } else if (operation == 1 && modelInfo.status == STATUS_LOADED) {
233                // Start the sound model
234                int r = soundTriggerService.startRecognition(new ParcelUuid(modelInfo.model.uuid),
235                        spyCallback, config);
236                assertEquals("Could Not Start Recognition with code: " + r,
237                        android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
238                modelInfo.status = STATUS_STARTED;
239            } else if (operation == 2 && modelInfo.status == STATUS_STARTED) {
240                // Send trigger to stub HAL
241                Socket socket = new Socket(InetAddress.getLocalHost(), 14035);
242                DataOutputStream out = new DataOutputStream(socket.getOutputStream());
243                out.writeBytes("trig " + modelInfo.model.uuid + "\r\n");
244                out.flush();
245                socket.close();
246
247                // Verify trigger was received
248                verify(spyCallback, timeout(100)).onGenericSoundTriggerDetected(any());
249                reset(spyCallback);
250            } else if (operation == 3 && modelInfo.status == STATUS_STARTED) {
251                // Stop recognition
252                int r = soundTriggerService.stopRecognition(new ParcelUuid(modelInfo.model.uuid),
253                        spyCallback);
254                assertEquals("Could Not Stop Recognition with code: " + r,
255                        android.hardware.soundtrigger.SoundTrigger.STATUS_OK, r);
256                modelInfo.status = STATUS_LOADED;
257            } else if (operation == 4 && modelInfo.status != STATUS_UNLOADED) {
258                // Delete sound model
259                soundTriggerService.deleteSoundModel(new ParcelUuid(modelInfo.model.uuid));
260                loadedModelUuids.remove(modelInfo.model.uuid);
261
262                // Confirm it was deleted
263                GenericSoundModel returnedModel =
264                        soundTriggerService.getSoundModel(new ParcelUuid(modelInfo.model.uuid));
265                assertEquals(null, returnedModel);
266                modelInfo.status = STATUS_UNLOADED;
267            }
268        }
269    }
270
271    public class TestRecognitionStatusCallback extends IRecognitionStatusCallback.Stub {
272        @Override
273        public void onGenericSoundTriggerDetected(GenericRecognitionEvent recognitionEvent) {
274        }
275
276        @Override
277        public void onKeyphraseDetected(KeyphraseRecognitionEvent recognitionEvent) {
278        }
279
280        @Override
281        public void onError(int status) {
282        }
283
284        @Override
285        public void onRecognitionPaused() {
286        }
287
288        @Override
289        public void onRecognitionResumed() {
290        }
291    }
292}
293