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 import com.android.vcard.VCardEntry; 36 37 import java.io.IOException; 38 import java.util.ArrayList; 39 import java.util.HashMap; 40 41 import javax.obex.ClientSession; 42 import javax.obex.HeaderSet; 43 import javax.obex.ResponseCodes; 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 private static final int L2CAP_INVALID_PSM = -1; 107 108 public static final String PB_PATH = "telecom/pb.vcf"; 109 public static final String FAV_PATH = "telecom/fav.vcf"; 110 public static final String MCH_PATH = "telecom/mch.vcf"; 111 public static final String ICH_PATH = "telecom/ich.vcf"; 112 public static final String OCH_PATH = "telecom/och.vcf"; 113 public static final String SIM_PB_PATH = "SIM1/telecom/pb.vcf"; 114 public static final String SIM_MCH_PATH = "SIM1/telecom/mch.vcf"; 115 public static final String SIM_ICH_PATH = "SIM1/telecom/ich.vcf"; 116 public static final String SIM_OCH_PATH = "SIM1/telecom/och.vcf"; 117 118 // PBAP v1.2.3 Sec. 7.1.2 119 private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0; 120 private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1; 121 private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3; 122 123 public static final int PBAP_V1_2 = 0x0102; 124 public static final byte VCARD_TYPE_21 = 0; 125 public static final byte VCARD_TYPE_30 = 1; 126 127 private Account mAccount; 128 private AccountManager mAccountManager; 129 private BluetoothSocket mSocket; 130 private final BluetoothAdapter mAdapter; 131 private final BluetoothDevice mDevice; 132 // PSE SDP Record for current device. 133 private SdpPseRecord mPseRec = null; 134 private ClientSession mObexSession; 135 private Context mContext; 136 private BluetoothPbapObexAuthenticator mAuth = null; 137 private final PbapClientStateMachine mPbapClientStateMachine; 138 private boolean mAccountCreated; 139 PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine, BluetoothDevice device)140 PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine, 141 BluetoothDevice device) { 142 super(looper); 143 mAdapter = BluetoothAdapter.getDefaultAdapter(); 144 mDevice = device; 145 mContext = context; 146 mPbapClientStateMachine = stateMachine; 147 mAuth = new BluetoothPbapObexAuthenticator(this); 148 mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext()); 149 mAccount = 150 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type)); 151 } 152 153 /** 154 * Constructs PCEConnectionHandler object 155 * 156 * @param Builder To build BluetoothPbapClientHandler Instance. 157 */ PbapClientConnectionHandler(Builder pceHandlerbuild)158 PbapClientConnectionHandler(Builder pceHandlerbuild) { 159 super(pceHandlerbuild.mLooper); 160 mAdapter = BluetoothAdapter.getDefaultAdapter(); 161 mDevice = pceHandlerbuild.mDevice; 162 mContext = pceHandlerbuild.mContext; 163 mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine; 164 mAuth = new BluetoothPbapObexAuthenticator(this); 165 mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext()); 166 mAccount = 167 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type)); 168 } 169 170 public static class Builder { 171 172 private Looper mLooper; 173 private Context mContext; 174 private BluetoothDevice mDevice; 175 private PbapClientStateMachine mClientStateMachine; 176 setLooper(Looper loop)177 public Builder setLooper(Looper loop) { 178 this.mLooper = loop; 179 return this; 180 } 181 setClientSM(PbapClientStateMachine clientStateMachine)182 public Builder setClientSM(PbapClientStateMachine clientStateMachine) { 183 this.mClientStateMachine = clientStateMachine; 184 return this; 185 } 186 setRemoteDevice(BluetoothDevice device)187 public Builder setRemoteDevice(BluetoothDevice device) { 188 this.mDevice = device; 189 return this; 190 } 191 setContext(Context context)192 public Builder setContext(Context context) { 193 this.mContext = context; 194 return this; 195 } 196 build()197 public PbapClientConnectionHandler build() { 198 PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this); 199 return pbapClientHandler; 200 } 201 202 } 203 204 @Override handleMessage(Message msg)205 public void handleMessage(Message msg) { 206 if (DBG) { 207 Log.d(TAG, "Handling Message = " + msg.what); 208 } 209 switch (msg.what) { 210 case MSG_CONNECT: 211 mPseRec = (SdpPseRecord) msg.obj; 212 /* To establish a connection, first open a socket and then create an OBEX session */ 213 if (connectSocket()) { 214 if (DBG) { 215 Log.d(TAG, "Socket connected"); 216 } 217 } else { 218 Log.w(TAG, "Socket CONNECT Failure "); 219 mPbapClientStateMachine.sendMessage( 220 PbapClientStateMachine.MSG_CONNECTION_FAILED); 221 return; 222 } 223 224 if (connectObexSession()) { 225 mPbapClientStateMachine.sendMessage( 226 PbapClientStateMachine.MSG_CONNECTION_COMPLETE); 227 } else { 228 mPbapClientStateMachine.sendMessage( 229 PbapClientStateMachine.MSG_CONNECTION_FAILED); 230 } 231 break; 232 233 case MSG_DISCONNECT: 234 if (DBG) { 235 Log.d(TAG, "Starting Disconnect"); 236 } 237 try { 238 if (mObexSession != null) { 239 if (DBG) { 240 Log.d(TAG, "obexSessionDisconnect" + mObexSession); 241 } 242 mObexSession.disconnect(null); 243 mObexSession.close(); 244 } 245 246 if (DBG) { 247 Log.d(TAG, "Closing Socket"); 248 } 249 closeSocket(); 250 } catch (IOException e) { 251 Log.w(TAG, "DISCONNECT Failure ", e); 252 } 253 if (DBG) { 254 Log.d(TAG, "Completing Disconnect"); 255 } 256 removeAccount(mAccount); 257 removeCallLog(mAccount); 258 259 mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED); 260 break; 261 262 case MSG_DOWNLOAD: 263 mAccountCreated = addAccount(mAccount); 264 if (!mAccountCreated) { 265 Log.e(TAG, "Account creation failed."); 266 return; 267 } 268 if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) { 269 downloadContacts(FAV_PATH); 270 } 271 if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) { 272 downloadContacts(PB_PATH); 273 } 274 if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) { 275 downloadContacts(SIM_PB_PATH); 276 } 277 278 HashMap<String, Integer> callCounter = new HashMap<>(); 279 downloadCallLog(MCH_PATH, callCounter); 280 downloadCallLog(ICH_PATH, callCounter); 281 downloadCallLog(OCH_PATH, callCounter); 282 break; 283 284 default: 285 Log.w(TAG, "Received Unexpected Message"); 286 } 287 return; 288 } 289 290 /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified 291 * channel, or RFCOMM default channel. */ connectSocket()292 private synchronized boolean connectSocket() { 293 try { 294 /* Use BluetoothSocket to connect */ 295 if (mPseRec == null) { 296 // BackWardCompatability: Fall back to create RFCOMM through UUID. 297 if (VDBG) Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid()); 298 mSocket = 299 mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid()); 300 } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) { 301 if (VDBG) Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm()); 302 mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm()); 303 } else { 304 if (VDBG) Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber()); 305 mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber()); 306 } 307 308 if (mSocket != null) { 309 mSocket.connect(); 310 return true; 311 } else { 312 Log.w(TAG, "Could not create socket"); 313 } 314 } catch (IOException e) { 315 Log.e(TAG, "Error while connecting socket", e); 316 } 317 return false; 318 } 319 320 /* Connect an OBEX session over the already connected socket. First establish an OBEX Transport 321 * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */ connectObexSession()322 private boolean connectObexSession() { 323 boolean connectionSuccessful = false; 324 325 try { 326 if (VDBG) { 327 Log.v(TAG, "Start Obex Client Session"); 328 } 329 BluetoothObexTransport transport = new BluetoothObexTransport(mSocket); 330 mObexSession = new ClientSession(transport); 331 mObexSession.setAuthenticator(mAuth); 332 333 HeaderSet connectionRequest = new HeaderSet(); 334 connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET); 335 336 if (mPseRec != null) { 337 if (DBG) { 338 Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures()); 339 } 340 341 ObexAppParameters oap = new ObexAppParameters(); 342 343 if (mPseRec.getProfileVersion() >= PBAP_V1_2) { 344 oap.add(BluetoothPbapRequest.OAP_TAGID_PBAP_SUPPORTED_FEATURES, 345 PBAP_SUPPORTED_FEATURE); 346 } 347 348 oap.addToHeaderSet(connectionRequest); 349 } 350 HeaderSet connectionResponse = mObexSession.connect(connectionRequest); 351 352 connectionSuccessful = 353 (connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK); 354 if (DBG) { 355 Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful)); 356 } 357 } catch (IOException | NullPointerException e) { 358 // Will get NPE if a null mSocket is passed to BluetoothObexTransport. 359 // mSocket can be set to null if an abort() --> closeSocket() was called between 360 // the calls to connectSocket() and connectObexSession(). 361 Log.w(TAG, "CONNECT Failure " + e.toString()); 362 closeSocket(); 363 } 364 return connectionSuccessful; 365 } 366 abort()367 public void abort() { 368 // Perform forced cleanup, it is ok if the handler throws an exception this will free the 369 // handler to complete what it is doing and finish with cleanup. 370 closeSocket(); 371 this.getLooper().getThread().interrupt(); 372 } 373 closeSocket()374 private synchronized void closeSocket() { 375 try { 376 if (mSocket != null) { 377 if (DBG) { 378 Log.d(TAG, "Closing socket" + mSocket); 379 } 380 mSocket.close(); 381 mSocket = null; 382 } 383 } catch (IOException e) { 384 Log.e(TAG, "Error when closing socket", e); 385 mSocket = null; 386 } 387 } 388 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 downloadCallLog(String path, HashMap<String, Integer> callCounter)442 void downloadCallLog(String path, HashMap<String, Integer> callCounter) { 443 try { 444 BluetoothPbapRequestPullPhoneBook request = 445 new BluetoothPbapRequestPullPhoneBook(path, mAccount, 0, VCARD_TYPE_30, 0, 0); 446 request.execute(mObexSession); 447 CallLogPullRequest processor = 448 new CallLogPullRequest(mPbapClientStateMachine.getContext(), path, 449 callCounter, mAccount); 450 processor.setResults(request.getList()); 451 processor.onPullComplete(); 452 } catch (IOException e) { 453 Log.w(TAG, "Download call log failure"); 454 } 455 } 456 addAccount(Account account)457 private boolean addAccount(Account account) { 458 if (mAccountManager.addAccountExplicitly(account, null, null)) { 459 if (DBG) { 460 Log.d(TAG, "Added account " + mAccount); 461 } 462 return true; 463 } 464 return false; 465 } 466 removeAccount(Account account)467 private void removeAccount(Account account) { 468 if (mAccountManager.removeAccountExplicitly(account)) { 469 if (DBG) { 470 Log.d(TAG, "Removed account " + account); 471 } 472 } else { 473 Log.e(TAG, "Failed to remove account " + mAccount); 474 } 475 } 476 removeCallLog(Account account)477 private void removeCallLog(Account account) { 478 try { 479 // need to check call table is exist ? 480 if (mContext.getContentResolver() == null) { 481 if (DBG) { 482 Log.d(TAG, "CallLog ContentResolver is not found"); 483 } 484 return; 485 } 486 mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, 487 Calls.PHONE_ACCOUNT_ID + "=?", new String[]{mAccount.name}); 488 } catch (IllegalArgumentException e) { 489 Log.d(TAG, "Call Logs could not be deleted, they may not exist yet."); 490 } 491 } 492 isRepositorySupported(int mask)493 private boolean isRepositorySupported(int mask) { 494 if (mPseRec == null) { 495 if (VDBG) Log.v(TAG, "No PBAP Server SDP Record"); 496 return false; 497 } 498 return (mask & mPseRec.getSupportedRepositories()) != 0; 499 } 500 } 501