blob: 498c470940684482b951fb75e8fff0568e9fe21d [file] [log] [blame]
/*
* 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 android.util.Log;
import com.android.bluetooth.avrcpcontroller.BipImageDescriptor;
import com.android.bluetooth.avrcpcontroller.BipImageProperties;
import com.android.bluetooth.avrcpcontroller.ParseException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import javax.obex.HeaderSet;
import javax.obex.Operation;
import javax.obex.ResponseCodes;
import javax.obex.ServerRequestHandler;
/**
* A class responsible for handling requests from a specific client connection
*/
public class AvrcpBipObexServer extends ServerRequestHandler {
private static final String TAG = "AvrcpBipObexServer";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final AvrcpCoverArtService mAvrcpCoverArtService;
// AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1
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 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 byte HEADER_ID_IMG_HANDLE = 0x30;
private static final byte HEADER_ID_IMG_DESCRIPTOR = 0x71;
private final Callback mCallback;
/**
* A set of callbacks to notify the creator of important AVRCP BIP events.
*/
public interface Callback {
/**
* Receive a notification when this server session connects to a device
*/
void onConnected();
/**
* Receive a notification when this server session disconnects with a device
*/
void onDisconnected();
/**
* Receive a notification when this server session closes a connection with a device
*/
void onClose();
}
public AvrcpBipObexServer(AvrcpCoverArtService service, Callback callback) {
super();
mAvrcpCoverArtService = service;
mCallback = callback;
}
@Override
public int onConnect(final HeaderSet request, HeaderSet reply) {
debug("onConnect");
try {
byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET);
debug("onConnect - uuid=" + Arrays.toString(uuid));
if (!Arrays.equals(uuid, BLUETOOTH_UUID_AVRCP_COVER_ART)) {
warn("onConnect - uuid didn't match. Not Acceptable");
return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
}
// ...
} catch (IOException e) {
warn("onConnect - Something bad happened");
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
reply.setHeader(HeaderSet.WHO, BLUETOOTH_UUID_AVRCP_COVER_ART);
debug("onConnect - Successful");
if (mCallback != null) {
mCallback.onConnected();
}
return ResponseCodes.OBEX_HTTP_OK;
}
@Override
public void onDisconnect(final HeaderSet request, HeaderSet reply) {
debug("onDisconnect");
if (mCallback != null) {
mCallback.onDisconnected();
}
}
@Override
public int onGet(final Operation op) {
debug("onGet");
try {
HeaderSet request = op.getReceivedHeader();
if (request == null) return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
// Route the request to the proper response handler
String type = (String) request.getHeader(HeaderSet.TYPE);
if (TYPE_GET_LINKED_THUMBNAIL.equals(type)) {
return handleGetImageThumbnail(op);
} else if (TYPE_GET_IMAGE_PROPERTIES.equals(type)) {
return handleGetImageProperties(op);
} else if (TYPE_GET_IMAGE.equals(type)) {
return handleGetImage(op);
} else {
warn("Received unknown type '" + type + "'");
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
} catch (IllegalArgumentException e) {
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
} catch (ParseException e) {
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
} catch (Exception e) {
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
}
@Override
public int onPut(final Operation op) {
return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
}
@Override
public int onAbort(final HeaderSet request, HeaderSet reply) {
return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
}
@Override
public int onSetPath(final HeaderSet request, HeaderSet reply, final boolean backup,
final boolean create) {
return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
}
@Override
public void onClose() {
debug("Connection closed");
if (mCallback != null) {
mCallback.onClose();
}
}
/**
* Determine if a given image handle is valid in format
*
* An image handle a 9 character string of numbers 0-9 only. Anything else is invalid. This is
* defined in section 4.4.4 (Image Handles) of the BIP specification, which is inherited by the
* AVRCP specification.
*
* @return True if the image handle is valid, false otherwise.
*/
private boolean isImageHandleValid(String handle) {
if (handle == null || handle.length() != 7) return false;
for (int i = 0; i < 7; i++) {
char c = handle.charAt(i);
if (!Character.isDigit(c)) return false;
}
return true;
}
private int handleGetImageThumbnail(Operation op)throws IOException {
HeaderSet request = op.getReceivedHeader();
String imageHandle = (String) request.getHeader(HEADER_ID_IMG_HANDLE);
debug("Received GetImageThumbnail(handle='" + imageHandle + "')");
if (imageHandle == null) {
warn("Received GetImageThumbnail without an image handle");
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
if (!isImageHandleValid(imageHandle)) {
debug("Received GetImageThumbnail with an invalid image handle");
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
CoverArt image = mAvrcpCoverArtService.getImage(imageHandle);
if (image == null) {
warn("No image stored at handle '" + imageHandle + "'");
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
byte[] thumbnail = image.getThumbnail();
if (thumbnail == null) {
warn("Failed to serialize image");
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
HeaderSet replyHeaders = new HeaderSet();
return sendResponse(op, replyHeaders, thumbnail);
}
private int handleGetImageProperties(Operation op) throws IOException {
HeaderSet request = op.getReceivedHeader();
String imageHandle = (String) request.getHeader(HEADER_ID_IMG_HANDLE);
debug("Received GetImageProperties(handle='" + imageHandle + "')");
if (imageHandle == null) {
warn("Received GetImageProperties without an image handle");
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
if (!isImageHandleValid(imageHandle)) {
debug("Received GetImageProperties with an invalid image handle");
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
CoverArt image = mAvrcpCoverArtService.getImage(imageHandle);
if (image == null) {
warn("No image stored at handle '" + imageHandle + "'");
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
BipImageProperties properties = image.getImageProperties();
if (properties == null) {
warn("Failed to get properties for known image");
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
byte[] propertiesBytes = properties.serialize();
if (propertiesBytes == null) {
debug("Failed to serialize properties for image");
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
debug("Sending image properties: " + properties);
HeaderSet replyHeaders = new HeaderSet();
return sendResponse(op, replyHeaders, propertiesBytes);
}
private int handleGetImage(Operation op) throws IOException {
HeaderSet request = op.getReceivedHeader();
String imageHandle = (String) request.getHeader(HEADER_ID_IMG_HANDLE);
byte[] descriptorBytes = (byte[]) request.getHeader(HEADER_ID_IMG_DESCRIPTOR);
BipImageDescriptor descriptor = null;
if (descriptorBytes != null) {
descriptor = new BipImageDescriptor(new ByteArrayInputStream(descriptorBytes));
}
debug("Received GetImage(handle='" + imageHandle + "', descriptor='" + descriptor + "')");
if (imageHandle == null) {
warn("Received GetImage without an image handle");
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
if (!isImageHandleValid(imageHandle)) {
debug("Received GetImage with an invalid image handle");
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
CoverArt image = mAvrcpCoverArtService.getImage(imageHandle);
if (image == null) {
warn("No image stored at handle '" + imageHandle + "'");
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
byte[] imageBytes = null;
if (descriptor == null) {
debug("Received GetImage without an image descriptor. Returning native format");
imageBytes = image.getImage();
} else {
imageBytes = image.getImage(descriptor);
}
if (imageBytes == null) {
warn("Failed to serialize image with given format");
return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; // BIP Section 5.3 unsupported format
}
debug("Sending image");
HeaderSet replyHeaders = new HeaderSet();
replyHeaders.setHeader(HeaderSet.LENGTH, null); // Section 4.5.8, Required, null is fine
return sendResponse(op, replyHeaders, imageBytes);
}
/**
* Send a response to the given operation using the given headers and bytes.
*/
private int sendResponse(Operation op, HeaderSet replyHeaders, byte[] bytes) {
if (op != null && bytes != null && replyHeaders != null) {
OutputStream outStream = null;
int maxChunkSize = 0;
int bytesToWrite = 0;
int bytesWritten = 0;
try {
op.sendHeaders(replyHeaders); // Do this before getting chunk size
maxChunkSize = op.getMaxPacketSize();
outStream = op.openOutputStream();
while (bytesWritten < bytes.length) {
bytesToWrite = Math.min(maxChunkSize, bytes.length - bytesWritten);
outStream.write(bytes, bytesWritten, bytesToWrite);
bytesWritten += bytesToWrite;
}
} catch (IOException e) {
warn("An exception occurred while writing response, e=" + e);
} finally {
// Make sure we close
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) { }
}
}
// If we didn't write everything then send the error code
if (bytesWritten != bytes.length) {
warn("Failed to write entire response");
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
// Otherwise, success!
return ResponseCodes.OBEX_HTTP_OK;
}
// If had no header or no body to send then assume we didn't find anything at all
return ResponseCodes.OBEX_HTTP_NOT_FOUND;
}
private void warn(String msg) {
Log.w(TAG, msg);
}
private void debug(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}