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.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothSocket; 21 import android.content.Context; 22 import android.util.Log; 23 24 import com.android.bluetooth.BluetoothObexTransport; 25 import com.android.bluetooth.IObexConnectionHandler; 26 import com.android.bluetooth.ObexServerSockets; 27 import com.android.bluetooth.audio_util.Image; 28 29 import java.io.IOException; 30 import java.util.Arrays; 31 import java.util.HashMap; 32 33 import javax.obex.ServerSession; 34 35 /** 36 * The AVRCP Cover Art Service 37 * 38 * This service handles allocation of image handles and storage of images. It also owns the BIP OBEX 39 * server that handles requests to get AVRCP cover artwork. 40 */ 41 public class AvrcpCoverArtService { 42 private static final String TAG = "AvrcpCoverArtService"; 43 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 44 45 private static final int COVER_ART_STORAGE_MAX_ITEMS = 32; 46 47 private final Context mContext; 48 49 // Cover Art and Image Handle objects 50 private final AvrcpCoverArtStorage mStorage; 51 52 // BIP Server Objects 53 private volatile boolean mShutdown = true; 54 private final SocketAcceptor mAcceptThread; 55 private ObexServerSockets mServerSockets = null; 56 private final HashMap<BluetoothDevice, ServerSession> mClients = 57 new HashMap<BluetoothDevice, ServerSession>(); 58 private final Object mClientsLock = new Object(); 59 private final Object mServerLock = new Object(); 60 61 // Native interface 62 private AvrcpNativeInterface mNativeInterface; 63 AvrcpCoverArtService(Context context)64 public AvrcpCoverArtService(Context context) { 65 mContext = context; 66 mNativeInterface = AvrcpNativeInterface.getInterface(); 67 mAcceptThread = new SocketAcceptor(); 68 mStorage = new AvrcpCoverArtStorage(COVER_ART_STORAGE_MAX_ITEMS); 69 } 70 71 /** 72 * Start the AVRCP Cover Art Service. 73 * 74 * This will start up a BIP OBEX server and record the l2cap psm in the SDP record, and begin 75 * accepting connections. 76 */ start()77 public boolean start() { 78 debug("Starting service"); 79 if (!isShutdown()) { 80 error("Service already started"); 81 return true; 82 } 83 mStorage.clear(); 84 return startBipServer(); 85 } 86 87 /** 88 * Stop the AVRCP Cover Art Service. 89 * 90 * Tear down existing connections, remove ourselved from the SDP record. 91 */ stop()92 public boolean stop() { 93 debug("Stopping service"); 94 stopBipServer(); 95 synchronized (mClientsLock) { 96 for (ServerSession session : mClients.values()) { 97 session.close(); 98 } 99 mClients.clear(); 100 } 101 mStorage.clear(); 102 return true; 103 } 104 startBipServer()105 private boolean startBipServer() { 106 debug("Starting BIP OBEX server"); 107 synchronized (mServerLock) { 108 mServerSockets = ObexServerSockets.create(mAcceptThread); 109 if (mServerSockets == null) { 110 error("Failed to get a server socket. Can't setup cover art service"); 111 return false; 112 } 113 registerBipServer(mServerSockets.getL2capPsm()); 114 mShutdown = false; 115 debug("Service started, psm=" + mServerSockets.getL2capPsm()); 116 } 117 return true; 118 } 119 stopBipServer()120 private boolean stopBipServer() { 121 debug("Stopping BIP OBEX server"); 122 synchronized (mServerLock) { 123 mShutdown = true; 124 unregisterBipServer(); 125 if (mServerSockets != null) { 126 mServerSockets.shutdown(false); 127 mServerSockets = null; 128 } 129 } 130 return true; 131 } 132 isShutdown()133 private boolean isShutdown() { 134 synchronized (mServerLock) { 135 return mShutdown; 136 } 137 } 138 getL2capPsm()139 private int getL2capPsm() { 140 synchronized (mServerLock) { 141 return (mServerLock != null ? mServerSockets.getL2capPsm() : 0); 142 } 143 } 144 145 /** 146 * Store an image with the service and gets the image handle it's associated with. 147 */ storeImage(Image image)148 public String storeImage(Image image) { 149 debug("storeImage(image='" + image + "')"); 150 if (image == null || image.getImage() == null) return null; 151 return mStorage.storeImage(new CoverArt(image)); 152 } 153 154 /** 155 * Get the image stored at the given image handle, if it exists 156 */ getImage(String imageHandle)157 public CoverArt getImage(String imageHandle) { 158 debug("getImage(" + imageHandle + ")"); 159 return mStorage.getImage(imageHandle); 160 } 161 162 /** 163 * Add a BIP L2CAP PSM to the AVRCP Target SDP Record 164 */ registerBipServer(int psm)165 private void registerBipServer(int psm) { 166 debug("Add our PSM (" + psm + ") to the AVRCP Target SDP record"); 167 mNativeInterface.registerBipServer(psm); 168 return; 169 } 170 171 /** 172 * Remove any BIP L2CAP PSM from the AVRCP Target SDP Record 173 */ unregisterBipServer()174 private void unregisterBipServer() { 175 debug("Remove the PSM remove the AVRCP Target SDP record"); 176 mNativeInterface.unregisterBipServer(); 177 return; 178 } 179 180 /** 181 * Connect a device with the server 182 * 183 * Since the server cannot explicitly make clients connect, this function is internal only and 184 * provides a place for us to do required book keeping when we've decided to accept a client 185 */ connect(BluetoothDevice device, BluetoothSocket socket)186 private boolean connect(BluetoothDevice device, BluetoothSocket socket) { 187 debug("Connect '" + device + "'"); 188 synchronized (mClientsLock) { 189 190 // Only allow one client at all 191 if (mClients.size() >= 1) return false; 192 193 // Only allow one client per device 194 if (mClients.containsKey(device)) return false; 195 196 // Create a BIP OBEX Server session for the client and connect 197 AvrcpBipObexServer s = new AvrcpBipObexServer(this, new AvrcpBipObexServer.Callback() { 198 public void onConnected() { 199 mNativeInterface.setBipClientStatus(device.getAddress(), true); 200 } 201 202 public void onDisconnected() { 203 mNativeInterface.setBipClientStatus(device.getAddress(), false); 204 } 205 206 public void onClose() { 207 disconnect(device); 208 } 209 }); 210 BluetoothObexTransport transport = new BluetoothObexTransport(socket); 211 try { 212 ServerSession session = new ServerSession(transport, s, null); 213 mClients.put(device, session); 214 return true; 215 } catch (IOException e) { 216 error(e.toString()); 217 return false; 218 } 219 } 220 } 221 222 /** 223 * Explicitly disconnect a device from our BIP server if its connected. 224 */ disconnect(BluetoothDevice device)225 public void disconnect(BluetoothDevice device) { 226 debug("disconnect '" + device + "'"); 227 // Closing the server session closes the underlying transport, which closes the underlying 228 // socket as well. No need to maintain and close anything else. 229 synchronized (mClientsLock) { 230 if (mClients.containsKey(device)) { 231 mNativeInterface.setBipClientStatus(device.getAddress(), false); 232 ServerSession session = mClients.get(device); 233 mClients.remove(device); 234 session.close(); 235 } 236 } 237 } 238 239 /** 240 * A Socket Acceptor to handle incoming connections to our BIP Server. 241 * 242 * If we are accepting connections and the device is permitted, then this class will create a 243 * session with our AvrcpBipObexServer. 244 */ 245 private class SocketAcceptor implements IObexConnectionHandler { 246 @Override onConnect(BluetoothDevice device, BluetoothSocket socket)247 public synchronized boolean onConnect(BluetoothDevice device, BluetoothSocket socket) { 248 debug("onConnect() - device=" + device + ", socket=" + socket); 249 if (isShutdown()) return false; 250 return connect(device, socket); 251 } 252 253 @Override onAcceptFailed()254 public synchronized void onAcceptFailed() { 255 error("OnAcceptFailed()"); 256 if (isShutdown()) { 257 error("Failed to accept incoming connection due to shutdown"); 258 } else { 259 // restart 260 stop(); 261 start(); 262 } 263 } 264 } 265 266 /** 267 * Dump useful debug information about this service to a string 268 */ dump(StringBuilder sb)269 public void dump(StringBuilder sb) { 270 int psm = getL2capPsm(); 271 sb.append("AvrcpCoverArtService:"); 272 sb.append("\n\tpsm = " + (psm == 0 ? "null" : psm)); 273 mStorage.dump(sb); 274 synchronized (mClientsLock) { 275 sb.append("\n\tclients = " + Arrays.toString(mClients.keySet().toArray())); 276 } 277 sb.append("\n"); 278 } 279 280 /** 281 * Print a message to DEBUG if debug output is enabled 282 */ debug(String msg)283 private void debug(String msg) { 284 if (DEBUG) { 285 Log.d(TAG, msg); 286 } 287 } 288 289 /** 290 * Print a message to ERROR 291 */ error(String msg)292 private void error(String msg) { 293 Log.e(TAG, msg); 294 } 295 } 296