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 package com.android.bluetooth.pbapclient; 17 18 import android.accounts.Account; 19 import android.accounts.AccountManager; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothSocket; 23 import android.bluetooth.BluetoothUuid; 24 import android.bluetooth.SdpPseRecord; 25 import android.content.Context; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.provider.CallLog; 30 import android.provider.CallLog.Calls; 31 import android.util.Log; 32 33 import com.android.bluetooth.BluetoothObexTransport; 34 import com.android.bluetooth.R; 35 36 import java.io.IOException; 37 import java.util.HashMap; 38 39 import javax.obex.ClientSession; 40 import javax.obex.HeaderSet; 41 import javax.obex.ResponseCodes; 42 43 /* Bluetooth/pbapclient/PbapClientConnectionHandler is responsible 44 * for connecting, disconnecting and downloading contacts from the 45 * PBAP PSE when commanded. It receives all direction from the 46 * controlling state machine. 47 */ 48 class PbapClientConnectionHandler extends Handler { 49 static final String TAG = "PBAP PCE handler"; 50 static final boolean DBG = true; 51 static final int MSG_CONNECT = 1; 52 static final int MSG_DISCONNECT = 2; 53 static final int MSG_DOWNLOAD = 3; 54 55 // The following constants are pulled from the Bluetooth Phone Book Access Profile specification 56 // 1.1 57 private static final byte[] PBAP_TARGET = new byte[]{ 58 0x79, 59 0x61, 60 0x35, 61 (byte) 0xf0, 62 (byte) 0xf0, 63 (byte) 0xc5, 64 0x11, 65 (byte) 0xd8, 66 0x09, 67 0x66, 68 0x08, 69 0x00, 70 0x20, 71 0x0c, 72 (byte) 0x9a, 73 0x66 74 }; 75 76 private static final int PBAP_FEATURE_DEFAULT_IMAGE_FORMAT = 0x00000200; 77 private static final int PBAP_FEATURE_BROWSING = 0x00000002; 78 private static final int PBAP_FEATURE_DOWNLOADING = 0x00000001; 79 80 private static final long PBAP_FILTER_VERSION = 1 << 0; 81 private static final long PBAP_FILTER_FN = 1 << 1; 82 private static final long PBAP_FILTER_N = 1 << 2; 83 private static final long PBAP_FILTER_PHOTO = 1 << 3; 84 private static final long PBAP_FILTER_ADR = 1 << 5; 85 private static final long PBAP_FILTER_TEL = 1 << 7; 86 private static final long PBAP_FILTER_EMAIL = 1 << 8; 87 private static final long PBAP_FILTER_NICKNAME = 1 << 23; 88 89 private static final int PBAP_SUPPORTED_FEATURE = 90 PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_BROWSING | PBAP_FEATURE_DOWNLOADING; 91 private static final long PBAP_REQUESTED_FIELDS = 92 PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO 93 | PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME; 94 private static final int PBAP_V1_2 = 0x0102; 95 private static final int L2CAP_INVALID_PSM = -1; 96 97 public static final String PB_PATH = "telecom/pb.vcf"; 98 public static final String MCH_PATH = "telecom/mch.vcf"; 99 public static final String ICH_PATH = "telecom/ich.vcf"; 100 public static final String OCH_PATH = "telecom/och.vcf"; 101 102 public static final byte VCARD_TYPE_21 = 0; 103 public static final byte VCARD_TYPE_30 = 1; 104 105 private Account mAccount; 106 private AccountManager mAccountManager; 107 private BluetoothSocket mSocket; 108 private final BluetoothAdapter mAdapter; 109 private final BluetoothDevice mDevice; 110 // PSE SDP Record for current device. 111 private SdpPseRecord mPseRec = null; 112 private ClientSession mObexSession; 113 private Context mContext; 114 private BluetoothPbapObexAuthenticator mAuth = null; 115 private final PbapClientStateMachine mPbapClientStateMachine; 116 private boolean mAccountCreated; 117 118 PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine, 119 BluetoothDevice device) { 120 super(looper); 121 mAdapter = BluetoothAdapter.getDefaultAdapter(); 122 mDevice = device; 123 mContext = context; 124 mPbapClientStateMachine = stateMachine; 125 mAuth = new BluetoothPbapObexAuthenticator(this); 126 mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext()); 127 mAccount = 128 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type)); 129 } 130 131 /** 132 * Constructs PCEConnectionHandler object 133 * 134 * @param Builder To build BluetoothPbapClientHandler Instance. 135 */ 136 PbapClientConnectionHandler(Builder pceHandlerbuild) { 137 super(pceHandlerbuild.mLooper); 138 mAdapter = BluetoothAdapter.getDefaultAdapter(); 139 mDevice = pceHandlerbuild.mDevice; 140 mContext = pceHandlerbuild.mContext; 141 mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine; 142 mAuth = new BluetoothPbapObexAuthenticator(this); 143 mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext()); 144 mAccount = 145 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type)); 146 } 147 148 public static class Builder { 149 150 private Looper mLooper; 151 private Context mContext; 152 private BluetoothDevice mDevice; 153 private PbapClientStateMachine mClientStateMachine; 154 155 public Builder setLooper(Looper loop) { 156 this.mLooper = loop; 157 return this; 158 } 159 160 public Builder setClientSM(PbapClientStateMachine clientStateMachine) { 161 this.mClientStateMachine = clientStateMachine; 162 return this; 163 } 164 165 public Builder setRemoteDevice(BluetoothDevice device) { 166 this.mDevice = device; 167 return this; 168 } 169 170 public Builder setContext(Context context) { 171 this.mContext = context; 172 return this; 173 } 174 175 public PbapClientConnectionHandler build() { 176 PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this); 177 return pbapClientHandler; 178 } 179 180 } 181 182 @Override 183 public void handleMessage(Message msg) { 184 if (DBG) { 185 Log.d(TAG, "Handling Message = " + msg.what); 186 } 187 switch (msg.what) { 188 case MSG_CONNECT: 189 mPseRec = (SdpPseRecord) msg.obj; 190 /* To establish a connection, first open a socket and then create an OBEX session */ 191 if (connectSocket()) { 192 if (DBG) { 193 Log.d(TAG, "Socket connected"); 194 } 195 } else { 196 Log.w(TAG, "Socket CONNECT Failure "); 197 mPbapClientStateMachine.obtainMessage( 198 PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget(); 199 return; 200 } 201 202 if (connectObexSession()) { 203 mPbapClientStateMachine.obtainMessage( 204 PbapClientStateMachine.MSG_CONNECTION_COMPLETE).sendToTarget(); 205 } else { 206 mPbapClientStateMachine.obtainMessage( 207 PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget(); 208 } 209 break; 210 211 case MSG_DISCONNECT: 212 if (DBG) { 213 Log.d(TAG, "Starting Disconnect"); 214 } 215 try { 216 if (mObexSession != null) { 217 if (DBG) { 218 Log.d(TAG, "obexSessionDisconnect" + mObexSession); 219 } 220 mObexSession.disconnect(null); 221 mObexSession.close(); 222 } 223 224 if (DBG) { 225 Log.d(TAG, "Closing Socket"); 226 } 227 closeSocket(); 228 } catch (IOException e) { 229 Log.w(TAG, "DISCONNECT Failure ", e); 230 } 231 if (DBG) { 232 Log.d(TAG, "Completing Disconnect"); 233 } 234 removeAccount(mAccount); 235 removeCallLog(mAccount); 236 237 mPbapClientStateMachine.obtainMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED) 238 .sendToTarget(); 239 break; 240 241 case MSG_DOWNLOAD: 242 try { 243 mAccountCreated = addAccount(mAccount); 244 if (!mAccountCreated) { 245 Log.e(TAG, "Account creation failed."); 246 return; 247 } 248 // Start at contact 1 to exclued Owner Card PBAP 1.1 sec 3.1.5.2 249 BluetoothPbapRequestPullPhoneBook request = 250 new BluetoothPbapRequestPullPhoneBook(PB_PATH, mAccount, 251 PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, 0, 1); 252 request.execute(mObexSession); 253 PhonebookPullRequest processor = 254 new PhonebookPullRequest(mPbapClientStateMachine.getContext(), 255 mAccount); 256 processor.setResults(request.getList()); 257 processor.onPullComplete(); 258 HashMap<String, Integer> callCounter = new HashMap<>(); 259 downloadCallLog(MCH_PATH, callCounter); 260 downloadCallLog(ICH_PATH, callCounter); 261 downloadCallLog(OCH_PATH, callCounter); 262 } catch (IOException e) { 263 Log.w(TAG, "DOWNLOAD_CONTACTS Failure" + e.toString()); 264 } 265 break; 266 267 default: 268 Log.w(TAG, "Received Unexpected Message"); 269 } 270 return; 271 } 272 273 /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified 274 * channel, or RFCOMM default channel. */ 275 private boolean connectSocket() { 276 try { 277 /* Use BluetoothSocket to connect */ 278 if (mPseRec == null) { 279 // BackWardCompatability: Fall back to create RFCOMM through UUID. 280 Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid()); 281 mSocket = 282 mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid()); 283 } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) { 284 Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm()); 285 mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm()); 286 } else { 287 Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber()); 288 mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber()); 289 } 290 291 if (mSocket != null) { 292 mSocket.connect(); 293 return true; 294 } else { 295 Log.w(TAG, "Could not create socket"); 296 } 297 } catch (IOException e) { 298 Log.e(TAG, "Error while connecting socket", e); 299 } 300 return false; 301 } 302 303 /* Connect an OBEX session over the already connected socket. First establish an OBEX Transport 304 * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */ 305 private boolean connectObexSession() { 306 boolean connectionSuccessful = false; 307 308 try { 309 if (DBG) { 310 Log.v(TAG, "Start Obex Client Session"); 311 } 312 BluetoothObexTransport transport = new BluetoothObexTransport(mSocket); 313 mObexSession = new ClientSession(transport); 314 mObexSession.setAuthenticator(mAuth); 315 316 HeaderSet connectionRequest = new HeaderSet(); 317 connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET); 318 319 if (mPseRec != null) { 320 if (DBG) { 321 Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures()); 322 } 323 324 ObexAppParameters oap = new ObexAppParameters(); 325 326 if (mPseRec.getProfileVersion() >= PBAP_V1_2) { 327 oap.add(BluetoothPbapRequest.OAP_TAGID_PBAP_SUPPORTED_FEATURES, 328 PBAP_SUPPORTED_FEATURE); 329 } 330 331 oap.addToHeaderSet(connectionRequest); 332 } 333 HeaderSet connectionResponse = mObexSession.connect(connectionRequest); 334 335 connectionSuccessful = 336 (connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK); 337 if (DBG) { 338 Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful)); 339 } 340 } catch (IOException e) { 341 Log.w(TAG, "CONNECT Failure " + e.toString()); 342 closeSocket(); 343 } 344 return connectionSuccessful; 345 } 346 347 public void abort() { 348 // Perform forced cleanup, it is ok if the handler throws an exception this will free the 349 // handler to complete what it is doing and finish with cleanup. 350 closeSocket(); 351 this.getLooper().getThread().interrupt(); 352 } 353 354 private void closeSocket() { 355 try { 356 if (mSocket != null) { 357 if (DBG) { 358 Log.d(TAG, "Closing socket" + mSocket); 359 } 360 mSocket.close(); 361 mSocket = null; 362 } 363 } catch (IOException e) { 364 Log.e(TAG, "Error when closing socket", e); 365 mSocket = null; 366 } 367 } 368 369 void downloadCallLog(String path, HashMap<String, Integer> callCounter) { 370 try { 371 BluetoothPbapRequestPullPhoneBook request = 372 new BluetoothPbapRequestPullPhoneBook(path, mAccount, 0, VCARD_TYPE_30, 0, 0); 373 request.execute(mObexSession); 374 CallLogPullRequest processor = 375 new CallLogPullRequest(mPbapClientStateMachine.getContext(), path, 376 callCounter, mAccount); 377 processor.setResults(request.getList()); 378 processor.onPullComplete(); 379 } catch (IOException e) { 380 Log.w(TAG, "Download call log failure"); 381 } 382 } 383 384 private boolean addAccount(Account account) { 385 if (mAccountManager.addAccountExplicitly(account, null, null)) { 386 if (DBG) { 387 Log.d(TAG, "Added account " + mAccount); 388 } 389 return true; 390 } 391 return false; 392 } 393 394 private void removeAccount(Account account) { 395 if (mAccountManager.removeAccountExplicitly(account)) { 396 if (DBG) { 397 Log.d(TAG, "Removed account " + account); 398 } 399 } else { 400 Log.e(TAG, "Failed to remove account " + mAccount); 401 } 402 } 403 404 private void removeCallLog(Account account) { 405 try { 406 // need to check call table is exist ? 407 if (mContext.getContentResolver() == null) { 408 if (DBG) { 409 Log.d(TAG, "CallLog ContentResolver is not found"); 410 } 411 return; 412 } 413 String where = Calls.PHONE_ACCOUNT_ID + "=" + account.hashCode(); 414 mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, where, null); 415 } catch (IllegalArgumentException e) { 416 Log.d(TAG, "Call Logs could not be deleted, they may not exist yet."); 417 } 418 } 419 } 420