Updates to the sound trigger test app

Moved the logic for talking to the system service into a service in the
app - this now lets you close the activity and still be able to receive
events. Additionally, we get CLI support using commands with the intent
com.android.intent.action.MANAGE_SOUND_TRIGGER.

Bug: 29073629

Change-Id: Ie904b73b4414f2c9fded013aeb5e6c6c3a67f5d3
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestActivity.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestActivity.java
new file mode 100644
index 0000000..4841bc5
--- /dev/null
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestActivity.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.soundtrigger;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.text.Editable;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.test.soundtrigger.SoundTriggerTestService.SoundTriggerTestBinder;
+
+public class SoundTriggerTestActivity extends Activity implements SoundTriggerTestService.UserActivity {
+    private static final String TAG = "SoundTriggerTest";
+    private static final int AUDIO_PERMISSIONS_REQUEST = 1;
+
+    private SoundTriggerTestService mService = null;
+
+    private static UUID mSelectedModelUuid = null;
+
+    private Map<RadioButton, UUID> mButtonModelUuidMap;
+    private Map<UUID, RadioButton> mModelButtons;
+    private Map<UUID, String> mModelNames;
+    private List<RadioButton> mModelRadioButtons;
+
+    private TextView mDebugView = null;
+    private ScrollView mScrollView = null;
+    private Button mPlayTriggerButton = null;
+    private PowerManager.WakeLock mScreenWakelock;
+    private Handler mHandler;
+    private RadioGroup mRadioGroup;
+    private CheckBox mCaptureAudioCheckBox;
+    private Button mPlayCapturedAudioButton = null;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        // Make sure that this activity can punch through the lockscreen if needed.
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
+                             WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+        mDebugView = (TextView) findViewById(R.id.console);
+        mScrollView = (ScrollView) findViewById(R.id.scroller_id);
+        mRadioGroup = (RadioGroup) findViewById(R.id.model_group_id);
+        mPlayTriggerButton = (Button) findViewById(R.id.play_trigger_id);
+        mDebugView.setText(mDebugView.getText(), TextView.BufferType.EDITABLE);
+        mDebugView.setMovementMethod(new ScrollingMovementMethod());
+        mCaptureAudioCheckBox = (CheckBox) findViewById(R.id.caputre_check_box);
+        mPlayCapturedAudioButton = (Button) findViewById(R.id.play_captured_id);
+        mHandler = new Handler();
+        mButtonModelUuidMap = new HashMap();
+        mModelButtons = new HashMap();
+        mModelNames = new HashMap();
+        mModelRadioButtons = new LinkedList();
+
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        // Make sure that the service is started, so even if our activity goes down, we'll still
+        // have a request for it to run.
+        startService(new Intent(getBaseContext(), SoundTriggerTestService.class));
+
+        // Bind to SoundTriggerTestService.
+        Intent intent = new Intent(this, SoundTriggerTestService.class);
+        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        // Unbind from the service.
+        if (mService != null) {
+            mService.setUserActivity(null);
+            unbindService(mConnection);
+        }
+    }
+
+    @Override
+    public void addModel(UUID modelUuid, String name) {
+        // Create a new widget for this model, and insert everything we'd need into the map.
+        RadioButton button = new RadioButton(this);
+        mModelRadioButtons.add(button);
+        button.setText(name);
+        button.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                onRadioButtonClicked(v);
+            }
+        });
+        mButtonModelUuidMap.put(button, modelUuid);
+        mModelButtons.put(modelUuid, button);
+        mModelNames.put(modelUuid, name);
+
+        // Sort all the radio buttons by name, then push them into the group in order.
+        Collections.sort(mModelRadioButtons, new Comparator<RadioButton>(){
+            @Override
+            public int compare(RadioButton button0, RadioButton button1) {
+                return button0.getText().toString().compareTo(button1.getText().toString());
+            }
+        });
+        mRadioGroup.removeAllViews();
+        for (View v : mModelRadioButtons) {
+            mRadioGroup.addView(v);
+        }
+
+        // If we don't have something selected, select this first thing.
+        if (mSelectedModelUuid == null || mSelectedModelUuid.equals(modelUuid)) {
+            button.setChecked(true);
+            onRadioButtonClicked(button);
+        }
+    }
+
+    @Override
+    public void setModelState(UUID modelUuid, String state) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                String newButtonText = mModelNames.get(modelUuid);
+                if (state != null) {
+                    newButtonText += ": " + state;
+                }
+                mModelButtons.get(modelUuid).setText(newButtonText);
+                updateSelectModelSpecificUiElements();
+            }
+        });
+    }
+
+    @Override
+    public void showMessage(String msg, boolean showToast) {
+        // Append the message to the text field, then show the toast if requested.
+        this.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                ((Editable) mDebugView.getText()).append(msg + "\n");
+                mScrollView.post(new Runnable() {
+                    public void run() {
+                        mScrollView.smoothScrollTo(0, mDebugView.getBottom());
+                    }
+                });
+                if (showToast) {
+                    Toast.makeText(SoundTriggerTestActivity.this, msg, Toast.LENGTH_SHORT).show();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void handleDetection(UUID modelUuid) {
+        screenWakeup();
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                screenRelease();
+            }
+        }, 1000L);
+    }
+
+    private void screenWakeup() {
+        if (mScreenWakelock == null) {
+            PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE));
+            mScreenWakelock =  pm.newWakeLock(
+                    PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG);
+        }
+        mScreenWakelock.acquire();
+    }
+
+    private void screenRelease() {
+        mScreenWakelock.release();
+    }
+
+    public void onLoadButtonClicked(View v) {
+        if (mService == null) {
+            Log.e(TAG, "Could not load sound model: not bound to SoundTriggerTestService");
+        } else {
+            mService.loadModel(mSelectedModelUuid);
+        }
+    }
+
+    public void onUnloadButtonClicked(View v) {
+        if (mService == null) {
+           Log.e(TAG, "Can't unload model: not bound to SoundTriggerTestService");
+        } else {
+            mService.unloadModel(mSelectedModelUuid);
+        }
+    }
+
+    public void onReloadButtonClicked(View v) {
+        if (mService == null) {
+            Log.e(TAG, "Can't reload model: not bound to SoundTriggerTestService");
+        } else {
+            mService.reloadModel(mSelectedModelUuid);
+        }
+    }
+
+    public void onStartRecognitionButtonClicked(View v) {
+        if (mService == null) {
+            Log.e(TAG, "Can't start recognition: not bound to SoundTriggerTestService");
+        } else {
+            mService.startRecognition(mSelectedModelUuid);
+        }
+    }
+
+    public void onStopRecognitionButtonClicked(View v) {
+        if (mService == null) {
+            Log.e(TAG, "Can't stop recognition: not bound to SoundTriggerTestService");
+        } else {
+            mService.stopRecognition(mSelectedModelUuid);
+        }
+    }
+
+    public synchronized void onPlayTriggerButtonClicked(View v) {
+        if (mService == null) {
+            Log.e(TAG, "Can't play trigger audio: not bound to SoundTriggerTestService");
+        } else {
+            mService.playTriggerAudio(mSelectedModelUuid);
+        }
+    }
+
+    public synchronized void onCaptureAudioCheckboxClicked(View v) {
+        // See if we have the right permissions
+        if (!mService.hasMicrophonePermission()) {
+            requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},
+                    AUDIO_PERMISSIONS_REQUEST);
+            return;
+        } else {
+            mService.setCaptureAudio(mSelectedModelUuid, mCaptureAudioCheckBox.isChecked());
+        }
+    }
+
+    @Override
+    public synchronized void onRequestPermissionsResult(int requestCode, String permissions[],
+                                                        int[] grantResults) {
+        if (requestCode == AUDIO_PERMISSIONS_REQUEST) {
+            if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+                // Make sure that the check box is set to false.
+                mCaptureAudioCheckBox.setChecked(false);
+            }
+            mService.setCaptureAudio(mSelectedModelUuid, mCaptureAudioCheckBox.isChecked());
+        }
+    }
+
+    public synchronized void onPlayCapturedAudioButtonClicked(View v) {
+        if (mService == null) {
+            Log.e(TAG, "Can't play captured audio: not bound to SoundTriggerTestService");
+        } else {
+            mService.playCapturedAudio(mSelectedModelUuid);
+        }
+    }
+
+    public synchronized void onRadioButtonClicked(View view) {
+        // Is the button now checked?
+        boolean checked = ((RadioButton) view).isChecked();
+        if (checked) {
+            mSelectedModelUuid = mButtonModelUuidMap.get(view);
+            showMessage("Selected " + mModelNames.get(mSelectedModelUuid), false);
+            updateSelectModelSpecificUiElements();
+        }
+    }
+
+    private synchronized void updateSelectModelSpecificUiElements() {
+        // Set the play trigger button to be enabled only if we actually have some audio.
+        mPlayTriggerButton.setEnabled(mService.modelHasTriggerAudio((mSelectedModelUuid)));
+        // Similar logic for the captured audio.
+        mCaptureAudioCheckBox.setChecked(
+                mService.modelWillCaptureTriggerAudio(mSelectedModelUuid));
+        mPlayCapturedAudioButton.setEnabled(mService.modelHasCapturedAudio((mSelectedModelUuid)));
+    }
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            synchronized (SoundTriggerTestActivity.this) {
+                // We've bound to LocalService, cast the IBinder and get LocalService instance
+                SoundTriggerTestBinder binder = (SoundTriggerTestBinder) service;
+                mService = binder.getService();
+                mService.setUserActivity(SoundTriggerTestActivity.this);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            synchronized (SoundTriggerTestActivity.this) {
+                mService.setUserActivity(null);
+                mService = null;
+            }
+        }
+    };
+}
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java
new file mode 100644
index 0000000..0ff95c4
--- /dev/null
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.soundtrigger;
+
+import android.Manifest;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.MediaPlayer;
+import android.media.soundtrigger.SoundTriggerDetector;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.UUID;
+
+public class SoundTriggerTestService extends Service {
+    private static final String TAG = "SoundTriggerTestSrv";
+    private static final String INTENT_ACTION = "com.android.intent.action.MANAGE_SOUND_TRIGGER";
+
+    // Binder given to clients.
+    private final IBinder mBinder;
+    private final Map<UUID, ModelInfo> mModelInfoMap;
+    private SoundTriggerUtil mSoundTriggerUtil;
+    private Random mRandom;
+    private UserActivity mUserActivity;
+
+    public interface UserActivity {
+        void addModel(UUID modelUuid, String state);
+        void setModelState(UUID modelUuid, String state);
+        void showMessage(String msg, boolean showToast);
+        void handleDetection(UUID modelUuid);
+    }
+
+    public SoundTriggerTestService() {
+        super();
+        mRandom = new Random();
+        mModelInfoMap = new HashMap();
+        mBinder = new SoundTriggerTestBinder();
+    }
+
+    @Override
+    public synchronized int onStartCommand(Intent intent, int flags, int startId) {
+        if (mModelInfoMap.isEmpty()) {
+            mSoundTriggerUtil = new SoundTriggerUtil(this);
+            loadModelsInDataDir();
+        }
+
+        // If we get killed, after returning from here, restart
+        return START_STICKY;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(INTENT_ACTION);
+        registerReceiver(mBroadcastReceiver, filter);
+
+        // Make sure the data directory exists, and we're the owner of it.
+        try {
+            getFilesDir().mkdir();
+        } catch (Exception e) {
+            // Don't care - we either made it, or it already exists.
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        stopAllRecognitions();
+        unregisterReceiver(mBroadcastReceiver);
+    }
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent != null && INTENT_ACTION.equals(intent.getAction())) {
+                String command = intent.getStringExtra("command");
+                if (command == null) {
+                    Log.e(TAG, "No 'command' specified in " + INTENT_ACTION);
+                } else {
+                    try {
+                        if (command.equals("load")) {
+                            loadModel(getModelUuidFromIntent(intent));
+                        } else if (command.equals("unload")) {
+                            unloadModel(getModelUuidFromIntent(intent));
+                        } else if (command.equals("start")) {
+                            startRecognition(getModelUuidFromIntent(intent));
+                        } else if (command.equals("stop")) {
+                            stopRecognition(getModelUuidFromIntent(intent));
+                        } else if (command.equals("play_trigger")) {
+                            playTriggerAudio(getModelUuidFromIntent(intent));
+                        } else if (command.equals("play_captured")) {
+                            playCapturedAudio(getModelUuidFromIntent(intent));
+                        } else if (command.equals("set_capture")) {
+                            setCaptureAudio(getModelUuidFromIntent(intent),
+                                    intent.getBooleanExtra("enabled", true));
+                        } else if (command.equals("set_capture_timeout")) {
+                            setCaptureAudioTimeout(getModelUuidFromIntent(intent),
+                                    intent.getIntExtra("timeout", 5000));
+                        } else {
+                            Log.e(TAG, "Unknown command '" + command + "'");
+                        }
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to process " + command, e);
+                    }
+                }
+            }
+        }
+    };
+
+    private UUID getModelUuidFromIntent(Intent intent) {
+        // First, see if the specified the UUID straight up.
+        String value = intent.getStringExtra("modelUuid");
+        if (value != null) {
+            return UUID.fromString(value);
+        }
+
+        // If they specified a name, use that to iterate through the map of models and find it.
+        value = intent.getStringExtra("name");
+        if (value != null) {
+            for (ModelInfo modelInfo : mModelInfoMap.values()) {
+                if (value.equals(modelInfo.name)) {
+                    return modelInfo.modelUuid;
+                }
+            }
+            Log.e(TAG, "Failed to find a matching model with name '" + value + "'");
+        }
+
+        // We couldn't figure out what they were asking for.
+        throw new RuntimeException("Failed to get model from intent - specify either " +
+                "'modelUuid' or 'name'");
+    }
+
+    /**
+     * Will be called when the service is killed (through swipe aways, not if we're force killed).
+     */
+    @Override
+    public void onTaskRemoved(Intent rootIntent) {
+        super.onTaskRemoved(rootIntent);
+        stopAllRecognitions();
+        stopSelf();
+    }
+
+    @Override
+    public synchronized IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    public class SoundTriggerTestBinder extends Binder {
+        SoundTriggerTestService getService() {
+            // Return instance of our parent so clients can call public methods.
+            return SoundTriggerTestService.this;
+        }
+    }
+
+    public synchronized void setUserActivity(UserActivity activity) {
+        mUserActivity = activity;
+        if (mUserActivity != null) {
+            for (Map.Entry<UUID, ModelInfo> entry : mModelInfoMap.entrySet()) {
+                mUserActivity.addModel(entry.getKey(), entry.getValue().name);
+                mUserActivity.setModelState(entry.getKey(), entry.getValue().state);
+            }
+        }
+    }
+
+    private synchronized void stopAllRecognitions() {
+        for (ModelInfo modelInfo : mModelInfoMap.values()) {
+            if (modelInfo.detector != null) {
+                Log.i(TAG, "Stopping recognition for " + modelInfo.name);
+                try {
+                    modelInfo.detector.stopRecognition();
+                } catch (Exception e) {
+                    Log.e(TAG, "Failed to stop recognition", e);
+                }
+            }
+        }
+    }
+
+    // Helper struct for holding information about a model.
+    public static class ModelInfo {
+        public String name;
+        public String state;
+        public UUID modelUuid;
+        public UUID vendorUuid;
+        public MediaPlayer triggerAudioPlayer;
+        public SoundTriggerDetector detector;
+        public byte modelData[];
+        public boolean captureAudio;
+        public int captureAudioMs;
+        public AudioTrack captureAudioTrack;
+    }
+
+    private GenericSoundModel createNewSoundModel(ModelInfo modelInfo) {
+        return new GenericSoundModel(modelInfo.modelUuid, modelInfo.vendorUuid,
+                modelInfo.modelData);
+    }
+
+    public synchronized void loadModel(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+
+        postMessage("Loading model: " + modelInfo.name);
+
+        GenericSoundModel soundModel = createNewSoundModel(modelInfo);
+
+        boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(soundModel);
+        if (status) {
+            postToast("Successfully loaded " + modelInfo.name + ", UUID=" + soundModel.uuid);
+            setModelState(modelInfo, "Loaded");
+        } else {
+            postErrorToast("Failed to load " + modelInfo.name + ", UUID=" + soundModel.uuid + "!");
+            setModelState(modelInfo, "Failed to load");
+        }
+    }
+
+    public synchronized void unloadModel(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+
+        postMessage("Unloading model: " + modelInfo.name);
+
+        GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
+        if (soundModel == null) {
+            postErrorToast("Sound model not found for " + modelInfo.name + "!");
+            return;
+        }
+        modelInfo.detector = null;
+        boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid);
+        if (status) {
+            postToast("Successfully unloaded " + modelInfo.name + ", UUID=" + soundModel.uuid);
+            setModelState(modelInfo, "Unloaded");
+        } else {
+            postErrorToast("Failed to unload " +
+                    modelInfo.name + ", UUID=" + soundModel.uuid + "!");
+            setModelState(modelInfo, "Failed to unload");
+        }
+    }
+
+    public synchronized void reloadModel(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+        postMessage("Reloading model: " + modelInfo.name);
+        GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
+        if (soundModel == null) {
+            postErrorToast("Sound model not found for " + modelInfo.name + "!");
+            return;
+        }
+        GenericSoundModel updated = createNewSoundModel(modelInfo);
+        boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated);
+        if (status) {
+            postToast("Successfully reloaded " + modelInfo.name + ", UUID=" + modelInfo.modelUuid);
+            setModelState(modelInfo, "Reloaded");
+        } else {
+            postErrorToast("Failed to reload "
+                    + modelInfo.name + ", UUID=" + modelInfo.modelUuid + "!");
+            setModelState(modelInfo, "Failed to reload");
+        }
+    }
+
+    public synchronized void startRecognition(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+
+        if (modelInfo.detector == null) {
+            postMessage("Creating SoundTriggerDetector for " + modelInfo.name);
+            modelInfo.detector = mSoundTriggerUtil.createSoundTriggerDetector(
+                    modelUuid, new DetectorCallback(modelInfo));
+        }
+
+        postMessage("Starting recognition for " + modelInfo.name + ", UUID=" + modelInfo.modelUuid);
+        if (modelInfo.detector.startRecognition(modelInfo.captureAudio ?
+                SoundTriggerDetector.RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO :
+                SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) {
+            setModelState(modelInfo, "Started");
+        } else {
+            postErrorToast("Fast failure attempting to start recognition for " +
+                    modelInfo.name + ", UUID=" + modelInfo.modelUuid);
+            setModelState(modelInfo, "Failed to start");
+        }
+    }
+
+    public synchronized void stopRecognition(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+
+        if (modelInfo.detector == null) {
+            postErrorToast("Stop called on null detector for " +
+                    modelInfo.name + ", UUID=" + modelInfo.modelUuid);
+            return;
+        }
+        postMessage("Triggering stop recognition for " +
+                modelInfo.name + ", UUID=" + modelInfo.modelUuid);
+        if (modelInfo.detector.stopRecognition()) {
+            setModelState(modelInfo, "Stopped");
+        } else {
+            postErrorToast("Fast failure attempting to stop recognition for " +
+                    modelInfo.name + ", UUID=" + modelInfo.modelUuid);
+            setModelState(modelInfo, "Failed to stop");
+        }
+    }
+
+    public synchronized void playTriggerAudio(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+        if (modelInfo.triggerAudioPlayer != null) {
+            postMessage("Playing trigger audio for " + modelInfo.name);
+            modelInfo.triggerAudioPlayer.start();
+        } else {
+            postMessage("No trigger audio for " + modelInfo.name);
+        }
+    }
+
+    public synchronized void playCapturedAudio(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+        if (modelInfo.captureAudioTrack != null) {
+            postMessage("Playing captured audio for " + modelInfo.name);
+            modelInfo.captureAudioTrack.stop();
+            modelInfo.captureAudioTrack.reloadStaticData();
+            modelInfo.captureAudioTrack.play();
+        } else {
+            postMessage("No captured audio for " + modelInfo.name);
+        }
+    }
+
+    public synchronized void setCaptureAudioTimeout(UUID modelUuid, int captureTimeoutMs) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+        modelInfo.captureAudioMs = captureTimeoutMs;
+        Log.i(TAG, "Set " + modelInfo.name + " capture audio timeout to " +
+                captureTimeoutMs + "ms");
+    }
+
+    public synchronized void setCaptureAudio(UUID modelUuid, boolean captureAudio) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        if (modelInfo == null) {
+            postError("Could not find model for: " + modelUuid.toString());
+            return;
+        }
+        modelInfo.captureAudio = captureAudio;
+        Log.i(TAG, "Set " + modelInfo.name + " capture audio to " + captureAudio);
+    }
+
+    public synchronized boolean hasMicrophonePermission() {
+        return getBaseContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    public synchronized boolean modelHasTriggerAudio(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        return modelInfo != null && modelInfo.triggerAudioPlayer != null;
+    }
+
+    public synchronized boolean modelWillCaptureTriggerAudio(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        return modelInfo != null && modelInfo.captureAudio;
+    }
+
+    public synchronized boolean modelHasCapturedAudio(UUID modelUuid) {
+        ModelInfo modelInfo = mModelInfoMap.get(modelUuid);
+        return modelInfo != null && modelInfo.captureAudioTrack != null;
+    }
+
+    private void loadModelsInDataDir() {
+        // Load all the models in the data dir.
+        boolean loadedModel = false;
+        for (File file : getFilesDir().listFiles()) {
+            // Find meta-data in .properties files, ignore everything else.
+            if (!file.getName().endsWith(".properties")) {
+                continue;
+            }
+            try {
+                Properties properties = new Properties();
+                properties.load(new FileInputStream(file));
+                createModelInfo(properties);
+                loadedModel = true;
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to load properties file " + file.getName());
+            }
+        }
+
+        // Create a few dummy models if we didn't load anything.
+        if (!loadedModel) {
+            Properties dummyModelProperties = new Properties();
+            for (String name : new String[]{"1", "2", "3"}) {
+                dummyModelProperties.setProperty("name", "Model " + name);
+                createModelInfo(dummyModelProperties);
+            }
+        }
+    }
+
+    /** Parses a Properties collection to generate a sound model.
+     *
+     * Missing keys are filled in with default/random values.
+     * @param properties Has the required 'name' property, but the remaining 'modelUuid',
+     *                   'vendorUuid', 'triggerAudio', and 'dataFile' optional properties.
+     *
+     */
+    private synchronized void createModelInfo(Properties properties) {
+        try {
+            ModelInfo modelInfo = new ModelInfo();
+
+            if (!properties.containsKey("name")) {
+                throw new RuntimeException("must have a 'name' property");
+            }
+            modelInfo.name = properties.getProperty("name");
+
+            if (properties.containsKey("modelUuid")) {
+                modelInfo.modelUuid = UUID.fromString(properties.getProperty("modelUuid"));
+            } else {
+                modelInfo.modelUuid = UUID.randomUUID();
+            }
+
+            if (properties.containsKey("vendorUuid")) {
+                modelInfo.vendorUuid = UUID.fromString(properties.getProperty("vendorUuid"));
+            } else {
+                modelInfo.vendorUuid = UUID.randomUUID();
+            }
+
+            if (properties.containsKey("triggerAudio")) {
+                modelInfo.triggerAudioPlayer = MediaPlayer.create(this, Uri.parse(
+                        getFilesDir().getPath() + "/" + properties.getProperty("triggerAudio")));
+                if (modelInfo.triggerAudioPlayer.getDuration() == 0) {
+                    modelInfo.triggerAudioPlayer.release();
+                    modelInfo.triggerAudioPlayer = null;
+                }
+            }
+
+            if (properties.containsKey("dataFile")) {
+                File modelDataFile = new File(
+                        getFilesDir().getPath() + "/" + properties.getProperty("dataFile"));
+                modelInfo.modelData = new byte[(int) modelDataFile.length()];
+                FileInputStream input = new FileInputStream(modelDataFile);
+                input.read(modelInfo.modelData, 0, modelInfo.modelData.length);
+            } else {
+                modelInfo.modelData = new byte[1024];
+                mRandom.nextBytes(modelInfo.modelData);
+            }
+
+            modelInfo.captureAudioMs = Integer.parseInt((String) properties.getOrDefault(
+                    "captureAudioDurationMs", "5000"));
+
+            // TODO: Add property support for keyphrase models when they're exposed by the
+            // service.
+
+            // Update our maps containing the button -> id and id -> modelInfo.
+            mModelInfoMap.put(modelInfo.modelUuid, modelInfo);
+            if (mUserActivity != null) {
+                mUserActivity.addModel(modelInfo.modelUuid, modelInfo.name);
+                mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error parsing properties for " + properties.getProperty("name"), e);
+        }
+    }
+
+    private class CaptureAudioRecorder implements Runnable {
+        private final ModelInfo mModelInfo;
+        private final SoundTriggerDetector.EventPayload mEvent;
+
+        public CaptureAudioRecorder(ModelInfo modelInfo, SoundTriggerDetector.EventPayload event) {
+            mModelInfo = modelInfo;
+            mEvent = event;
+        }
+
+        @Override
+        public void run() {
+            AudioFormat format = mEvent.getCaptureAudioFormat();
+            if (format == null) {
+                postErrorToast("No audio format in recognition event.");
+                return;
+            }
+
+            AudioRecord audioRecord = null;
+            AudioTrack playbackTrack = null;
+            try {
+                // Inform the audio flinger that we really do want the stream from the soundtrigger.
+                AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
+                attributesBuilder.setInternalCapturePreset(1999);
+                AudioAttributes attributes = attributesBuilder.build();
+
+                // Make sure we understand this kind of playback so we know how many bytes to read.
+                String encoding;
+                int bytesPerSample;
+                switch (format.getEncoding()) {
+                    case AudioFormat.ENCODING_PCM_8BIT:
+                        encoding = "8bit";
+                        bytesPerSample = 1;
+                        break;
+                    case AudioFormat.ENCODING_PCM_16BIT:
+                        encoding = "16bit";
+                        bytesPerSample = 2;
+                        break;
+                    case AudioFormat.ENCODING_PCM_FLOAT:
+                        encoding = "float";
+                        bytesPerSample = 4;
+                        break;
+                    default:
+                        throw new RuntimeException("Unhandled audio format in event");
+                }
+
+                int bytesRequired = format.getSampleRate() * format.getChannelCount() *
+                        bytesPerSample * mModelInfo.captureAudioMs / 1000;
+                int minBufferSize = AudioRecord.getMinBufferSize(
+                        format.getSampleRate(), format.getChannelMask(), format.getEncoding());
+                if (minBufferSize > bytesRequired) {
+                    bytesRequired = minBufferSize;
+                }
+
+                // Make an AudioTrack so we can play the data back out after it's finished
+                // recording.
+                try {
+                    int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+                    if (format.getChannelCount() == 2) {
+                        channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+                    } else if (format.getChannelCount() >= 3) {
+                        throw new RuntimeException(
+                                "Too many channels in captured audio for playback");
+                    }
+
+                    playbackTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
+                            format.getSampleRate(), channelConfig, format.getEncoding(),
+                            bytesRequired, AudioTrack.MODE_STATIC);
+                } catch (Exception e) {
+                    Log.e(TAG, "Exception creating playback track", e);
+                    postErrorToast("Failed to create playback track: " + e.getMessage());
+                }
+
+                audioRecord = new AudioRecord(attributes, format, bytesRequired,
+                        mEvent.getCaptureSession());
+
+                byte[] buffer = new byte[bytesRequired];
+
+                // Create a file so we can save the output data there for analysis later.
+                FileOutputStream fos  = null;
+                try {
+                    fos = new FileOutputStream( new File(
+                            getFilesDir() + File.separator + mModelInfo.name.replace(' ', '_') +
+                                    "_capture_" + format.getChannelCount() + "ch_" +
+                                    format.getSampleRate() + "hz_" + encoding + ".pcm"));
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to open output for saving PCM data", e);
+                    postErrorToast("Failed to open output for saving PCM data: " + e.getMessage());
+                }
+
+                // Inform the user we're recording.
+                setModelState(mModelInfo, "Recording");
+                audioRecord.startRecording();
+                while (bytesRequired > 0) {
+                    int bytesRead = audioRecord.read(buffer, 0, buffer.length);
+                    if (bytesRead == -1) {
+                        break;
+                    }
+                    if (fos != null) {
+                        fos.write(buffer, 0, bytesRead);
+                    }
+                    if (playbackTrack != null) {
+                        playbackTrack.write(buffer, 0, bytesRead);
+                    }
+                    bytesRequired -= bytesRead;
+                }
+                audioRecord.stop();
+            } catch (Exception e) {
+                Log.e(TAG, "Error recording trigger audio", e);
+                postErrorToast("Error recording trigger audio: " + e.getMessage());
+            } finally {
+                if (audioRecord != null) {
+                    audioRecord.release();
+                }
+                synchronized (SoundTriggerTestService.this) {
+                    if (mModelInfo.captureAudioTrack != null) {
+                        mModelInfo.captureAudioTrack.release();
+                    }
+                    mModelInfo.captureAudioTrack = playbackTrack;
+                }
+                setModelState(mModelInfo, "Recording finished");
+            }
+        }
+    }
+
+    // Implementation of SoundTriggerDetector.Callback.
+    private class DetectorCallback extends SoundTriggerDetector.Callback {
+        private final ModelInfo mModelInfo;
+
+        public DetectorCallback(ModelInfo modelInfo) {
+            mModelInfo = modelInfo;
+        }
+
+        public void onAvailabilityChanged(int status) {
+            postMessage(mModelInfo.name + "Availability changed to: " + status);
+        }
+
+        public void onDetected(SoundTriggerDetector.EventPayload event) {
+            postMessage(mModelInfo.name + "onDetected(): " + eventPayloadToString(event));
+            synchronized (SoundTriggerTestService.this) {
+                if (mUserActivity != null) {
+                    mUserActivity.handleDetection(mModelInfo.modelUuid);
+                }
+                if (mModelInfo.captureAudio) {
+                    new Thread(new CaptureAudioRecorder(mModelInfo, event)).start();
+                }
+            }
+        }
+
+        public void onError() {
+            postMessage(mModelInfo.name + "onError()");
+            setModelState(mModelInfo, "Error");
+        }
+
+        public void onRecognitionPaused() {
+            postMessage(mModelInfo.name + " onRecognitionPaused()");
+            setModelState(mModelInfo, "Paused");
+        }
+
+        public void onRecognitionResumed() {
+            postMessage(mModelInfo.name + "onRecognitionResumed()");
+            setModelState(mModelInfo, "Resumed");
+        }
+    }
+
+    private String eventPayloadToString(SoundTriggerDetector.EventPayload event) {
+        String result = "EventPayload(";
+        AudioFormat format =  event.getCaptureAudioFormat();
+        result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString());
+        byte[] triggerAudio = event.getTriggerAudio();
+        result = result + "TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length);
+        result = result + "CaptureSession: " + event.getCaptureSession();
+        result += " )";
+        return result;
+    }
+
+    private void postMessage(String msg) {
+        showMessage(msg, Log.INFO, false);
+    }
+
+    private void postError(String msg) {
+        showMessage(msg, Log.ERROR, false);
+    }
+
+    private void postToast(String msg) {
+        showMessage(msg, Log.INFO, true);
+    }
+
+    private void postErrorToast(String msg) {
+        showMessage(msg, Log.ERROR, true);
+    }
+
+    /** Logs the message at the specified level, then forwards it to the activity if present. */
+    private synchronized void showMessage(String msg, int logLevel, boolean showToast) {
+        Log.println(logLevel, TAG, msg);
+        if (mUserActivity != null) {
+            mUserActivity.showMessage(msg, showToast);
+        }
+    }
+
+    private synchronized void setModelState(ModelInfo modelInfo, String state) {
+        modelInfo.state = state;
+        if (mUserActivity != null) {
+            mUserActivity.setModelState(modelInfo.modelUuid, modelInfo.state);
+        }
+    }
+}
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
index 1c95c25..8e5ed32 100644
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
+++ b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java
@@ -18,7 +18,6 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
-import android.hardware.soundtrigger.SoundTrigger;
 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
 import android.media.soundtrigger.SoundTriggerDetector;
 import android.media.soundtrigger.SoundTriggerManager;
