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