1 /*
2  * Copyright (C) 2019 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.avrcpcontroller;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothProfile;
21 import android.bluetooth.BluetoothSocket;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.util.Log;
27 
28 import com.android.bluetooth.BluetoothObexTransport;
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.obex.ClientSession;
31 import com.android.obex.HeaderSet;
32 import com.android.obex.ResponseCodes;
33 
34 import java.io.IOException;
35 import java.lang.ref.WeakReference;
36 
37 /**
38  * A client to a remote device's BIP Image Pull Server, as defined by a PSM passed in at
39  * construction time.
40  *
41  * <p>Once the client connection is established you can use this client to get image properties and
42  * download images. The connection to the server is held open to service multiple requests.
43  *
44  * <p>Client is good for one connection lifecycle. Please call shutdown() to clean up safely. Once a
45  * disconnection has occurred, please create a new client.
46  */
47 public class AvrcpBipClient {
48     private static final String TAG = AvrcpBipClient.class.getSimpleName();
49 
50     // AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1
51     private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART =
52             new byte[] {
53                 (byte) 0x71,
54                 (byte) 0x63,
55                 (byte) 0xDD,
56                 (byte) 0x54,
57                 (byte) 0x4A,
58                 (byte) 0x7E,
59                 (byte) 0x11,
60                 (byte) 0xE2,
61                 (byte) 0xB4,
62                 (byte) 0x7C,
63                 (byte) 0x00,
64                 (byte) 0x50,
65                 (byte) 0xC2,
66                 (byte) 0x49,
67                 (byte) 0x00,
68                 (byte) 0x48
69             };
70 
71     private static final int CONNECT = 0;
72     private static final int DISCONNECT = 1;
73     private static final int REQUEST = 2;
74     private static final int REFRESH_OBEX_SESSION = 3;
75 
76     private final Handler mHandler;
77     private final HandlerThread mThread;
78 
79     private final BluetoothDevice mDevice;
80     private final int mPsm;
81     private int mState = BluetoothProfile.STATE_DISCONNECTED;
82 
83     private BluetoothSocket mSocket;
84     private BluetoothObexTransport mTransport;
85     private ClientSession mSession;
86 
87     private final Callback mCallback;
88 
89     /** Callback object used to be notified of when a request has been completed. */
90     interface Callback {
91 
92         /**
93          * Notify of a connection state change in the client
94          *
95          * @param oldState The old state of the client
96          * @param newState The new state of the client
97          */
onConnectionStateChanged(int oldState, int newState)98         void onConnectionStateChanged(int oldState, int newState);
99 
100         /**
101          * Notify of a get image properties completing
102          *
103          * @param status A status code to indicate a success or error
104          * @param properties The BipImageProperties object returned if successful, null otherwise
105          */
onGetImagePropertiesComplete( int status, String imageHandle, BipImageProperties properties)106         void onGetImagePropertiesComplete(
107                 int status, String imageHandle, BipImageProperties properties);
108 
109         /**
110          * Notify of a get image operation completing
111          *
112          * @param status A status code of the request. success or error
113          * @param image The BipImage object returned if successful, null otherwise
114          */
onGetImageComplete(int status, String imageHandle, BipImage image)115         void onGetImageComplete(int status, String imageHandle, BipImage image);
116     }
117 
118     /**
119      * Creates a BIP image pull client
120      *
121      * <p>{@link connectAsync()} must be called separately.
122      */
AvrcpBipClient(BluetoothDevice remoteDevice, int psm, Callback callback)123     public AvrcpBipClient(BluetoothDevice remoteDevice, int psm, Callback callback) {
124         if (remoteDevice == null) {
125             throw new NullPointerException("Remote device is null");
126         }
127         if (callback == null) {
128             throw new NullPointerException("Callback is null");
129         }
130 
131         mDevice = remoteDevice;
132         mPsm = psm;
133         mCallback = callback;
134 
135         mThread = new HandlerThread("AvrcpBipClient");
136         mThread.start();
137 
138         Looper looper = mThread.getLooper();
139 
140         mHandler = new AvrcpBipClientHandler(looper, this);
141     }
142 
143     /** Refreshes this client's OBEX session */
refreshSession()144     public void refreshSession() {
145         debug("Refresh client session");
146         if (!isConnected()) {
147             error("Tried to do a reconnect operation on a client that is not connected");
148             return;
149         }
150         try {
151             mHandler.obtainMessage(REFRESH_OBEX_SESSION).sendToTarget();
152         } catch (IllegalStateException e) {
153             // Means we haven't been started or we're already stopped. Doing this makes this call
154             // always safe no matter the state.
155             return;
156         }
157     }
158 
159     /** Safely disconnects the client from the server */
shutdown()160     public void shutdown() {
161         debug("Shutdown client");
162         try {
163             disconnectAsync();
164         } catch (IllegalStateException e) {
165             // Means we haven't been started or we're already stopped. Doing this makes this call
166             // always safe no matter the state.
167             return;
168         }
169         mThread.quitSafely();
170     }
171 
172     /**
173      * Determines if this client is connected to the server
174      *
175      * @return True if connected, False otherwise
176      */
getState()177     public synchronized int getState() {
178         return mState;
179     }
180 
181     /**
182      * Determines if this client is connected to the server
183      *
184      * @return True if connected, False otherwise
185      */
isConnected()186     public boolean isConnected() {
187         return getState() == BluetoothProfile.STATE_CONNECTED;
188     }
189 
190     /**
191      * Return the L2CAP PSM used to connect to the server.
192      *
193      * @return The L2CAP PSM
194      */
getL2capPsm()195     public int getL2capPsm() {
196         return mPsm;
197     }
198 
199     /** Retrieve the image properties associated with the given imageHandle */
getImageProperties(String imageHandle)200     public boolean getImageProperties(String imageHandle) {
201         RequestGetImageProperties request = new RequestGetImageProperties(imageHandle);
202         boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
203         if (!status) {
204             error("Adding messages failed, connection state: " + isConnected());
205             return false;
206         }
207         return true;
208     }
209 
210     /** Download the image object associated with the given imageHandle */
getImage(String imageHandle, BipImageDescriptor descriptor)211     public boolean getImage(String imageHandle, BipImageDescriptor descriptor) {
212         RequestGetImage request = new RequestGetImage(imageHandle, descriptor);
213         boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
214         if (!status) {
215             error("Adding messages failed, connection state: " + isConnected());
216             return false;
217         }
218         return true;
219     }
220 
221     /** Update our client's connection state and notify of the new status */
222     @VisibleForTesting
setConnectionState(int state)223     void setConnectionState(int state) {
224         int oldState = -1;
225         synchronized (this) {
226             oldState = mState;
227             mState = state;
228         }
229         if (oldState != state) {
230             mCallback.onConnectionStateChanged(oldState, mState);
231         }
232     }
233 
234     /** Connects asynchronously */
connectAsync()235     void connectAsync() {
236         mHandler.obtainMessage(CONNECT).sendToTarget();
237     }
238 
239     /** Connects to the remote device's BIP Image Pull server */
connect()240     private synchronized void connect() {
241         debug("Connect using psm: " + mPsm);
242         if (isConnected()) {
243             warn("Already connected");
244             return;
245         }
246 
247         try {
248             setConnectionState(BluetoothProfile.STATE_CONNECTING);
249 
250             mSocket = mDevice.createL2capSocket(mPsm);
251             mSocket.connect();
252 
253             mTransport = new BluetoothObexTransport(mSocket);
254             mSession = new ClientSession(mTransport);
255 
256             HeaderSet headerSet = new HeaderSet();
257             headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART);
258 
259             headerSet = mSession.connect(headerSet);
260             int responseCode = headerSet.getResponseCode();
261             if (responseCode == ResponseCodes.OBEX_HTTP_OK) {
262                 setConnectionState(BluetoothProfile.STATE_CONNECTED);
263                 debug("Connection established");
264             } else {
265                 error("Error connecting, code: " + responseCode);
266                 disconnect();
267             }
268         } catch (IOException e) {
269             error("Exception while connecting to AVRCP BIP server", e);
270             disconnect();
271         }
272     }
273 
274     /** Disconnect and reconnect the OBEX session. */
refreshObexSession()275     private synchronized void refreshObexSession() {
276         if (mSession == null) return;
277 
278         try {
279             setConnectionState(BluetoothProfile.STATE_DISCONNECTING);
280             mSession.disconnect(null);
281             debug("Disconnected from OBEX session");
282         } catch (IOException e) {
283             error("Exception while disconnecting from AVRCP BIP server", e);
284             disconnect();
285             return;
286         }
287 
288         try {
289             setConnectionState(BluetoothProfile.STATE_CONNECTING);
290 
291             HeaderSet headerSet = new HeaderSet();
292             headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART);
293 
294             headerSet = mSession.connect(headerSet);
295             int responseCode = headerSet.getResponseCode();
296             if (responseCode == ResponseCodes.OBEX_HTTP_OK) {
297                 setConnectionState(BluetoothProfile.STATE_CONNECTED);
298                 debug("Reconnection established");
299             } else {
300                 error("Error reconnecting, code: " + responseCode);
301                 disconnect();
302             }
303         } catch (IOException e) {
304             error("Exception while reconnecting to AVRCP BIP server", e);
305             disconnect();
306         }
307     }
308 
309     /** Disconnects asynchronously */
disconnectAsync()310     void disconnectAsync() {
311         mHandler.obtainMessage(DISCONNECT).sendToTarget();
312     }
313 
314     /**
315      * Permanently disconnects this client from the remote device's BIP server and notifies of the
316      * new connection status.
317      */
disconnect()318     private synchronized void disconnect() {
319         if (mSession != null) {
320             setConnectionState(BluetoothProfile.STATE_DISCONNECTING);
321 
322             try {
323                 mSession.disconnect(null);
324                 debug("Disconnected from OBEX session");
325             } catch (IOException e) {
326                 error("Exception while disconnecting from AVRCP BIP server: " + e.toString());
327             }
328 
329             try {
330                 mSession.close();
331                 mTransport.close();
332                 mSocket.close();
333                 debug("Closed underlying session, transport and socket");
334             } catch (IOException e) {
335                 error("Exception while closing AVRCP BIP session: ", e);
336             }
337 
338             mSession = null;
339             mTransport = null;
340             mSocket = null;
341         }
342         setConnectionState(BluetoothProfile.STATE_DISCONNECTED);
343     }
344 
executeRequest(BipRequest request)345     private void executeRequest(BipRequest request) {
346         if (!isConnected()) {
347             error("Cannot execute request " + request.toString() + ", we're not connected");
348             notifyCaller(request);
349             return;
350         }
351 
352         try {
353             request.execute(mSession);
354             notifyCaller(request);
355             debug("Completed request - " + request.toString());
356         } catch (IOException e) {
357             error("Request failed: " + request.toString());
358             notifyCaller(request);
359             disconnect();
360         }
361     }
362 
notifyCaller(BipRequest request)363     private void notifyCaller(BipRequest request) {
364         int type = request.getType();
365         int responseCode = request.getResponseCode();
366         String imageHandle = null;
367 
368         debug("Notifying caller of request complete - " + request.toString());
369         switch (type) {
370             case BipRequest.TYPE_GET_IMAGE_PROPERTIES:
371                 imageHandle = ((RequestGetImageProperties) request).getImageHandle();
372                 BipImageProperties properties =
373                         ((RequestGetImageProperties) request).getImageProperties();
374                 mCallback.onGetImagePropertiesComplete(responseCode, imageHandle, properties);
375                 break;
376             case BipRequest.TYPE_GET_IMAGE:
377                 imageHandle = ((RequestGetImage) request).getImageHandle();
378                 BipImage image = ((RequestGetImage) request).getImage();
379                 mCallback.onGetImageComplete(responseCode, imageHandle, image);
380                 break;
381         }
382     }
383 
384     /** Handles this AVRCP BIP Image Pull Client's requests */
385     private static class AvrcpBipClientHandler extends Handler {
386         WeakReference<AvrcpBipClient> mInst;
387 
AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst)388         AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst) {
389             super(looper);
390             mInst = new WeakReference<>(inst);
391         }
392 
393         @Override
handleMessage(Message msg)394         public void handleMessage(Message msg) {
395             AvrcpBipClient inst = mInst.get();
396             switch (msg.what) {
397                 case CONNECT:
398                     if (!inst.isConnected()) {
399                         inst.connect();
400                     }
401                     break;
402 
403                 case DISCONNECT:
404                     if (inst.isConnected()) {
405                         inst.disconnect();
406                     }
407                     break;
408 
409                 case REFRESH_OBEX_SESSION:
410                     if (inst.isConnected()) {
411                         inst.refreshObexSession();
412                     }
413                     break;
414 
415                 case REQUEST:
416                     if (inst.isConnected()) {
417                         inst.executeRequest((BipRequest) msg.obj);
418                     }
419                     break;
420             }
421         }
422     }
423 
424     @VisibleForTesting
getStateName()425     String getStateName() {
426         int state = getState();
427         switch (state) {
428             case BluetoothProfile.STATE_DISCONNECTED:
429                 return "Disconnected";
430             case BluetoothProfile.STATE_CONNECTING:
431                 return "Connecting";
432             case BluetoothProfile.STATE_CONNECTED:
433                 return "Connected";
434             case BluetoothProfile.STATE_DISCONNECTING:
435                 return "Disconnecting";
436         }
437         return "Unknown";
438     }
439 
440     @Override
toString()441     public String toString() {
442         return "<AvrcpBipClient"
443                 + " device="
444                 + mDevice
445                 + " psm="
446                 + mPsm
447                 + " state="
448                 + getStateName()
449                 + ">";
450     }
451 
452     /** Print to debug if debug is enabled for this class */
debug(String msg)453     private void debug(String msg) {
454         Log.d(TAG, "[" + mDevice + "] " + msg);
455     }
456 
457     /** Print to warn */
warn(String msg)458     private void warn(String msg) {
459         Log.w(TAG, "[" + mDevice + "] " + msg);
460     }
461 
462     /** Print to error */
error(String msg)463     private void error(String msg) {
464         Log.e(TAG, "[" + mDevice + "] " + msg);
465     }
466 
error(String msg, Throwable e)467     private void error(String msg, Throwable e) {
468         Log.e(TAG, "[" + mDevice + "] " + msg, e);
469     }
470 }
471