@@ -36,7 +35,7 @@
  * Utility class for the managing sound trigger sound models.
  */
 public class SoundTriggerUtil {
-    private static final String TAG = "TestSoundTriggerUtil:Hotsound";
+    private static final String TAG = "SoundTriggerTestUtil";
 
     private final ISoundTriggerService mSoundTriggerService;
     private final SoundTriggerManager mSoundTriggerManager;
@@ -68,10 +67,6 @@
         return true;
     }
 
-    public void addOrUpdateSoundModel(SoundTriggerManager.Model soundModel) {
-        mSoundTriggerManager.updateModel(soundModel);
-    }
-
     /**
      * Gets the sound model for the given keyphrase, null if none exists.
      * If a sound model for a given keyphrase exists, and it needs to be updated,
@@ -91,7 +86,7 @@
         }
 
         if (model == null) {
-            Log.w(TAG, "No models present for the gien keyphrase ID");
+            Log.w(TAG, "No models present for the given keyphrase ID");
             return null;
         } else {
             return model;
@@ -109,18 +104,14 @@
         try {
             mSoundTriggerService.deleteSoundModel(new ParcelUuid(modelId));
         } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in updateSoundModel");
+            Log.e(TAG, "RemoteException in deleteSoundModel");
+            return false;
         }
         return true;
     }
 
-    public void deleteSoundModelUsingManager(UUID modelId) {
-            mSoundTriggerManager.deleteModel(modelId);
-    }
-
     public SoundTriggerDetector createSoundTriggerDetector(UUID modelId,
             SoundTriggerDetector.Callback callback) {
         return mSoundTriggerManager.createSoundTriggerDetector(modelId, callback, null);
     }
-
 }
diff --git a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java b/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
deleted file mode 100644
index 5fd38e9..0000000
--- a/tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java
+++ /dev/null
@@ -1,400 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.test.soundtrigger;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Random;
-import java.util.UUID;
-
-import android.app.Activity;
-import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
-import android.hardware.soundtrigger.SoundTrigger;
-import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.media.MediaPlayer;
-import android.media.soundtrigger.SoundTriggerDetector;
-import android.media.soundtrigger.SoundTriggerManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.os.UserManager;
-import android.text.Editable;
-import android.text.method.ScrollingMovementMethod;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.RadioButton;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-import android.widget.ScrollView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-public class TestSoundTriggerActivity extends Activity {
-    private static final String TAG = "TestSoundTriggerActivity";
-    private static final boolean DBG = false;
-
-    private SoundTriggerUtil mSoundTriggerUtil;
-    private Random mRandom;
-
-    private Map<Integer, ModelInfo> mModelInfoMap;
-    private Map<View, Integer> mModelIdMap;
-
-    private TextView mDebugView = null;
-    private int mSelectedModelId = -1;
-    private ScrollView mScrollView = null;
-    private Button mPlayTriggerButton = null;
-    private PowerManager.WakeLock mScreenWakelock;
-    private Handler mHandler;
-    private RadioGroup mRadioGroup;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        if (DBG) Log.d(TAG, "onCreate");
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.main);
-        mDebugView = (TextView) findViewById(R.id.console);
-        mScrollView = (ScrollView) findViewById(R.id.scroller_id);
-        mRadioGroup = (RadioGroup) findViewById(R.id.model_group_id);
-        mPlayTriggerButton = (Button) findViewById(R.id.play_trigger_id);
-        mDebugView.setText(mDebugView.getText(), TextView.BufferType.EDITABLE);
-        mDebugView.setMovementMethod(new ScrollingMovementMethod());
-        mSoundTriggerUtil = new SoundTriggerUtil(this);
-        mRandom = new Random();
-        mHandler = new Handler();
-
-        mModelInfoMap = new HashMap();
-        mModelIdMap = new HashMap();
-
-        setVolumeControlStream(AudioManager.STREAM_MUSIC);
-
-        // Load all the models in the data dir.
-        for (File file : getFilesDir().listFiles()) {
-            // Find meta-data in .properties files, ignore everything else.
-            if (!file.getName().endsWith(".properties")) {
-                continue;
-            }
-            try {
-                Properties properties = new Properties();
-                properties.load(new FileInputStream(file));
-                createModelInfoAndWidget(properties);
-            } catch (Exception e) {
-                Log.e(TAG, "Failed to load properties file " + file.getName());
-            }
-        }
-
-        // Create a few dummy models if we didn't load anything.
-        if (mModelIdMap.isEmpty()) {
-            Properties dummyModelProperties = new Properties();
-            for (String name : new String[]{"One", "Two", "Three"}) {
-                dummyModelProperties.setProperty("name", "Model " + name);
-                createModelInfoAndWidget(dummyModelProperties);
-            }
-        }
-    }
-
-    private void createModelInfoAndWidget(Properties properties) {
-        try {
-            ModelInfo modelInfo = new ModelInfo();
-
-            if (!properties.containsKey("name")) {
-                throw new RuntimeException("must have a 'name' property");
-            }
-            modelInfo.name = properties.getProperty("name");
-
-            if (properties.containsKey("modelUuid")) {
-                modelInfo.modelUuid = UUID.fromString(properties.getProperty("modelUuid"));
-            } else {
-                modelInfo.modelUuid = UUID.randomUUID();
-            }
-
-            if (properties.containsKey("vendorUuid")) {
-                modelInfo.vendorUuid = UUID.fromString(properties.getProperty("vendorUuid"));
-            } else {
-                modelInfo.vendorUuid = UUID.randomUUID();
-            }
-
-            if (properties.containsKey("triggerAudio")) {
-                modelInfo.triggerAudioPlayer = MediaPlayer.create(this, Uri.parse(
-                        getFilesDir().getPath() + "/" + properties.getProperty("triggerAudio")));
-            }
-
-            if (properties.containsKey("dataFile")) {
-                File modelDataFile = new File(
-                        getFilesDir().getPath() + "/" + properties.getProperty("dataFile"));
-                modelInfo.modelData = new byte[(int) modelDataFile.length()];
-                FileInputStream input = new FileInputStream(modelDataFile);
-                input.read(modelInfo.modelData, 0, modelInfo.modelData.length);
-            } else {
-                modelInfo.modelData = new byte[1024];
-                mRandom.nextBytes(modelInfo.modelData);
-            }
-
-            // TODO: Add property support for keyphrase models when they're exposed by the
-            // service. Also things like how much audio they should record with the capture session
-            // provided in the callback.
-
-            // Add a widget into the radio group.
-            RadioButton button = new RadioButton(this);
-            mRadioGroup.addView(button);
-            button.setText(modelInfo.name);
-            button.setOnClickListener(new View.OnClickListener() {
-                public void onClick(View v) {
-                    onRadioButtonClicked(v);
-                }
-            });
-
-            // Update our maps containing the button -> id and id -> modelInfo.
-            int newModelId = mModelIdMap.size() + 1;
-            mModelIdMap.put(button, newModelId);
-            mModelInfoMap.put(newModelId, modelInfo);
-
-            // If we don't have something selected, select this first thing.
-            if (mSelectedModelId < 0) {
-                button.setChecked(true);
-                onRadioButtonClicked(button);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error parsing properties for " + properties.getProperty("name"), e);
-        }
-    }
-
-    private void postMessage(String msg) {
-        Log.i(TAG, "Posted: " + msg);
-        ((Editable) mDebugView.getText()).append(msg + "\n");
-        if ((mDebugView.getMeasuredHeight() - mScrollView.getScrollY()) <=
-                (mScrollView.getHeight() + mDebugView.getLineHeight())) {
-            scrollToBottom();
-        }
-    }
-
-    private void scrollToBottom() {
-        mScrollView.post(new Runnable() {
-            public void run() {
-                mScrollView.smoothScrollTo(0, mDebugView.getBottom());
-            }
-        });
-    }
-
-    private synchronized UUID getSelectedUuid() {
-        return mModelInfoMap.get(mSelectedModelId).modelUuid;
-    }
-
-    private synchronized void setDetector(SoundTriggerDetector detector) {
-        mModelInfoMap.get(mSelectedModelId).detector = detector;
-    }
-
-    private synchronized SoundTriggerDetector getDetector() {
-        return mModelInfoMap.get(mSelectedModelId).detector;
-    }
-
-    private void screenWakeup() {
-        PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE));
-        if (mScreenWakelock == null) {
-            mScreenWakelock =  pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "TAG");
-        }
-        mScreenWakelock.acquire();
-    }
-
-    private void screenRelease() {
-        PowerManager pm = ((PowerManager)getSystemService(POWER_SERVICE));
-        mScreenWakelock.release();
-    }
-
-    /** TODO: Should return the abstract sound model that can be then sent to the service. */
-    private GenericSoundModel createNewSoundModel() {
-        ModelInfo modelInfo = mModelInfoMap.get(mSelectedModelId);
-        return new GenericSoundModel(modelInfo.modelUuid, modelInfo.vendorUuid,
-                modelInfo.modelData);
-    }
-
-    /**
-     * Called when the user clicks the enroll button.
-     * Performs a fresh enrollment.
-     */
-    public void onEnrollButtonClicked(View v) {
-        postMessage("Loading model: " + mSelectedModelId);
-
-        GenericSoundModel model = createNewSoundModel();
-
-        boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(model);
-        if (status) {
-            Toast.makeText(
-                    this, "Successfully created sound trigger model UUID=" + model.uuid,
-                    Toast.LENGTH_SHORT).show();
-        } else {
-            Toast.makeText(this, "Failed to enroll!!!" + model.uuid, Toast.LENGTH_SHORT).show();
-        }
-
-        // Test the SoundManager API.
-    }
-
-    /**
-     * Called when the user clicks the un-enroll button.
-     * Clears the enrollment information for the user.
-     */
-    public void onUnEnrollButtonClicked(View v) {
-        postMessage("Unloading model: " + mSelectedModelId);
-        UUID modelUuid = getSelectedUuid();
-        GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
-        if (soundModel == null) {
-            Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
-            return;
-        }
-        boolean status = mSoundTriggerUtil.deleteSoundModel(modelUuid);
-        if (status) {
-            Toast.makeText(this, "Successfully deleted model UUID=" + soundModel.uuid,
-                    Toast.LENGTH_SHORT)
-                    .show();
-        } else {
-            Toast.makeText(this, "Failed to delete sound model!!!", Toast.LENGTH_SHORT).show();
-        }
-    }
-
-    /**
-     * Called when the user clicks the re-enroll button.
-     * Uses the previously enrolled sound model and makes changes to it before pushing it back.
-     */
-    public void onReEnrollButtonClicked(View v) {
-        postMessage("Re-loading model: " + mSelectedModelId);
-        UUID modelUuid = getSelectedUuid();
-        GenericSoundModel soundModel = mSoundTriggerUtil.getSoundModel(modelUuid);
-        if (soundModel == null) {
-            Toast.makeText(this, "Sound model not found!!!", Toast.LENGTH_SHORT).show();
-            return;
-        }
-        GenericSoundModel updated = createNewSoundModel();
-        boolean status = mSoundTriggerUtil.addOrUpdateSoundModel(updated);
-        if (status) {
-            Toast.makeText(this, "Successfully re-enrolled, model UUID=" + updated.uuid,
-                    Toast.LENGTH_SHORT)
-                    .show();
-        } else {
-            Toast.makeText(this, "Failed to re-enroll!!!", Toast.LENGTH_SHORT).show();
-        }
-    }
-
-    public void onStartRecognitionButtonClicked(View v) {
-        UUID modelUuid = getSelectedUuid();
-        SoundTriggerDetector detector = getDetector();
-        if (detector == null) {
-            Log.i(TAG, "Created an instance of the SoundTriggerDetector for model #" +
-                    mSelectedModelId);
-            postMessage("Created an instance of the SoundTriggerDetector for model #" +
-                    mSelectedModelId);
-            detector = mSoundTriggerUtil.createSoundTriggerDetector(modelUuid,
-                    new DetectorCallback());
-            setDetector(detector);
-        }
-        postMessage("Triggering start recognition for model: " + mSelectedModelId);
-        if (!detector.startRecognition(
-                SoundTriggerDetector.RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS)) {
-            Log.e(TAG, "Fast failure attempting to start recognition.");
-            postMessage("Fast failure attempting to start recognition:" + mSelectedModelId);
-        }
-    }
-
-    public void onStopRecognitionButtonClicked(View v) {
-        SoundTriggerDetector detector = getDetector();
-        if (detector == null) {
-            Log.e(TAG, "Stop called on null detector.");
-            postMessage("Error: Stop called on null detector.");
-            return;
-        }
-        postMessage("Triggering stop recognition for model: " + mSelectedModelId);
-        if (!detector.stopRecognition()) {
-            Log.e(TAG, "Fast failure attempting to stop recognition.");
-            postMessage("Fast failure attempting to stop recognition: " + mSelectedModelId);
-        }
-    }
-
-    public synchronized void onRadioButtonClicked(View view) {
-        // Is the button now checked?
-        boolean checked = ((RadioButton) view).isChecked();
-        if (checked) {
-            mSelectedModelId = mModelIdMap.get(view);
-            ModelInfo modelInfo = mModelInfoMap.get(mSelectedModelId);
-            postMessage("Selected " + modelInfo.name);
-
-            // Set the play trigger button to be enabled only if we actually have some audio.
-            mPlayTriggerButton.setEnabled(modelInfo.triggerAudioPlayer != null);
-        }
-    }
-
-    public synchronized void onPlayTriggerButtonClicked(View v) {
-        ModelInfo modelInfo = mModelInfoMap.get(mSelectedModelId);
-        modelInfo.triggerAudioPlayer.start();
-        postMessage("Playing trigger audio for " + modelInfo.name);
-    }
-
-    // Helper struct for holding information about a model.
-    private static class ModelInfo {
-      public String name;
-      public UUID modelUuid;
-      public UUID vendorUuid;
-      public MediaPlayer triggerAudioPlayer;
-      public SoundTriggerDetector detector;
-      public byte modelData[];
-    };
-
-    // Implementation of SoundTriggerDetector.Callback.
-    public class DetectorCallback extends SoundTriggerDetector.Callback {
-        public void onAvailabilityChanged(int status) {
-            postMessage("Availability changed to: " + status);
-        }
-
-        public void onDetected(SoundTriggerDetector.EventPayload event) {
-            postMessage("onDetected(): " + eventPayloadToString(event));
-            screenWakeup();
-            mHandler.postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                   screenRelease();
-                }
-            }, 1000L);
-        }
-
-        public void onError() {
-            postMessage("onError()");
-        }
-
-        public void onRecognitionPaused() {
-            postMessage("onRecognitionPaused()");
-        }
-
-        public void onRecognitionResumed() {
-            postMessage("onRecognitionResumed()");
-        }
-    }
-
-    private String eventPayloadToString(SoundTriggerDetector.EventPayload event) {
-        String result = "EventPayload(";
-        AudioFormat format =  event.getCaptureAudioFormat();
-        result = result + "AudioFormat: " + ((format == null) ? "null" : format.toString());
-        byte[] triggerAudio = event.getTriggerAudio();
-        result = result + "TriggerAudio: " + (triggerAudio == null ? "null" : triggerAudio.length);
-        result = result + "CaptureSession: " + event.getCaptureSession();
-        result += " )";
-        return result;
-    }
-}