/* * Copyright 2020 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.bluetooth.avrcp; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.audio_util.Image; import com.android.bluetooth.avrcpcontroller.BipEncoding; import com.android.bluetooth.avrcpcontroller.BipImageDescriptor; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import javax.obex.HeaderSet; import javax.obex.Operation; import javax.obex.ResponseCodes; @RunWith(AndroidJUnit4.class) public class AvrcpBipObexServerTest { private static final String TYPE_GET_LINKED_THUMBNAIL = "x-bt/img-thm"; private static final String TYPE_GET_IMAGE_PROPERTIES = "x-bt/img-properties"; private static final String TYPE_GET_IMAGE = "x-bt/img-img"; private static final String TYPE_BAD = "x-bt/bad-type"; private static final byte HEADER_ID_IMG_HANDLE = 0x30; private static final byte HEADER_ID_IMG_DESCRIPTOR = 0x71; private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] { (byte) 0x71, (byte) 0x63, (byte) 0xDD, (byte) 0x54, (byte) 0x4A, (byte) 0x7E, (byte) 0x11, (byte) 0xE2, (byte) 0xB4, (byte) 0x7C, (byte) 0x00, (byte) 0x50, (byte) 0xC2, (byte) 0x49, (byte) 0x00, (byte) 0x48 }; private static final byte[] NOT_BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; private static final String IMAGE_HANDLE_1 = "0000001"; private static final String IMAGE_HANDLE_UNSTORED = "0000256"; private static final String IMAGE_HANDLE_INVALID = "abc1234"; // no non-numeric characters private Context mTargetContext; private Resources mTestResources; private CoverArt mCoverArt; private AvrcpCoverArtService mAvrcpCoverArtService = null; private AvrcpBipObexServer.Callback mCallback = null; private HeaderSet mRequest = null; private HeaderSet mReply = null; private ByteArrayOutputStream mOutputStream = null; private AvrcpBipObexServer mAvrcpBipObexServer; @Before public void setUp() throws Exception { mTargetContext = InstrumentationRegistry.getTargetContext(); try { mTestResources = mTargetContext.getPackageManager() .getResourcesForApplication("com.android.bluetooth.tests"); } catch (PackageManager.NameNotFoundException e) { assertWithMessage("Setup Failure Unable to get resources" + e.toString()).fail(); } mCoverArt = loadCoverArt(com.android.bluetooth.tests.R.raw.image_200_200); mAvrcpCoverArtService = mock(AvrcpCoverArtService.class); mCallback = mock(AvrcpBipObexServer.Callback.class); mRequest = new HeaderSet(); mReply = new HeaderSet(); mOutputStream = new ByteArrayOutputStream(); mAvrcpBipObexServer = new AvrcpBipObexServer(mAvrcpCoverArtService, mCallback); } @After public void tearDown() throws Exception { mAvrcpBipObexServer = null; mOutputStream = null; mReply = null; mRequest = null; mCallback = null; mAvrcpCoverArtService = null; mCoverArt = null; mTargetContext = null; mTestResources = null; } private CoverArt loadCoverArt(int resId) { InputStream imageInputStream = mTestResources.openRawResource(resId); Bitmap bitmap = BitmapFactory.decodeStream(imageInputStream); Image image = new Image(null, bitmap); return new CoverArt(image); } private void setCoverArtAvailableAtHandle(String handle, CoverArt art) { art.setImageHandle(handle); when(mAvrcpCoverArtService.getImage(handle)).thenReturn(art); } /** * Creates a mocked operation that can be used by our server as a client request * * Our server will use: * - getReceivedHeader * - sendHeaders * - getMaxPacketSize * - openOutputStream */ private Operation makeOperation(HeaderSet requestHeaders, OutputStream os) throws Exception { Operation op = mock(Operation.class); when(op.getReceivedHeader()).thenReturn(requestHeaders); when(op.getMaxPacketSize()).thenReturn(256); when(op.openOutputStream()).thenReturn(os); return op; } private byte[] makeDescriptor(int encoding, int width, int height) { return new BipImageDescriptor.Builder() .setEncoding(encoding) .setFixedDimensions(width, height) .build().serialize(); } /** * Make sure we let a connection through with a valid UUID */ @Test public void testConnectWithValidUuidHeader() throws Exception { mRequest.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); int responseCode = mAvrcpBipObexServer.onConnect(mRequest, mReply); verify(mCallback, times(1)).onConnected(); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); } /** * Make sure we deny a connection when there is an invalid UUID */ @Test public void testConnectWithInvalidUuidHeader() throws Exception { mRequest.setHeader(HeaderSet.TARGET, NOT_BLUETOOTH_UUID_AVRCP_COVER_ART); int responseCode = mAvrcpBipObexServer.onConnect(mRequest, mReply); verify(mCallback, never()).onConnected(); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE); } /** * Make sure onDisconnect notifies the callbacks in the proper way */ @Test public void testDisonnect() { mAvrcpBipObexServer.onDisconnect(mRequest, mReply); verify(mCallback, times(1)).onDisconnected(); } /** * Make sure onClose notifies the callbacks in the proper way */ @Test public void testOnClose() { mAvrcpBipObexServer.onClose(); verify(mCallback, times(1)).onClose(); } /** * Make sure onGet handles null headers gracefully */ @Test public void testOnGetNoHeaders() throws Exception { Operation op = makeOperation(null, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); } /** * Make sure onGet handles bad type gracefully */ @Test public void testOnGetBadType() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_BAD); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); } /** * Make sure onGet handles no type gracefully */ @Test public void testOnGetNoType() throws Exception { mRequest.setHeader(HeaderSet.TYPE, null); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); } /** * Make sure a getImageThumbnail request with a valid handle works */ @Test public void testGetLinkedThumbnailWithValidHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); } /** * Make sure a getImageThumbnail request with a unstored handle returns OBEX_HTTP_NOT_FOUND */ @Test public void testGetLinkedThumbnailWithValidUnstoredHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_UNSTORED); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_FOUND); } /** * Make sure a getImageThumbnail request with an invalidly formatted handle returns * OBEX_HTTP_BAD_REQUEST */ @Test public void testGetLinkedThumbnailWithInvalidHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_INVALID); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_PRECON_FAILED); } /** * Make sure a getImageThumbnail request with an invalidly formatted handle returns * OBEX_HTTP_BAD_REQUEST */ @Test public void testGetLinkedThumbnailWithNullHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); mRequest.setHeader(HEADER_ID_IMG_HANDLE, null); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); } /** * Make sure a getImageProperties request with a valid handle returns a valie properties object */ @Test public void testGetImagePropertiesWithValidHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); } /** * Make sure a getImageProperties request with a unstored handle returns OBEX_HTTP_NOT_FOUND */ @Test public void testGetImagePropertiesWithValidUnstoredHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_UNSTORED); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_FOUND); } /** * Make sure a getImageProperties request with an invalidly formatted handle returns * OBEX_HTTP_BAD_REQUEST */ @Test public void testGetImagePropertiesWithInvalidHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_INVALID); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_PRECON_FAILED); } /** * Make sure a getImageProperties request with an invalidly formatted handle returns * OBEX_HTTP_BAD_REQUEST */ @Test public void testGetImagePropertiesWithNullHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); mRequest.setHeader(HEADER_ID_IMG_HANDLE, null); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); } /** * Make sure a GetImage request with a null descriptor returns a native image */ @Test public void testGetImageWithValidHandleAndNullDescriptor() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, null); setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); } /** * Make sure a GetImage request with a valid descriptor returns an image */ @Test public void testGetImageWithValidHandleAndValidDescriptor() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); } /** * Make sure a GetImage request with a valid, but unsupported descriptor, returns NOT_ACCEPTABLE */ @Test public void testGetImageWithValidHandleAndInvalidDescriptor() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.WBMP /* No Android support, won't work */, 200, 200)); setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE); } /** * Make sure a GetImage request with a unstored handle returns OBEX_HTTP_NOT_FOUND */ @Test public void testGetImageWithValidUnstoredHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_UNSTORED); mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_FOUND); } /** * Make sure a getImage request with an invalidly formatted handle returns OBEX_HTTP_BAD_REQUEST */ @Test public void testGetImageWithInvalidHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_INVALID); mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_PRECON_FAILED); } /** * Make sure a getImage request with a null handle returns OBEX_HTTP_BAD_REQUEST */ @Test public void testGetImageWithNullHandle() throws Exception { mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); mRequest.setHeader(HEADER_ID_IMG_HANDLE, null); mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); Operation op = makeOperation(mRequest, mOutputStream); int responseCode = mAvrcpBipObexServer.onGet(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); } /** * Make sure onPut is not a supported action */ @Test public void testOnPut() { Operation op = null; int responseCode = mAvrcpBipObexServer.onPut(op); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED); } /** * Make sure onAbort is not a supported action */ @Test public void testOnAbort() { HeaderSet request = null; HeaderSet reply = null; int responseCode = mAvrcpBipObexServer.onAbort(request, reply); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED); } /** * Make sure onSetPath is not a supported action */ @Test public void testOnSetPath() { HeaderSet request = null; HeaderSet reply = null; boolean backup = false; boolean create = false; int responseCode = mAvrcpBipObexServer.onSetPath(request, reply, backup, create); assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED); } }