blob: a2385d695450a18c606e2cdd22135ef652a4ff7f [file] [log] [blame]
Chris Thorntondfa7c3b2016-06-30 22:05:51 -07001/*
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 com.android.test.soundtrigger;
18
19import android.Manifest;
20import android.app.Service;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.pm.PackageManager;
26import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
27import android.media.AudioAttributes;
28import android.media.AudioFormat;
29import android.media.AudioManager;
30import android.media.AudioRecord;
31import android.media.AudioTrack;
32import android.media.MediaPlayer;
33import android.media.soundtrigger.SoundTriggerDetector;
34import android.net.Uri;
35import android.os.Binder;
36import android.os.IBinder;
37import android.util.Log;
38
39import java.io.File;
40import java.io.FileInputStream;
41import java.io.FileOutputStream;
42import java.io.IOException;
43import java.util.HashMap;
44import java.util.Map;
45import java.util.Properties;
46import java.util.Random;
47import java.util.UUID;
48
49public class SoundTriggerTestService extends Service {
50 private static final String TAG = "SoundTriggerTestSrv";
51 private static final String INTENT_ACTION = "com.android.intent.action.MANAGE_SOUND_TRIGGER";
52
53 // Binder given to clients.
54 private final IBinder mBinder;
55 private final Map<UUID, ModelInfo> mModelInfoMap;
56 private SoundTriggerUtil mSoundTriggerUtil;
57 private Random mRandom;
58 private UserActivity mUserActivity;
59
60 public interface UserActivity {
61 void addModel(UUID modelUuid, String state);
62 void setModelState(UUID modelUuid, String state);
63 void showMessage(String msg, boolean showToast);
64 void handleDetection(UUID modelUuid);
65 }
66
67 public SoundTriggerTestService() {
68 super();
69 mRandom = new Random();
70 mModelInfoMap = new HashMap();
71 mBinder = new SoundTriggerTestBinder();
72 }
73
74 @Override
75 public synchronized int onStartCommand(Intent intent, int flags, int startId) {
76 if (mModelInfoMap.isEmpty()) {
77 mSoundTriggerUtil = new SoundTriggerUtil(this);
78 loadModelsInDataDir();
79 }
80
81 // If we get killed, after returning from here, restart
82 return START_STICKY;
83 }
84
85 @Override
86 public void onCreate() {
87 super.onCreate();
88 IntentFilter filter = new IntentFilter();
89 filter.addAction(INTENT_ACTION);
90 registerReceiver(mBroadcastReceiver, filter);
91
92 // Make sure the data directory exists, and we're the owner of it.
93 try {
94 getFilesDir().mkdir();
95 } catch (Exception e) {
96 // Don't care - we either made it, or it already exists.
97 }
98 }
99
100 @Override
101 public void onDestroy() {
102 super.onDestroy();
103 stopAllRecognitions();
104 unregisterReceiver(mBroadcastReceiver);
105 }
106
107 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
108 @Override
109 public void onReceive(Context context, Intent intent) {
110 if (intent != null && INTENT_ACTION.equals(intent.getAction())) {
111 String command = intent.getStringExtra("command");
112 if (command == null) {
113 Log.e(TAG, "No 'command' specified in " + INTENT_ACTION);
114 } else {
115 try {
116 if (command.equals("load")) {
117 loadModel(getModelUuidFromIntent(intent));
118 } else if (command.equals("unload")) {
119 unloadModel(getModelUuidFromIntent(intent));
120 } else if (command.equals("start")) {
121 startRecognition(getModelUuidFromIntent(intent));
122 } else if (command.equals("stop")) {
123 stopRecognition(getModelUuidFromIntent(intent));
124 } else if (command.equals("play_trigger")) {
125 playTriggerAudio(getModelUuidFromIntent(intent));
126 } else if (command.equals("play_captured")) {
127 playCapturedAudio(getModelUuidFromIntent(intent));
128 } else if (command.equals("set_capture")) {
129 setCaptureAudio(getModelUuidFromIntent(intent),
130 intent.getBooleanExtra("enabled", true));
131 } else if (command.equals("set_capture_timeout")) {
132 setCaptureAudioTimeout(getModelUuidFromIntent(intent),
133 intent.getIntExtra("timeout", 5000));
134 } else {
135 Log.e(TAG, "Unknown command '" + command + "'");
136 }
137 } catch (Exception e) {
138 Log.e(TAG, "Failed to process " + command, e);
139 }
140 }
141 }
142 }
143 };
144
145 private UUID getModelUuidFromIntent(Intent intent) {
146 // First, see if the specified the UUID straight up.
147 String value = intent.getStringExtra("modelUuid");
148 if (value != null) {
149 return UUID.fromString(value);
150 }
151
152 // If they specified a name, use that to iterate through the map of models and find it.
153 value = intent.getStringExtra("name");
154 if (value != null) {
155 for (ModelInfo modelInfo : mModelInfoMap.values()) {
156 if (value.equals(modelInfo.name)) {
157 return modelInfo.modelUuid;
158 }
159 }
160 Log.e(TAG, "Failed to find a matching model with name '" + value + "'");
161 }
162
163 // We couldn't figure out what they were asking for.
164 throw new RuntimeException("Failed to get model from intent - specify either " +
165 "'modelUuid' or 'name'");
166 }
167
168 /**
169 * Will be called when the service is killed (through swipe aways, not if we're force killed).
170 */
171 @Override
172 public void onTaskRemoved(Intent rootIntent) {
173 super.onTaskRemoved(rootIntent);
174 stopAllRecognitions();
175 stopSelf();
176 }
177
178 @Override
179 public synchronized IBinder onBind(Intent intent) {
180 return mBinder;
181 }
182
183 public class SoundTriggerTestBinder extends Binder {
184 SoundTriggerTestService getService() {
185 // Return instance of our parent so clients can call public methods.
186 return SoundTriggerTestService.this;
187 }
188 }
189
190 public synchronized void setUserActivity(UserActivity activity) {
191 mUserActivity = activity;
192 if (mUserActivity != null) {
193 for (Map.Entry<UUID, ModelInfo> entry : mModelInfoMap.entrySet()) {
194 mUserActivity.addModel(entry.getKey(), entry.getValue().name);
195 mUserActivity.setModelState(entry.getKey(), entry.getValue().state);
196 }
197 }
198 }
199
200 private synchronized void stopAllRecognitions() {
201 for (ModelInfo modelInfo : mModelInfoMap.values()) {
202 if (modelInfo.detector != null) {
203 Log.i(TAG, "Stopping recognition for " + modelInfo.name);
204 try {
205 modelInfo.detector.stopRecognition();
206 } catch (Exception e) {
207 Log.e(TAG, "Failed to stop recognition", e);
208 }
209 }
210 }
211 }
212
213 // Helper struct for holding information about a model.
214 public static class ModelInfo {
215 public String name;
216 public String state;
217 public UUID modelUuid;
218 public UUID vendorUuid;
219 public MediaPlayer triggerAudioPlayer;
220 public SoundTriggerDetector detector;
221 public byte modelData[];
222 public boolean captureAudio;
223 public int captureAudioMs;
224 public AudioTrack captureAudioTrack;
225 }
226
227 private GenericSoundModel createNewSoundModel(ModelInfo modelInfo) {
228 return new GenericSoundModel(modelInfo.modelUuid, modelInfo.vendorUuid,
229 modelInfo.modelData);
230 }
231
232 public synchronized void loadModel(UUID modelUuid) {
233 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
234 if (modelInfo == null) {
235 postError("Could not find model for: " + modelUuid.toString());
236 return;
237 }
238
239 postMessage("Loading model: " + modelInfo.name);
240
241 GenericSoundModel soundModel = createNewSoundModel(modelInfo);
242
243 boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(soundModel);
244 if (status) {
245 postToast("Successfully loaded " + modelInfo.name + ", UUID=" + soundModel.uuid);
246 setModelState(modelInfo, "Loaded");
247 } else {
248 postErrorToast("Failed to load " + modelInfo.name + ", UUID=" + soundModel.uuid + "!");
249 setModelState(modelInfo, "Failed to load");
250 }
251 }
252
253 public synchronized void unloadModel(UUID modelUuid) {
254 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
255 if (modelInfo == null) {
256 postError("Could not find model for: " + modelUuid.toString());
257 return;
258 }
259
260 postMessage("Unloading model: " + modelInfo.name);
261
262 GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
263 if (soundModel == null) {
264 postErrorToast("Sound model not found for " + modelInfo.name + "!");
265 return;
266 }
267 modelInfo.detector = null;
268 boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid);
269 if (status) {
270 postToast("Successfully unloaded " + modelInfo.name + ", UUID=" + soundModel.uuid);
271 setModelState(modelInfo, "Unloaded");
272 } else {
273 postErrorToast("Failed to unload " +
274 modelInfo.name + ", UUID=" + soundModel.uuid + "!");
275 setModelState(modelInfo, "Failed to unload");
276 }
277 }
278
279 public synchronized void reloadModel(UUID modelUuid) {
280 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
281 if (modelInfo == null) {
282 postError("Could not find model for: " + modelUuid.toString());
283 return;
284 }
285 postMessage("Reloading model: " + modelInfo.name);
286 GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
287 if (soundModel == null) {
288 postErrorToast("Sound model not found for " + modelInfo.name + "!");
289 return;
290 }
291 GenericSoundModel updated = createNewSoundModel(modelInfo);
292 boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated);
293 if (status) {
294 postToast("Successfully reloaded " + modelInfo.name + ", UUID=" + modelInfo.modelUuid);
295 setModelState(modelInfo, "Reloaded");
296 } else {
297 postErrorToast("Failed to reload "
298 + modelInfo.name + ", UUID=" + modelInfo.modelUuid + "!");
299 setModelState(modelInfo, "Failed to reload");
300 }
301 }
302
303 public synchronized void startRecognition(UUID modelUuid) {
304 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
305 if (modelInfo == null) {
306 postError("Could not find model for: " + modelUuid.toString());
307 return;
308 }
309
310 if (modelInfo.detector == null) {
311 postMessage("Creating SoundTriggerDetector for " + modelInfo.name);
312 modelInfo.detector = mSoundTriggerUtil.createSoundTriggerDetector(
313 modelUuid, new DetectorCallback(modelInfo));
314 }
315
316 postMessage("Starting recognition for " + modelInfo.name + ", UUID=" + modelInfo.modelUuid);
317 if (modelInfo.detector.startRecognition(modelInfo.captureAudio ?
318 SoundTriggerDetector.RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO :
319 SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) {
320 setModelState(modelInfo, "Started");
321 } else {
322 postErrorToast("Fast failure attempting to start recognition for " +
323 modelInfo.name + ", UUID=" + modelInfo.modelUuid);
324 setModelState(modelInfo, "Failed to start");
325 }
326 }
327
328 public synchronized void stopRecognition(UUID modelUuid) {
329 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
330 if (modelInfo == null) {
331 postError("Could not find model for: " + modelUuid.toString());
332 return;
333 }
334
335 if (modelInfo.detector == null) {
336 postErrorToast("Stop called on null detector for " +
337 modelInfo.name + ", UUID=" + modelInfo.modelUuid);
338 return;
339 }
340 postMessage("Triggering stop recognition for " +
341 modelInfo.name + ", UUID=" + modelInfo.modelUuid);
342 if (modelInfo.detector.stopRecognition()) {
343 setModelState(modelInfo, "Stopped");
344 } else {
345 postErrorToast("Fast failure attempting to stop recognition for " +
346 modelInfo.name + ", UUID=" + modelInfo.modelUuid);
347 setModelState(modelInfo, "Failed to stop");
348 }
349 }
350
351 public synchronized void playTriggerAudio(UUID modelUuid) {
352 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
353 if (modelInfo == null) {
354 postError("Could not find model for: " + modelUuid.toString());
355 return;
356 }
357 if (modelInfo.triggerAudioPlayer != null) {
358 postMessage("Playing trigger audio for " + modelInfo.name);
359 modelInfo.triggerAudioPlayer.start();
360 } else {
361 postMessage("No trigger audio for " + modelInfo.name);
362 }
363 }
364
365 public synchronized void playCapturedAudio(UUID modelUuid) {
366 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
367 if (modelInfo == null) {
368 postError("Could not find model for: " + modelUuid.toString());
369 return;
370 }
371 if (modelInfo.captureAudioTrack != null) {
372 postMessage("Playing captured audio for " + modelInfo.name);
373 modelInfo.captureAudioTrack.stop();
374 modelInfo.captureAudioTrack.reloadStaticData();
375 modelInfo.captureAudioTrack.play();
376 } else {
377 postMessage("No captured audio for " + modelInfo.name);
378 }
379 }
380
381 public synchronized void setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs) {
382 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
383 if (modelInfo == null) {
384 postError("Could not find model for: " + modelUuid.toString());
385 return;
386 }
387 modelInfo.captureAudioMs = captureTimeoutMs;
388 Log.i(TAG, "Set " + modelInfo.name + " capture audio timeout to " +
389 captureTimeoutMs + "ms");
390 }
391
392 public synchronized void setCaptureAudio(UUID modelUuid, boolean captureAudio) {
393 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
394 if (modelInfo == null) {
395 postError("Could not find model for: " + modelUuid.toString());
396 return;
397 }
398 modelInfo.captureAudio = captureAudio;
399 Log.i(TAG, "Set " + modelInfo.name + " capture audio to " + captureAudio);
400 }
401
402 public synchronized boolean hasMicrophonePermission() {
403 return getBaseContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO)
404 == PackageManager.PERMISSION_GRANTED;
405 }
406
407 public synchronized boolean modelHasTriggerAudio(UUID modelUuid) {
408 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
409 return modelInfo != null && modelInfo.triggerAudioPlayer != null;
410 }
411
412 public synchronized boolean modelWillCaptureTriggerAudio(UUID modelUuid) {
413 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
414 return modelInfo != null && modelInfo.captureAudio;
415 }
416
417 public synchronized boolean modelHasCapturedAudio(UUID modelUuid) {
418 ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
419 return modelInfo != null && modelInfo.captureAudioTrack != null;
420 }
421
422 private void loadModelsInDataDir() {
423 // Load all the models in the data dir.
424 boolean loadedModel = false;
425 for (File file : getFilesDir().listFiles()) {
426 // Find meta-data in .properties files, ignore everything else.
427 if (!file.getName().endsWith(".properties")) {
428 continue;
429 }
430 try {
431 Properties properties = new Properties();
432 properties.load(new FileInputStream(file));
433 createModelInfo(properties);
434 loadedModel = true;
435 } catch (Exception e) {
436 Log.e(TAG, "Failed to load properties file " + file.getName());
437 }
438 }
439
440 // Create a few dummy models if we didn't load anything.
441 if (!loadedModel) {
442 Properties dummyModelProperties = new Properties();
443 for (String name : new String[]{"1", "2", "3"}) {
444 dummyModelProperties.setProperty("name", "Model " + name);
445 createModelInfo(dummyModelProperties);
446 }
447 }
448 }
449
450 /** Parses a Properties collection to generate a sound model.
451 *
452 * Missing keys are filled in with default/random values.
453 * @param properties Has the required 'name' property, but the remaining 'modelUuid',
454 * 'vendorUuid', 'triggerAudio', and 'dataFile' optional properties.
455 *
456 */
457 private synchronized void createModelInfo(Properties properties) {
458 try {
459 ModelInfo modelInfo = new ModelInfo();
460
461 if (!properties.containsKey("name")) {
462 throw new RuntimeException("must have a 'name' property");
463 }
464 modelInfo.name = properties.getProperty("name");
465
466 if (properties.containsKey("modelUuid")) {
467 modelInfo.modelUuid = UUID.fromString(properties.getProperty("modelUuid"));
468 } else {
469 modelInfo.modelUuid = UUID.randomUUID();
470 }
471
472 if (properties.containsKey("vendorUuid")) {
473 modelInfo.vendorUuid = UUID.fromString(properties.getProperty("vendorUuid"));
474 } else {
475 modelInfo.vendorUuid = UUID.randomUUID();
476 }
477
478 if (properties.containsKey("triggerAudio")) {
479 modelInfo.triggerAudioPlayer = MediaPlayer.create(this, Uri.parse(
480 getFilesDir().getPath() + "/" + properties.getProperty("triggerAudio")));
481 if (modelInfo.triggerAudioPlayer.getDuration() == 0) {
482 modelInfo.triggerAudioPlayer.release();
483 modelInfo.triggerAudioPlayer = null;
484 }
485 }
486
487 if (properties.containsKey("dataFile")) {
488 File modelDataFile = new File(
489 getFilesDir().getPath() + "/" + properties.getProperty("dataFile"));
490 modelInfo.modelData = new byte[(int) modelDataFile.length()];
491 FileInputStream input = new FileInputStream(modelDataFile);
492 input.read(modelInfo.modelData, 0, modelInfo.modelData.length);
493 } else {
494 modelInfo.modelData = new byte[1024];
495 mRandom.nextBytes(modelInfo.modelData);
496 }
497
498 modelInfo.captureAudioMs = Integer.parseInt((String) properties.getOrDefault(
499 "captureAudioDurationMs", "5000"));
500
501 // TODO: Add property support for keyphrase models when they're exposed by the
502 // service.
503
504 // Update our maps containing the button -> id and id -> modelInfo.
505 mModelInfoMap.put(modelInfo.modelUuid, modelInfo);
506 if (mUserActivity != null) {
507 mUserActivity.addModel(modelInfo.modelUuid, modelInfo.name);
508 mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state);
509 }
510 } catch (IOException e) {
511 Log.e(TAG, "Error parsing properties for " + properties.getProperty("name"), e);
512 }
513 }
514
515 private class CaptureAudioRecorder implements Runnable {
516 private final ModelInfo mModelInfo;
517 private final SoundTriggerDetector.EventPayload mEvent;
518
519 public CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event) {
520 mModelInfo = modelInfo;
521 mEvent = event;
522 }
523
524 @Override
525 public void run() {
526 AudioFormat format = mEvent.getCaptureAudioFormat();
527 if (format == null) {
528 postErrorToast("No audio format in recognition event.");
529 return;
530 }
531
532 AudioRecord audioRecord = null;
533 AudioTrack playbackTrack = null;
534 try {
535 // Inform the audio flinger that we really do want the stream from the soundtrigger.
536 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
537 attributesBuilder.setInternalCapturePreset(1999);
538 AudioAttributes attributes = attributesBuilder.build();
539
540 // Make sure we understand this kind of playback so we know how many bytes to read.
541 String encoding;
542 int bytesPerSample;
543 switch (format.getEncoding()) {
544 case AudioFormat.ENCODING_PCM_8BIT:
545 encoding = "8bit";
546 bytesPerSample = 1;
547 break;
548 case AudioFormat.ENCODING_PCM_16BIT:
549 encoding = "16bit";
550 bytesPerSample = 2;
551 break;
552 case AudioFormat.ENCODING_PCM_FLOAT:
553 encoding = "float";
554 bytesPerSample = 4;
555 break;
556 default:
557 throw new RuntimeException("Unhandled audio format in event");
558 }
559
560 int bytesRequired = format.getSampleRate() * format.getChannelCount() *
561 bytesPerSample * mModelInfo.captureAudioMs / 1000;
562 int minBufferSize = AudioRecord.getMinBufferSize(
563 format.getSampleRate(), format.getChannelMask(), format.getEncoding());
564 if (minBufferSize > bytesRequired) {
565 bytesRequired = minBufferSize;
566 }
567
568 // Make an AudioTrack so we can play the data back out after it's finished
569 // recording.
570 try {
571 int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
572 if (format.getChannelCount() == 2) {
573 channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
574 } else if (format.getChannelCount() >= 3) {
575 throw new RuntimeException(
576 "Too many channels in captured audio for playback");
577 }
578
579 playbackTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
580 format.getSampleRate(), channelConfig, format.getEncoding(),
581 bytesRequired, AudioTrack.MODE_STATIC);
582 } catch (Exception e) {
583 Log.e(TAG, "Exception creating playback track", e);
584 postErrorToast("Failed to create playback track: " + e.getMessage());
585 }
586
587 audioRecord = new AudioRecord(attributes, format, bytesRequired,
588 mEvent.getCaptureSession());
589
590 byte[] buffer = new byte[bytesRequired];
591
592 // Create a file so we can save the output data there for analysis later.
593 FileOutputStream fos = null;
594 try {
595 fos = new FileOutputStream( new File(
596 getFilesDir() + File.separator + mModelInfo.name.replace(' ', '_') +
597 "_capture_" + format.getChannelCount() + "ch_" +
598 format.getSampleRate() + "hz_" + encoding + ".pcm"));
599 } catch (IOException e) {
600 Log.e(TAG, "Failed to open output for saving PCM data", e);
601 postErrorToast("Failed to open output for saving PCM data: " + e.getMessage());
602 }
603
604 // Inform the user we're recording.
605 setModelState(mModelInfo, "Recording");
606 audioRecord.startRecording();
607 while (bytesRequired > 0) {
608 int bytesRead = audioRecord.read(buffer, 0, buffer.length);
609 if (bytesRead == -1) {
610 break;
611 }
612 if (fos != null) {
613 fos.write(buffer, 0, bytesRead);
614 }
615 if (playbackTrack != null) {
616 playbackTrack.write(buffer, 0, bytesRead);
617 }
618 bytesRequired -= bytesRead;
619 }
620 audioRecord.stop();
621 } catch (Exception e) {
622 Log.e(TAG, "Error recording trigger audio", e);
623 postErrorToast("Error recording trigger audio: " + e.getMessage());
624 } finally {
625 if (audioRecord != null) {
626 audioRecord.release();
627 }
628 synchronized (SoundTriggerTestService.this) {
629 if (mModelInfo.captureAudioTrack != null) {
630 mModelInfo.captureAudioTrack.release();
631 }
632 mModelInfo.captureAudioTrack = playbackTrack;
633 }
634 setModelState(mModelInfo, "Recording finished");
635 }
636 }
637 }
638
639 // Implementation of SoundTriggerDetector.Callback.
640 private class DetectorCallback extends SoundTriggerDetector.Callback {
641 private final ModelInfo mModelInfo;
642
643 public DetectorCallback(ModelInfo modelInfo) {
644 mModelInfo = modelInfo;
645 }
646
647 public void onAvailabilityChanged(int status) {
Chris Thornton2d2ba9d02016-07-12 15:05:18 -0700648 postMessage(mModelInfo.name + " availability changed to: " + status);
Chris Thorntondfa7c3b2016-06-30 22:05:51 -0700649 }
650
651 public void onDetected(SoundTriggerDetector.EventPayload event) {
Chris Thornton2d2ba9d02016-07-12 15:05:18 -0700652 postMessage(mModelInfo.name + " onDetected(): " + eventPayloadToString(event));
Chris Thorntondfa7c3b2016-06-30 22:05:51 -0700653 synchronized (SoundTriggerTestService.this) {
654 if (mUserActivity != null) {
655 mUserActivity.handleDetection(mModelInfo.modelUuid);
656 }
657 if (mModelInfo.captureAudio) {
658 new Thread(new CaptureAudioRecorder(mModelInfo, event)).start();
659 }
660 }
661 }
662
663 public void onError() {
Chris Thornton2d2ba9d02016-07-12 15:05:18 -0700664 postMessage(mModelInfo.name + " onError()");
Chris Thorntondfa7c3b2016-06-30 22:05:51 -0700665 setModelState(mModelInfo, "Error");
666 }
667
668 public void onRecognitionPaused() {
669 postMessage(mModelInfo.name + " onRecognitionPaused()");
670 setModelState(mModelInfo, "Paused");
671 }
672
673 public void onRecognitionResumed() {
Chris Thornton2d2ba9d02016-07-12 15:05:18 -0700674 postMessage(mModelInfo.name + " onRecognitionResumed()");
Chris Thorntondfa7c3b2016-06-30 22:05:51 -0700675 setModelState(mModelInfo, "Resumed");
676 }
677 }
678
679 private String eventPayloadToString(SoundTriggerDetector.EventPayload event) {
680 String result = "EventPayload(";
681 AudioFormat format = event.getCaptureAudioFormat();
682 result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString());
683 byte[] triggerAudio = event.getTriggerAudio();
684 result = result + "TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length);
685 result = result + "CaptureSession: " + event.getCaptureSession();
686 result += " )";
687 return result;
688 }
689
690 private void postMessage(String msg) {
691 showMessage(msg, Log.INFO, false);
692 }
693
694 private void postError(String msg) {
695 showMessage(msg, Log.ERROR, false);
696 }
697
698 private void postToast(String msg) {
699 showMessage(msg, Log.INFO, true);
700 }
701
702 private void postErrorToast(String msg) {
703 showMessage(msg, Log.ERROR, true);
704 }
705
706 /** Logs the message at the specified level, then forwards it to the activity if present. */
707 private synchronized void showMessage(String msg, int logLevel, boolean showToast) {
708 Log.println(logLevel, TAG, msg);
709 if (mUserActivity != null) {
710 mUserActivity.showMessage(msg, showToast);
711 }
712 }
713
714 private synchronized void setModelState(ModelInfo modelInfo, String state) {
715 modelInfo.state = state;
716 if (mUserActivity != null) {
717 mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state);
718 }
719 }
720}