Loading android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java +27 −17 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import com.android.bluetooth.BluetoothPrefs; import com.android.bluetooth.R; import com.android.bluetooth.Utils; import com.android.bluetooth.a2dpsink.A2dpSinkService; import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService.BrowseResult; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -321,10 +322,10 @@ public class AvrcpControllerService extends ProfileService { * Get a List of MediaItems that are children of the specified media Id * * @param parentMediaId The player or folder to get the contents of * @return List of Children if available, an empty list if there are none, * or null if a search must be performed. * @return List of Children if available, an empty list if there are none, or null if a search * must be performed. */ public synchronized List<MediaItem> getContents(String parentMediaId) { public synchronized BrowseResult getContents(String parentMediaId) { if (DBG) Log.d(TAG, "getContents(" + parentMediaId + ")"); BrowseTree.BrowseNode requestedNode = sBrowseTree.findBrowseNodeByID(parentMediaId); Loading @@ -337,28 +338,37 @@ public class AvrcpControllerService extends ProfileService { } } } // If we don't find a node in the tree then do not have any way to browse for the contents. // Return an empty list instead. if (requestedNode == null) { if (DBG) Log.d(TAG, "Didn't find a node"); return new ArrayList(0); } else { return new BrowseResult(new ArrayList(0), BrowseResult.ERROR_MEDIA_ID_INVALID); } if (parentMediaId.equals(BrowseTree.ROOT) && requestedNode.getChildrenCount() == 0) { return new BrowseResult(null, BrowseResult.NO_DEVICE_CONNECTED); } // If we found a node and it belongs to a device then go ahead and make it active BluetoothDevice device = requestedNode.getDevice(); if (device != null) { setActiveDevice(device); } List<MediaItem> contents = requestedNode.getContents(); if (DBG) Log.d(TAG, "Returning contents"); if (!requestedNode.isCached()) { if (DBG) Log.d(TAG, "node is not cached"); refreshContents(requestedNode); /* Ongoing downloads can have partial results and we want to make sure they get sent * to the client. If a download gets kicked off as a result of this request, the * contents will be null until the first results arrive. */ return new BrowseResult(contents, BrowseResult.DOWNLOAD_PENDING); } if (DBG) Log.d(TAG, "Returning contents"); return requestedNode.getContents(); } return new BrowseResult(contents, BrowseResult.SUCCESS); } @Override protected IProfileServiceBinder initBinder() { return new AvrcpControllerServiceBinder(this); Loading android/app/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java +66 −6 Original line number Diff line number Diff line Loading @@ -132,11 +132,66 @@ public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { mReceiver = null; } List<MediaItem> getContents(final String parentMediaId) { /** * BrowseResult is used to return the contents of a node along with a status. The status is * used to indicate success, a pending download, or error conditions. BrowseResult is used in * onLoadChildren() and getContents() in BluetoothMediaBrowserService and in getContents() in * AvrcpControllerService. * The following statuses have been implemented: * 1. SUCCESS - Contents have been retrieved successfully. * 2. DOWNLOAD_PENDING - Download is in progress and may or may not have contents to return. * 3. NO_DEVICE_CONNECTED - If no device is connected there are no contents to be retrieved. * 4. ERROR_MEDIA_ID_INVALID - Contents could not be retrieved as the media ID is invalid. * 5. ERROR_NO_AVRCP_SERVICE - Contents could not be retrieved as AvrcpControllerService is not * connected. */ public static class BrowseResult { // Possible statuses for onLoadChildren public static final byte SUCCESS = 0x00; public static final byte DOWNLOAD_PENDING = 0x01; public static final byte NO_DEVICE_CONNECTED = 0x02; public static final byte ERROR_MEDIA_ID_INVALID = 0x03; public static final byte ERROR_NO_AVRCP_SERVICE = 0x04; private List<MediaItem> mResults; private final byte mStatus; List<MediaItem> getResults() { return mResults; } byte getStatus() { return mStatus; } String getStatusString() { switch (mStatus) { case DOWNLOAD_PENDING: return "DOWNLOAD_PENDING"; case SUCCESS: return "SUCCESS"; case NO_DEVICE_CONNECTED: return "NO_DEVICE_CONNECTED"; case ERROR_MEDIA_ID_INVALID: return "ERROR_MEDIA_ID_INVALID"; case ERROR_NO_AVRCP_SERVICE: return "ERROR_NO_AVRCP_SERVICE"; default: return "UNDEFINED_ERROR_CASE"; } } BrowseResult(List<MediaItem> results, byte status) { mResults = results; mStatus = status; } } BrowseResult getContents(final String parentMediaId) { AvrcpControllerService avrcpControllerService = AvrcpControllerService.getAvrcpControllerService(); if (avrcpControllerService == null) { return new ArrayList(0); return new BrowseResult(new ArrayList(0), BrowseResult.ERROR_NO_AVRCP_SERVICE); } else { return avrcpControllerService.getContents(parentMediaId); } Loading Loading @@ -173,11 +228,16 @@ public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { public synchronized void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { if (DBG) Log.d(TAG, "onLoadChildren parentMediaId= " + parentMediaId); List<MediaItem> contents = getContents(parentMediaId); if (contents == null) { BrowseResult contents = getContents(parentMediaId); byte status = contents.getStatus(); if (status == BrowseResult.DOWNLOAD_PENDING && contents == null) { Log.i(TAG, "Download pending - no contents, id= " + parentMediaId); result.detach(); } else { result.sendResult(contents); if (DBG) { Log.d(TAG, "id= " + parentMediaId + ", status= " + contents.getStatusString()); } result.sendResult(contents.getResults()); } } Loading android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceTest.java +87 −1 Original line number Diff line number Diff line Loading @@ -19,9 +19,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -36,6 +38,7 @@ import androidx.test.filters.MediumTest; import androidx.test.rule.ServiceTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService.BrowseResult; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; Loading Loading @@ -174,7 +177,90 @@ public class AvrcpControllerServiceTest { mService.getContents(parentMediaId); verify(node).getContents(); verify(node, atLeastOnce()).getContents(); } /** * Pre-conditions: No node in BrowseTree for specified media ID * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status ERROR_MEDIA_ID_INVALID */ @Test public void testGetContentsNoNode_returnInvalidMediaIdStatus() { String parentMediaId = "test_parent_media_id"; when(mStateMachine.findNode(parentMediaId)).thenReturn(null); BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.ERROR_MEDIA_ID_INVALID); } /** * Pre-conditions: No device is connected - parent media ID is at the root of the BrowseTree * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status NO_DEVICE_CONNECTED */ @Test public void getContentsNoDeviceConnected_returnNoDeviceConnectedStatus() { String parentMediaId = BrowseTree.ROOT; BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.NO_DEVICE_CONNECTED); } /** * Pre-conditions: At least one device is connected * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status SUCCESS */ @Test public void getContentsOneDeviceConnected_returnSuccessStatus() { String parentMediaId = BrowseTree.ROOT; mService.sBrowseTree.onConnected(mRemoteDevice); BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.SUCCESS); } /** * Pre-conditions: Node for specified media ID is not cached * Test: {@link BrowseTree.BrowseNode#getContents} returns {@code null} when the node has no * children/items and the node is not cached. * When {@link AvrcpControllerService#getContents} receives a node that is not cached, * it should interpret the status as `DOWNLOAD_PENDING`. * Expected Output: BrowseResult object with status DOWNLOAD_PENDING; verify that a download * request has been sent by checking if mStateMachine.requestContents() is called */ @Test public void getContentsNodeNotCached_returnDownloadPendingStatus() { String parentMediaId = "test_parent_media_id"; BrowseTree.BrowseNode node = mock(BrowseTree.BrowseNode.class); when(mStateMachine.findNode(parentMediaId)).thenReturn(node); when(node.isCached()).thenReturn(false); when(node.getDevice()).thenReturn(mRemoteDevice); when(node.getID()).thenReturn(parentMediaId); BrowseResult result = mService.getContents(parentMediaId); verify(mStateMachine, times(1)).requestContents(eq(node)); assertThat(result.getStatus()).isEqualTo(BrowseResult.DOWNLOAD_PENDING); } /** * Pre-conditions: Parent media ID that is not BrowseTree.ROOT; isCached returns true * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status SUCCESS */ @Test public void getContentsNoErrorConditions_returnsSuccessStatus() { String parentMediaId = "test_parent_media_id"; BrowseTree.BrowseNode node = mock(BrowseTree.BrowseNode.class); when(mStateMachine.findNode(parentMediaId)).thenReturn(node); when(node.getContents()).thenReturn(new ArrayList(0)); when(node.isCached()).thenReturn(true); BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.SUCCESS); } @Test Loading Loading
android/app/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java +27 −17 Original line number Diff line number Diff line Loading @@ -34,6 +34,7 @@ import com.android.bluetooth.BluetoothPrefs; import com.android.bluetooth.R; import com.android.bluetooth.Utils; import com.android.bluetooth.a2dpsink.A2dpSinkService; import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService.BrowseResult; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.internal.annotations.VisibleForTesting; Loading Loading @@ -321,10 +322,10 @@ public class AvrcpControllerService extends ProfileService { * Get a List of MediaItems that are children of the specified media Id * * @param parentMediaId The player or folder to get the contents of * @return List of Children if available, an empty list if there are none, * or null if a search must be performed. * @return List of Children if available, an empty list if there are none, or null if a search * must be performed. */ public synchronized List<MediaItem> getContents(String parentMediaId) { public synchronized BrowseResult getContents(String parentMediaId) { if (DBG) Log.d(TAG, "getContents(" + parentMediaId + ")"); BrowseTree.BrowseNode requestedNode = sBrowseTree.findBrowseNodeByID(parentMediaId); Loading @@ -337,28 +338,37 @@ public class AvrcpControllerService extends ProfileService { } } } // If we don't find a node in the tree then do not have any way to browse for the contents. // Return an empty list instead. if (requestedNode == null) { if (DBG) Log.d(TAG, "Didn't find a node"); return new ArrayList(0); } else { return new BrowseResult(new ArrayList(0), BrowseResult.ERROR_MEDIA_ID_INVALID); } if (parentMediaId.equals(BrowseTree.ROOT) && requestedNode.getChildrenCount() == 0) { return new BrowseResult(null, BrowseResult.NO_DEVICE_CONNECTED); } // If we found a node and it belongs to a device then go ahead and make it active BluetoothDevice device = requestedNode.getDevice(); if (device != null) { setActiveDevice(device); } List<MediaItem> contents = requestedNode.getContents(); if (DBG) Log.d(TAG, "Returning contents"); if (!requestedNode.isCached()) { if (DBG) Log.d(TAG, "node is not cached"); refreshContents(requestedNode); /* Ongoing downloads can have partial results and we want to make sure they get sent * to the client. If a download gets kicked off as a result of this request, the * contents will be null until the first results arrive. */ return new BrowseResult(contents, BrowseResult.DOWNLOAD_PENDING); } if (DBG) Log.d(TAG, "Returning contents"); return requestedNode.getContents(); } return new BrowseResult(contents, BrowseResult.SUCCESS); } @Override protected IProfileServiceBinder initBinder() { return new AvrcpControllerServiceBinder(this); Loading
android/app/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java +66 −6 Original line number Diff line number Diff line Loading @@ -132,11 +132,66 @@ public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { mReceiver = null; } List<MediaItem> getContents(final String parentMediaId) { /** * BrowseResult is used to return the contents of a node along with a status. The status is * used to indicate success, a pending download, or error conditions. BrowseResult is used in * onLoadChildren() and getContents() in BluetoothMediaBrowserService and in getContents() in * AvrcpControllerService. * The following statuses have been implemented: * 1. SUCCESS - Contents have been retrieved successfully. * 2. DOWNLOAD_PENDING - Download is in progress and may or may not have contents to return. * 3. NO_DEVICE_CONNECTED - If no device is connected there are no contents to be retrieved. * 4. ERROR_MEDIA_ID_INVALID - Contents could not be retrieved as the media ID is invalid. * 5. ERROR_NO_AVRCP_SERVICE - Contents could not be retrieved as AvrcpControllerService is not * connected. */ public static class BrowseResult { // Possible statuses for onLoadChildren public static final byte SUCCESS = 0x00; public static final byte DOWNLOAD_PENDING = 0x01; public static final byte NO_DEVICE_CONNECTED = 0x02; public static final byte ERROR_MEDIA_ID_INVALID = 0x03; public static final byte ERROR_NO_AVRCP_SERVICE = 0x04; private List<MediaItem> mResults; private final byte mStatus; List<MediaItem> getResults() { return mResults; } byte getStatus() { return mStatus; } String getStatusString() { switch (mStatus) { case DOWNLOAD_PENDING: return "DOWNLOAD_PENDING"; case SUCCESS: return "SUCCESS"; case NO_DEVICE_CONNECTED: return "NO_DEVICE_CONNECTED"; case ERROR_MEDIA_ID_INVALID: return "ERROR_MEDIA_ID_INVALID"; case ERROR_NO_AVRCP_SERVICE: return "ERROR_NO_AVRCP_SERVICE"; default: return "UNDEFINED_ERROR_CASE"; } } BrowseResult(List<MediaItem> results, byte status) { mResults = results; mStatus = status; } } BrowseResult getContents(final String parentMediaId) { AvrcpControllerService avrcpControllerService = AvrcpControllerService.getAvrcpControllerService(); if (avrcpControllerService == null) { return new ArrayList(0); return new BrowseResult(new ArrayList(0), BrowseResult.ERROR_NO_AVRCP_SERVICE); } else { return avrcpControllerService.getContents(parentMediaId); } Loading Loading @@ -173,11 +228,16 @@ public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat { public synchronized void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { if (DBG) Log.d(TAG, "onLoadChildren parentMediaId= " + parentMediaId); List<MediaItem> contents = getContents(parentMediaId); if (contents == null) { BrowseResult contents = getContents(parentMediaId); byte status = contents.getStatus(); if (status == BrowseResult.DOWNLOAD_PENDING && contents == null) { Log.i(TAG, "Download pending - no contents, id= " + parentMediaId); result.detach(); } else { result.sendResult(contents); if (DBG) { Log.d(TAG, "id= " + parentMediaId + ", status= " + contents.getStatusString()); } result.sendResult(contents.getResults()); } } Loading
android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceTest.java +87 −1 Original line number Diff line number Diff line Loading @@ -19,9 +19,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -36,6 +38,7 @@ import androidx.test.filters.MediumTest; import androidx.test.rule.ServiceTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService.BrowseResult; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; Loading Loading @@ -174,7 +177,90 @@ public class AvrcpControllerServiceTest { mService.getContents(parentMediaId); verify(node).getContents(); verify(node, atLeastOnce()).getContents(); } /** * Pre-conditions: No node in BrowseTree for specified media ID * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status ERROR_MEDIA_ID_INVALID */ @Test public void testGetContentsNoNode_returnInvalidMediaIdStatus() { String parentMediaId = "test_parent_media_id"; when(mStateMachine.findNode(parentMediaId)).thenReturn(null); BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.ERROR_MEDIA_ID_INVALID); } /** * Pre-conditions: No device is connected - parent media ID is at the root of the BrowseTree * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status NO_DEVICE_CONNECTED */ @Test public void getContentsNoDeviceConnected_returnNoDeviceConnectedStatus() { String parentMediaId = BrowseTree.ROOT; BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.NO_DEVICE_CONNECTED); } /** * Pre-conditions: At least one device is connected * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status SUCCESS */ @Test public void getContentsOneDeviceConnected_returnSuccessStatus() { String parentMediaId = BrowseTree.ROOT; mService.sBrowseTree.onConnected(mRemoteDevice); BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.SUCCESS); } /** * Pre-conditions: Node for specified media ID is not cached * Test: {@link BrowseTree.BrowseNode#getContents} returns {@code null} when the node has no * children/items and the node is not cached. * When {@link AvrcpControllerService#getContents} receives a node that is not cached, * it should interpret the status as `DOWNLOAD_PENDING`. * Expected Output: BrowseResult object with status DOWNLOAD_PENDING; verify that a download * request has been sent by checking if mStateMachine.requestContents() is called */ @Test public void getContentsNodeNotCached_returnDownloadPendingStatus() { String parentMediaId = "test_parent_media_id"; BrowseTree.BrowseNode node = mock(BrowseTree.BrowseNode.class); when(mStateMachine.findNode(parentMediaId)).thenReturn(node); when(node.isCached()).thenReturn(false); when(node.getDevice()).thenReturn(mRemoteDevice); when(node.getID()).thenReturn(parentMediaId); BrowseResult result = mService.getContents(parentMediaId); verify(mStateMachine, times(1)).requestContents(eq(node)); assertThat(result.getStatus()).isEqualTo(BrowseResult.DOWNLOAD_PENDING); } /** * Pre-conditions: Parent media ID that is not BrowseTree.ROOT; isCached returns true * Test: Call AvrcpControllerService.getContents() * Expected Output: BrowseResult object with status SUCCESS */ @Test public void getContentsNoErrorConditions_returnsSuccessStatus() { String parentMediaId = "test_parent_media_id"; BrowseTree.BrowseNode node = mock(BrowseTree.BrowseNode.class); when(mStateMachine.findNode(parentMediaId)).thenReturn(node); when(node.getContents()).thenReturn(new ArrayList(0)); when(node.isCached()).thenReturn(true); BrowseResult result = mService.getContents(parentMediaId); assertThat(result.getStatus()).isEqualTo(BrowseResult.SUCCESS); } @Test Loading