1 /*
2  * Copyright (C) 2016 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.pbapclient;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothSocket;
22 import android.os.Handler;
23 import android.os.Handler.Callback;
24 import android.os.HandlerThread;
25 import android.os.Message;
26 import android.os.Process;
27 import android.util.Log;
28 
29 import java.io.IOException;
30 import java.util.UUID;
31 
32 class BluetoothPbapSession implements Callback {
33     //TODO consider cleaning file organization and naming.
34     private static final String TAG = "com.android.bluetooth.pbapclient.BluetoothPbapSession";
35 
36     /* local use only */
37     private static final int RFCOMM_CONNECTED = 1;
38     private static final int RFCOMM_FAILED = 2;
39 
40     /* to BluetoothPbapClient */
41     public static final int REQUEST_COMPLETED = 3;
42     public static final int REQUEST_FAILED = 4;
43     public static final int SESSION_CONNECTING = 5;
44     public static final int SESSION_CONNECTED = 6;
45     public static final int SESSION_DISCONNECTED = 7;
46     public static final int AUTH_REQUESTED = 8;
47     public static final int AUTH_TIMEOUT = 9;
48 
49     public static final int ACTION_LISTING = 14;
50     public static final int ACTION_VCARD = 15;
51     public static final int ACTION_PHONEBOOK_SIZE = 16;
52 
53     private static final String PBAP_UUID =
54             "0000112f-0000-1000-8000-00805f9b34fb";
55 
56     private final BluetoothAdapter mAdapter;
57     private final BluetoothDevice mDevice;
58 
59     private final Handler mParentHandler;
60 
61     private final HandlerThread mHandlerThread;
62     private final Handler mSessionHandler;
63 
64     private RfcommConnectThread mConnectThread;
65     private BluetoothPbapObexTransport mTransport;
66 
67     private BluetoothPbapObexSession mObexSession;
68 
69     private BluetoothPbapRequest mPendingRequest = null;
70 
BluetoothPbapSession(BluetoothDevice device, Handler handler)71     public BluetoothPbapSession(BluetoothDevice device, Handler handler) {
72 
73         mAdapter = BluetoothAdapter.getDefaultAdapter();
74         if (mAdapter == null) {
75             throw new NullPointerException("No Bluetooth adapter in the system");
76         }
77 
78         mDevice = device;
79         mParentHandler = handler;
80         mConnectThread = null;
81         mTransport = null;
82         mObexSession = null;
83 
84         mHandlerThread = new HandlerThread("PBAP session handler",
85                 Process.THREAD_PRIORITY_BACKGROUND);
86         mHandlerThread.start();
87         mSessionHandler = new Handler(mHandlerThread.getLooper(), this);
88     }
89 
90     @Override
handleMessage(Message msg)91     public boolean handleMessage(Message msg) {
92         Log.d(TAG, "Handler: msg: " + msg.what);
93 
94         switch (msg.what) {
95             case RFCOMM_FAILED:
96                 mConnectThread = null;
97 
98                 mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
99 
100                 if (mPendingRequest != null) {
101                     mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
102                     mPendingRequest = null;
103                 }
104                 break;
105 
106             case RFCOMM_CONNECTED:
107                 mConnectThread = null;
108                 mTransport = (BluetoothPbapObexTransport) msg.obj;
109                 startObexSession();
110                 break;
111 
112             case BluetoothPbapObexSession.OBEX_SESSION_FAILED:
113                 stopObexSession();
114 
115                 mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
116 
117                 if (mPendingRequest != null) {
118                     mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
119                     mPendingRequest = null;
120                 }
121                 break;
122 
123             case BluetoothPbapObexSession.OBEX_SESSION_CONNECTED:
124                 mParentHandler.obtainMessage(SESSION_CONNECTED).sendToTarget();
125 
126                 if (mPendingRequest != null) {
127                     mObexSession.schedule(mPendingRequest);
128                     mPendingRequest = null;
129                 }
130                 break;
131 
132             case BluetoothPbapObexSession.OBEX_SESSION_DISCONNECTED:
133                 mParentHandler.obtainMessage(SESSION_DISCONNECTED).sendToTarget();
134                 stopRfcomm();
135                 break;
136 
137             case BluetoothPbapObexSession.OBEX_SESSION_REQUEST_COMPLETED:
138                 /* send to parent, process there */
139                 mParentHandler.obtainMessage(REQUEST_COMPLETED, msg.obj).sendToTarget();
140                 break;
141 
142             case BluetoothPbapObexSession.OBEX_SESSION_REQUEST_FAILED:
143                 /* send to parent, process there */
144                 mParentHandler.obtainMessage(REQUEST_FAILED, msg.obj).sendToTarget();
145                 break;
146 
147             case BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_REQUEST:
148                 /* send to parent, process there */
149                 mParentHandler.obtainMessage(AUTH_REQUESTED).sendToTarget();
150 
151                 mSessionHandler
152                         .sendMessageDelayed(
153                                 mSessionHandler
154                                         .obtainMessage(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT),
155                                 30000);
156                 break;
157 
158             case BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT:
159                 /* stop authentication */
160                 setAuthResponse(null);
161 
162                 mParentHandler.obtainMessage(AUTH_TIMEOUT).sendToTarget();
163                 break;
164 
165             default:
166                 return false;
167         }
168 
169         return true;
170     }
171 
start()172     public void start() {
173         Log.d(TAG, "start");
174 
175         startRfcomm();
176     }
177 
stop()178     public void stop() {
179         Log.d(TAG, "Stop");
180 
181         stopObexSession();
182         stopRfcomm();
183     }
184 
abort()185     public void abort() {
186         Log.d(TAG, "abort");
187 
188         /* fail pending request immediately */
189         if (mPendingRequest != null) {
190             mParentHandler.obtainMessage(REQUEST_FAILED, mPendingRequest).sendToTarget();
191             mPendingRequest = null;
192         }
193 
194         if (mObexSession != null) {
195             mObexSession.abort();
196         }
197     }
198 
makeRequest(BluetoothPbapRequest request)199     public boolean makeRequest(BluetoothPbapRequest request) {
200         Log.v(TAG, "makeRequest: " + request.getClass().getSimpleName());
201 
202         if (mPendingRequest != null) {
203             Log.w(TAG, "makeRequest: request already queued, exiting");
204             return false;
205         }
206 
207         if (mObexSession == null) {
208             mPendingRequest = request;
209 
210             /*
211              * since there is no pending request and no session it's safe to
212              * assume that RFCOMM does not exist either and we should start
213              * connecting it
214              */
215             startRfcomm();
216 
217             return true;
218         }
219 
220         return mObexSession.schedule(request);
221     }
222 
setAuthResponse(String key)223     public boolean setAuthResponse(String key) {
224         Log.d(TAG, "setAuthResponse key=" + key);
225 
226         mSessionHandler
227                 .removeMessages(BluetoothPbapObexSession.OBEX_SESSION_AUTHENTICATION_TIMEOUT);
228 
229         /* does not make sense to set auth response when OBEX session is down */
230         if (mObexSession == null) {
231             return false;
232         }
233 
234         return mObexSession.setAuthReply(key);
235     }
236 
startRfcomm()237     private void startRfcomm() {
238         Log.d(TAG, "startRfcomm");
239 
240         if (mConnectThread == null && mObexSession == null) {
241             mParentHandler.obtainMessage(SESSION_CONNECTING).sendToTarget();
242 
243             mConnectThread = new RfcommConnectThread();
244             mConnectThread.start();
245         }
246 
247         /*
248          * don't care if mConnectThread is not null - it means RFCOMM is being
249          * connected anyway
250          */
251     }
252 
stopRfcomm()253     private void stopRfcomm() {
254         Log.d(TAG, "stopRfcomm");
255 
256         if (mConnectThread != null) {
257             try {
258                 // Force close the socket in case the thread is stuck doing the connect()
259                 // call.
260                 mConnectThread.closeSocket();
261                 // TODO: Add timed join if closeSocket does not clean up the state.
262                 mConnectThread.join();
263             } catch (InterruptedException e) {
264             }
265 
266             mConnectThread = null;
267         }
268 
269         if (mTransport != null) {
270             try {
271                 mTransport.close();
272             } catch (IOException e) {
273             }
274 
275             mTransport = null;
276         }
277     }
278 
startObexSession()279     private void startObexSession() {
280         Log.d(TAG, "startObexSession");
281 
282         mObexSession = new BluetoothPbapObexSession(mTransport);
283         mObexSession.start(mSessionHandler);
284     }
285 
stopObexSession()286     private void stopObexSession() {
287         Log.d(TAG, "stopObexSession");
288 
289         if (mObexSession != null) {
290             mObexSession.stop();
291             mObexSession = null;
292         }
293     }
294 
295     private class RfcommConnectThread extends Thread {
296         private static final String TAG = "RfcommConnectThread";
297 
298         private BluetoothSocket mSocket;
299 
RfcommConnectThread()300         public RfcommConnectThread() {
301             super("RfcommConnectThread");
302         }
303 
304         @Override
run()305         public void run() {
306             if (mAdapter.isDiscovering()) {
307                 mAdapter.cancelDiscovery();
308             }
309 
310             try {
311                 mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(PBAP_UUID));
312                 mSocket.connect();
313 
314                 BluetoothPbapObexTransport transport;
315                 transport = new BluetoothPbapObexTransport(mSocket);
316 
317                 mSessionHandler.obtainMessage(RFCOMM_CONNECTED, transport).sendToTarget();
318             } catch (IOException e) {
319                 closeSocket();
320                 mSessionHandler.obtainMessage(RFCOMM_FAILED).sendToTarget();
321             }
322 
323         }
324 
325         // This method may be called from outside the thread if the connect() call above is stuck.
closeSocket()326         public void closeSocket() {
327             try {
328                 if (mSocket != null) {
329                     mSocket.close();
330                 }
331             } catch (IOException e) {
332                 Log.e(TAG, "Error when closing socket", e);
333             }
334         }
335     }
336 }
337