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 android.util.Log;
20 
21 import com.android.bluetooth.avrcpcontroller.BipImageDescriptor;
22 import com.android.bluetooth.avrcpcontroller.BipImageProperties;
23 import com.android.bluetooth.avrcpcontroller.ParseException;
24 
25 import java.io.ByteArrayInputStream;
26 import java.io.IOException;
27 import java.io.OutputStream;
28 import java.util.Arrays;
29 
30 import javax.obex.HeaderSet;
31 import javax.obex.Operation;
32 import javax.obex.ResponseCodes;
33 import javax.obex.ServerRequestHandler;
34 
35 /**
36  * A class responsible for handling requests from a specific client connection
37  */
38 public class AvrcpBipObexServer extends ServerRequestHandler {
39     private static final String TAG = "AvrcpBipObexServer";
40     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
41 
42     private final AvrcpCoverArtService mAvrcpCoverArtService;
43 
44     // AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1
45     private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = new byte[] {
46         (byte) 0x71,
47         (byte) 0x63,
48         (byte) 0xDD,
49         (byte) 0x54,
50         (byte) 0x4A,
51         (byte) 0x7E,
52         (byte) 0x11,
53         (byte) 0xE2,
54         (byte) 0xB4,
55         (byte) 0x7C,
56         (byte) 0x00,
57         (byte) 0x50,
58         (byte) 0xC2,
59         (byte) 0x49,
60         (byte) 0x00,
61         (byte) 0x48
62     };
63 
64     private static final String TYPE_GET_LINKED_THUMBNAIL = "x-bt/img-thm";
65     private static final String TYPE_GET_IMAGE_PROPERTIES = "x-bt/img-properties";
66     private static final String TYPE_GET_IMAGE = "x-bt/img-img";
67 
68     private static final byte HEADER_ID_IMG_HANDLE = 0x30;
69     private static final byte HEADER_ID_IMG_DESCRIPTOR = 0x71;
70 
71     private final Callback mCallback;
72 
73     /**
74      * A set of callbacks to notify the creator of important AVRCP BIP events.
75      */
76     public interface Callback {
77         /**
78          * Receive a notification when this server session connects to a device
79          */
onConnected()80         void onConnected();
81 
82         /**
83          * Receive a notification when this server session disconnects with a device
84          */
onDisconnected()85         void onDisconnected();
86          /**
87           * Receive a notification when this server session closes a connection with a device
88           */
onClose()89         void onClose();
90     }
91 
AvrcpBipObexServer(AvrcpCoverArtService service, Callback callback)92     public AvrcpBipObexServer(AvrcpCoverArtService service, Callback callback) {
93         super();
94         mAvrcpCoverArtService = service;
95         mCallback = callback;
96     }
97 
98     @Override
onConnect(final HeaderSet request, HeaderSet reply)99     public int onConnect(final HeaderSet request, HeaderSet reply) {
100         debug("onConnect");
101         try {
102             byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET);
103             debug("onConnect - uuid=" + Arrays.toString(uuid));
104             if (!Arrays.equals(uuid, BLUETOOTH_UUID_AVRCP_COVER_ART)) {
105                 warn("onConnect - uuid didn't match. Not Acceptable");
106                 return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
107             }
108             // ...
109         } catch (IOException e) {
110             warn("onConnect - Something bad happened");
111             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
112         }
113 
114         reply.setHeader(HeaderSet.WHO, BLUETOOTH_UUID_AVRCP_COVER_ART);
115         debug("onConnect - Successful");
116         if (mCallback != null) {
117             mCallback.onConnected();
118         }
119         return ResponseCodes.OBEX_HTTP_OK;
120     }
121 
122     @Override
onDisconnect(final HeaderSet request, HeaderSet reply)123     public void onDisconnect(final HeaderSet request, HeaderSet reply) {
124         debug("onDisconnect");
125         if (mCallback != null) {
126             mCallback.onDisconnected();
127         }
128     }
129 
130     @Override
onGet(final Operation op)131     public int onGet(final Operation op) {
132         debug("onGet");
133         try {
134             HeaderSet request = op.getReceivedHeader();
135             if (request == null) return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
136 
137             // Route the request to the proper response handler
138             String type = (String) request.getHeader(HeaderSet.TYPE);
139             if (TYPE_GET_LINKED_THUMBNAIL.equals(type)) {
140                 return handleGetImageThumbnail(op);
141             } else if (TYPE_GET_IMAGE_PROPERTIES.equals(type)) {
142                 return handleGetImageProperties(op);
143             } else if (TYPE_GET_IMAGE.equals(type)) {
144                 return handleGetImage(op);
145             } else {
146                 warn("Received unknown type '" + type + "'");
147                 return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
148             }
149         } catch (IllegalArgumentException e) {
150             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
151         } catch (ParseException e) {
152             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
153         } catch (Exception e) {
154             return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
155         }
156     }
157 
158     @Override
onPut(final Operation op)159     public int onPut(final Operation op) {
160         return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
161     }
162 
163     @Override
onAbort(final HeaderSet request, HeaderSet reply)164     public int onAbort(final HeaderSet request, HeaderSet reply) {
165         return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
166     }
167 
168     @Override
onSetPath(final HeaderSet request, HeaderSet reply, final boolean backup, final boolean create)169     public int onSetPath(final HeaderSet request, HeaderSet reply, final boolean backup,
170             final boolean create) {
171         return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
172     }
173 
174     @Override
onClose()175     public void onClose() {
176         debug("Connection closed");
177         if (mCallback != null) {
178             mCallback.onClose();
179         }
180     }
181 
182     /**
183      * Determine if a given image handle is valid in format
184      *
185      * An image handle a 9 character string of numbers 0-9 only. Anything else is invalid. This is
186      * defined in section 4.4.4 (Image Handles) of the BIP specification, which is inherited by the
187      * AVRCP specification.
188      *
189      * @return True if the image handle is valid, false otherwise.
190      */
isImageHandleValid(String handle)191     private boolean isImageHandleValid(String handle) {
192         if (handle == null || handle.length() != 7) return false;
193         for (int i = 0; i < 7; i++) {
194             char c = handle.charAt(i);
195             if (!Character.isDigit(c)) return false;
196         }
197         return true;
198     }
199 
handleGetImageThumbnail(Operation op)200     private int handleGetImageThumbnail(Operation op)throws IOException  {
201         HeaderSet request = op.getReceivedHeader();
202         String imageHandle = (String) request.getHeader(HEADER_ID_IMG_HANDLE);
203 
204         debug("Received GetImageThumbnail(handle='" + imageHandle + "')");
205 
206         if (imageHandle == null) {
207             warn("Received GetImageThumbnail without an image handle");
208             return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
209         }
210 
211         if (!isImageHandleValid(imageHandle)) {
212             debug("Received GetImageThumbnail with an invalid image handle");
213             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
214         }
215 
216         CoverArt image = mAvrcpCoverArtService.getImage(imageHandle);
217         if (image == null) {
218             warn("No image stored at handle '" + imageHandle + "'");
219             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
220         }
221 
222         byte[] thumbnail = image.getThumbnail();
223         if (thumbnail == null) {
224             warn("Failed to serialize image");
225             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
226         }
227 
228         HeaderSet replyHeaders = new HeaderSet();
229         return sendResponse(op, replyHeaders, thumbnail);
230     }
231 
handleGetImageProperties(Operation op)232     private int handleGetImageProperties(Operation op) throws IOException {
233         HeaderSet request = op.getReceivedHeader();
234         String imageHandle = (String) request.getHeader(HEADER_ID_IMG_HANDLE);
235 
236         debug("Received GetImageProperties(handle='" + imageHandle + "')");
237 
238         if (imageHandle == null) {
239             warn("Received GetImageProperties without an image handle");
240             return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
241         }
242 
243         if (!isImageHandleValid(imageHandle)) {
244             debug("Received GetImageProperties with an invalid image handle");
245             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
246         }
247 
248         CoverArt image = mAvrcpCoverArtService.getImage(imageHandle);
249         if (image == null) {
250             warn("No image stored at handle '" + imageHandle + "'");
251             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
252         }
253         BipImageProperties properties = image.getImageProperties();
254         if (properties == null) {
255             warn("Failed to get properties for known image");
256             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
257         }
258 
259         byte[] propertiesBytes = properties.serialize();
260         if (propertiesBytes == null) {
261             debug("Failed to serialize properties for image");
262             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
263         }
264 
265         debug("Sending image properties: " + properties);
266         HeaderSet replyHeaders = new HeaderSet();
267         return sendResponse(op, replyHeaders, propertiesBytes);
268     }
269 
handleGetImage(Operation op)270     private int handleGetImage(Operation op) throws IOException {
271         HeaderSet request = op.getReceivedHeader();
272         String imageHandle = (String) request.getHeader(HEADER_ID_IMG_HANDLE);
273         byte[] descriptorBytes = (byte[]) request.getHeader(HEADER_ID_IMG_DESCRIPTOR);
274         BipImageDescriptor descriptor = null;
275 
276         if (descriptorBytes != null) {
277             descriptor = new BipImageDescriptor(new ByteArrayInputStream(descriptorBytes));
278         }
279 
280         debug("Received GetImage(handle='" + imageHandle + "', descriptor='" + descriptor + "')");
281 
282         if (imageHandle == null) {
283             warn("Received GetImage without an image handle");
284             return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
285         }
286 
287         if (!isImageHandleValid(imageHandle)) {
288             debug("Received GetImage with an invalid image handle");
289             return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
290         }
291 
292         CoverArt image = mAvrcpCoverArtService.getImage(imageHandle);
293         if (image == null) {
294             warn("No image stored at handle '" + imageHandle + "'");
295             return ResponseCodes.OBEX_HTTP_NOT_FOUND;
296         }
297 
298         byte[] imageBytes = null;
299         if (descriptor == null) {
300             debug("Received GetImage without an image descriptor. Returning native format");
301             imageBytes = image.getImage();
302         } else {
303             imageBytes = image.getImage(descriptor);
304         }
305 
306         if (imageBytes == null) {
307             warn("Failed to serialize image with given format");
308             return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; // BIP Section 5.3 unsupported format
309         }
310 
311         debug("Sending image");
312         HeaderSet replyHeaders = new HeaderSet();
313         replyHeaders.setHeader(HeaderSet.LENGTH, null); // Section 4.5.8, Required, null is fine
314         return sendResponse(op, replyHeaders, imageBytes);
315     }
316 
317     /**
318      * Send a response to the given operation using the given headers and bytes.
319      */
sendResponse(Operation op, HeaderSet replyHeaders, byte[] bytes)320     private int sendResponse(Operation op, HeaderSet replyHeaders, byte[] bytes) {
321         if (op != null && bytes != null && replyHeaders != null) {
322             OutputStream outStream = null;
323             int maxChunkSize = 0;
324             int bytesToWrite = 0;
325             int bytesWritten = 0;
326             try {
327                 op.sendHeaders(replyHeaders); // Do this before getting chunk size
328                 maxChunkSize = op.getMaxPacketSize();
329                 outStream = op.openOutputStream();
330                 while (bytesWritten < bytes.length) {
331                     bytesToWrite = Math.min(maxChunkSize, bytes.length - bytesWritten);
332                     outStream.write(bytes, bytesWritten, bytesToWrite);
333                     bytesWritten += bytesToWrite;
334                 }
335             } catch (IOException e) {
336                 warn("An exception occurred while writing response, e=" + e);
337             } finally {
338                 // Make sure we close
339                 if (outStream != null) {
340                     try {
341                         outStream.close();
342                     } catch (IOException e) { }
343                 }
344             }
345             // If we didn't write everything then send the error code
346             if (bytesWritten != bytes.length) {
347                 warn("Failed to write entire response");
348                 return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
349             }
350             // Otherwise, success!
351             return ResponseCodes.OBEX_HTTP_OK;
352         }
353         // If had no header or no body to send then assume we didn't find anything at all
354         return ResponseCodes.OBEX_HTTP_NOT_FOUND;
355     }
356 
warn(String msg)357     private void warn(String msg) {
358         Log.w(TAG, msg);
359     }
360 
debug(String msg)361     private void debug(String msg) {
362         if (DEBUG) {
363             Log.d(TAG, msg);
364         }
365     }
366 }
367