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.BluetoothDevice; 21 import android.bluetooth.BluetoothSocket; 22 import android.bluetooth.BluetoothUuid; 23 import android.bluetooth.SdpPseRecord; 24 import android.content.Context; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.provider.CallLog; 29 import android.provider.CallLog.Calls; 30 import android.util.Log; 31 32 import com.android.bluetooth.BluetoothObexTransport; 33 import com.android.bluetooth.ObexAppParameters; 34 import com.android.bluetooth.R; 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.obex.ClientSession; 37 import com.android.obex.HeaderSet; 38 import com.android.obex.ResponseCodes; 39 import com.android.vcard.VCardEntry; 40 41 import java.io.IOException; 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 45 /* Bluetooth/pbapclient/PbapClientConnectionHandler is responsible 46 * for connecting, disconnecting and downloading contacts from the 47 * PBAP PSE when commanded. It receives all direction from the 48 * controlling state machine. 49 */ 50 class PbapClientConnectionHandler extends Handler { 51 // Tradeoff: larger BATCH_SIZE leads to faster download rates, while smaller 52 // BATCH_SIZE is less prone to IO Exceptions if there is a download in 53 // progress when Bluetooth stack is torn down. 54 private static final int DEFAULT_BATCH_SIZE = 250; 55 56 // Upper limit on the indices of the vcf cards/entries, inclusive, 57 // i.e., valid indices are [0, 1, ... , UPPER_LIMIT] 58 private static final int UPPER_LIMIT = 65535; 59 60 static final String TAG = "PbapClientConnHandler"; 61 static final boolean DBG = Utils.DBG; 62 static final boolean VDBG = Utils.VDBG; 63 static final int MSG_CONNECT = 1; 64 static final int MSG_DISCONNECT = 2; 65 static final int MSG_DOWNLOAD = 3; 66 67 // The following constants are pulled from the Bluetooth Phone Book Access Profile specification 68 // 1.1 69 private static final byte[] PBAP_TARGET = new byte[]{ 70 0x79, 71 0x61, 72 0x35, 73 (byte) 0xf0, 74 (byte) 0xf0, 75 (byte) 0xc5, 76 0x11, 77 (byte) 0xd8, 78 0x09, 79 0x66, 80 0x08, 81 0x00, 82 0x20, 83 0x0c, 84 (byte) 0x9a, 85 0x66 86 }; 87 88 private static final int PBAP_FEATURE_DEFAULT_IMAGE_FORMAT = 0x00000200; 89 private static final int PBAP_FEATURE_BROWSING = 0x00000002; 90 private static final int PBAP_FEATURE_DOWNLOADING = 0x00000001; 91 92 private static final long PBAP_FILTER_VERSION = 1 << 0; 93 private static final long PBAP_FILTER_FN = 1 << 1; 94 private static final long PBAP_FILTER_N = 1 << 2; 95 private static final long PBAP_FILTER_PHOTO = 1 << 3; 96 private static final long PBAP_FILTER_ADR = 1 << 5; 97 private static final long PBAP_FILTER_TEL = 1 << 7; 98 private static final long PBAP_FILTER_EMAIL = 1 << 8; 99 private static final long PBAP_FILTER_NICKNAME = 1 << 23; 100 101 private static final int PBAP_SUPPORTED_FEATURE = 102 PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_DOWNLOADING; 103 private static final long PBAP_REQUESTED_FIELDS = 104 PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO 105 | PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME; 106 107 @VisibleForTesting 108 static final int L2CAP_INVALID_PSM = -1; 109 110 public static final String PB_PATH = "telecom/pb.vcf"; 111 public static final String FAV_PATH = "telecom/fav.vcf"; 112 public static final String MCH_PATH = "telecom/mch.vcf"; 113 public static final String ICH_PATH = "telecom/ich.vcf"; 114 public static final String OCH_PATH = "telecom/och.vcf"; 115 public static final String SIM_PB_PATH = "SIM1/telecom/pb.vcf"; 116 public static final String SIM_MCH_PATH = "SIM1/telecom/mch.vcf"; 117 public static final String SIM_ICH_PATH = "SIM1/telecom/ich.vcf"; 118 public static final String SIM_OCH_PATH = "SIM1/telecom/och.vcf"; 119 120 // PBAP v1.2.3 Sec. 7.1.2 121 private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0; 122 private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1; 123 private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3; 124 125 public static final int PBAP_V1_2 = 0x0102; 126 public static final byte VCARD_TYPE_21 = 0; 127 public static final byte VCARD_TYPE_30 = 1; 128 129 private Account mAccount; 130 private AccountManager mAccountManager; 131 private BluetoothSocket mSocket; 132 private final BluetoothDevice mDevice; 133 // PSE SDP Record for current device. 134 private SdpPseRecord mPseRec = null; 135 private ClientSession mObexSession; 136 private Context mContext; 137 private BluetoothPbapObexAuthenticator mAuth = null; 138 private final PbapClientStateMachine mPbapClientStateMachine; 139 private boolean mAccountCreated; 140 141 /** 142 * Constructs PCEConnectionHandler object 143 * 144 * @param Builder To build BluetoothPbapClientHandler Instance. 145 */ PbapClientConnectionHandler(Builder pceHandlerbuild)146 PbapClientConnectionHandler(Builder pceHandlerbuild) { 147 super(pceHandlerbuild.mLooper); 148 mDevice = pceHandlerbuild.mDevice; 149 mContext = pceHandlerbuild.mContext; 150 mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine; 151 mAuth = new BluetoothPbapObexAuthenticator(this); 152 mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext()); 153 mAccount = 154 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type)); 155 } 156 157 public static class Builder { 158 159 private Looper mLooper; 160 private Context mContext; 161 private BluetoothDevice mDevice; 162 private PbapClientStateMachine mClientStateMachine; 163 setLooper(Looper loop)164 public Builder setLooper(Looper loop) { 165 this.mLooper = loop; 166 return this; 167 } 168 setClientSM(PbapClientStateMachine clientStateMachine)169 public Builder setClientSM(PbapClientStateMachine clientStateMachine) { 170 this.mClientStateMachine = clientStateMachine; 171 return this; 172 } 173 setRemoteDevice(BluetoothDevice device)174 public Builder setRemoteDevice(BluetoothDevice device) { 175 this.mDevice = device; 176 return this; 177 } 178 setContext(Context context)179 public Builder setContext(Context context) { 180 this.mContext = context; 181 return this; 182 } 183 build()184 public PbapClientConnectionHandler build() { 185 PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this); 186 return pbapClientHandler; 187 } 188 189 } 190 191 @Override handleMessage(Message msg)192 public void handleMessage(Message msg) { 193 if (DBG) { 194 Log.d(TAG, "Handling Message = " + msg.what); 195 } 196 switch (msg.what) { 197 case MSG_CONNECT: 198 mPseRec = (SdpPseRecord) msg.obj; 199 /* To establish a connection, first open a socket and then create an OBEX session */ 200 if (connectSocket()) { 201 if (DBG) { 202 Log.d(TAG, "Socket connected"); 203 } 204 } else { 205 Log.w(TAG, "Socket CONNECT Failure "); 206 mPbapClientStateMachine.sendMessage( 207 PbapClientStateMachine.MSG_CONNECTION_FAILED); 208 return; 209 } 210 211 if (connectObexSession()) { 212 mPbapClientStateMachine.sendMessage( 213 PbapClientStateMachine.MSG_CONNECTION_COMPLETE); 214 } else { 215 mPbapClientStateMachine.sendMessage( 216 PbapClientStateMachine.MSG_CONNECTION_FAILED); 217 } 218 break; 219 220 case MSG_DISCONNECT: 221 if (DBG) { 222 Log.d(TAG, "Starting Disconnect"); 223 } 224 try { 225 if (mObexSession != null) { 226 if (DBG) { 227 Log.d(TAG, "obexSessionDisconnect" + mObexSession); 228 } 229 mObexSession.disconnect(null); 230 mObexSession.close(); 231 } 232 233 if (DBG) { 234 Log.d(TAG, "Closing Socket"); 235 } 236 closeSocket(); 237 } catch (IOException e) { 238 Log.w(TAG, "DISCONNECT Failure ", e); 239 } 240 if (DBG) { 241 Log.d(TAG, "Completing Disconnect"); 242 } 243 removeAccount(); 244 removeCallLog(); 245 246 mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED); 247 break; 248 249 case MSG_DOWNLOAD: 250 mAccountCreated = addAccount(); 251 if (!mAccountCreated) { 252 Log.e(TAG, "Account creation failed."); 253 return; 254 } 255 if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) { 256 downloadContacts(FAV_PATH); 257 } 258 if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) { 259 downloadContacts(PB_PATH); 260 } 261 if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) { 262 downloadContacts(SIM_PB_PATH); 263 } 264 265 HashMap<String, Integer> callCounter = new HashMap<>(); 266 downloadCallLog(MCH_PATH, callCounter); 267 downloadCallLog(ICH_PATH, callCounter); 268 downloadCallLog(OCH_PATH, callCounter); 269 break; 270 271 default: 272 Log.w(TAG, "Received Unexpected Message"); 273 } 274 return; 275 } 276 277 @VisibleForTesting setPseRecord(SdpPseRecord record)278 synchronized void setPseRecord(SdpPseRecord record) { 279 mPseRec = record; 280 } 281 282 @VisibleForTesting getSocket()283 synchronized BluetoothSocket getSocket() { 284 return mSocket; 285 } 286 287 /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified 288 * channel, or RFCOMM default channel. */ 289 @VisibleForTesting connectSocket()290 synchronized boolean connectSocket() { 291 try { 292 /* Use BluetoothSocket to connect */ 293 if (mPseRec == null) { 294 // BackWardCompatability: Fall back to create RFCOMM through UUID. 295 if (VDBG) Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid()); 296 mSocket = 297 mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid()); 298 } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) { 299 if (VDBG) Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm()); 300 mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm()); 301 } else { 302 if (VDBG) Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber()); 303 mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber()); 304 } 305 306 if (mSocket != null) { 307 mSocket.connect(); 308 return true; 309 } else { 310 Log.w(TAG, "Could not create socket"); 311 } 312 } catch (IOException e) { 313 Log.e(TAG, "Error while connecting socket", e); 314 } 315 return false; 316 } 317 318 /* Connect an OBEX session over the already connected socket. First establish an OBEX Transport 319 * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */ 320 @VisibleForTesting connectObexSession()321 boolean connectObexSession() { 322 boolean connectionSuccessful = false; 323 324 try { 325 if (VDBG) { 326 Log.v(TAG, "Start Obex Client Session"); 327 } 328 BluetoothObexTransport transport = new BluetoothObexTransport(mSocket); 329 mObexSession = new ClientSession(transport); 330 mObexSession.setAuthenticator(mAuth); 331 332 HeaderSet connectionRequest = new HeaderSet(); 333 connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET); 334 335 if (mPseRec != null) { 336 if (DBG) { 337 Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures()); 338 } 339 340 ObexAppParameters oap = new ObexAppParameters(); 341 342 if (mPseRec.getProfileVersion() >= PBAP_V1_2) { 343 oap.add(BluetoothPbapRequest.OAP_TAGID_PBAP_SUPPORTED_FEATURES, 344 PBAP_SUPPORTED_FEATURE); 345 } 346 347 oap.addToHeaderSet(connectionRequest); 348 } 349 HeaderSet connectionResponse = mObexSession.connect(connectionRequest); 350 351 connectionSuccessful = 352 (connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK); 353 if (DBG) { 354 Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful)); 355 } 356 } catch (IOException | NullPointerException e) { 357 // Will get NPE if a null mSocket is passed to BluetoothObexTransport. 358 // mSocket can be set to null if an abort() --> closeSocket() was called between 359 // the calls to connectSocket() and connectObexSession(). 360 Log.w(TAG, "CONNECT Failure ", e); 361 closeSocket(); 362 } 363 return connectionSuccessful; 364 } 365 abort()366 void abort() { 367 // Perform forced cleanup, it is ok if the handler throws an exception this will free the 368 // handler to complete what it is doing and finish with cleanup. 369 closeSocket(); 370 this.getLooper().getThread().interrupt(); 371 } 372 closeSocket()373 private synchronized void closeSocket() { 374 try { 375 if (mSocket != null) { 376 if (DBG) { 377 Log.d(TAG, "Closing socket" + mSocket); 378 } 379 mSocket.close(); 380 mSocket = null; 381 } 382 } catch (IOException e) { 383 Log.e(TAG, "Error when closing socket", e); 384 mSocket = null; 385 } 386 } 387 388 @VisibleForTesting downloadContacts(String path)389 void downloadContacts(String path) { 390 try { 391 PhonebookPullRequest processor = 392 new PhonebookPullRequest(mPbapClientStateMachine.getContext(), 393 mAccount); 394 395 // Download contacts in batches of size DEFAULT_BATCH_SIZE 396 BluetoothPbapRequestPullPhoneBookSize requestPbSize = 397 new BluetoothPbapRequestPullPhoneBookSize(path, 398 PBAP_REQUESTED_FIELDS); 399 requestPbSize.execute(mObexSession); 400 401 int numberOfContactsRemaining = requestPbSize.getSize(); 402 int startOffset = 0; 403 if (PB_PATH.equals(path)) { 404 // PBAP v1.2.3, Sec 3.1.5. The first contact in pb is owner card 0.vcf, which we 405 // do not want to download. The other phonebook objects (e.g., fav) don't have an 406 // owner card, so they don't need an offset. 407 startOffset = 1; 408 // "-1" because Owner Card 0.vcf is also included in /pb, but not in /fav. 409 numberOfContactsRemaining -= 1; 410 } 411 412 while ((numberOfContactsRemaining > 0) && (startOffset <= UPPER_LIMIT)) { 413 int numberOfContactsToDownload = 414 Math.min(Math.min(DEFAULT_BATCH_SIZE, numberOfContactsRemaining), 415 UPPER_LIMIT - startOffset + 1); 416 BluetoothPbapRequestPullPhoneBook request = 417 new BluetoothPbapRequestPullPhoneBook(path, mAccount, 418 PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, 419 numberOfContactsToDownload, startOffset); 420 request.execute(mObexSession); 421 ArrayList<VCardEntry> vcards = request.getList(); 422 if (path == FAV_PATH) { 423 // mark each vcard as a favorite 424 for (VCardEntry v : vcards) { 425 v.setStarred(true); 426 } 427 } 428 processor.setResults(vcards); 429 processor.onPullComplete(); 430 431 startOffset += numberOfContactsToDownload; 432 numberOfContactsRemaining -= numberOfContactsToDownload; 433 } 434 if ((startOffset > UPPER_LIMIT) && (numberOfContactsRemaining > 0)) { 435 Log.w(TAG, "Download contacts incomplete, index exceeded upper limit."); 436 } 437 } catch (IOException e) { 438 Log.w(TAG, "Download contacts failure" + e.toString()); 439 } 440 } 441 442 @VisibleForTesting downloadCallLog(String path, HashMap<String, Integer> callCounter)443 void downloadCallLog(String path, HashMap<String, Integer> callCounter) { 444 try { 445 BluetoothPbapRequestPullPhoneBook request = 446 new BluetoothPbapRequestPullPhoneBook(path, mAccount, 0, VCARD_TYPE_30, 0, 0); 447 request.execute(mObexSession); 448 CallLogPullRequest processor = 449 new CallLogPullRequest(mPbapClientStateMachine.getContext(), path, 450 callCounter, mAccount); 451 processor.setResults(request.getList()); 452 processor.onPullComplete(); 453 } catch (IOException e) { 454 Log.w(TAG, "Download call log failure"); 455 } 456 } 457 458 @VisibleForTesting addAccount()459 boolean addAccount() { 460 if (mAccountManager.addAccountExplicitly(mAccount, null, null)) { 461 if (DBG) { 462 Log.d(TAG, "Added account " + mAccount); 463 } 464 return true; 465 } 466 return false; 467 } 468 469 @VisibleForTesting removeAccount()470 void removeAccount() { 471 if (mAccountManager.removeAccountExplicitly(mAccount)) { 472 if (DBG) { 473 Log.d(TAG, "Removed account " + mAccount); 474 } 475 } else { 476 Log.e(TAG, "Failed to remove account " + mAccount); 477 } 478 } 479 480 @VisibleForTesting removeCallLog()481 void removeCallLog() { 482 try { 483 // need to check call table is exist ? 484 if (mContext.getContentResolver() == null) { 485 if (DBG) { 486 Log.d(TAG, "CallLog ContentResolver is not found"); 487 } 488 return; 489 } 490 mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, 491 Calls.PHONE_ACCOUNT_ID + "=?", new String[]{mAccount.name}); 492 } catch (IllegalArgumentException e) { 493 Log.d(TAG, "Call Logs could not be deleted, they may not exist yet."); 494 } 495 } 496 497 @VisibleForTesting isRepositorySupported(int mask)498 boolean isRepositorySupported(int mask) { 499 if (mPseRec == null) { 500 if (VDBG) Log.v(TAG, "No PBAP Server SDP Record"); 501 return false; 502 } 503 return (mask & mPseRec.getSupportedRepositories()) != 0; 504 } 505 } 506