diff options
Diffstat (limited to 'tests')
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; + } + +} |