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.mapclient;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothSocket;
21 import android.bluetooth.SdpMasRecord;
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.bluetooth.ObexAppParameters;
30 import com.android.internal.util.StateMachine;
31 import com.android.obex.ClientSession;
32 import com.android.obex.HeaderSet;
33 import com.android.obex.ResponseCodes;
34 
35 import java.io.IOException;
36 import java.lang.ref.WeakReference;
37 
38 /* MasClient is a one time use connection to a server defined by the SDP record passed in at
39  * construction.  After use shutdown() must be called to properly clean up.
40  */
41 public class MasClient {
42     private static final String TAG = MasClient.class.getSimpleName();
43 
44     private static final int CONNECT = 0;
45     private static final int DISCONNECT = 1;
46     private static final int REQUEST = 2;
47     private static final byte[] BLUETOOTH_UUID_OBEX_MAS =
48             new byte[] {
49                 (byte) 0xbb,
50                 0x58,
51                 0x2b,
52                 0x40,
53                 0x42,
54                 0x0c,
55                 0x11,
56                 (byte) 0xdb,
57                 (byte) 0xb0,
58                 (byte) 0xde,
59                 0x08,
60                 0x00,
61                 0x20,
62                 0x0c,
63                 (byte) 0x9a,
64                 0x66
65             };
66     private static final byte OAP_TAGID_MAP_SUPPORTED_FEATURES = 0x29;
67     private static final int L2CAP_INVALID_PSM = -1;
68     private static final int MAP_FEATURE_NOTIFICATION_REGISTRATION = 0x00000001;
69     private static final int MAP_FEATURE_NOTIFICATION = 0x00000002;
70     private static final int MAP_FEATURE_BROWSING = 0x00000004;
71     private static final int MAP_FEATURE_UPLOADING = 0x00000008;
72     private static final int MAP_FEATURE_EXTENDED_EVENT_REPORT_1_1 = 0x00000040;
73     static final int MAP_SUPPORTED_FEATURES =
74             MAP_FEATURE_NOTIFICATION_REGISTRATION
75                     | MAP_FEATURE_NOTIFICATION
76                     | MAP_FEATURE_BROWSING
77                     | MAP_FEATURE_UPLOADING
78                     | MAP_FEATURE_EXTENDED_EVENT_REPORT_1_1;
79 
80     private final StateMachine mCallback;
81     private Handler mHandler;
82     private BluetoothSocket mSocket;
83     private BluetoothObexTransport mTransport;
84     private BluetoothDevice mRemoteDevice;
85     private ClientSession mSession;
86     private HandlerThread mThread;
87     private boolean mConnected = false;
88     SdpMasRecord mSdpMasRecord;
89 
MasClient( BluetoothDevice remoteDevice, StateMachine callback, SdpMasRecord sdpMasRecord)90     public MasClient(
91             BluetoothDevice remoteDevice, StateMachine callback, SdpMasRecord sdpMasRecord) {
92         if (remoteDevice == null) {
93             throw new NullPointerException("Obex transport is null");
94         }
95         mRemoteDevice = remoteDevice;
96         mCallback = callback;
97         mSdpMasRecord = sdpMasRecord;
98         mThread = new HandlerThread("Client");
99         mThread.start();
100         /* This will block until the looper have started, hence it will be safe to use it,
101         when the constructor completes */
102         Looper looper = mThread.getLooper();
103         mHandler = new MasClientHandler(looper, this);
104 
105         mHandler.obtainMessage(CONNECT).sendToTarget();
106     }
107 
connect()108     private void connect() {
109         try {
110             int l2capSocket = mSdpMasRecord.getL2capPsm();
111 
112             if (l2capSocket != L2CAP_INVALID_PSM) {
113                 Log.d(TAG, "Connecting to OBEX on L2CAP channel " + l2capSocket);
114                 mSocket = mRemoteDevice.createL2capSocket(l2capSocket);
115             } else {
116                 Log.d(
117                         TAG,
118                         "Connecting to OBEX on RFCOM channel "
119                                 + mSdpMasRecord.getRfcommCannelNumber());
120                 mSocket = mRemoteDevice.createRfcommSocket(mSdpMasRecord.getRfcommCannelNumber());
121             }
122             Log.d(TAG, mRemoteDevice.toString() + "Socket: " + mSocket.toString());
123             mSocket.connect();
124             mTransport = new BluetoothObexTransport(mSocket);
125 
126             mSession = new ClientSession(mTransport);
127             HeaderSet headerset = new HeaderSet();
128             headerset.setHeader(HeaderSet.TARGET, BLUETOOTH_UUID_OBEX_MAS);
129             ObexAppParameters oap = new ObexAppParameters();
130 
131             oap.add(OAP_TAGID_MAP_SUPPORTED_FEATURES, MAP_SUPPORTED_FEATURES);
132 
133             oap.addToHeaderSet(headerset);
134 
135             headerset = mSession.connect(headerset);
136             Log.d(TAG, "Connection results" + headerset.getResponseCode());
137 
138             if (headerset.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
139                 Log.d(TAG, "Connection Successful");
140                 mConnected = true;
141                 mCallback.sendMessage(MceStateMachine.MSG_MAS_CONNECTED);
142             } else {
143                 disconnect();
144             }
145 
146         } catch (IOException e) {
147             Log.e(TAG, "Caught an exception " + e.toString());
148             disconnect();
149         }
150     }
151 
disconnect()152     private void disconnect() {
153         if (mSession != null) {
154             try {
155                 mSession.disconnect(null);
156             } catch (IOException e) {
157                 Log.e(TAG, "Caught an exception while disconnecting:" + e.toString());
158             }
159 
160             try {
161                 mSession.close();
162             } catch (IOException e) {
163                 Log.e(TAG, "Caught an exception while closing:" + e.toString());
164             }
165         }
166 
167         mConnected = false;
168         mCallback.sendMessage(MceStateMachine.MSG_MAS_DISCONNECTED);
169     }
170 
executeRequest(Request request)171     private void executeRequest(Request request) {
172         try {
173             request.execute(mSession);
174             mCallback.sendMessage(MceStateMachine.MSG_MAS_REQUEST_COMPLETED, request);
175         } catch (IOException e) {
176             Log.d(TAG, "Request failed: " + request);
177             // Disconnect to cleanup.
178             disconnect();
179         }
180     }
181 
makeRequest(Request request)182     public boolean makeRequest(Request request) {
183         Log.d(TAG, "makeRequest called with: " + request);
184 
185         boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
186         if (!status) {
187             Log.e(TAG, "Adding messages failed, state: " + mConnected);
188             return false;
189         }
190         return true;
191     }
192 
193     /**
194      * Invokes {@link Request#abort} and removes it from the Handler's message queue.
195      *
196      * @param request The {@link Request} to abort.
197      */
abortRequest(Request request)198     public void abortRequest(Request request) {
199         Log.d(TAG, "abortRequest called with: " + request);
200 
201         request.abort();
202         mHandler.removeMessages(REQUEST, request);
203     }
204 
shutdown()205     public void shutdown() {
206         mHandler.obtainMessage(DISCONNECT).sendToTarget();
207         mThread.quitSafely();
208     }
209 
210     public enum CharsetType {
211         NATIVE,
212         UTF_8;
213     }
214 
getSdpMasRecord()215     SdpMasRecord getSdpMasRecord() {
216         return mSdpMasRecord;
217     }
218 
219     private static class MasClientHandler extends Handler {
220         WeakReference<MasClient> mInst;
221 
MasClientHandler(Looper looper, MasClient inst)222         MasClientHandler(Looper looper, MasClient inst) {
223             super(looper);
224             mInst = new WeakReference<>(inst);
225         }
226 
227         @Override
handleMessage(Message msg)228         public void handleMessage(Message msg) {
229             MasClient inst = mInst.get();
230             switch (msg.what) {
231                 case CONNECT:
232                     if (!inst.mConnected) {
233                         inst.connect();
234                     }
235                     break;
236 
237                 case DISCONNECT:
238                     if (inst.mConnected) {
239                         inst.disconnect();
240                     }
241                     break;
242 
243                 case REQUEST:
244                     if (inst.mConnected) {
245                         inst.executeRequest((Request) msg.obj);
246                     }
247                     break;
248             }
249         }
250     }
251 }
252