Merge "AVRCP: Try update media metadata on app force quit"
diff --git a/src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java b/src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java
index b5886c2..486610f 100644
--- a/src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java
+++ b/src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java
@@ -74,6 +74,7 @@
for (ResolveInfo info : players) {
BrowsedPlayerWrapper player = BrowsedPlayerWrapper.wrap(
context,
+ looper,
info.serviceInfo.packageName,
info.serviceInfo.name);
newWrapper.mPendingPlayers.add(player);
diff --git a/src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java b/src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java
index b704aae..783da64 100644
--- a/src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java
+++ b/src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java
@@ -16,9 +16,14 @@
package com.android.bluetooth.avrcp;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.util.Log;
import java.util.ArrayList;
@@ -34,7 +39,7 @@
*/
class BrowsedPlayerWrapper {
private static final String TAG = "AvrcpBrowsedPlayerWrapper";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
enum ConnectionState {
DISCONNECTED,
@@ -46,6 +51,10 @@
void run(int status, BrowsedPlayerWrapper wrapper);
}
+ interface PlaybackCallback {
+ void run(int status);
+ }
+
interface BrowseCallback {
void run(int status, String mediaId, List<ListItem> results);
}
@@ -53,13 +62,16 @@
public static final int STATUS_SUCCESS = 0;
public static final int STATUS_CONN_ERROR = 1;
public static final int STATUS_LOOKUP_ERROR = 2;
+ public static final int STATUS_PLAYBACK_TIMEOUT_ERROR = 3;
private MediaBrowser mWrappedBrowser;
// TODO (apanicke): Store the context in the factories so that we don't need to save this.
// As long as the service is alive those factories will have a valid context.
- private Context mContext;
- private String mPackageName;
+ private final Context mContext;
+ private final Looper mLooper;
+ private final String mPackageName;
+ private final Object mCallbackLock = new Object();
private ConnectionCallback mCallback;
// TODO(apanicke): We cache this because normally you can only grab the root
@@ -91,10 +103,11 @@
// TODO (apanicke): Investigate if there is a way to create this just by passing in the
// MediaBrowser. Right now there is no obvious way to create the browser then update the
// connection callback without being forced to re-create the object every time.
- private BrowsedPlayerWrapper(Context context, String packageName, String className) {
+ private BrowsedPlayerWrapper(Context context, Looper looper, String packageName,
+ String className) {
mContext = context;
mPackageName = packageName;
-
+ mLooper = looper;
mWrappedBrowser = MediaBrowserFactory.make(
context,
new ComponentName(packageName, className),
@@ -102,35 +115,79 @@
null);
}
- static BrowsedPlayerWrapper wrap(Context context, String packageName, String className) {
+ static BrowsedPlayerWrapper wrap(Context context, Looper looper, String packageName,
+ String className) {
Log.i(TAG, "Wrapping Media Browser " + packageName);
BrowsedPlayerWrapper wrapper =
- new BrowsedPlayerWrapper(context, packageName, className);
+ new BrowsedPlayerWrapper(context, looper, packageName, className);
return wrapper;
}
- void connect(ConnectionCallback cb) {
+ /**
+ * Connect to the media application's MediaBrowserService
+ *
+ * Connections are asynchronous in nature. The given callback will be invoked once the
+ * connection is established. The connection will be torn down once your callback is executed
+ * when using this function. If you wish to control the lifecycle of the connection on your own
+ * then use {@link #setCallbackAndConnect(ConnectionCallback)} instead.
+ *
+ * @param cb A callback to execute once the connection is established
+ * @return True if we successfully make a connection attempt, False otherwise
+ */
+ boolean connect(ConnectionCallback cb) {
if (cb == null) {
Log.wtfStack(TAG, "connect: Trying to connect to " + mPackageName
+ "with null callback");
}
- if (mCallback != null) {
- Log.w(TAG, "connect: Already trying to connect to " + mPackageName);
- return;
- }
-
- if (DEBUG) Log.d(TAG, "connect: Connecting to browsable player: " + mPackageName);
- mCallback = (int status, BrowsedPlayerWrapper wrapper) -> {
+ return setCallbackAndConnect((int status, BrowsedPlayerWrapper wrapper) -> {
cb.run(status, wrapper);
- wrapper.disconnect();
- };
- mWrappedBrowser.connect();
+ disconnect();
+ });
}
+ /**
+ * Disconnect from the media application's MediaBrowserService
+ *
+ * This clears any pending requests. This function is safe to call even if a connection isn't
+ * currently open.
+ */
void disconnect() {
if (DEBUG) Log.d(TAG, "disconnect: Disconnecting from " + mPackageName);
mWrappedBrowser.disconnect();
- mCallback = null;
+ clearCallback();
+ }
+
+ boolean setCallbackAndConnect(ConnectionCallback callback) {
+ synchronized (mCallbackLock) {
+ if (mCallback != null) {
+ Log.w(TAG, "setCallbackAndConnect: Already trying to connect to ");
+ return false;
+ }
+ mCallback = callback;
+ }
+ if (DEBUG) Log.d(TAG, "Set mCallback, connecting to " + mPackageName);
+ mWrappedBrowser.connect();
+ return true;
+ }
+
+ void executeCallback(int status, BrowsedPlayerWrapper player) {
+ final ConnectionCallback callback;
+ synchronized (mCallbackLock) {
+ if (mCallback == null) {
+ Log.w(TAG, "Callback is NULL. Cannot execute");
+ return;
+ }
+ callback = mCallback;
+ }
+ if (DEBUG) Log.d(TAG, "Executing callback");
+ callback.run(status, player);
+ }
+
+ void clearCallback() {
+ synchronized (mCallbackLock) {
+ mCallback = null;
+ }
+ if (DEBUG) Log.d(TAG, "mCallback = null");
}
public String getPackageName() {
@@ -141,23 +198,39 @@
return mRoot;
}
- public void playItem(String mediaId) {
- if (DEBUG) Log.d(TAG, "playItem: Play Item from media ID: " + mediaId);
- connect((int status, BrowsedPlayerWrapper wrapper) -> {
+ /**
+ * Requests to play a media item with a given media ID
+ *
+ * @param mediaId A string indicating the piece of media you would like to play
+ * @return False if any other requests are being serviced, True otherwise
+ */
+ public boolean playItem(String mediaId) {
+ if (DEBUG) Log.d(TAG, "playItem: Play item from media ID: " + mediaId);
+ return setCallbackAndConnect((int status, BrowsedPlayerWrapper wrapper) -> {
if (DEBUG) Log.d(TAG, "playItem: Connected to browsable player " + mPackageName);
-
MediaController controller = MediaControllerFactory.make(mContext,
wrapper.mWrappedBrowser.getSessionToken());
MediaController.TransportControls ctrl = controller.getTransportControls();
Log.i(TAG, "playItem: Playing " + mediaId);
ctrl.playFromMediaId(mediaId, null);
+
+ MediaPlaybackListener mpl = new MediaPlaybackListener(mLooper, controller);
+ mpl.waitForPlayback((int playbackStatus) -> {
+ Log.i(TAG, "playItem: Media item playback returned, status: " + playbackStatus);
+ disconnect();
+ });
});
- return;
}
- // Returns false if the player is in the connecting state. Wait for it to either be
- // connected or disconnected.
- //
+ /**
+ * Request the contents of a folder item identified by the given media ID
+ *
+ * Contents must be loaded from a service and are returned asynchronously.
+ *
+ * @param mediaId A string indicating the piece of media you would like to play
+ * @param cb A Callback that returns the loaded contents of the requested media ID
+ * @return False if any other requests are being serviced, True otherwise
+ */
// TODO (apanicke): Determine what happens when we subscribe to the same item while a
// callback is in flight.
//
@@ -172,27 +245,18 @@
}
if (cb == null) {
- Log.wtfStack(TAG, "connect: Trying to connect to " + mPackageName
- + "with null callback");
- }
- if (mCallback != null) {
- Log.w(TAG, "connect: Already trying to connect to " + mPackageName);
- return false;
+ Log.wtfStack(TAG, "getFolderItems: Trying to connect to " + mPackageName
+ + "with null browse callback");
}
- if (DEBUG) Log.d(TAG, "connect: Connecting to browsable player: " + mPackageName);
- mCallback = (int status, BrowsedPlayerWrapper wrapper) -> {
+ if (DEBUG) Log.d(TAG, "getFolderItems: Connecting to browsable player: " + mPackageName);
+ return setCallbackAndConnect((int status, BrowsedPlayerWrapper wrapper) -> {
Log.i(TAG, "getFolderItems: Connected to browsable player: " + mPackageName);
if (status != STATUS_SUCCESS) {
cb.run(status, "", new ArrayList<ListItem>());
}
-
- // This will disconnect when the callback is called
getFolderItemsInternal(mediaId, cb);
- };
- mWrappedBrowser.connect();
-
- return true;
+ });
}
// Internal function to call once the Browser is connected
@@ -208,44 +272,128 @@
// Get the root while connected because we may need to use it when disconnected.
mRoot = mWrappedBrowser.getRoot();
- if (mCallback == null) return;
-
if (mRoot == null || mRoot.isEmpty()) {
- mCallback.run(STATUS_CONN_ERROR, BrowsedPlayerWrapper.this);
+ executeCallback(STATUS_CONN_ERROR, BrowsedPlayerWrapper.this);
return;
}
- mCallback.run(STATUS_SUCCESS, BrowsedPlayerWrapper.this);
- mCallback = null;
+ executeCallback(STATUS_SUCCESS, BrowsedPlayerWrapper.this);
}
@Override
public void onConnectionFailed() {
Log.w(TAG, "onConnectionFailed: Connection Failed with " + mPackageName);
- if (mCallback != null) mCallback.run(STATUS_CONN_ERROR, BrowsedPlayerWrapper.this);
- mCallback = null;
+ executeCallback(STATUS_CONN_ERROR, BrowsedPlayerWrapper.this);
+ // No need to call disconnect as we never connected. Just need to remove our callback.
+ clearCallback();
}
// TODO (apanicke): Add a check to list a player as unbrowsable if it suspends immediately
// after connection.
@Override
public void onConnectionSuspended() {
- mWrappedBrowser.disconnect();
+ executeCallback(STATUS_CONN_ERROR, BrowsedPlayerWrapper.this);
+ disconnect();
Log.i(TAG, "onConnectionSuspended: Connection Suspended with " + mPackageName);
}
}
+ class TimeoutHandler extends Handler {
+ static final int MSG_TIMEOUT = 0;
+ static final long CALLBACK_TIMEOUT_MS = 5000;
+
+ private PlaybackCallback mPlaybackCallback = null;
+
+ TimeoutHandler(Looper looper, PlaybackCallback cb) {
+ super(looper);
+ mPlaybackCallback = cb;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what != MSG_TIMEOUT) {
+ Log.wtf(TAG, "Unknown message on timeout handler: " + msg.what);
+ return;
+ }
+
+ Log.e(TAG, "Timeout while waiting for playback to begin on " + mPackageName);
+ mPlaybackCallback.run(STATUS_PLAYBACK_TIMEOUT_ERROR);
+ }
+ }
+
+ class MediaPlaybackListener extends MediaController.Callback {
+ private final Object mTimeoutHandlerLock = new Object();
+ private Handler mTimeoutHandler = null;
+ private Looper mLooper = null;
+ private MediaController mController = null;
+ private PlaybackCallback mPlaybackCallback = null;
+
+ MediaPlaybackListener(Looper looper, MediaController controller) {
+ synchronized (mTimeoutHandlerLock) {
+ mController = controller;
+ mLooper = looper;
+ }
+ }
+
+ void waitForPlayback(PlaybackCallback cb) {
+ synchronized (mTimeoutHandlerLock) {
+ mPlaybackCallback = cb;
+
+ // If we don't already have the proper state then register the callbacks to execute
+ // on the same thread as the timeout thread. This prevents a race condition where a
+ // timeout happens at the same time as an update. Then set the timeout
+ PlaybackState state = mController.getPlaybackState();
+ if (state == null || state.getState() != PlaybackState.STATE_PLAYING) {
+ Log.d(TAG, "MediaPlayback: Waiting for media to play for " + mPackageName);
+ mTimeoutHandler = new TimeoutHandler(mLooper, mPlaybackCallback);
+ mController.registerCallback(this, mTimeoutHandler);
+ mTimeoutHandler.sendEmptyMessageDelayed(TimeoutHandler.MSG_TIMEOUT,
+ TimeoutHandler.CALLBACK_TIMEOUT_MS);
+ } else {
+ Log.d(TAG, "MediaPlayback: Media is already playing for " + mPackageName);
+ mPlaybackCallback.run(STATUS_SUCCESS);
+ cleanup();
+ }
+ }
+ }
+
+ void cleanup() {
+ synchronized (mTimeoutHandlerLock) {
+ if (mController != null) {
+ mController.unregisterCallback(this);
+ }
+ mController = null;
+
+ if (mTimeoutHandler != null) {
+ mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT);
+ }
+ mTimeoutHandler = null;
+ mPlaybackCallback = null;
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(@Nullable PlaybackState state) {
+ if (DEBUG) Log.d(TAG, "MediaPlayback: " + mPackageName + " -> " + state.toString());
+ if (state.getState() == PlaybackState.STATE_PLAYING) {
+ mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT);
+ mPlaybackCallback.run(STATUS_SUCCESS);
+ cleanup();
+ }
+ }
+ }
+
/**
* Subscription callback handler. Subscribe to a folder to get its contents. We generate a new
* instance for this class for each subscribe call to make it easier to differentiate between
* the callers.
*/
private class BrowserSubscriptionCallback extends MediaBrowser.SubscriptionCallback {
- BrowseCallback mCallback = null;
+ BrowseCallback mBrowseCallback = null;
BrowserSubscriptionCallback(BrowseCallback cb) {
- mCallback = cb;
+ mBrowseCallback = cb;
}
@Override
@@ -254,7 +402,7 @@
Log.d(TAG, "onChildrenLoaded: mediaId=" + parentId + " size= " + children.size());
}
- if (mCallback == null) {
+ if (mBrowseCallback == null) {
Log.w(TAG, "onChildrenLoaded: " + mPackageName
+ " children loaded while callback is null");
}
@@ -288,8 +436,8 @@
mCachedFolders.put(parentId, return_list);
// Clone the list so that the callee can mutate it without affecting the cached data
- mCallback.run(STATUS_SUCCESS, parentId, Util.cloneList(return_list));
- mCallback = null;
+ mBrowseCallback.run(STATUS_SUCCESS, parentId, Util.cloneList(return_list));
+ mBrowseCallback = null;
disconnect();
}
@@ -297,7 +445,7 @@
@Override
public void onError(String id) {
Log.e(TAG, "BrowserSubscriptionCallback: Could not get folder items");
- mCallback.run(STATUS_LOOKUP_ERROR, id, new ArrayList<ListItem>());
+ mBrowseCallback.run(STATUS_LOOKUP_ERROR, id, new ArrayList<ListItem>());
disconnect();
}
}
diff --git a/src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java b/src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java
index 4e0b812..f0309fd 100644
--- a/src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java
+++ b/src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java
@@ -135,8 +135,9 @@
}
long getActiveQueueID() {
- if (mMediaController.getPlaybackState() == null) return -1;
- return mMediaController.getPlaybackState().getActiveQueueItemId();
+ PlaybackState state = mMediaController.getPlaybackState();
+ if (state == null) return -1;
+ return state.getActiveQueueItemId();
}
List<Metadata> getCurrentQueue() {
diff --git a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index c5485f9..06c4822 100644
--- a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -1198,6 +1198,21 @@
}
}
+ class MyAudioServerStateCallback extends AudioManager.AudioServerStateCallback {
+ @Override
+ public void onAudioServerDown() {
+ logi("onAudioServerDown");
+ }
+
+ @Override
+ public void onAudioServerUp() {
+ logi("onAudioServerUp restoring audio parameters");
+ setAudioParameters();
+ }
+ }
+
+ MyAudioServerStateCallback mAudioServerStateCallback = new MyAudioServerStateCallback();
+
class AudioOn extends ConnectedBase {
@Override
int getAudioStateInt() {
@@ -1216,10 +1231,21 @@
mHeadsetService.setActiveDevice(mDevice);
}
setAudioParameters();
+
+ mSystemInterface.getAudioManager().setAudioServerStateCallback(
+ mHeadsetService.getMainExecutor(), mAudioServerStateCallback);
+
broadcastStateTransitions();
}
@Override
+ public void exit() {
+ super.exit();
+
+ mSystemInterface.getAudioManager().clearAudioServerStateCallback();
+ }
+
+ @Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT: {
diff --git a/src/com/android/bluetooth/opp/BluetoothOppTransfer.java b/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
index 91af9ca..06562e4 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppTransfer.java
@@ -474,7 +474,7 @@
mBatch.mStatus = Constants.BATCH_STATUS_FAILED;
return;
}
-
+ registerConnectionreceiver();
if (mHandlerThread == null) {
if (V) {
Log.v(TAG, "Create handler thread for batch " + mBatch.mId);
@@ -495,7 +495,7 @@
startObexSession();
}
}
- registerConnectionreceiver();
+
}
/**
diff --git a/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java b/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
index fc45d3f..a7c14ac 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
@@ -375,24 +375,13 @@
mTransInfo.mID);
} else if (mWhichDialog == DIALOG_SEND_COMPLETE_FAIL) {
// "try again"
-
// make current transfer "hidden"
BluetoothOppUtility.updateVisibilityToHidden(this, mUri);
// clear correspondent notification item
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(
mTransInfo.mID);
-
- // retry the failed transfer
- Uri uri = BluetoothOppUtility.originalUri(Uri.parse(mTransInfo.mFileUri));
- BluetoothOppSendFileInfo sendFileInfo =
- BluetoothOppSendFileInfo.generateFileInfo(BluetoothOppTransferActivity
- .this, uri, mTransInfo.mFileType, false);
- uri = BluetoothOppUtility.generateUri(uri, sendFileInfo);
- BluetoothOppUtility.putSendFileInfo(uri, sendFileInfo);
- mTransInfo.mFileUri = uri.toString();
- BluetoothOppUtility.retryTransfer(this, mTransInfo);
-
+ retryFailedTrasfer();
BluetoothDevice remoteDevice = mAdapter.getRemoteDevice(mTransInfo.mDestAddr);
// Display toast message
@@ -502,4 +491,23 @@
.setText(getString(R.string.upload_fail_cancel));
}
}
+
+ /*
+ * Retry the failed transfer in background thread
+ */
+ private void retryFailedTrasfer() {
+ new Thread() {
+ @Override
+ public void run() {
+ Uri uri = BluetoothOppUtility.originalUri(Uri.parse(mTransInfo.mFileUri));
+ BluetoothOppSendFileInfo sendFileInfo =
+ BluetoothOppSendFileInfo.generateFileInfo(BluetoothOppTransferActivity
+ .this, uri, mTransInfo.mFileType, false);
+ uri = BluetoothOppUtility.generateUri(uri, sendFileInfo);
+ BluetoothOppUtility.putSendFileInfo(uri, sendFileInfo);
+ mTransInfo.mFileUri = uri.toString();
+ BluetoothOppUtility.retryTransfer(BluetoothOppTransferActivity.this, mTransInfo);
+ }
+ }.start();
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java b/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java
index 38d9c58..760edae 100644
--- a/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java
+++ b/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java
@@ -20,6 +20,9 @@
import android.media.MediaDescription;
import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.HandlerThread;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -42,15 +45,22 @@
@Captor ArgumentCaptor<MediaBrowser.ConnectionCallback> mBrowserConnCb;
@Captor ArgumentCaptor<MediaBrowser.SubscriptionCallback> mSubscriptionCb;
+ @Captor ArgumentCaptor<MediaController.Callback> mControllerCb;
+ @Captor ArgumentCaptor<Handler> mTimeoutHandler;
@Captor ArgumentCaptor<List<ListItem>> mWrapperBrowseCb;
@Mock MediaBrowser mMockBrowser;
@Mock BrowsedPlayerWrapper.ConnectionCallback mConnCb;
@Mock BrowsedPlayerWrapper.BrowseCallback mBrowseCb;
+ private HandlerThread mThread;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ // Set up Looper thread for the timeout handler
+ mThread = new HandlerThread("MediaPlayerWrapperTestThread");
+ mThread.start();
+
when(mMockBrowser.getRoot()).thenReturn("root_folder");
MediaBrowserFactory.inject(mMockBrowser);
@@ -58,7 +68,8 @@
@Test
public void testWrap() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
wrapper.connect(mConnCb);
verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
verify(mMockBrowser).connect();
@@ -71,26 +82,54 @@
}
@Test
- public void testConnect() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ public void testConnect_Successful() {
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
wrapper.connect(mConnCb);
verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
- browserConnCb.onConnectionFailed();
- verify(mConnCb).run(eq(BrowsedPlayerWrapper.STATUS_CONN_ERROR), eq(wrapper));
-
- wrapper.connect(mConnCb);
- verify(mMockBrowser, times(2)).connect();
-
+ verify(mMockBrowser, times(1)).connect();
browserConnCb.onConnected();
verify(mConnCb).run(eq(BrowsedPlayerWrapper.STATUS_SUCCESS), eq(wrapper));
+ verify(mMockBrowser, times(1)).disconnect();
+ }
+
+ @Test
+ public void testConnect_Suspended() {
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
+ wrapper.connect(mConnCb);
+ verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
+ MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
+
+ verify(mMockBrowser, times(1)).connect();
+ browserConnCb.onConnectionSuspended();
+ verify(mConnCb).run(eq(BrowsedPlayerWrapper.STATUS_CONN_ERROR), eq(wrapper));
+ // Twice because our mConnCb is wrapped when using the plain connect() call and disconnect
+ // is called for us when the callback is invoked in addition to error handling calling
+ // disconnect.
verify(mMockBrowser, times(2)).disconnect();
}
@Test
+ public void testConnect_Failed() {
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
+ wrapper.connect(mConnCb);
+ verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
+ MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
+
+ verify(mMockBrowser, times(1)).connect();
+ browserConnCb.onConnectionFailed();
+ verify(mConnCb).run(eq(BrowsedPlayerWrapper.STATUS_CONN_ERROR), eq(wrapper));
+ verify(mMockBrowser, times(1)).disconnect();
+ }
+
+ @Test
public void testEmptyRoot() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
doReturn("").when(mMockBrowser).getRoot();
@@ -107,7 +146,8 @@
@Test
public void testDisconnect() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
wrapper.connect(mConnCb);
verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
@@ -117,7 +157,8 @@
@Test
public void testGetRootId() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
wrapper.connect(mConnCb);
verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
@@ -129,7 +170,8 @@
@Test
public void testPlayItem() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
@@ -144,12 +186,56 @@
browserConnCb.onConnected();
verify(mockTransport).playFromMediaId(eq("test_item"), eq(null));
- verify(mMockBrowser).disconnect();
+
+ // Do not immediately disconnect. Non-foreground playback serves will likely fail
+ verify(mMockBrowser, times(0)).disconnect();
+
+ verify(mockController).registerCallback(mControllerCb.capture(), any());
+ MediaController.Callback controllerCb = mControllerCb.getValue();
+ PlaybackState.Builder builder = new PlaybackState.Builder();
+
+ // Do not disconnect on an event that isn't "playing"
+ builder.setState(PlaybackState.STATE_PAUSED, 0, 1);
+ controllerCb.onPlaybackStateChanged(builder.build());
+ verify(mMockBrowser, times(0)).disconnect();
+
+ // Once we're told we're playing, make sure we disconnect
+ builder.setState(PlaybackState.STATE_PLAYING, 0, 1);
+ controllerCb.onPlaybackStateChanged(builder.build());
+ verify(mMockBrowser, times(1)).disconnect();
+ }
+
+ @Test
+ public void testPlayItem_Timeout() {
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
+ verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
+ MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();
+
+ wrapper.playItem("test_item");
+ verify(mMockBrowser, times(1)).connect();
+
+ MediaController mockController = mock(MediaController.class);
+ MediaController.TransportControls mockTransport =
+ mock(MediaController.TransportControls.class);
+ when(mockController.getTransportControls()).thenReturn(mockTransport);
+ MediaControllerFactory.inject(mockController);
+
+ browserConnCb.onConnected();
+ verify(mockTransport).playFromMediaId(eq("test_item"), eq(null));
+
+ verify(mockController).registerCallback(any(), mTimeoutHandler.capture());
+ Handler timeoutHandler = mTimeoutHandler.getValue();
+
+ timeoutHandler.sendEmptyMessage(BrowsedPlayerWrapper.TimeoutHandler.MSG_TIMEOUT);
+
+ verify(mMockBrowser, timeout(2000).times(1)).disconnect();
}
@Test
public void testGetFolderItems() {
- BrowsedPlayerWrapper wrapper = BrowsedPlayerWrapper.wrap(null, "test", "test");
+ BrowsedPlayerWrapper wrapper =
+ BrowsedPlayerWrapper.wrap(null, mThread.getLooper(), "test", "test");
verify(mMockBrowser).testInit(any(), any(), mBrowserConnCb.capture(), any());
MediaBrowser.ConnectionCallback browserConnCb = mBrowserConnCb.getValue();