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 java.io.File; 20 import java.io.FileInputStream; 21 import java.io.IOException; 22 import java.util.HashMap; 23 import java.util.Map; 24 import java.util.Properties; 25 import java.util.Random; 26 import java.util.UUID; 27 28 import android.app.Activity; 29 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel; 30 import android.hardware.soundtrigger.SoundTrigger; 31 import android.media.AudioFormat; 32 import android.media.AudioManager; 33 import android.media.MediaPlayer; 34 import android.media.soundtrigger.SoundTriggerDetector; 35 import android.media.soundtrigger.SoundTriggerManager; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.PowerManager; 40 import android.os.UserManager; 41 import android.text.Editable; 42 import android.text.method.ScrollingMovementMethod; 43 import android.util.Log; 44 import android.view.View; 45 import android.widget.Button; 46 import android.widget.RadioButton; 47 import android.widget.RadioButton; 48 import android.widget.RadioGroup; 49 import android.widget.ScrollView; 50 import android.widget.TextView; 51 import android.widget.Toast; 52 53 public class TestSoundTriggerActivity extends Activity { 54 private static final String TAG = "TestSoundTriggerActivity"; 55 private static final boolean DBG = false; 56 57 private SoundTriggerUtil mSoundTriggerUtil; 58 private Random mRandom; 59 60 private Map<Integer, ModelInfo> mModelInfoMap; 61 private Map<View, Integer> mModelIdMap; 62 63 private TextView mDebugView = null; 64 private int mSelectedModelId = -1; 65 private ScrollView mScrollView = null; 66 private Button mPlayTriggerButton = null; 67 private PowerManager.WakeLock mScreenWakelock; 68 private Handler mHandler; 69 private RadioGroup mRadioGroup; 70 71 @Override onCreate(Bundle savedInstanceState)72 protected void onCreate(Bundle savedInstanceState) { 73 if (DBG) Log.d(TAG, "onCreate"); 74 super.onCreate(savedInstanceState); 75 setContentView(R.layout.main); 76 mDebugView = (TextView) findViewById(R.id.console); 77 mScrollView = (ScrollView) findViewById(R.id.scroller_id); 78 mRadioGroup = (RadioGroup) findViewById(R.id.model_group_id); 79 mPlayTriggerButton = (Button) findViewById(R.id.play_trigger_id); 80 mDebugView.setText(mDebugView.getText(), TextView.BufferType.EDITABLE); 81 mDebugView.setMovementMethod(new ScrollingMovementMethod()); 82 mSoundTriggerUtil = new SoundTriggerUtil(this); 83 mRandom = new Random(); 84 mHandler = new Handler(); 85 86 mModelInfoMap = new HashMap(); 87 mModelIdMap = new HashMap(); 88 89 setVolumeControlStream(AudioManager.STREAM_MUSIC); 90 91 // Load all the models in the data dir. 92 for (File file : getFilesDir().listFiles()) { 93 // Find meta-data in .properties files, ignore everything else. 94 if (!file.getName().endsWith(".properties")) { 95 continue; 96 } 97 try { 98 Properties properties = new Properties(); 99 properties.load(new FileInputStream(file)); 100 createModelInfoAndWidget(properties); 101 } catch (Exception e) { 102 Log.e(TAG, "Failed to load properties file " + file.getName()); 103 } 104 } 105 106 // Create a few dummy models if we didn't load anything. 107 if (mModelIdMap.isEmpty()) { 108 Properties dummyModelProperties = new Properties(); 109 for (String name : new String[]{"One", "Two", "Three"}) { 110 dummyModelProperties.setProperty("name", "Model " + name); 111 createModelInfoAndWidget(dummyModelProperties); 112 } 113 } 114 } 115 createModelInfoAndWidget(Properties properties)116 private void createModelInfoAndWidget(Properties properties) { 117 try { 118 ModelInfo modelInfo = new ModelInfo(); 119 120 if (!properties.containsKey("name")) { 121 throw new RuntimeException("must have a 'name' property"); 122 } 123 modelInfo.name = properties.getProperty("name"); 124 125 if (properties.containsKey("modelUuid")) { 126 modelInfo.modelUuid = UUID.fromString(properties.getProperty("modelUuid")); 127 } else { 128 modelInfo.modelUuid = UUID.randomUUID(); 129 } 130 131 if (properties.containsKey("vendorUuid")) { 132 modelInfo.vendorUuid = UUID.fromString(properties.getProperty("vendorUuid")); 133 } else { 134 modelInfo.vendorUuid = UUID.randomUUID(); 135 } 136 137 if (properties.containsKey("triggerAudio")) { 138 modelInfo.triggerAudioPlayer = MediaPlayer.create(this, Uri.parse( 139 getFilesDir().getPath() + "/" + properties.getProperty("triggerAudio"))); 140 } 141 142 if (properties.containsKey("dataFile")) { 143 File modelDataFile = new File( 144 getFilesDir().getPath() + "/" + properties.getProperty("dataFile")); 145 modelInfo.modelData = new byte[(int) modelDataFile.length()]; 146 FileInputStream input = new FileInputStream(modelDataFile); 147 input.read(modelInfo.modelData, 0, modelInfo.modelData.length); 148 } else { 149 modelInfo.modelData = new byte[1024]; 150 mRandom.nextBytes(modelInfo.modelData); 151 } 152 153 // TODO: Add property support for keyphrase models when they're exposed by the 154 // service. Also things like how much audio they should record with the capture session 155 // provided in the callback. 156 157 // Add a widget into the radio group. 158 RadioButton button = new RadioButton(this); 159 mRadioGroup.addView(button); 160 button.setText(modelInfo.name); 161 button.setOnClickListener(new View.OnClickListener() { 162 public void onClick(View v) { 163 onRadioButtonClicked(v); 164 } 165 }); 166 167 // Update our maps containing the button -> id and id -> modelInfo. 168 int newModelId = mModelIdMap.size() + 1; 169 mModelIdMap.put(button, newModelId); 170 mModelInfoMap.put(newModelId, modelInfo); 171 172 // If we don't have something selected, select this first thing. 173 if (mSelectedModelId < 0) { 174 button.setChecked(true); 175 onRadioButtonClicked(button); 176 } 177 } catch (IOException e) { 178 Log.e(TAG, "Error parsing properties for " + properties.getProperty("name"), e); 179 } 180 } 181 postMessage(String msg)182 private void postMessage(String msg) { 183 Log.i(TAG, "Posted: " + msg); 184 ((Editable) mDebugView.getText()).append(msg + "\n"); 185 if ((mDebugView.getMeasuredHeight() - mScrollView.getScrollY()) <= 186 (mScrollView.getHeight() + mDebugView.getLineHeight())) { 187 scrollToBottom(); 188 } 189 } 190 scrollToBottom()191 private void scrollToBottom() { 192 mScrollView.post(new Runnable() { 193 public void run() { 194 mScrollView.smoothScrollTo(0, mDebugView.getBottom()); 195 } 196 }); 197 } 198 getSelectedUuid()199 private synchronized UUID getSelectedUuid() { 200 return mModelInfoMap.get(mSelectedModelId).modelUuid; 201 } 202 setDetector(SoundTriggerDetector detector)203 private synchronized void setDetector(SoundTriggerDetector detector) { 204 mModelInfoMap.get(mSelectedModelId).detector = detector; 205 } 206 getDetector()207 private synchronized SoundTriggerDetector getDetector() { 208 return mModelInfoMap.get(mSelectedModelId).detector; 209 } 210 screenWakeup()211 private void screenWakeup() { 212 PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE)); 213 if (mScreenWakelock == null) { 214 mScreenWakelock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "TAG"); 215 } 216 mScreenWakelock.acquire(); 217 } 218 screenRelease()219 private void screenRelease() { 220 PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE)); 221 mScreenWakelock.release(); 222 } 223 224 /** TODO: Should return the abstract sound model that can be then sent to the service. */ createNewSoundModel()225 private GenericSoundModel createNewSoundModel() { 226 ModelInfo modelInfo = mModelInfoMap.get(mSelectedModelId); 227 return new GenericSoundModel(modelInfo.modelUuid, modelInfo.vendorUuid, 228 modelInfo.modelData); 229 } 230 231 /** 232 * Called when the user clicks the enroll button. 233 * Performs a fresh enrollment. 234 */ onEnrollButtonClicked(View v)235 public void onEnrollButtonClicked(View v) { 236 postMessage("Loading model: " + mSelectedModelId); 237 238 GenericSoundModel model = createNewSoundModel(); 239 240 boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(model); 241 if (status) { 242 Toast.makeText( 243 this, "Successfully created sound trigger model UUID=" + model.uuid, 244 Toast.LENGTH_SHORT).show(); 245 } else { 246 Toast.makeText(this, "Failed to enroll!!!" + model.uuid, Toast.LENGTH_SHORT).show(); 247 } 248 249 // Test the SoundManager API. 250 } 251 252 /** 253 * Called when the user clicks the un-enroll button. 254 * Clears the enrollment information for the user. 255 */ onUnEnrollButtonClicked(View v)256 public void onUnEnrollButtonClicked(View v) { 257 postMessage("Unloading model: " + mSelectedModelId); 258 UUID modelUuid = getSelectedUuid(); 259 GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid); 260 if (soundModel == null) { 261 Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show(); 262 return; 263 } 264 boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid); 265 if (status) { 266 Toast.makeText(this, "Successfully deleted model UUID=" + soundModel.uuid, 267 Toast.LENGTH_SHORT) 268 .show(); 269 } else { 270 Toast.makeText(this, "Failed to delete sound model!!!", Toast.LENGTH_SHORT).show(); 271 } 272 } 273 274 /** 275 * Called when the user clicks the re-enroll button. 276 * Uses the previously enrolled sound model and makes changes to it before pushing it back. 277 */ onReEnrollButtonClicked(View v)278 public void onReEnrollButtonClicked(View v) { 279 postMessage("Re-loading model: " + mSelectedModelId); 280 UUID modelUuid = getSelectedUuid(); 281 GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid); 282 if (soundModel == null) { 283 Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show(); 284 return; 285 } 286 GenericSoundModel updated = createNewSoundModel(); 287 boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated); 288 if (status) { 289 Toast.makeText(this, "Successfully re-enrolled, model UUID=" + updated.uuid, 290 Toast.LENGTH_SHORT) 291 .show(); 292 } else { 293 Toast.makeText(this, "Failed to re-enroll!!!", Toast.LENGTH_SHORT).show(); 294 } 295 } 296 onStartRecognitionButtonClicked(View v)297 public void onStartRecognitionButtonClicked(View v) { 298 UUID modelUuid = getSelectedUuid(); 299 SoundTriggerDetector detector = getDetector(); 300 if (detector == null) { 301 Log.i(TAG, "Created an instance of the SoundTriggerDetector for model #" + 302 mSelectedModelId); 303 postMessage("Created an instance of the SoundTriggerDetector for model #" + 304 mSelectedModelId); 305 detector = mSoundTriggerUtil.createSoundTriggerDetector(modelUuid, 306 new DetectorCallback()); 307 setDetector(detector); 308 } 309 postMessage("Triggering start recognition for model: " + mSelectedModelId); 310 if (!detector.startRecognition( 311 SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) { 312 Log.e(TAG, "Fast failure attempting to start recognition."); 313 postMessage("Fast failure attempting to start recognition:" + mSelectedModelId); 314 } 315 } 316 onStopRecognitionButtonClicked(View v)317 public void onStopRecognitionButtonClicked(View v) { 318 SoundTriggerDetector detector = getDetector(); 319 if (detector == null) { 320 Log.e(TAG, "Stop called on null detector."); 321 postMessage("Error: Stop called on null detector."); 322 return; 323 } 324 postMessage("Triggering stop recognition for model: " + mSelectedModelId); 325 if (!detector.stopRecognition()) { 326 Log.e(TAG, "Fast failure attempting to stop recognition."); 327 postMessage("Fast failure attempting to stop recognition: " + mSelectedModelId); 328 } 329 } 330 onRadioButtonClicked(View view)331 public synchronized void onRadioButtonClicked(View view) { 332 // Is the button now checked? 333 boolean checked = ((RadioButton) view).isChecked(); 334 if (checked) { 335 mSelectedModelId = mModelIdMap.get(view); 336 ModelInfo modelInfo = mModelInfoMap.get(mSelectedModelId); 337 postMessage("Selected " + modelInfo.name); 338 339 // Set the play trigger button to be enabled only if we actually have some audio. 340 mPlayTriggerButton.setEnabled(modelInfo.triggerAudioPlayer != null); 341 } 342 } 343 onPlayTriggerButtonClicked(View v)344 public synchronized void onPlayTriggerButtonClicked(View v) { 345 ModelInfo modelInfo = mModelInfoMap.get(mSelectedModelId); 346 modelInfo.triggerAudioPlayer.start(); 347 postMessage("Playing trigger audio for " + modelInfo.name); 348 } 349 350 // Helper struct for holding information about a model. 351 private static class ModelInfo { 352 public String name; 353 public UUID modelUuid; 354 public UUID vendorUuid; 355 public MediaPlayer triggerAudioPlayer; 356 public SoundTriggerDetector detector; 357 public byte modelData[]; 358 }; 359 360 // Implementation of SoundTriggerDetector.Callback. 361 public class DetectorCallback extends SoundTriggerDetector.Callback { onAvailabilityChanged(int status)362 public void onAvailabilityChanged(int status) { 363 postMessage("Availability changed to: " + status); 364 } 365 onDetected(SoundTriggerDetector.EventPayload event)366 public void onDetected(SoundTriggerDetector.EventPayload event) { 367 postMessage("onDetected(): " + eventPayloadToString(event)); 368 screenWakeup(); 369 mHandler.postDelayed(new Runnable() { 370 @Override 371 public void run() { 372 screenRelease(); 373 } 374 }, 1000L); 375 } 376 onError()377 public void onError() { 378 postMessage("onError()"); 379 } 380 onRecognitionPaused()381 public void onRecognitionPaused() { 382 postMessage("onRecognitionPaused()"); 383 } 384 onRecognitionResumed()385 public void onRecognitionResumed() { 386 postMessage("onRecognitionResumed()"); 387 } 388 } 389 eventPayloadToString(SoundTriggerDetector.EventPayload event)390 private String eventPayloadToString(SoundTriggerDetector.EventPayload event) { 391 String result = "EventPayload("; 392 AudioFormat format = event.getCaptureAudioFormat(); 393 result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString()); 394 byte[] triggerAudio = event.getTriggerAudio(); 395 result = result + "TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length); 396 result = result + "CaptureSession: " + event.getCaptureSession(); 397 result += " )"; 398 return result; 399 } 400 } 401