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