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 17 package com.android.test.soundtrigger; 18 19 import android.Manifest; 20 import android.app.Service; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.pm.PackageManager; 26 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel; 27 import android.media.AudioAttributes; 28 import android.media.AudioFormat; 29 import android.media.AudioManager; 30 import android.media.AudioRecord; 31 import android.media.AudioTrack; 32 import android.media.MediaPlayer; 33 import android.media.soundtrigger.SoundTriggerDetector; 34 import android.net.Uri; 35 import android.os.Binder; 36 import android.os.IBinder; 37 import android.util.Log; 38 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.util.HashMap; 44 import java.util.Map; 45 import java.util.Properties; 46 import java.util.Random; 47 import java.util.UUID; 48 49 public 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) { 648 postMessage(mModelInfo.name + " availability changed to: " + status); 649 } 650 651 public void onDetected(SoundTriggerDetector.EventPayload event) { 652 postMessage(mModelInfo.name + " onDetected(): " + eventPayloadToString(event)); 653 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() { 664 postMessage(mModelInfo.name + " onError()"); 665 setModelState(mModelInfo, "Error"); 666 } 667 668 public void onRecognitionPaused() { 669 postMessage(mModelInfo.name + " onRecognitionPaused()"); 670 setModelState(mModelInfo, "Paused"); 671 } 672 673 public void onRecognitionResumed() { 674 postMessage(mModelInfo.name + " onRecognitionResumed()"); 675 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 } 721