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 
30 import java.io.IOException;
31 import java.lang.ref.WeakReference;
32 
33 import javax.obex.ClientSession;
34 import javax.obex.HeaderSet;
35 import javax.obex.ResponseCodes;
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  * 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  * 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";
49     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
50 
51     // AVRCP Controller BIP Image Initiator/Cover Art UUID - AVRCP 1.6 Section 5.14.2.1
52     private static final byte[] BLUETOOTH_UUID_AVRCP_COVER_ART = 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     /**
90      * Callback object used to be notified of when a request has been completed.
91      */
92     interface Callback {
93 
94         /**
95          * Notify of a connection state change in the client
96          *
97          * @param oldState The old state of the client
98          * @param newState The new state of the client
99          */
onConnectionStateChanged(int oldState, int newState)100         void onConnectionStateChanged(int oldState, int newState);
101 
102         /**
103          * Notify of a get image properties completing
104          *
105          * @param status A status code to indicate a success or error
106          * @param properties The BipImageProperties object returned if successful, null otherwise
107          */
onGetImagePropertiesComplete(int status, String imageHandle, BipImageProperties properties)108         void onGetImagePropertiesComplete(int status, String imageHandle,
109                 BipImageProperties properties);
110 
111         /**
112          * Notify of a get image operation completing
113          *
114          * @param status A status code of the request. success or error
115          * @param image The BipImage object returned if successful, null otherwise
116          */
onGetImageComplete(int status, String imageHandle, BipImage image)117         void onGetImageComplete(int status, String imageHandle, BipImage image);
118     }
119 
120     /**
121      * Creates a BIP image pull client and connects to a remote device's BIP image push server.
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         mHandler.obtainMessage(CONNECT).sendToTarget();
142     }
143 
144     /**
145      * Refreshes this client's OBEX session
146      */
refreshSession()147     public void refreshSession() {
148         debug("Refresh client session");
149         if (!isConnected()) {
150             error("Tried to do a reconnect operation on a client that is not connected");
151             return;
152         }
153         try {
154             mHandler.obtainMessage(REFRESH_OBEX_SESSION).sendToTarget();
155         } catch (IllegalStateException e) {
156             // Means we haven't been started or we're already stopped. Doing this makes this call
157             // always safe no matter the state.
158             return;
159         }
160     }
161 
162     /**
163      * Safely disconnects the client from the server
164      */
shutdown()165     public void shutdown() {
166         debug("Shutdown client");
167         try {
168             mHandler.obtainMessage(DISCONNECT).sendToTarget();
169         } catch (IllegalStateException e) {
170             // Means we haven't been started or we're already stopped. Doing this makes this call
171             // always safe no matter the state.
172             return;
173         }
174         mThread.quitSafely();
175     }
176 
177     /**
178      * Determines if this client is connected to the server
179      *
180      * @return True if connected, False otherwise
181      */
getState()182     public synchronized int getState() {
183         return mState;
184     }
185 
186     /**
187      * Determines if this client is connected to the server
188      *
189      * @return True if connected, False otherwise
190      */
isConnected()191     public boolean isConnected() {
192         return getState() == BluetoothProfile.STATE_CONNECTED;
193     }
194 
195     /**
196      * Return the L2CAP PSM used to connect to the server.
197      *
198      * @return The L2CAP PSM
199      */
getL2capPsm()200     public int getL2capPsm() {
201         return mPsm;
202     }
203 
204     /**
205      * Retrieve the image properties associated with the given imageHandle
206      */
getImageProperties(String imageHandle)207     public boolean getImageProperties(String imageHandle) {
208         RequestGetImageProperties request =  new RequestGetImageProperties(imageHandle);
209         boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
210         if (!status) {
211             error("Adding messages failed, connection state: " + isConnected());
212             return false;
213         }
214         return true;
215     }
216 
217     /**
218      * Download the image object associated with the given imageHandle
219      */
getImage(String imageHandle, BipImageDescriptor descriptor)220     public boolean getImage(String imageHandle, BipImageDescriptor descriptor) {
221         RequestGetImage request =  new RequestGetImage(imageHandle, descriptor);
222         boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
223         if (!status) {
224             error("Adding messages failed, connection state: " + isConnected());
225             return false;
226         }
227         return true;
228     }
229 
230     /**
231      * Update our client's connection state and notify of the new status
232      */
setConnectionState(int state)233     private void setConnectionState(int state) {
234         int oldState = -1;
235         synchronized (this) {
236             oldState = mState;
237             mState = state;
238         }
239         if (oldState != state)  {
240             mCallback.onConnectionStateChanged(oldState, mState);
241         }
242     }
243 
244     /**
245      * Connects to the remote device's BIP Image Pull server
246      */
connect()247     private synchronized void connect() {
248         debug("Connect using psm: " + mPsm);
249         if (isConnected()) {
250             warn("Already connected");
251             return;
252         }
253 
254         try {
255             setConnectionState(BluetoothProfile.STATE_CONNECTING);
256 
257             mSocket = mDevice.createL2capSocket(mPsm);
258             mSocket.connect();
259 
260             mTransport = new BluetoothObexTransport(mSocket);
261             mSession = new ClientSession(mTransport);
262 
263             HeaderSet headerSet = new HeaderSet();
264             headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART);
265 
266             headerSet = mSession.connect(headerSet);
267             int responseCode = headerSet.getResponseCode();
268             if (responseCode == ResponseCodes.OBEX_HTTP_OK) {
269                 setConnectionState(BluetoothProfile.STATE_CONNECTED);
270                 debug("Connection established");
271             } else {
272                 error("Error connecting, code: " + responseCode);
273                 disconnect();
274             }
275         } catch (IOException e) {
276             error("Exception while connecting to AVRCP BIP server", e);
277             disconnect();
278         }
279     }
280 
281     /**
282      * Disconnect and reconnect the OBEX session.
283      */
refreshObexSession()284     private synchronized void refreshObexSession() {
285         if (mSession == null) return;
286 
287         try {
288             setConnectionState(BluetoothProfile.STATE_DISCONNECTING);
289             mSession.disconnect(null);
290             debug("Disconnected from OBEX session");
291         } catch (IOException e) {
292             error("Exception while disconnecting from AVRCP BIP server", e);
293             disconnect();
294             return;
295         }
296 
297         try {
298             setConnectionState(BluetoothProfile.STATE_CONNECTING);
299 
300             HeaderSet headerSet = new HeaderSet();
301             headerSet.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_AVRCP_COVER_ART);
302 
303             headerSet = mSession.connect(headerSet);
304             int responseCode = headerSet.getResponseCode();
305             if (responseCode == ResponseCodes.OBEX_HTTP_OK) {
306                 setConnectionState(BluetoothProfile.STATE_CONNECTED);
307                 debug("Reconnection established");
308             } else {
309                 error("Error reconnecting, code: " + responseCode);
310                 disconnect();
311             }
312         } catch (IOException e) {
313             error("Exception while reconnecting to AVRCP BIP server", e);
314             disconnect();
315         }
316     }
317 
318     /**
319      * Permanently disconnects this client from the remote device's BIP server and notifies of the
320      * new connection status.
321      *
322      */
disconnect()323     private synchronized void disconnect() {
324         if (mSession != null) {
325             setConnectionState(BluetoothProfile.STATE_DISCONNECTING);
326 
327             try {
328                 mSession.disconnect(null);
329                 debug("Disconnected from OBEX session");
330             } catch (IOException e) {
331                 error("Exception while disconnecting from AVRCP BIP server: " + e.toString());
332             }
333 
334             try {
335                 mSession.close();
336                 mTransport.close();
337                 mSocket.close();
338                 debug("Closed underlying session, transport and socket");
339             } catch (IOException e) {
340                 error("Exception while closing AVRCP BIP session: ", e);
341             }
342 
343             mSession = null;
344             mTransport = null;
345             mSocket = null;
346         }
347         setConnectionState(BluetoothProfile.STATE_DISCONNECTED);
348     }
349 
executeRequest(BipRequest request)350     private void executeRequest(BipRequest request) {
351         if (!isConnected()) {
352             error("Cannot execute request " + request.toString()
353                     + ", we're not connected");
354             notifyCaller(request);
355             return;
356         }
357 
358         try {
359             request.execute(mSession);
360             notifyCaller(request);
361             debug("Completed request - " + request.toString());
362         } catch (IOException e) {
363             error("Request failed: " + request.toString());
364             notifyCaller(request);
365             disconnect();
366         }
367     }
368 
notifyCaller(BipRequest request)369     private void notifyCaller(BipRequest request) {
370         int type = request.getType();
371         int responseCode = request.getResponseCode();
372         String imageHandle = null;
373 
374         debug("Notifying caller of request complete - " + request.toString());
375         switch (type) {
376             case BipRequest.TYPE_GET_IMAGE_PROPERTIES:
377                 imageHandle = ((RequestGetImageProperties) request).getImageHandle();
378                 BipImageProperties properties =
379                         ((RequestGetImageProperties) request).getImageProperties();
380                 mCallback.onGetImagePropertiesComplete(responseCode, imageHandle, properties);
381                 break;
382             case BipRequest.TYPE_GET_IMAGE:
383                 imageHandle = ((RequestGetImage) request).getImageHandle();
384                 BipImage image = ((RequestGetImage) request).getImage();
385                 mCallback.onGetImageComplete(responseCode, imageHandle, image);
386                 break;
387         }
388     }
389 
390     /**
391      * Handles this AVRCP BIP Image Pull Client's requests
392      */
393     private static class AvrcpBipClientHandler extends Handler {
394         WeakReference<AvrcpBipClient> mInst;
395 
AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst)396         AvrcpBipClientHandler(Looper looper, AvrcpBipClient inst) {
397             super(looper);
398             mInst = new WeakReference<>(inst);
399         }
400 
401         @Override
handleMessage(Message msg)402         public void handleMessage(Message msg) {
403             AvrcpBipClient inst = mInst.get();
404             switch (msg.what) {
405                 case CONNECT:
406                     if (!inst.isConnected()) {
407                         inst.connect();
408                     }
409                     break;
410 
411                 case DISCONNECT:
412                     if (inst.isConnected()) {
413                         inst.disconnect();
414                     }
415                     break;
416 
417                 case REFRESH_OBEX_SESSION:
418                     if (inst.isConnected()) {
419                         inst.refreshObexSession();
420                     }
421                     break;
422 
423                 case REQUEST:
424                     if (inst.isConnected()) {
425                         inst.executeRequest((BipRequest) msg.obj);
426                     }
427                     break;
428             }
429         }
430     }
431 
getStateName()432     private String getStateName() {
433         int state = getState();
434         switch (state) {
435             case BluetoothProfile.STATE_DISCONNECTED:
436                 return "Disconnected";
437             case BluetoothProfile.STATE_CONNECTING:
438                 return "Connecting";
439             case BluetoothProfile.STATE_CONNECTED:
440                 return "Connected";
441             case BluetoothProfile.STATE_DISCONNECTING:
442                 return "Disconnecting";
443         }
444         return "Unknown";
445     }
446 
447     @Override
toString()448     public String toString() {
449         return "<AvrcpBipClient" + " device=" + mDevice.getAddress() + " psm=" + mPsm
450                 + " state=" + getStateName() + ">";
451     }
452 
453     /**
454      * Print to debug if debug is enabled for this class
455      */
debug(String msg)456     private void debug(String msg) {
457         if (DBG) {
458             Log.d(TAG, "[" + mDevice.getAddress() + "] " + msg);
459         }
460     }
461 
462     /**
463      * Print to warn
464      */
warn(String msg)465     private void warn(String msg) {
466         Log.w(TAG, "[" + mDevice.getAddress() + "] " + msg);
467     }
468 
469     /**
470      * Print to error
471      */
error(String msg)472     private void error(String msg) {
473         Log.e(TAG, "[" + mDevice.getAddress() + "] " + msg);
474     }
475 
error(String msg, Throwable e)476     private void error(String msg, Throwable e) {
477         Log.e(TAG, "[" + mDevice.getAddress() + "] " + msg, e);
478     }
479 }
480