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