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