[go: nahoru, domu]

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 com.android.test.soundtrigger;
18
19import java.io.File;
20import java.io.FileInputStream;
21import java.io.IOException;
22import java.util.HashMap;
23import java.util.Map;
24import java.util.Properties;
25import java.util.Random;
26import java.util.UUID;
27
28import android.app.Activity;
29import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
30import android.hardware.soundtrigger.SoundTrigger;
31import android.media.AudioFormat;
32import android.media.AudioManager;
33import android.media.MediaPlayer;
34import android.media.soundtrigger.SoundTriggerDetector;
35import android.media.soundtrigger.SoundTriggerManager;
36import android.net.Uri;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.PowerManager;
40import android.os.UserManager;
41import android.text.Editable;
42import android.text.method.ScrollingMovementMethod;
43import android.util.Log;
44import android.view.View;
45import android.widget.Button;
46import android.widget.RadioButton;
47import android.widget.RadioButton;
48import android.widget.RadioGroup;
49import android.widget.ScrollView;
50import android.widget.TextView;
51import android.widget.Toast;
52
53public 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
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
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
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
191    private void scrollToBottom() {
192        mScrollView.post(new Runnable() {
193            public void run() {
194                mScrollView.smoothScrollTo(0, mDebugView.getBottom());
195            }
196        });
197    }
198
199    private synchronized UUID getSelectedUuid() {
200        return mModelInfoMap.get(mSelectedModelId).modelUuid;
201    }
202
203    private synchronized void setDetector(SoundTriggerDetector detector) {
204        mModelInfoMap.get(mSelectedModelId).detector = detector;
205    }
206
207    private synchronized SoundTriggerDetector getDetector() {
208        return mModelInfoMap.get(mSelectedModelId).detector;
209    }
210
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
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. */
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     */
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     */
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     */
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
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
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
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
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 {
362        public void onAvailabilityChanged(int status) {
363            postMessage("Availability changed to: " + status);
364        }
365
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
377        public void onError() {
378            postMessage("onError()");
379        }
380
381        public void onRecognitionPaused() {
382            postMessage("onRecognitionPaused()");
383        }
384
385        public void onRecognitionResumed() {
386            postMessage("onRecognitionResumed()");
387        }
388    }
389
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