Merge "Updates to the sound trigger test app" into nyc-mr1-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
e1fdcc49fd
@@ -1,25 +1,28 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.android.test.soundtrigger">
|
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.MANAGE_SOUND_TRIGGER" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name="TestSoundTriggerActivity"
|
android:name=".SoundTriggerTestActivity"
|
||||||
android:label="SoundTrigger Test Application"
|
android:label="SoundTrigger Test Application"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:theme="@android:style/Theme.Material">
|
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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -18,81 +18,107 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
>
|
|
||||||
|
|
||||||
<Button
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<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/stop_recog"
|
||||||
|
android:onClick="onStopRecognitionButtonClicked"
|
||||||
|
android:padding="20dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/unload"
|
||||||
|
android:onClick="onUnloadButtonClicked"
|
||||||
|
android:padding="20dp" />
|
||||||
|
|
||||||
|
</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:layout_height="wrap_content"
|
||||||
android:text="@string/enroll"
|
android:layout_gravity="right">
|
||||||
android:onClick="onEnrollButtonClicked"
|
|
||||||
android:padding="20dp" />
|
|
||||||
|
|
||||||
<Button
|
<CheckBox
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/reenroll"
|
android:text="@string/capture"
|
||||||
android:onClick="onReEnrollButtonClicked"
|
android:id="@+id/caputre_check_box"
|
||||||
android:padding="20dp" />
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:padding="20dp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/play_captured_id"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:text="@string/start_recog"
|
android:layout_height="wrap_content"
|
||||||
android:onClick="onStartRecognitionButtonClicked"
|
android:text="@string/play_capture"
|
||||||
android:padding="20dp" />
|
android:padding="20dp"
|
||||||
|
android:enabled="false" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<Button
|
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
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: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>
|
|
||||||
|
|
||||||
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/model_group_id"
|
android:id="@+id/model_group_id"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="20dp"
|
android:padding="20dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical" />
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/scroller_id"
|
android:id="@+id/scroller_id"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
android:fillViewport="true">
|
android:fillViewport="true">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/console"
|
android:id="@+id/console"
|
||||||
android:paddingTop="20pt"
|
android:paddingTop="20pt"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="fill_parent"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:textSize="14dp"
|
android:textSize="14dp"
|
||||||
android:layout_weight="1.0"
|
android:layout_weight="1.0"
|
||||||
android:text="@string/none">
|
android:text="@string/none" />
|
||||||
</TextView>
|
</ScrollView>
|
||||||
</ScrollView>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -16,11 +16,14 @@
|
|||||||
-->
|
-->
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
|
||||||
<string name="enroll">Load</string>
|
<string name="load">Load</string>
|
||||||
<string name="reenroll">Re-load</string>
|
<string name="reload">Reload</string>
|
||||||
<string name="unenroll">Un-load</string>
|
<string name="unload">Unload</string>
|
||||||
<string name="start_recog">Start</string>
|
<string name="start_recog">Start</string>
|
||||||
<string name="stop_recog">Stop</string>
|
<string name="stop_recog">Stop</string>
|
||||||
<string name="play_trigger">Play Trigger Audio</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>
|
<string name="none">Debug messages appear here:\n</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ package com.android.test.soundtrigger;
|
|||||||
|
|
||||||
import android.annotation.Nullable;
|
import android.annotation.Nullable;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.soundtrigger.SoundTrigger;
|
|
||||||
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
|
import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
|
||||||
import android.media.soundtrigger.SoundTriggerDetector;
|
import android.media.soundtrigger.SoundTriggerDetector;
|
||||||
import android.media.soundtrigger.SoundTriggerManager;
|
import android.media.soundtrigger.SoundTriggerManager;
|
||||||
@@ -36,7 +35,7 @@ import java.util.UUID;
|
|||||||
* Utility class for the managing sound trigger sound models.
|
* Utility class for the managing sound trigger sound models.
|
||||||
*/
|
*/
|
||||||
public class SoundTriggerUtil {
|
public class SoundTriggerUtil {
|
||||||
private static final String TAG = "TestSoundTriggerUtil:Hotsound";
|
private static final String TAG = "SoundTriggerTestUtil";
|
||||||
|
|
||||||
private final ISoundTriggerService mSoundTriggerService;
|
private final ISoundTriggerService mSoundTriggerService;
|
||||||
private final SoundTriggerManager mSoundTriggerManager;
|
private final SoundTriggerManager mSoundTriggerManager;
|
||||||
@@ -68,10 +67,6 @@ public class SoundTriggerUtil {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addOrUpdateSoundModel(SoundTriggerManager.Model soundModel) {
|
|
||||||
mSoundTriggerManager.updateModel(soundModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the sound model for the given keyphrase, null if none exists.
|
* 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,
|
* 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) {
|
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;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return model;
|
return model;
|
||||||
@@ -109,18 +104,14 @@ public class SoundTriggerUtil {
|
|||||||
try {
|
try {
|
||||||
mSoundTriggerService.deleteSoundModel(new ParcelUuid(modelId));
|
mSoundTriggerService.deleteSoundModel(new ParcelUuid(modelId));
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "RemoteException in updateSoundModel");
|
Log.e(TAG, "RemoteException in deleteSoundModel");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteSoundModelUsingManager(UUID modelId) {
|
|
||||||
mSoundTriggerManager.deleteModel(modelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SoundTriggerDetector createSoundTriggerDetector(UUID modelId,
|
public SoundTriggerDetector createSoundTriggerDetector(UUID modelId,
|
||||||
SoundTriggerDetector.Callback callback) {
|
SoundTriggerDetector.Callback callback) {
|
||||||
return mSoundTriggerManager.createSoundTriggerDetector(modelId, callback, null);
|
return mSoundTriggerManager.createSoundTriggerDetector(modelId, callback, 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user