diff --git a/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java b/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java index 6e0a252cca914c95523bd770b71afca639e76b98..fd18ca08423680ef8b77ee3d5cb663663d1f016d 100644 --- a/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java +++ b/android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java @@ -187,7 +187,7 @@ class AvrcpControllerStateMachine extends StateMachine { } BrowseTree.BrowseNode findNode(String parentMediaId) { - logD("FindNode"); + logD("findNode(device=" + mDevice + ", mediaId=" + parentMediaId + ")"); return mBrowseTree.findBrowseNodeByID(parentMediaId); } @@ -288,7 +288,8 @@ class AvrcpControllerStateMachine extends StateMachine { @Override protected void unhandledMessage(Message msg) { - Log.w(TAG, "Unhandled message in state " + getCurrentState() + "msg.what=" + msg.what); + Log.w(TAG, "Unhandled message in state " + getCurrentState() + "msg.what=" + + eventToString(msg.what)); } private static void logD(String message) { @@ -405,9 +406,8 @@ class AvrcpControllerStateMachine extends StateMachine { } void nowPlayingContentChanged() { - mBrowseTree.mNowPlayingNode.setCached(false); removeUnusedArtworkFromBrowseTree(); - sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode); + requestContents(mBrowseTree.mNowPlayingNode); } protected class Disconnected extends State { @@ -471,7 +471,7 @@ class AvrcpControllerStateMachine extends StateMachine { @Override public boolean processMessage(Message msg) { - logD(STATE_TAG + " processMessage " + msg.what); + logD(STATE_TAG + " processMessage " + eventToString(msg.what)); switch (msg.what) { case ACTIVE_DEVICE_CHANGE: int state = msg.arg1; @@ -636,6 +636,7 @@ class AvrcpControllerStateMachine extends StateMachine { // invalid mBrowseTree.mNowPlayingNode.setCached(false); if (isActive()) { + logD("Addressed player change has invalidated the now playing list"); BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode); } removeUnusedArtworkFromBrowseTree(); @@ -661,13 +662,14 @@ class AvrcpControllerStateMachine extends StateMachine { // guaranteed to have the addressed player now. mAddressedPlayer = mAvailablePlayerList.get(mAddressedPlayerId); - // Fetch metadata including the now playing list if the new player supports the - // now playing feature + // Fetch metadata including the now playing list. The specification claims that + // the player feature bit only incidates if the player *natively* supports a now + // playing list. However, now playing is mandatory if browsing is supported, + // even if the player doesn't support it. A list of one item can be returned + // instead. mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice)); mService.getPlaybackStateNative(Utils.getByteAddress(mDevice)); - if (mAddressedPlayer.supportsFeature(AvrcpPlayer.FEATURE_NOW_PLAYING)) { - sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode); - } + requestContents(mBrowseTree.mNowPlayingNode); logD("AddressedPlayer = " + mAddressedPlayer); return true; @@ -816,29 +818,28 @@ class AvrcpControllerStateMachine extends StateMachine { @Override public void enter() { - logD(STATE_TAG + " Entering GetFolderList"); + logD(STATE_TAG + ": Entering GetFolderList"); // Setup the timeouts. sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS); super.enter(); mAbort = false; Message msg = getCurrentMessage(); if (msg.what == MESSAGE_GET_FOLDER_ITEMS) { - { - logD(STATE_TAG + " new Get Request"); - mBrowseNode = (BrowseTree.BrowseNode) msg.obj; - } + mBrowseNode = (BrowseTree.BrowseNode) msg.obj; + logD(STATE_TAG + ": new fetch request, node=" + mBrowseNode); } if (mBrowseNode == null) { transitionTo(mConnected); } else { + mBrowseNode.setCached(false); navigateToFolderOrRetrieve(mBrowseNode); } } @Override public boolean processMessage(Message msg) { - logD(STATE_TAG + " processMessage " + msg.what); + logD(STATE_TAG + " processMessage " + eventToString(msg.what)); switch (msg.what) { case MESSAGE_PROCESS_GET_FOLDER_ITEMS: ArrayList<AvrcpItem> folderList = (ArrayList<AvrcpItem>) msg.obj; @@ -941,10 +942,7 @@ class AvrcpControllerStateMachine extends StateMachine { mAddressedPlayer = mAvailablePlayerList.get(mAddressedPlayerId); mService.getCurrentMetadataNative(Utils.getByteAddress(mDevice)); mService.getPlaybackStateNative(Utils.getByteAddress(mDevice)); - mBrowseTree.mNowPlayingNode.setCached(false); - if (mAddressedPlayer.supportsFeature(AvrcpPlayer.FEATURE_NOW_PLAYING)) { - sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode); - } + requestContents(mBrowseTree.mNowPlayingNode); } logD("AddressedPlayer = " + mAddressedPlayer); @@ -979,20 +977,23 @@ class AvrcpControllerStateMachine extends StateMachine { break; case MESSAGE_GET_FOLDER_ITEMS: - if (!mBrowseNode.equals(msg.obj)) { - if (shouldAbort(mBrowseNode.getScope(), - ((BrowseTree.BrowseNode) msg.obj).getScope())) { + BrowseTree.BrowseNode requested = (BrowseTree.BrowseNode) msg.obj; + if (!mBrowseNode.equals(requested) || requested.isNowPlaying()) { + if (shouldAbort(mBrowseNode.getScope(), requested.getScope())) { mAbort = true; } deferMessage(msg); - logD("GetFolderItems: Go Get Another Directory"); + logD("GetFolderItems: Enqueue new request for node=" + requested + + ", abort=" + mAbort); } else { - logD("GetFolderItems: Get The Same Directory, ignore"); + logD("GetFolderItems: Ignore request, node=" + requested); } break; default: // All of these messages should be handled by parent state immediately. + logD("GetFolderItems: Passing message to parent state, type=" + + eventToString(msg.what)); return false; } return true; @@ -1094,6 +1095,7 @@ class AvrcpControllerStateMachine extends StateMachine { @Override public void exit() { + logd("GetFolderItems: fetch complete, node=" + mBrowseNode); removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT); mBrowseNode = null; super.exit(); @@ -1328,4 +1330,77 @@ class AvrcpControllerStateMachine extends StateMachine { return mService.getResources() .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus); } + + private static String eventToString(int event) { + switch (event) { + case CONNECT: + return "CONNECT"; + case DISCONNECT: + return "DISCONNECT"; + case ACTIVE_DEVICE_CHANGE: + return "ACTIVE_DEVICE_CHANGE"; + case AUDIO_FOCUS_STATE_CHANGE: + return "AUDIO_FOCUS_STATE_CHANGE"; + case CLEANUP: + return "CLEANUP"; + case CONNECT_TIMEOUT: + return "CONNECT_TIMEOUT"; + case MESSAGE_INTERNAL_ABS_VOL_TIMEOUT: + return "MESSAGE_INTERNAL_ABS_VOL_TIMEOUT"; + case STACK_EVENT: + return "STACK_EVENT"; + case MESSAGE_INTERNAL_CMD_TIMEOUT: + return "MESSAGE_INTERNAL_CMD_TIMEOUT"; + case MESSAGE_PROCESS_SET_ABS_VOL_CMD: + return "MESSAGE_PROCESS_SET_ABS_VOL_CMD"; + case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: + return "MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION"; + case MESSAGE_PROCESS_TRACK_CHANGED: + return "MESSAGE_PROCESS_TRACK_CHANGED"; + case MESSAGE_PROCESS_PLAY_POS_CHANGED: + return "MESSAGE_PROCESS_PLAY_POS_CHANGED"; + case MESSAGE_PROCESS_PLAY_STATUS_CHANGED: + return "MESSAGE_PROCESS_PLAY_STATUS_CHANGED"; + case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: + return "MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION"; + case MESSAGE_PROCESS_GET_FOLDER_ITEMS: + return "MESSAGE_PROCESS_GET_FOLDER_ITEMS"; + case MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE: + return "MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE"; + case MESSAGE_PROCESS_GET_PLAYER_ITEMS: + return "MESSAGE_PROCESS_GET_PLAYER_ITEMS"; + case MESSAGE_PROCESS_FOLDER_PATH: + return "MESSAGE_PROCESS_FOLDER_PATH"; + case MESSAGE_PROCESS_SET_BROWSED_PLAYER: + return "MESSAGE_PROCESS_SET_BROWSED_PLAYER"; + case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER: + return "MESSAGE_PROCESS_SET_ADDRESSED_PLAYER"; + case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED: + return "MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED"; + case MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED: + return "MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED"; + case MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS: + return "MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS"; + case MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS: + return "MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS"; + case MESSAGE_PROCESS_AVAILABLE_PLAYER_CHANGED: + return "MESSAGE_PROCESS_AVAILABLE_PLAYER_CHANGED"; + case MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM: + return "MESSAGE_PROCESS_RECEIVED_COVER_ART_PSM"; + case MESSAGE_GET_FOLDER_ITEMS: + return "MESSAGE_GET_FOLDER_ITEMS"; + case MESSAGE_PLAY_ITEM: + return "MESSAGE_PLAY_ITEM"; + case MSG_AVRCP_PASSTHRU: + return "MSG_AVRCP_PASSTHRU"; + case MSG_AVRCP_SET_SHUFFLE: + return "MSG_AVRCP_SET_SHUFFLE"; + case MSG_AVRCP_SET_REPEAT: + return "MSG_AVRCP_SET_REPEAT"; + case MESSAGE_PROCESS_IMAGE_DOWNLOADED: + return "MESSAGE_PROCESS_IMAGE_DOWNLOADED"; + default: + return "UNKNOWN_EVENT_ID_" + event; + } + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java index 4388ac7b1409ed9691b2e9c5455d982f15394669..f6a237d21cda8436ccce20017dcbef369c696f8f 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java @@ -325,14 +325,30 @@ public class AvrcpControllerStateMachineTest { assertNowPlayingList(nowPlayingList); } + private String avrcpItemListToString(List<AvrcpItem> items) { + StringBuilder s = new StringBuilder(); + s.append("["); + if (items != null) { + for (int i = 0; i < items.size(); i++) { + AvrcpItem item = items.get(i); + s.append((item != null ? Long.toString(item.getUid()) : "null")); + if (i != items.size() - 1) s.append(", "); + } + } + s.append("]"); + return s.toString(); + } + /** * Assert that the Now Playing list is a particular value */ private void assertNowPlayingList(List<AvrcpItem> expected) { List<AvrcpItem> current = getNowPlayingList(); - Assert.assertEquals(expected.size(), current.size()); + String err = "Now playing list incorrect, expected=" + + avrcpItemListToString(expected) + ", actual=" + avrcpItemListToString(current); + Assert.assertEquals(err, expected.size(), current.size()); for (int i = 0; i < expected.size(); i++) { - Assert.assertEquals(expected.get(i), current.get(i)); + Assert.assertEquals(err, expected.get(i), current.get(i)); } } @@ -690,14 +706,9 @@ public class AvrcpControllerStateMachineTest { testPlayers.add(playerOne); mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS, testPlayers); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); //Verify that the player object is available. - mAvrcpStateMachine.requestContents(results); - verify(mAvrcpControllerService, - timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getPlayerListNative(eq(mTestAddress), - eq(1), eq(0)); - mAvrcpStateMachine.sendMessage( - AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE); playerNodes = mAvrcpStateMachine.findNode(results.getID()); Assert.assertEquals(true, results.isCached()); Assert.assertEquals("MediaItem{mFlags=1, mDescription=" + playerName + ", null, null}", @@ -746,10 +757,14 @@ public class AvrcpControllerStateMachineTest { setUpConnectedState(true, true); final String rootName = "__ROOT__"; - // Set an addressed player that will be in the available players set + // Set an addressed player that will be in the available players set. A new player triggers + // a now playing list download, so send back nothing. mAvrcpStateMachine.sendMessage( AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, 1); TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); clearInvocations(mAvrcpControllerService); // Send an available players have changed event @@ -1546,4 +1561,260 @@ public class AvrcpControllerStateMachineTest { verify(mAvrcpControllerService, never()).sendPassThroughCommandNative(eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PLAY), eq(KEY_UP)); } + + /** + * Test receiving a now playing content changed event while downloading now playing content and + * make sure our final now playing content downloaded matches what's expected + */ + @Test + public void testNowPlayingListChangedWhileFetchingNowPlayingList_fetchAbortedAndRestarted() { + setUpConnectedState(true, true); + sendAudioFocusUpdate(AudioManager.AUDIOFOCUS_GAIN); + + // Fill the list with songs 1 -> 25, more than download step size + List<AvrcpItem> nowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 1; i <= 25; i++) { + String title = "Song " + Integer.toString(i); + nowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Fill the list with songs 26 -> 50 + List<AvrcpItem> updatedNowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 26; i <= 50; i++) { + String title = "Song " + Integer.toString(i); + updatedNowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Hand hold the download events + BrowseTree.BrowseNode nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + mAvrcpStateMachine.requestContents(nowPlaying); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Verify download attempt and send some elements over, verify next set is requested + verify(mAvrcpControllerService, times(1)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(nowPlayingList.subList(0, 20))); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + verify(mAvrcpControllerService, times(1)).getNowPlayingListNative( + eq(mTestAddress), eq(20), eq(39)); + + // Force a now playing content invalidation and verify attempted download + mAvrcpStateMachine.nowPlayingContentChanged(); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Send requested items, they're likely from the new list at this point, but it shouldn't + // matter what they are because we should toss them out and restart our download next. + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(nowPlayingList.subList(20, 25))); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + verify(mAvrcpControllerService, times(2)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(0, 20))); + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(20, 25))); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE); + + // Wait for the now playing list to be propagated + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Make sure its set by re grabbing the node and checking its contents are cached + nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + Assert.assertTrue(nowPlaying.isCached()); + assertNowPlayingList(updatedNowPlayingList); + } + + /** + * Test receiving a now playing content changed event right after we queued a fetch of some now + * playing items. Make sure our final now playing content downloaded matches what's expected + */ + @Test + public void testNowPlayingListChangedQueuedFetchingNowPlayingList_fetchAbortedAndRestarted() { + setUpConnectedState(true, true); + sendAudioFocusUpdate(AudioManager.AUDIOFOCUS_GAIN); + + // Fill the list with songs 1 -> 25, more than download step size + List<AvrcpItem> nowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 1; i <= 25; i++) { + String title = "Song " + Integer.toString(i); + nowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Fill the list with songs 26 -> 50 + List<AvrcpItem> updatedNowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 26; i <= 50; i++) { + String title = "Song " + Integer.toString(i); + updatedNowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Hand hold the download events + BrowseTree.BrowseNode nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + mAvrcpStateMachine.requestContents(nowPlaying); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Verify download attempt and send some elements over, verify next set is requested + verify(mAvrcpControllerService, times(1)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + mAvrcpStateMachine.nowPlayingContentChanged(); + + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(nowPlayingList.subList(0, 20))); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Receiving the previous members should cause our fetch process to realize we're aborted + // and a new (second) request should be triggered for the list from the beginning + verify(mAvrcpControllerService, times(2)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + + // Send whole list + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(0, 20))); + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(20, 25))); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE); + + // Wait for the now playing list to be propagated + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Make sure its set by re grabbing the node and checking its contents are cached + nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + Assert.assertTrue(nowPlaying.isCached()); + assertNowPlayingList(updatedNowPlayingList); + } + + /** + * Test receiving an addressed player changed event while downloading now playing content and + * make sure our final now playing content downloaded matches what's expected. + */ + @Test + public void testAddressedPlayerChangedWhileFetchingNowPlayingList_fetchAbortedAndRestarted() { + setUpConnectedState(true, true); + sendAudioFocusUpdate(AudioManager.AUDIOFOCUS_GAIN); + + // Fill the list with songs 1 -> 25, more than download step size + List<AvrcpItem> nowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 1; i <= 25; i++) { + String title = "Song " + Integer.toString(i); + nowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Fill the list with songs 26 -> 50 + List<AvrcpItem> updatedNowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 26; i <= 50; i++) { + String title = "Song " + Integer.toString(i); + updatedNowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Hand hold the download events + BrowseTree.BrowseNode nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + mAvrcpStateMachine.requestContents(nowPlaying); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Verify download attempt and send some elements over, verify next set is requested + verify(mAvrcpControllerService, times(1)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(nowPlayingList.subList(0, 20))); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + verify(mAvrcpControllerService, times(1)).getNowPlayingListNative( + eq(mTestAddress), eq(20), eq(39)); + + // Force a now playing content invalidation due to addressed player change + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, 1); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Send requested items, they're likely from the new list at this point, but it shouldn't + // matter what they are because we should toss them out and restart our download next. + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(nowPlayingList.subList(20, 25))); + TestUtils.waitForLooperToBeIdle(mAvrcpStateMachine.getHandler().getLooper()); + + verify(mAvrcpControllerService, times(2)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(0, 20))); + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(20, 25))); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE); + + // Wait for the now playing list to be propagated + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Make sure its set by re grabbing the node and checking its contents are cached + nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + Assert.assertTrue(nowPlaying.isCached()); + assertNowPlayingList(updatedNowPlayingList); + } + + /** + * Test receiving an addressed player changed event while downloading now playing content and + * make sure our final now playing content downloaded matches what's expected. + */ + @Test + public void testAddressedPlayerChangedQueuedFetchingNowPlayingList_fetchAbortedAndRestarted() { + setUpConnectedState(true, true); + sendAudioFocusUpdate(AudioManager.AUDIOFOCUS_GAIN); + + // Fill the list with songs 1 -> 25, more than download step size + List<AvrcpItem> nowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 1; i <= 25; i++) { + String title = "Song " + Integer.toString(i); + nowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Fill the list with songs 26 -> 50 + List<AvrcpItem> updatedNowPlayingList = new ArrayList<AvrcpItem>(); + for (int i = 26; i <= 50; i++) { + String title = "Song " + Integer.toString(i); + updatedNowPlayingList.add(makeNowPlayingItem(i, title)); + } + + // Hand hold the download events + BrowseTree.BrowseNode nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + mAvrcpStateMachine.requestContents(nowPlaying); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Verify download attempt and send some elements over, verify next set is requested + verify(mAvrcpControllerService, times(1)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + + // Force a now playing content invalidation due to addressed player change, happening + // before we've received any items from the remote device. + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, 1); + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Now, send the items in and let it process + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(nowPlayingList.subList(0, 20))); + TestUtils.waitForLooperToBeIdle(mAvrcpStateMachine.getHandler().getLooper()); + + verify(mAvrcpControllerService, times(2)).getNowPlayingListNative( + eq(mTestAddress), eq(0), eq(19)); + + // Send requested items, they're likely from the new list at this point, but it shouldn't + // matter what they are because we should toss them out and restart our download next. + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(0, 20))); + mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, + new ArrayList<AvrcpItem>(updatedNowPlayingList.subList(20, 25))); + mAvrcpStateMachine.sendMessage( + AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE); + + // Wait for the now playing list to be propagated + TestUtils.waitForLooperToFinishScheduledTask(mAvrcpStateMachine.getHandler().getLooper()); + + // Make sure its set by re grabbing the node and checking its contents are cached + nowPlaying = mAvrcpStateMachine.findNode("NOW_PLAYING"); + Assert.assertTrue(nowPlaying.isCached()); + assertNowPlayingList(updatedNowPlayingList); + } }