1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.avrcp; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.mockito.Mockito.mock; 23 import static org.mockito.Mockito.never; 24 import static org.mockito.Mockito.times; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.when; 27 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.content.res.Resources; 31 import android.graphics.Bitmap; 32 import android.graphics.BitmapFactory; 33 34 import androidx.test.InstrumentationRegistry; 35 import androidx.test.runner.AndroidJUnit4; 36 37 import com.android.bluetooth.TestUtils; 38 import com.android.bluetooth.audio_util.Image; 39 import com.android.bluetooth.avrcpcontroller.BipEncoding; 40 import com.android.bluetooth.avrcpcontroller.BipImageDescriptor; 41 42 import org.junit.After; 43 import org.junit.Before; 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 47 import java.io.ByteArrayOutputStream; 48 import java.io.InputStream; 49 import java.io.OutputStream; 50 51 import com.android.obex.HeaderSet; 52 import com.android.obex.Operation; 53 import com.android.obex.ResponseCodes; 54 55 @RunWith(AndroidJUnit4.class) 56 public class AvrcpBipObexServerTest { 57 private static final String TYPE_GET_LINKED_THUMBNAIL = "x-bt/img-thm"; 58 private static final String TYPE_GET_IMAGE_PROPERTIES = "x-bt/img-properties"; 59 private static final String TYPE_GET_IMAGE = "x-bt/img-img"; 60 private static final String TYPE_BAD = "x-bt/bad-type"; 61 62 private static final byte HEADER_ID_IMG_HANDLE = 0x30; 63 private static final byte HEADER_ID_IMG_DESCRIPTOR = 0x71; 64 65 private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] { 66 (byte) 0x71, 67 (byte) 0x63, 68 (byte) 0xDD, 69 (byte) 0x54, 70 (byte) 0x4A, 71 (byte) 0x7E, 72 (byte) 0x11, 73 (byte) 0xE2, 74 (byte) 0xB4, 75 (byte) 0x7C, 76 (byte) 0x00, 77 (byte) 0x50, 78 (byte) 0xC2, 79 (byte) 0x49, 80 (byte) 0x00, 81 (byte) 0x48 82 }; 83 84 private static final byte[] NOT_BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] { 85 (byte) 0x00, 86 (byte) 0x00, 87 (byte) 0x00, 88 (byte) 0x00, 89 (byte) 0x00, 90 (byte) 0x00, 91 (byte) 0x00, 92 (byte) 0x00, 93 (byte) 0x00, 94 (byte) 0x00, 95 (byte) 0x00, 96 (byte) 0x00, 97 (byte) 0x00, 98 (byte) 0x00, 99 (byte) 0x00, 100 (byte) 0x00 101 }; 102 103 private static final String IMAGE_HANDLE_1 = "0000001"; 104 private static final String IMAGE_HANDLE_UNSTORED = "0000256"; 105 private static final String IMAGE_HANDLE_INVALID = "abc1234"; // no non-numeric characters 106 107 private Resources mTestResources; 108 private CoverArt mCoverArt; 109 110 private AvrcpCoverArtService mAvrcpCoverArtService = null; 111 private AvrcpBipObexServer.Callback mCallback = null; 112 113 private HeaderSet mRequest = null; 114 private HeaderSet mReply = null; 115 private ByteArrayOutputStream mOutputStream = null; 116 117 private AvrcpBipObexServer mAvrcpBipObexServer; 118 119 @Before setUp()120 public void setUp() throws Exception { 121 mTestResources = TestUtils.getTestApplicationResources( 122 InstrumentationRegistry.getTargetContext()); 123 124 mCoverArt = loadCoverArt(com.android.bluetooth.tests.R.raw.image_200_200); 125 126 mAvrcpCoverArtService = mock(AvrcpCoverArtService.class); 127 mCallback = mock(AvrcpBipObexServer.Callback.class); 128 129 mRequest = new HeaderSet(); 130 mReply = new HeaderSet(); 131 mOutputStream = new ByteArrayOutputStream(); 132 133 mAvrcpBipObexServer = new AvrcpBipObexServer(mAvrcpCoverArtService, mCallback); 134 } 135 136 @After tearDown()137 public void tearDown() throws Exception { 138 mAvrcpBipObexServer = null; 139 mOutputStream = null; 140 mReply = null; 141 mRequest = null; 142 mCallback = null; 143 mAvrcpCoverArtService = null; 144 mCoverArt = null; 145 mTestResources = null; 146 } 147 loadCoverArt(int resId)148 private CoverArt loadCoverArt(int resId) { 149 InputStream imageInputStream = mTestResources.openRawResource(resId); 150 Bitmap bitmap = BitmapFactory.decodeStream(imageInputStream); 151 Image image = new Image(null, bitmap); 152 return new CoverArt(image); 153 } 154 setCoverArtAvailableAtHandle(String handle, CoverArt art)155 private void setCoverArtAvailableAtHandle(String handle, CoverArt art) { 156 art.setImageHandle(handle); 157 when(mAvrcpCoverArtService.getImage(handle)).thenReturn(art); 158 } 159 160 /** 161 * Creates a mocked operation that can be used by our server as a client request 162 * 163 * Our server will use: 164 * - getReceivedHeader 165 * - sendHeaders 166 * - getMaxPacketSize 167 * - openOutputStream 168 */ makeOperation(HeaderSet requestHeaders, OutputStream os)169 private Operation makeOperation(HeaderSet requestHeaders, OutputStream os) throws Exception { 170 Operation op = mock(Operation.class); 171 when(op.getReceivedHeader()).thenReturn(requestHeaders); 172 when(op.getMaxPacketSize()).thenReturn(256); 173 when(op.openOutputStream()).thenReturn(os); 174 return op; 175 } 176 makeDescriptor(int encoding, int width, int height)177 private byte[] makeDescriptor(int encoding, int width, int height) { 178 return new BipImageDescriptor.Builder() 179 .setEncoding(encoding) 180 .setFixedDimensions(width, height) 181 .build().serialize(); 182 } 183 184 /** 185 * Make sure we let a connection through with a valid UUID 186 */ 187 @Test testConnectWithValidUuidHeader()188 public void testConnectWithValidUuidHeader() throws Exception { 189 mRequest.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART); 190 int responseCode = mAvrcpBipObexServer.onConnect(mRequest, mReply); 191 verify(mCallback, times(1)).onConnected(); 192 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); 193 } 194 195 /** 196 * Make sure we deny a connection when there is an invalid UUID 197 */ 198 @Test testConnectWithInvalidUuidHeader()199 public void testConnectWithInvalidUuidHeader() throws Exception { 200 mRequest.setHeader(HeaderSet.TARGET, NOT_BLUETOOTH_UUID_AVRCP_COVER_ART); 201 int responseCode = mAvrcpBipObexServer.onConnect(mRequest, mReply); 202 verify(mCallback, never()).onConnected(); 203 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE); 204 } 205 206 /** 207 * Make sure onDisconnect notifies the callbacks in the proper way 208 */ 209 @Test testDisonnect()210 public void testDisonnect() { 211 mAvrcpBipObexServer.onDisconnect(mRequest, mReply); 212 verify(mCallback, times(1)).onDisconnected(); 213 } 214 215 /** 216 * Make sure onClose notifies the callbacks in the proper way 217 */ 218 @Test testOnClose()219 public void testOnClose() { 220 mAvrcpBipObexServer.onClose(); 221 verify(mCallback, times(1)).onClose(); 222 } 223 224 /** 225 * Make sure onGet handles null headers gracefully 226 */ 227 @Test testOnGetNoHeaders()228 public void testOnGetNoHeaders() throws Exception { 229 Operation op = makeOperation(null, mOutputStream); 230 int responseCode = mAvrcpBipObexServer.onGet(op); 231 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); 232 } 233 234 /** 235 * Make sure onGet handles bad type gracefully 236 */ 237 @Test testOnGetBadType()238 public void testOnGetBadType() throws Exception { 239 mRequest.setHeader(HeaderSet.TYPE, TYPE_BAD); 240 Operation op = makeOperation(mRequest, mOutputStream); 241 int responseCode = mAvrcpBipObexServer.onGet(op); 242 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); 243 } 244 245 /** 246 * Make sure onGet handles no type gracefully 247 */ 248 @Test testOnGetNoType()249 public void testOnGetNoType() throws Exception { 250 mRequest.setHeader(HeaderSet.TYPE, null); 251 Operation op = makeOperation(mRequest, mOutputStream); 252 int responseCode = mAvrcpBipObexServer.onGet(op); 253 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); 254 } 255 256 /** 257 * Make sure a getImageThumbnail request with a valid handle works 258 */ 259 @Test testGetLinkedThumbnailWithValidHandle()260 public void testGetLinkedThumbnailWithValidHandle() throws Exception { 261 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); 262 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); 263 setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); 264 Operation op = makeOperation(mRequest, mOutputStream); 265 int responseCode = mAvrcpBipObexServer.onGet(op); 266 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); 267 } 268 269 /** 270 * Make sure a getImageThumbnail request with a unstored handle returns OBEX_HTTP_NOT_FOUND 271 */ 272 @Test testGetLinkedThumbnailWithValidUnstoredHandle()273 public void testGetLinkedThumbnailWithValidUnstoredHandle() throws Exception { 274 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); 275 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_UNSTORED); 276 Operation op = makeOperation(mRequest, mOutputStream); 277 int responseCode = mAvrcpBipObexServer.onGet(op); 278 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_FOUND); 279 } 280 281 /** 282 * Make sure a getImageThumbnail request with an invalidly formatted handle returns 283 * OBEX_HTTP_BAD_REQUEST 284 */ 285 @Test testGetLinkedThumbnailWithInvalidHandle()286 public void testGetLinkedThumbnailWithInvalidHandle() throws Exception { 287 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); 288 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_INVALID); 289 Operation op = makeOperation(mRequest, mOutputStream); 290 int responseCode = mAvrcpBipObexServer.onGet(op); 291 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_PRECON_FAILED); 292 } 293 294 /** 295 * Make sure a getImageThumbnail request with an invalidly formatted handle returns 296 * OBEX_HTTP_BAD_REQUEST 297 */ 298 @Test testGetLinkedThumbnailWithNullHandle()299 public void testGetLinkedThumbnailWithNullHandle() throws Exception { 300 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_LINKED_THUMBNAIL); 301 mRequest.setHeader(HEADER_ID_IMG_HANDLE, null); 302 Operation op = makeOperation(mRequest, mOutputStream); 303 int responseCode = mAvrcpBipObexServer.onGet(op); 304 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); 305 } 306 307 /** 308 * Make sure a getImageProperties request with a valid handle returns a valie properties object 309 */ 310 @Test testGetImagePropertiesWithValidHandle()311 public void testGetImagePropertiesWithValidHandle() throws Exception { 312 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); 313 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); 314 setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); 315 Operation op = makeOperation(mRequest, mOutputStream); 316 int responseCode = mAvrcpBipObexServer.onGet(op); 317 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); 318 } 319 320 /** 321 * Make sure a getImageProperties request with a unstored handle returns OBEX_HTTP_NOT_FOUND 322 */ 323 @Test testGetImagePropertiesWithValidUnstoredHandle()324 public void testGetImagePropertiesWithValidUnstoredHandle() throws Exception { 325 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); 326 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_UNSTORED); 327 Operation op = makeOperation(mRequest, mOutputStream); 328 int responseCode = mAvrcpBipObexServer.onGet(op); 329 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_FOUND); 330 } 331 332 /** 333 * Make sure a getImageProperties request with an invalidly formatted handle returns 334 * OBEX_HTTP_BAD_REQUEST 335 */ 336 @Test testGetImagePropertiesWithInvalidHandle()337 public void testGetImagePropertiesWithInvalidHandle() throws Exception { 338 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); 339 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_INVALID); 340 Operation op = makeOperation(mRequest, mOutputStream); 341 int responseCode = mAvrcpBipObexServer.onGet(op); 342 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_PRECON_FAILED); 343 } 344 345 /** 346 * Make sure a getImageProperties request with an invalidly formatted handle returns 347 * OBEX_HTTP_BAD_REQUEST 348 */ 349 @Test testGetImagePropertiesWithNullHandle()350 public void testGetImagePropertiesWithNullHandle() throws Exception { 351 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE_PROPERTIES); 352 mRequest.setHeader(HEADER_ID_IMG_HANDLE, null); 353 Operation op = makeOperation(mRequest, mOutputStream); 354 int responseCode = mAvrcpBipObexServer.onGet(op); 355 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); 356 } 357 358 /** 359 * Make sure a GetImage request with a null descriptor returns a native image 360 */ 361 @Test testGetImageWithValidHandleAndNullDescriptor()362 public void testGetImageWithValidHandleAndNullDescriptor() throws Exception { 363 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); 364 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); 365 mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, null); 366 setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); 367 Operation op = makeOperation(mRequest, mOutputStream); 368 int responseCode = mAvrcpBipObexServer.onGet(op); 369 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); 370 } 371 372 /** 373 * Make sure a GetImage request with a valid descriptor returns an image 374 */ 375 @Test testGetImageWithValidHandleAndValidDescriptor()376 public void testGetImageWithValidHandleAndValidDescriptor() throws Exception { 377 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); 378 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); 379 mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); 380 setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); 381 Operation op = makeOperation(mRequest, mOutputStream); 382 int responseCode = mAvrcpBipObexServer.onGet(op); 383 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_OK); 384 } 385 386 /** 387 * Make sure a GetImage request with a valid, but unsupported descriptor, returns NOT_ACCEPTABLE 388 */ 389 @Test testGetImageWithValidHandleAndInvalidDescriptor()390 public void testGetImageWithValidHandleAndInvalidDescriptor() throws Exception { 391 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); 392 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_1); 393 mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, 394 makeDescriptor(BipEncoding.WBMP /* No Android support, won't work */, 200, 200)); 395 setCoverArtAvailableAtHandle(IMAGE_HANDLE_1, mCoverArt); 396 Operation op = makeOperation(mRequest, mOutputStream); 397 int responseCode = mAvrcpBipObexServer.onGet(op); 398 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE); 399 } 400 401 /** 402 * Make sure a GetImage request with a unstored handle returns OBEX_HTTP_NOT_FOUND 403 */ 404 @Test testGetImageWithValidUnstoredHandle()405 public void testGetImageWithValidUnstoredHandle() throws Exception { 406 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); 407 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_UNSTORED); 408 mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); 409 Operation op = makeOperation(mRequest, mOutputStream); 410 int responseCode = mAvrcpBipObexServer.onGet(op); 411 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_FOUND); 412 } 413 414 /** 415 * Make sure a getImage request with an invalidly formatted handle returns OBEX_HTTP_BAD_REQUEST 416 */ 417 @Test testGetImageWithInvalidHandle()418 public void testGetImageWithInvalidHandle() throws Exception { 419 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); 420 mRequest.setHeader(HEADER_ID_IMG_HANDLE, IMAGE_HANDLE_INVALID); 421 mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); 422 Operation op = makeOperation(mRequest, mOutputStream); 423 int responseCode = mAvrcpBipObexServer.onGet(op); 424 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_PRECON_FAILED); 425 } 426 427 /** 428 * Make sure a getImage request with a null handle returns OBEX_HTTP_BAD_REQUEST 429 */ 430 @Test testGetImageWithNullHandle()431 public void testGetImageWithNullHandle() throws Exception { 432 mRequest.setHeader(HeaderSet.TYPE, TYPE_GET_IMAGE); 433 mRequest.setHeader(HEADER_ID_IMG_HANDLE, null); 434 mRequest.setHeader(HEADER_ID_IMG_DESCRIPTOR, makeDescriptor(BipEncoding.JPEG, 200, 200)); 435 Operation op = makeOperation(mRequest, mOutputStream); 436 int responseCode = mAvrcpBipObexServer.onGet(op); 437 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_BAD_REQUEST); 438 } 439 440 /** 441 * Make sure onPut is not a supported action 442 */ 443 @Test testOnPut()444 public void testOnPut() { 445 Operation op = null; 446 int responseCode = mAvrcpBipObexServer.onPut(op); 447 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED); 448 } 449 450 /** 451 * Make sure onAbort is not a supported action 452 */ 453 @Test testOnAbort()454 public void testOnAbort() { 455 HeaderSet request = null; 456 HeaderSet reply = null; 457 int responseCode = mAvrcpBipObexServer.onAbort(request, reply); 458 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED); 459 } 460 461 /** 462 * Make sure onSetPath is not a supported action 463 */ 464 @Test testOnSetPath()465 public void testOnSetPath() { 466 HeaderSet request = null; 467 HeaderSet reply = null; 468 boolean backup = false; 469 boolean create = false; 470 int responseCode = mAvrcpBipObexServer.onSetPath(request, reply, backup, create); 471 assertThat(responseCode).isEqualTo(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED); 472 } 473 } 474