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