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