summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java1
-rw-r--r--tests/Assist/src/com/android/test/assist/AssistInteractionSession.java3
-rw-r--r--tests/SoundTriggerTestApp/AndroidManifest.xml19
-rw-r--r--tests/SoundTriggerTestApp/res/layout/main.xml140
-rw-r--r--tests/SoundTriggerTestApp/res/values/strings.xml9
-rw-r--r--tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestActivity.java329
-rw-r--r--tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerTestService.java720
-rw-r--r--tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/SoundTriggerUtil.java17
-rw-r--r--tests/SoundTriggerTestApp/src/com/android/test/soundtrigger/TestSoundTriggerActivity.java400
-rw-r--r--tests/UiBench/src/com/android/test/uibench/MainActivity.java34
-rw-r--r--tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java17
-rw-r--r--tests/WallpaperTest/res/values/strings.xml3
-rw-r--r--tests/WallpaperTest/res/xml/test_wallpaper.xml5
-rw-r--r--tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java8
-rw-r--r--tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java2
-rw-r--r--tests/utils/testutils/Android.mk30
-rw-r--r--tests/utils/testutils/java/android/app/test/MockAnswerUtil.java62
-rw-r--r--tests/utils/testutils/java/android/app/test/TestAlarmManager.java188
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooper.java283
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooperTest.java371
-rw-r--r--tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java96
-rw-r--r--tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java86
22 files changed, 2317 insertions, 506 deletions
diff --git a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java
index 2346f8547550..16a0def7d5a5 100644
--- a/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java
+++ b/tests/AppLaunch/src/com/android/tests/applaunch/AppLaunch.java
@@ -574,6 +574,7 @@ public class AppLaunch extends InstrumentationTestCase {
mLaunchIntent = intent;
mForceStopBeforeLaunch = forceStopBeforeLaunch;
mLaunchReason = launchReason;
+ mResult = -1L;
}
public Long getResult() {
diff --git a/tests/Assist/src/com/android/test/assist/AssistInteractionSession.java b/tests/Assist/src/com/android/test/assist/AssistInteractionSession.java
index 851bda92a5a5..07186285be2b 100644
--- a/tests/Assist/src/com/android/test/assist/AssistInteractionSession.java
+++ b/tests/Assist/src/com/android/test/assist/AssistInteractionSession.java
@@ -77,6 +77,9 @@ public class AssistInteractionSession extends VoiceInteractionSession {
} catch (InterruptedException e) {
e.printStackTrace();
}
+
+ getWindow().getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
@Override
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..a2385d695450
--- /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;
- }
-}
diff --git a/tests/UiBench/src/com/android/test/uibench/MainActivity.java b/tests/UiBench/src/com/android/test/uibench/MainActivity.java
index 2111274a93c0..79837b6ea250 100644
--- a/tests/UiBench/src/com/android/test/uibench/MainActivity.java
+++ b/tests/UiBench/src/com/android/test/uibench/MainActivity.java
@@ -38,6 +38,23 @@ public class MainActivity extends AppCompatActivity {
private static final String EXTRA_PATH = "activity_path";
private static final String CATEGORY_HWUI_TEST = "com.android.test.uibench.TEST";
+ public static class TestListFragment extends ListFragment {
+ @Override
+ @SuppressWarnings("unchecked")
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ Map<String, Object> map = (Map<String, Object>)l.getItemAtPosition(position);
+
+ Intent intent = (Intent) map.get("intent");
+ startActivity(intent);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setTextFilterEnabled(true);
+ }
+ }
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -54,22 +71,7 @@ public class MainActivity extends AppCompatActivity {
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentById(android.R.id.content) == null) {
- ListFragment listFragment = new ListFragment() {
- @Override
- @SuppressWarnings("unchecked")
- public void onListItemClick(ListView l, View v, int position, long id) {
- Map<String, Object> map = (Map<String, Object>)l.getItemAtPosition(position);
-
- Intent intent = (Intent) map.get("intent");
- startActivity(intent);
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- getListView().setTextFilterEnabled(true);
- }
- };
+ ListFragment listFragment = new TestListFragment();
listFragment.setListAdapter(new SimpleAdapter(this, getData(path),
android.R.layout.simple_list_item_1, new String[] { "title" },
new int[] { android.R.id.text1 }));
diff --git a/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java b/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java
index d32f0716fe98..88847eed17fa 100644
--- a/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java
+++ b/tests/UiBench/src/com/android/test/uibench/ShadowGridActivity.java
@@ -23,19 +23,22 @@ import android.view.View;
import android.widget.ArrayAdapter;
public class ShadowGridActivity extends AppCompatActivity {
+ public static class NoDividerListFragment extends ListFragment {
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setDivider(null);
+ }
+ };
+
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentManager fm = getSupportFragmentManager();
if (fm.findFragmentById(android.R.id.content) == null) {
- ListFragment listFragment = new ListFragment() {
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- getListView().setDivider(null);
- }
- };
+ ListFragment listFragment = new NoDividerListFragment();
listFragment.setListAdapter(new ArrayAdapter<>(this,
R.layout.card_row, R.id.card_text, TextUtils.buildSimpleStringList()));
diff --git a/tests/WallpaperTest/res/values/strings.xml b/tests/WallpaperTest/res/values/strings.xml
index fd212596f644..80a3d494e5bb 100644
--- a/tests/WallpaperTest/res/values/strings.xml
+++ b/tests/WallpaperTest/res/values/strings.xml
@@ -24,6 +24,9 @@ limitations under the License.
Test wallpaper for use with the wallpaper test app.
</string>
+ <string name="test_wallpaper_context_uri">https://www.google.com/maps/@37.8092876,-122.408986,1391m/data=!3m1!1e3</string>
+ <string name="test_wallpaper_context_description">Explore</string>
+
<string name="dimens">Dimens: </string>
<string name="width">Width: </string>
<string name="height">Height: </string>
diff --git a/tests/WallpaperTest/res/xml/test_wallpaper.xml b/tests/WallpaperTest/res/xml/test_wallpaper.xml
index 9f7d714b75cd..ba2247855165 100644
--- a/tests/WallpaperTest/res/xml/test_wallpaper.xml
+++ b/tests/WallpaperTest/res/xml/test_wallpaper.xml
@@ -23,4 +23,7 @@
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:author="@string/test_wallpaper_author"
android:description="@string/test_wallpaper_desc"
- android:thumbnail="@drawable/test_wallpaper_thumb" />
+ android:thumbnail="@drawable/test_wallpaper_thumb"
+ android:showMetadataInPreview="true"
+ android:contextUri="@string/test_wallpaper_context_uri"
+ android:contextDescription="@string/test_wallpaper_context_description"/>
diff --git a/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java b/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java
index 95db6d100b79..ab36c222113a 100644
--- a/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java
+++ b/tests/WallpaperTest/src/com/example/wallpapertest/TestWallpaper.java
@@ -144,6 +144,14 @@ public class TestWallpaper extends WallpaperService {
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
+
+ // Simulate some slowness, so we can test the loading process in the live wallpaper
+ // picker.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
@Override
diff --git a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
index a726a15f7cc2..063dd8631565 100644
--- a/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
+++ b/tests/permission/src/com/android/framework/permission/tests/WindowManagerPermissionTests.java
@@ -93,7 +93,7 @@ public class WindowManagerPermissionTests extends TestCase {
try {
mWm.addAppToken(0, null, 0, 0, 0, false, false, 0, 0, false, false, null,
- Configuration.EMPTY, 0, false, false, 0);
+ Configuration.EMPTY, 0, false, false, 0, -1);
fail("IWindowManager.addAppToken did not throw SecurityException as"
+ " expected");
} catch (SecurityException e) {
diff --git a/tests/utils/testutils/Android.mk b/tests/utils/testutils/Android.mk
new file mode 100644
index 000000000000..d53167f19ebe
--- /dev/null
+++ b/tests/utils/testutils/Android.mk
@@ -0,0 +1,30 @@
+#
+# Copyright (C) 2016 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := frameworks-base-testutils
+LOCAL_MODULE_TAG := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under,java)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ mockito-target
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java
new file mode 100644
index 000000000000..746c77dda4d4
--- /dev/null
+++ b/tests/utils/testutils/java/android/app/test/MockAnswerUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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 android.app.test;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Utilities for creating Answers for mock objects
+ */
+public class MockAnswerUtil {
+
+ /**
+ * Answer that calls the method in the Answer called "answer" that matches the type signature of
+ * the method being answered. An error will be thrown at runtime if the signature does not match
+ * exactly.
+ */
+ public static class AnswerWithArguments implements Answer<Object> {
+ @Override
+ public final Object answer(InvocationOnMock invocation) throws Throwable {
+ Method method = invocation.getMethod();
+ try {
+ Method implementation = getClass().getMethod("answer", method.getParameterTypes());
+ if (!implementation.getReturnType().equals(method.getReturnType())) {
+ throw new RuntimeException("Found answer method does not have expected return "
+ + "type. Expected: " + method.getReturnType() + ", got "
+ + implementation.getReturnType());
+ }
+ Object[] args = invocation.getArguments();
+ try {
+ return implementation.invoke(this, args);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Error invoking answer method", e);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Could not find answer method with the expected args "
+ + Arrays.toString(method.getParameterTypes()), e);
+ }
+ }
+ }
+
+}
diff --git a/tests/utils/testutils/java/android/app/test/TestAlarmManager.java b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java
new file mode 100644
index 000000000000..e90ea1e64803
--- /dev/null
+++ b/tests/utils/testutils/java/android/app/test/TestAlarmManager.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2015 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 android.app.test;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import android.app.AlarmManager;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Creates an AlarmManager whose alarm dispatch can be controlled
+ * Currently only supports alarm listeners
+ *
+ * Alarm listeners will be dispatched to the handler provided or will
+ * be dispatched immediately if they would have been sent to the main
+ * looper (handler was null).
+ */
+public class TestAlarmManager {
+ private final AlarmManager mAlarmManager;
+ private final List<PendingAlarm> mPendingAlarms;
+
+ public TestAlarmManager() throws Exception {
+ mPendingAlarms = new ArrayList<>();
+
+ mAlarmManager = mock(AlarmManager.class);
+ doAnswer(new SetListenerAnswer()).when(mAlarmManager).set(anyInt(), anyLong(), anyString(),
+ any(AlarmManager.OnAlarmListener.class), any(Handler.class));
+ doAnswer(new SetListenerAnswer()).when(mAlarmManager).setExact(anyInt(), anyLong(),
+ anyString(), any(AlarmManager.OnAlarmListener.class), any(Handler.class));
+ doAnswer(new CancelListenerAnswer())
+ .when(mAlarmManager).cancel(any(AlarmManager.OnAlarmListener.class));
+ }
+
+ public AlarmManager getAlarmManager() {
+ return mAlarmManager;
+ }
+
+ /**
+ * Dispatch a pending alarm with the given tag
+ * @return if any alarm was dispatched
+ */
+ public boolean dispatch(String tag) {
+ for (int i = 0; i < mPendingAlarms.size(); ++i) {
+ PendingAlarm alarm = mPendingAlarms.get(i);
+ if (Objects.equals(tag, alarm.getTag())) {
+ mPendingAlarms.remove(i);
+ alarm.dispatch();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return if an alarm with the given tag is pending
+ */
+ public boolean isPending(String tag) {
+ for (int i = 0; i < mPendingAlarms.size(); ++i) {
+ PendingAlarm alarm = mPendingAlarms.get(i);
+ if (Objects.equals(tag, alarm.getTag())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return trigger time of an pending alarm with the given tag
+ * -1 if no pending alarm with the given tag
+ */
+ public long getTriggerTimeMillis(String tag) {
+ for (int i = 0; i < mPendingAlarms.size(); ++i) {
+ PendingAlarm alarm = mPendingAlarms.get(i);
+ if (Objects.equals(tag, alarm.getTag())) {
+ return alarm.getTriggerTimeMillis();
+ }
+ }
+ return -1;
+ }
+
+ private static class PendingAlarm {
+ private final int mType;
+ private final long mTriggerAtMillis;
+ private final String mTag;
+ private final Runnable mCallback;
+
+ public PendingAlarm(int type, long triggerAtMillis, String tag, Runnable callback) {
+ mType = type;
+ mTriggerAtMillis = triggerAtMillis;
+ mTag = tag;
+ mCallback = callback;
+ }
+
+ public void dispatch() {
+ if (mCallback != null) {
+ mCallback.run();
+ }
+ }
+
+ public Runnable getCallback() {
+ return mCallback;
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
+ public long getTriggerTimeMillis() {
+ return mTriggerAtMillis;
+ }
+ }
+
+ private class SetListenerAnswer extends AnswerWithArguments {
+ public void answer(int type, long triggerAtMillis, String tag,
+ AlarmManager.OnAlarmListener listener, Handler handler) {
+ mPendingAlarms.add(new PendingAlarm(type, triggerAtMillis, tag,
+ new AlarmListenerRunnable(listener, handler)));
+ }
+ }
+
+ private class CancelListenerAnswer extends AnswerWithArguments {
+ public void answer(AlarmManager.OnAlarmListener listener) {
+ Iterator<PendingAlarm> alarmItr = mPendingAlarms.iterator();
+ while (alarmItr.hasNext()) {
+ PendingAlarm alarm = alarmItr.next();
+ if (alarm.getCallback() instanceof AlarmListenerRunnable) {
+ AlarmListenerRunnable alarmCallback =
+ (AlarmListenerRunnable) alarm.getCallback();
+ if (alarmCallback.getListener() == listener) {
+ alarmItr.remove();
+ }
+ }
+ }
+ }
+ }
+
+ private static class AlarmListenerRunnable implements Runnable {
+ private final AlarmManager.OnAlarmListener mListener;
+ private final Handler mHandler;
+ public AlarmListenerRunnable(AlarmManager.OnAlarmListener listener, Handler handler) {
+ mListener = listener;
+ mHandler = handler;
+ }
+
+ public AlarmManager.OnAlarmListener getListener() {
+ return mListener;
+ }
+
+ @Override
+ public void run() {
+ if (mHandler != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onAlarm();
+ }
+ });
+ } else { // normally gets dispatched in main looper
+ mListener.onAlarm();
+ }
+ }
+ }
+}
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
new file mode 100644
index 000000000000..e8ceb4a9b02d
--- /dev/null
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2015 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 android.os.test;
+
+import static org.junit.Assert.assertTrue;
+
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Creates a looper whose message queue can be manipulated
+ * This allows testing code that uses a looper to dispatch messages in a deterministic manner
+ * Creating a TestLooper will also install it as the looper for the current thread
+ */
+public class TestLooper {
+ protected final Looper mLooper;
+
+ private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
+ private static final Field THREAD_LOCAL_LOOPER_FIELD;
+ private static final Field MESSAGE_QUEUE_MESSAGES_FIELD;
+ private static final Field MESSAGE_NEXT_FIELD;
+ private static final Field MESSAGE_WHEN_FIELD;
+ private static final Method MESSAGE_MARK_IN_USE_METHOD;
+ private static final String TAG = "TestLooper";
+
+ private AutoDispatchThread mAutoDispatchThread;
+
+ static {
+ try {
+ LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE);
+ LOOPER_CONSTRUCTOR.setAccessible(true);
+ THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
+ THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
+ MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
+ MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
+ MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
+ MESSAGE_NEXT_FIELD.setAccessible(true);
+ MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
+ MESSAGE_WHEN_FIELD.setAccessible(true);
+ MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
+ MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
+ } catch (NoSuchFieldException | NoSuchMethodException e) {
+ throw new RuntimeException("Failed to initialize TestLooper", e);
+ }
+ }
+
+
+ public TestLooper() {
+ try {
+ mLooper = LOOPER_CONSTRUCTOR.newInstance(false);
+
+ ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD
+ .get(null);
+ threadLocalLooper.set(mLooper);
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+ throw new RuntimeException("Reflection error constructing or accessing looper", e);
+ }
+ }
+
+ public Looper getLooper() {
+ return mLooper;
+ }
+
+ private Message getMessageLinkedList() {
+ try {
+ MessageQueue queue = mLooper.getQueue();
+ return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
+ e);
+ }
+ }
+
+ public void moveTimeForward(long milliSeconds) {
+ try {
+ Message msg = getMessageLinkedList();
+ while (msg != null) {
+ long updatedWhen = msg.getWhen() - milliSeconds;
+ if (updatedWhen < 0) {
+ updatedWhen = 0;
+ }
+ MESSAGE_WHEN_FIELD.set(msg, updatedWhen);
+ msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Access failed in TestLooper: set - Message.when", e);
+ }
+ }
+
+ private Message messageQueueNext() {
+ try {
+ long now = SystemClock.uptimeMillis();
+
+ Message prevMsg = null;
+ Message msg = getMessageLinkedList();
+ if (msg != null && msg.getTarget() == null) {
+ // Stalled by a barrier. Find the next asynchronous message in
+ // the queue.
+ do {
+ prevMsg = msg;
+ msg = (Message) MESSAGE_NEXT_FIELD.get(msg);
+ } while (msg != null && !msg.isAsynchronous());
+ }
+ if (msg != null) {
+ if (now >= msg.getWhen()) {
+ // Got a message.
+ if (prevMsg != null) {
+ MESSAGE_NEXT_FIELD.set(prevMsg, MESSAGE_NEXT_FIELD.get(msg));
+ } else {
+ MESSAGE_QUEUE_MESSAGES_FIELD.set(mLooper.getQueue(),
+ MESSAGE_NEXT_FIELD.get(msg));
+ }
+ MESSAGE_NEXT_FIELD.set(msg, null);
+ MESSAGE_MARK_IN_USE_METHOD.invoke(msg);
+ return msg;
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException("Access failed in TestLooper", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * @return true if there are pending messages in the message queue
+ */
+ public synchronized boolean isIdle() {
+ Message messageList = getMessageLinkedList();
+
+ return messageList != null && SystemClock.uptimeMillis() >= messageList.getWhen();
+ }
+
+ /**
+ * @return the next message in the Looper's message queue or null if there is none
+ */
+ public synchronized Message nextMessage() {
+ if (isIdle()) {
+ return messageQueueNext();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Dispatch the next message in the queue
+ * Asserts that there is a message in the queue
+ */
+ public synchronized void dispatchNext() {
+ assertTrue(isIdle());
+ Message msg = messageQueueNext();
+ if (msg == null) {
+ return;
+ }
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ /**
+ * Dispatch all messages currently in the queue
+ * Will not fail if there are no messages pending
+ * @return the number of messages dispatched
+ */
+ public synchronized int dispatchAll() {
+ int count = 0;
+ while (isIdle()) {
+ dispatchNext();
+ ++count;
+ }
+ return count;
+ }
+
+ /**
+ * Thread used to dispatch messages when the main thread is blocked waiting for a response.
+ */
+ private class AutoDispatchThread extends Thread {
+ private static final int MAX_LOOPS = 100;
+ private static final int LOOP_SLEEP_TIME_MS = 10;
+
+ private RuntimeException mAutoDispatchException = null;
+
+ /**
+ * Run method for the auto dispatch thread.
+ * The thread loops a maximum of MAX_LOOPS times with a 10ms sleep between loops.
+ * The thread continues looping and attempting to dispatch all messages until at
+ * least one message has been dispatched.
+ */
+ @Override
+ public void run() {
+ int dispatchCount = 0;
+ for (int i = 0; i < MAX_LOOPS; i++) {
+ try {
+ dispatchCount = dispatchAll();
+ } catch (RuntimeException e) {
+ mAutoDispatchException = e;
+ }
+ Log.d(TAG, "dispatched " + dispatchCount + " messages");
+ if (dispatchCount > 0) {
+ return;
+ }
+ try {
+ Thread.sleep(LOOP_SLEEP_TIME_MS);
+ } catch (InterruptedException e) {
+ mAutoDispatchException = new IllegalStateException(
+ "stopAutoDispatch called before any messages were dispatched.");
+ return;
+ }
+ }
+ Log.e(TAG, "AutoDispatchThread did not dispatch any messages.");
+ mAutoDispatchException = new IllegalStateException(
+ "TestLooper did not dispatch any messages before exiting.");
+ }
+
+ /**
+ * Method allowing the TestLooper to pass any exceptions thrown by the thread to be passed
+ * to the main thread.
+ *
+ * @return RuntimeException Exception created by stopping without dispatching a message
+ */
+ public RuntimeException getException() {
+ return mAutoDispatchException;
+ }
+ }
+
+ /**
+ * Create and start a new AutoDispatchThread if one is not already running.
+ */
+ public void startAutoDispatch() {
+ if (mAutoDispatchThread != null) {
+ throw new IllegalStateException(
+ "startAutoDispatch called with the AutoDispatchThread already running.");
+ }
+ mAutoDispatchThread = new AutoDispatchThread();
+ mAutoDispatchThread.start();
+ }
+
+ /**
+ * If an AutoDispatchThread is currently running, stop and clean up.
+ */
+ public void stopAutoDispatch() {
+ if (mAutoDispatchThread != null) {
+ if (mAutoDispatchThread.isAlive()) {
+ mAutoDispatchThread.interrupt();
+ }
+ try {
+ mAutoDispatchThread.join();
+ } catch (InterruptedException e) {
+ // Catch exception from join.
+ }
+
+ RuntimeException e = mAutoDispatchThread.getException();
+ mAutoDispatchThread = null;
+ if (e != null) {
+ throw e;
+ }
+ } else {
+ // stopAutoDispatch was called when startAutoDispatch has not created a new thread.
+ throw new IllegalStateException(
+ "stopAutoDispatch called without startAutoDispatch.");
+ }
+ }
+}
diff --git a/tests/utils/testutils/java/android/os/test/TestLooperTest.java b/tests/utils/testutils/java/android/os/test/TestLooperTest.java
new file mode 100644
index 000000000000..40d83b5b9171
--- /dev/null
+++ b/tests/utils/testutils/java/android/os/test/TestLooperTest.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2016 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 android.os.test;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ErrorCollector;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test TestLooperAbstractTime which provides control over "time". Note that
+ * real-time is being used as well. Therefore small time increments are NOT
+ * reliable. All tests are in "K" units (i.e. *1000).
+ */
+
+@SmallTest
+public class TestLooperTest {
+ private TestLooper mTestLooper;
+ private Handler mHandler;
+ private Handler mHandlerSpy;
+
+ @Rule
+ public ErrorCollector collector = new ErrorCollector();
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTestLooper = new TestLooper();
+ mHandler = new Handler(mTestLooper.getLooper());
+ mHandlerSpy = spy(mHandler);
+ }
+
+ /**
+ * Basic test with no time stamps: dispatch 4 messages, check that all 4
+ * delivered (in correct order).
+ */
+ @Test
+ public void testNoTimeMovement() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageC));
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageC", messageC, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, A@10K. Don't move time.
+ * <p>
+ * Expected: only get A, B
+ */
+ @Test
+ public void testDelayedDispatchNoTimeMove() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, A@10K, Advance time by 5K.
+ * <p>
+ * Expected: only get A, B, C
+ */
+ @Test
+ public void testDelayedDispatchAdvanceTimeOnce() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 10000);
+ mTestLooper.moveTimeForward(5000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, Advance time by 4K, A@1K, B@2K Advance
+ * time by 1K.
+ * <p>
+ * Expected: get A, B, C, A
+ */
+ @Test
+ public void testDelayedDispatchAdvanceTimeTwice() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mTestLooper.moveTimeForward(4000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 1000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+ mTestLooper.moveTimeForward(1000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageA", messageA, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, Advance time by 4K, A@5K, B@2K Advance
+ * time by 3K.
+ * <p>
+ * Expected: get A, B, C, B
+ */
+ @Test
+ public void testDelayedDispatchReverseOrder() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mTestLooper.moveTimeForward(4000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+ mTestLooper.moveTimeForward(3000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test message sequence: A, B, C@5K, Advance time by 4K, dispatch all,
+ * A@5K, B@2K Advance time by 3K, dispatch all.
+ * <p>
+ * Expected: get A, B after first dispatch; then C, B after second dispatch
+ */
+ @Test
+ public void testDelayedDispatchAllMultipleTimes() {
+ final int messageA = 1;
+ final int messageB = 2;
+ final int messageC = 3;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageB));
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageC), 5000);
+ mTestLooper.moveTimeForward(4000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("1: messageA", messageA, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("2: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageA), 5000);
+ mHandlerSpy.sendMessageDelayed(mHandler.obtainMessage(messageB), 2000);
+ mTestLooper.moveTimeForward(3000);
+ mTestLooper.dispatchAll();
+
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("3: messageC", messageC, equalTo(messageCaptor.getValue().what));
+ inOrder.verify(mHandlerSpy).handleMessage(messageCaptor.capture());
+ collector.checkThat("4: messageB", messageB, equalTo(messageCaptor.getValue().what));
+
+ inOrder.verify(mHandlerSpy, never()).handleMessage(any(Message.class));
+ }
+
+ /**
+ * Test AutoDispatch for a single message.
+ * This test would ideally use the Channel sendMessageSynchronously. At this time, the setup to
+ * get a working test channel is cumbersome. Until this is fixed, we substitute with a
+ * sendMessage followed by a blocking call. The main test thread blocks until the test handler
+ * receives the test message (messageA) and sets a boolean true. Once the boolean is true, the
+ * main thread will exit the busy wait loop, stop autoDispatch and check the assert.
+ *
+ * Enable AutoDispatch, add message, block on message being handled and stop AutoDispatch.
+ * <p>
+ * Expected: handleMessage is called for messageA and stopAutoDispatch is called.
+ */
+ @Test
+ public void testAutoDispatchWithSingleMessage() {
+ final int mLoopSleepTimeMs = 5;
+
+ final int messageA = 1;
+
+ TestLooper mockLooper = new TestLooper();
+ class TestHandler extends Handler {
+ public volatile boolean handledMessage = false;
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == messageA) {
+ handledMessage = true;
+ }
+ }
+ }
+
+ TestHandler testHandler = new TestHandler(mockLooper.getLooper());
+ mockLooper.startAutoDispatch();
+ testHandler.sendMessage(testHandler.obtainMessage(messageA));
+ while (!testHandler.handledMessage) {
+ // Block until message is handled
+ try {
+ Thread.sleep(mLoopSleepTimeMs);
+ } catch (InterruptedException e) {
+ // Interrupted while sleeping.
+ }
+ }
+ mockLooper.stopAutoDispatch();
+ assertTrue("TestHandler should have received messageA", testHandler.handledMessage);
+ }
+
+ /**
+ * Test starting AutoDispatch while already running throws IllegalStateException
+ * Enable AutoDispatch two times in a row.
+ * <p>
+ * Expected: catch IllegalStateException on second call.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testRepeatedStartAutoDispatchThrowsException() {
+ mTestLooper.startAutoDispatch();
+ mTestLooper.startAutoDispatch();
+ }
+
+ /**
+ * Test stopping AutoDispatch without previously starting throws IllegalStateException
+ * Stop AutoDispatch
+ * <p>
+ * Expected: catch IllegalStateException on second call.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testStopAutoDispatchWithoutStartThrowsException() {
+ mTestLooper.stopAutoDispatch();
+ }
+
+ /**
+ * Test AutoDispatch exits and does not dispatch a later message.
+ * Start and stop AutoDispatch then add a message.
+ * <p>
+ * Expected: After AutoDispatch is stopped, dispatchAll will return 1.
+ */
+ @Test
+ public void testAutoDispatchStopsCleanlyWithoutDispatchingAMessage() {
+ final int messageA = 1;
+
+ InOrder inOrder = inOrder(mHandlerSpy);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+
+ mTestLooper.startAutoDispatch();
+ try {
+ mTestLooper.stopAutoDispatch();
+ } catch (IllegalStateException e) {
+ // Stopping without a dispatch will throw an exception.
+ }
+
+ mHandlerSpy.sendMessage(mHandler.obtainMessage(messageA));
+ assertEquals("One message should be dispatched", 1, mTestLooper.dispatchAll());
+ }
+
+ /**
+ * Test AutoDispatch throws an exception when no messages are dispatched.
+ * Start and stop AutoDispatch
+ * <p>
+ * Expected: Exception is thrown with the stopAutoDispatch call.
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testAutoDispatchThrowsExceptionWhenNoMessagesDispatched() {
+ mTestLooper.startAutoDispatch();
+ mTestLooper.stopAutoDispatch();
+ }
+}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java
new file mode 100644
index 000000000000..25cd5b9b9088
--- /dev/null
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannel.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 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.internal.util.test;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+
+
+/**
+ * Provides an AsyncChannel interface that implements the connection initiating half of a
+ * bidirectional channel as described in {@link com.android.internal.util.AsyncChannel}.
+ */
+public class BidirectionalAsyncChannel {
+ private static final String TAG = "BidirectionalAsyncChannel";
+
+ private AsyncChannel mChannel;
+ public enum ChannelState { DISCONNECTED, HALF_CONNECTED, CONNECTED, FAILURE };
+ private ChannelState mState = ChannelState.DISCONNECTED;
+
+ public void assertConnected() {
+ assertEquals("AsyncChannel was not fully connected", ChannelState.CONNECTED, mState);
+ }
+
+ public void connect(final Looper looper, final Messenger messenger,
+ final Handler incomingMessageHandler) {
+ assertEquals("AsyncChannel must be disconnected to connect",
+ ChannelState.DISCONNECTED, mState);
+ mChannel = new AsyncChannel();
+ Handler rawMessageHandler = new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
+ if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+ Log.d(TAG, "Successfully half connected " + this);
+ mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+ mState = ChannelState.HALF_CONNECTED;
+ } else {
+ Log.d(TAG, "Failed to connect channel " + this);
+ mState = ChannelState.FAILURE;
+ mChannel = null;
+ }
+ break;
+ case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
+ mState = ChannelState.CONNECTED;
+ Log.d(TAG, "Channel fully connected" + this);
+ break;
+ case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+ mState = ChannelState.DISCONNECTED;
+ mChannel = null;
+ Log.d(TAG, "Channel disconnected" + this);
+ break;
+ default:
+ incomingMessageHandler.handleMessage(msg);
+ break;
+ }
+ }
+ };
+ mChannel.connect(null, rawMessageHandler, messenger);
+ }
+
+ public void disconnect() {
+ assertEquals("AsyncChannel must be connected to disconnect",
+ ChannelState.CONNECTED, mState);
+ mChannel.sendMessage(AsyncChannel.CMD_CHANNEL_DISCONNECT);
+ mState = ChannelState.DISCONNECTED;
+ mChannel = null;
+ }
+
+ public void sendMessage(Message msg) {
+ assertEquals("AsyncChannel must be connected to send messages",
+ ChannelState.CONNECTED, mState);
+ mChannel.sendMessage(msg);
+ }
+}
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java
new file mode 100644
index 000000000000..49c833228b6c
--- /dev/null
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BidirectionalAsyncChannelServer.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016 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.internal.util.test;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides an interface for the server side implementation of a bidirectional channel as described
+ * in {@link com.android.internal.util.AsyncChannel}.
+ */
+public class BidirectionalAsyncChannelServer {
+
+ private static final String TAG = "BidirectionalAsyncChannelServer";
+
+ // Keeps track of incoming clients, which are identifiable by their messengers.
+ private final Map<Messenger, AsyncChannel> mClients = new HashMap<>();
+
+ private Messenger mMessenger;
+
+ public BidirectionalAsyncChannelServer(final Context context, final Looper looper,
+ final Handler messageHandler) {
+ Handler handler = new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncChannel channel = mClients.get(msg.replyTo);
+ switch (msg.what) {
+ case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
+ if (channel != null) {
+ Log.d(TAG, "duplicate client connection: " + msg.sendingUid);
+ channel.replyToMessage(msg,
+ AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+ AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED);
+ } else {
+ channel = new AsyncChannel();
+ mClients.put(msg.replyTo, channel);
+ channel.connected(context, this, msg.replyTo);
+ channel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+ AsyncChannel.STATUS_SUCCESSFUL);
+ }
+ break;
+ case AsyncChannel.CMD_CHANNEL_DISCONNECT:
+ channel.disconnect();
+ break;
+
+ case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+ mClients.remove(msg.replyTo);
+ break;
+
+ default:
+ messageHandler.handleMessage(msg);
+ break;
+ }
+ }
+ };
+ mMessenger = new Messenger(handler);
+ }
+
+ public Messenger getMessenger() {
+ return mMessenger;
+ }
+
+}