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