diff options
author | Chris Thornton <thorntonc@google.com> | 2016-06-30 22:05:51 -0700 |
---|---|---|
committer | Chris Thornton <thorntonc@google.com> | 2016-07-07 15:00:15 -0700 |
commit | dfa7c3b0dc623d6394b068ccd66b2fa7ddd6aa57 (patch) | |
tree | fc1286362baf8962122de51cbf3ec235ad14e576 /tests/SoundTriggerTestApp | |
parent | 018a2058246d1f034d25adbd69502e6b2eb81fc7 (diff) |
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
Diffstat (limited to 'tests/SoundTriggerTestApp')
7 files changed, 1153 insertions, 481 deletions
diff --git a/tests/SoundTriggerTestApp/AndroidManifest.xml b/tests/SoundTriggerTestApp/AndroidManifest.xml index 71d6001c3a56..87f3e92b3a60 100644 --- a/tests/SoundTriggerTestApp/AndroidManifest.xml +++ b/tests/SoundTriggerTestApp/AndroidManifest.xml @@ -1,25 +1,28 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.test.soundtrigger"> - + <uses-permission android:name="android.permission.CAPTURE_AUDIO_HOTWORD" /> <uses-permission android:name="android.permission.MANAGE_SOUND_TRIGGER" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application> <activity - android:name="TestSoundTriggerActivity" + android:name=".SoundTriggerTestActivity" android:label="SoundTrigger Test Application" android:screenOrientation="portrait" android:theme="@android:style/Theme.Material"> - <!-- - <intent-filter> - <action android:name="com.android.intent.action.MANAGE_SOUND_TRIGGER" /> - <category android:name="android.intent.category.DEFAULT" /> - </intent-filter> - --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + <service + android:name=".SoundTriggerTestService" + android:stopWithTask="false" + android:exported="true"> + <intent-filter> + <action android:name="com.android.intent.action.MANAGE_SOUND_TRIGGER" /> + </intent-filter> + </service> </application> </manifest> diff --git a/tests/SoundTriggerTestApp/res/layout/main.xml b/tests/SoundTriggerTestApp/res/layout/main.xml index 06949a0b6328..0fd8b12fafd7 100644 --- a/tests/SoundTriggerTestApp/res/layout/main.xml +++ b/tests/SoundTriggerTestApp/res/layout/main.xml @@ -18,81 +18,107 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical" - > -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - > + android:orientation="vertical"> - <Button - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/enroll" - android:onClick="onEnrollButtonClicked" - android:padding="20dp" /> + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <Button - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/reenroll" - android:onClick="onReEnrollButtonClicked" - android:padding="20dp" /> + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/load" + android:onClick="onLoadButtonClicked" + android:padding="20dp" /> - <Button - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/start_recog" - android:onClick="onStartRecognitionButtonClicked" - android:padding="20dp" /> + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/start_recog" + android:onClick="onStartRecognitionButtonClicked" + android:padding="20dp" /> - <Button - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/stop_recog" - android:onClick="onStopRecognitionButtonClicked" - android:padding="20dp" /> + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/stop_recog" + android:onClick="onStopRecognitionButtonClicked" + android:padding="20dp" /> - <Button - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/unenroll" - android:onClick="onUnEnrollButtonClicked" - android:padding="20dp" /> + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/unload" + android:onClick="onUnloadButtonClicked" + android:padding="20dp" /> - <Button - android:id="@+id/play_trigger_id" - android:layout_width="wrap_content" + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/reload" + android:onClick="onReloadButtonClicked" + android:padding="20dp" /> + + <Button + android:id="@+id/play_trigger_id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/play_trigger" + android:onClick="onPlayTriggerButtonClicked" + android:padding="20dp" /> + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/play_trigger" - android:onClick="onPlayTriggerButtonClicked" - android:padding="20dp" /> + android:layout_gravity="right"> -</LinearLayout> + <CheckBox + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/capture" + android:id="@+id/caputre_check_box" + android:layout_gravity="center_horizontal" + android:padding="20dp" /> + + <Button + android:id="@+id/play_captured_id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/play_capture" + android:padding="20dp" + android:enabled="false" /> + </LinearLayout> -<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android" + <RadioGroup xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/model_group_id" android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="20dp" - android:orientation="vertical"> -</RadioGroup> + android:orientation="vertical" /> -<ScrollView + <ScrollView android:id="@+id/scroller_id" android:layout_width="fill_parent" android:layout_height="wrap_content" android:scrollbars="vertical" android:fillViewport="true"> - <TextView - android:id="@+id/console" - android:paddingTop="20pt" - android:layout_height="fill_parent" - android:layout_width="fill_parent" - android:textSize="14dp" - android:layout_weight="1.0" - android:text="@string/none"> - </TextView> -</ScrollView> + <TextView + android:id="@+id/console" + android:paddingTop="20pt" + android:layout_height="fill_parent" + android:layout_width="fill_parent" + android:textSize="14dp" + android:layout_weight="1.0" + android:text="@string/none" /> + </ScrollView> </LinearLayout> diff --git a/tests/SoundTriggerTestApp/res/values/strings.xml b/tests/SoundTriggerTestApp/res/values/strings.xml index 7c1f64944e7f..c48b64884c5e 100644 --- a/tests/SoundTriggerTestApp/res/values/strings.xml +++ b/tests/SoundTriggerTestApp/res/values/strings.xml @@ -16,11 +16,14 @@ --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="enroll">Load</string> - <string name="reenroll">Re-load</string> - <string name="unenroll">Un-load</string> + <string name="load">Load</string> + <string name="reload">Reload</string> + <string name="unload">Unload</string> <string name="start_recog">Start</string> <string name="stop_recog">Stop</string> <string name="play_trigger">Play Trigger Audio</string> + <string name="capture">Capture Audio</string> + <string name="stop_capture">Stop Capturing Audio</string> + <string name="play_capture">Play Captured Audio</string> <string name="none">Debug messages appear here:\n</string> </resources> 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 000000000000..4841bc59c794 --- /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 000000000000..0ff95c4c49d4 --- /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 1c95c25370d2..8e5ed3210ab0 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 @@ package com.android.test.soundtrigger; 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 @@ import java.util.UUID; * 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 @@ public class SoundTriggerUtil { 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 @@ public class SoundTriggerUtil { } 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 @@ public class SoundTriggerUtil { 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 5fd38e953fda..000000000000 --- 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; - } -} |