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