1 /* 2 * Copyright (C) 2024 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 17 package com.android.bluetooth.pbapclient; 18 19 import static android.Manifest.permission.BLUETOOTH_CONNECT; 20 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; 21 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; 22 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING; 23 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED; 24 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING; 25 26 import android.accounts.Account; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothPbapClient; 29 import android.bluetooth.BluetoothProfile; 30 import android.bluetooth.BluetoothUuid; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.util.Log; 36 37 import com.android.bluetooth.Utils; 38 import com.android.bluetooth.btservice.AdapterService; 39 import com.android.bluetooth.btservice.MetricsLogger; 40 import com.android.bluetooth.btservice.ProfileService; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.internal.util.State; 43 import com.android.internal.util.StateMachine; 44 import com.android.obex.ResponseCodes; 45 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Map; 50 51 /** 52 * This object represents a connection over PBAP with a given remote device. It manages the account, 53 * SDP Record and PBAP OBEX Client for the remote device. It also uses the OBEX client to make 54 * simple requests, driving the overall contact download process. 55 */ 56 class PbapClientStateMachine extends StateMachine { 57 private static final String TAG = PbapClientStateMachine.class.getSimpleName(); 58 59 // Messages for handling connect/disconnect requests. 60 private static final int MSG_CONNECT = 1; 61 private static final int MSG_DISCONNECT = 2; 62 private static final int MSG_SDP_COMPLETE = 3; 63 private static final int MSG_OBEX_CLIENT_CONNECTED = 4; 64 private static final int MSG_OBEX_CLIENT_DISCONNECTED = 5; 65 private static final int MSG_STORAGE_READY = 6; 66 private static final int MSG_ACCOUNT_ADDED = 7; 67 private static final int MSG_ACCOUNT_REMOVED = 8; 68 private static final int MSG_DOWNLOAD = 9; 69 private static final int MSG_PHONEBOOK_METADATA_RECEIVED = 10; 70 private static final int MSG_PHONEBOOK_CONTACTS_RECEIVED = 11; 71 72 // Messages for handling error conditions. 73 public static final int MSG_CONNECT_TIMEOUT = 12; 74 public static final int MSG_DISCONNECT_TIMEOUT = 13; 75 private static final int MSG_SDP_FAILED = 14; 76 77 // Configurable Timeouts 78 @VisibleForTesting static final int CONNECT_TIMEOUT_MS = 12000; 79 @VisibleForTesting static final int DISCONNECT_TIMEOUT_MS = 3000; 80 81 // Constants for SDP. Note that these values come from the native stack, but no centralized 82 // constants exist for them as part of the various SDP APIs. 83 private static final int SDP_SUCCESS = 0; 84 private static final int SDP_FAILED = 1; 85 private static final int SDP_BUSY = 2; 86 87 // Supported features of our OBEX client 88 private static final int LOCAL_SUPPORTED_FEATURES = 89 PbapSdpRecord.FEATURE_DOWNLOADING | PbapSdpRecord.FEATURE_DEFAULT_IMAGE_FORMAT; 90 91 // Default configuration for VCard format -> prefer 3.0 to 2.1 92 private static final byte DEFAULT_VCARD_VERSION = PbapPhonebook.FORMAT_VCARD_30; 93 94 // Default property filter for downloaded contacts 95 private static final long DEFAULT_PROPERTIES = 96 PbapApplicationParameters.PROPERTY_VERSION 97 | PbapApplicationParameters.PROPERTY_FN 98 | PbapApplicationParameters.PROPERTY_N 99 | PbapApplicationParameters.PROPERTY_PHOTO 100 | PbapApplicationParameters.PROPERTY_ADR 101 | PbapApplicationParameters.PROPERTY_TEL 102 | PbapApplicationParameters.PROPERTY_EMAIL 103 | PbapApplicationParameters.PROPERTY_NICKNAME; 104 105 // Our internal batch size when downloading contacts. Batches let us deliver contacts sooner to 106 // the UI and applications that want contacts, and make our individual download operations 107 // shorter running, but come with the trade off of a greater overall time to download. 108 private static final int CONTACT_DOWNLOAD_BATCH_SIZE = 250; 109 110 /** A Callback interface for object creators to get events related to this state machine */ 111 interface Callback { 112 /** 113 * Receive connection state changes for this state machines so you can know when to clean it 114 * up. 115 * 116 * @param oldState The old state of the device state machine 117 * @param newState The new state of the device state machine 118 */ onConnectionStateChanged(int oldState, int newState)119 void onConnectionStateChanged(int oldState, int newState); 120 } 121 122 /** 123 * Internal Phonebook object to help drive downloads with batching and record download process 124 * metrics. 125 */ 126 private static final class Phonebook { 127 private final String mName; 128 private PbapPhonebookMetadata mMetadata; 129 private int mNumDownloaded; 130 Phonebook(String name)131 Phonebook(String name) { 132 mName = name; 133 mMetadata = null; 134 mNumDownloaded = 0; 135 } 136 setMetadata(PbapPhonebookMetadata metadata)137 public void setMetadata(PbapPhonebookMetadata metadata) { 138 mMetadata = metadata; 139 } 140 onContactsDownloaded(int numDownloaded)141 public void onContactsDownloaded(int numDownloaded) { 142 mNumDownloaded += numDownloaded; 143 } 144 getTotalNumberOfContacts()145 public int getTotalNumberOfContacts() { 146 return (mMetadata == null || mMetadata.size() == PbapPhonebookMetadata.INVALID_SIZE) 147 ? 0 148 : mMetadata.size(); 149 } 150 getNumberOfContactsDownloaded()151 public int getNumberOfContactsDownloaded() { 152 return mNumDownloaded; 153 } 154 155 @Override 156 @SuppressWarnings("ReferenceEquality") // equals() doesn't work because the constant is null toString()157 public String toString() { 158 if (mMetadata == null) { 159 return mName 160 + " [" 161 + getNumberOfContactsDownloaded() 162 + "/ UNKNOWN] (db:UNKNOWN, pc:UNKNOWN, sc:UNKNOWN)"; 163 } 164 165 String databaseIdentifier = mMetadata.databaseIdentifier(); 166 if (databaseIdentifier == PbapPhonebookMetadata.INVALID_DATABASE_IDENTIFIER) { 167 databaseIdentifier = "UNKNOWN"; 168 } 169 170 String primaryVersionCounter = mMetadata.primaryVersionCounter(); 171 if (primaryVersionCounter == PbapPhonebookMetadata.INVALID_VERSION_COUNTER) { 172 primaryVersionCounter = "UNKNOWN"; 173 } 174 175 String secondaryVersionCounter = mMetadata.secondaryVersionCounter(); 176 if (secondaryVersionCounter == PbapPhonebookMetadata.INVALID_VERSION_COUNTER) { 177 secondaryVersionCounter = "UNKNOWN"; 178 } 179 180 String totalContactsExpected = "UNKNOWN"; 181 if (mMetadata.size() != PbapPhonebookMetadata.INVALID_SIZE) { 182 totalContactsExpected = Integer.toString(mMetadata.size()); 183 } 184 185 return mName 186 + " [" 187 + (getNumberOfContactsDownloaded() + "/" + totalContactsExpected) 188 + "] (" 189 + ("db:" + databaseIdentifier) 190 + (", pc:" + primaryVersionCounter) 191 + (", sc:" + secondaryVersionCounter) 192 + ")"; 193 } 194 } 195 196 private final BluetoothDevice mDevice; 197 private final Context mContext; 198 private PbapSdpRecord mSdpRecord = null; 199 private final Account mAccount; 200 private final Map<String, Phonebook> mPhonebooks = new HashMap<String, Phonebook>(); 201 private final PbapClientObexClient mObexClient; 202 private final PbapClientContactsStorage mContactsStorage; 203 204 private final PbapClientContactsStorage.Callback mStorageCallback = 205 new PbapClientContactsStorage.Callback() { 206 @Override 207 public void onStorageReady() { 208 onPbapClientStorageReady(); 209 } 210 211 @Override 212 public void onStorageAccountsChanged( 213 List<Account> oldAccounts, List<Account> newAccounts) { 214 boolean inOld = oldAccounts.contains(mAccount); 215 boolean inNew = newAccounts.contains(mAccount); 216 if (!inOld && inNew) { 217 Log.i(TAG, "Storage accounts changed, account added"); 218 onPbapClientAccountAdded(); 219 } else if (inOld && !inNew) { 220 Log.i(TAG, "Storage accounts changed, account removed"); 221 onPbapClientAccountRemoved(); 222 } else { 223 Log.i(TAG, "Storage accounts changed, but no impact to our account"); 224 } 225 } 226 }; 227 228 private int mCurrentState = STATE_DISCONNECTED; 229 private State mDisconnected; 230 private State mConnecting; 231 private State mConnected; 232 private State mDownloading; 233 private State mDisconnecting; 234 235 private final Callback mCallback; 236 PbapClientStateMachine( BluetoothDevice device, PbapClientContactsStorage storage, Context context, Callback callback)237 PbapClientStateMachine( 238 BluetoothDevice device, 239 PbapClientContactsStorage storage, 240 Context context, 241 Callback callback) { 242 super(TAG); 243 244 mDevice = device; 245 mContext = context; 246 mContactsStorage = storage; 247 mCallback = callback; 248 mAccount = mContactsStorage.getStorageAccountForDevice(mDevice); 249 mObexClient = 250 new PbapClientObexClient( 251 device, LOCAL_SUPPORTED_FEATURES, new PbapClientObexClientCallback()); 252 253 initializeStates(); 254 } 255 256 @VisibleForTesting PbapClientStateMachine( BluetoothDevice device, PbapClientContactsStorage storage, Context context, Looper looper, Callback callback, PbapClientObexClient obexClient)257 PbapClientStateMachine( 258 BluetoothDevice device, 259 PbapClientContactsStorage storage, 260 Context context, 261 Looper looper, 262 Callback callback, 263 PbapClientObexClient obexClient) { 264 super(TAG, looper); 265 266 mDevice = device; 267 mContext = context; 268 mContactsStorage = storage; 269 mCallback = callback; 270 mAccount = mContactsStorage.getStorageAccountForDevice(mDevice); 271 mObexClient = obexClient; 272 273 initializeStates(); 274 } 275 initializeStates()276 private void initializeStates() { 277 mDisconnected = new Disconnected(); 278 mConnecting = new Connecting(); 279 mDisconnecting = new Disconnecting(); 280 mConnected = new Connected(); 281 mDownloading = new Downloading(); 282 283 addState(mDisconnected); 284 addState(mConnecting); 285 addState(mDisconnecting); 286 addState(mConnected); 287 addState(mDownloading, mConnected); 288 289 setInitialState(mDisconnected); 290 } 291 292 /** Request to connect the device this state machine represents */ connect()293 public void connect() { 294 debug("connect requested"); 295 sendMessage(MSG_CONNECT); 296 } 297 298 /** Request to disconnect the device this state machine represents */ disconnect()299 public void disconnect() { 300 debug("disconnect requested"); 301 sendMessage(MSG_DISCONNECT); 302 } 303 304 /** Request to start the contacts download procedure */ download()305 private void download() { 306 sendMessage(MSG_DOWNLOAD); 307 } 308 309 /** Notify this device state machine of a newly received SDP record */ onSdpResultReceived(int status, PbapSdpRecord record)310 public void onSdpResultReceived(int status, PbapSdpRecord record) { 311 if (status != SDP_SUCCESS) { 312 sendMessage(MSG_SDP_FAILED, status); 313 } else { 314 sendMessage(MSG_SDP_COMPLETE, record); 315 } 316 } 317 318 /** Notify this device state machine of a newly added device account */ onPbapClientStorageReady()319 private void onPbapClientStorageReady() { 320 obtainMessage(MSG_STORAGE_READY).sendToTarget(); 321 } 322 323 /** Notify this device state machine of a newly added device account */ onPbapClientAccountAdded()324 private void onPbapClientAccountAdded() { 325 obtainMessage(MSG_ACCOUNT_ADDED).sendToTarget(); 326 } 327 328 /** Notify this device state machine of that its device account was removed */ onPbapClientAccountRemoved()329 private void onPbapClientAccountRemoved() { 330 obtainMessage(MSG_ACCOUNT_REMOVED).sendToTarget(); 331 } 332 333 /** Notify this device state machine of downloaded metadata from our OBEX client */ onPhonebookMetadataReceived(PbapPhonebookMetadata metadata)334 private void onPhonebookMetadataReceived(PbapPhonebookMetadata metadata) { 335 obtainMessage(MSG_PHONEBOOK_METADATA_RECEIVED, metadata).sendToTarget(); 336 } 337 338 /** Notify this device state machine that a download metadata request failed */ onPhonebookMetadataDownloadFailed(String phonebook)339 private void onPhonebookMetadataDownloadFailed(String phonebook) { 340 PbapPhonebookMetadata emptyMetadata = 341 new PbapPhonebookMetadata(phonebook, 0, null, null, null); 342 obtainMessage(MSG_PHONEBOOK_METADATA_RECEIVED, emptyMetadata).sendToTarget(); 343 } 344 345 /** Notify this device state machine of downloaded contacts from our OBEX client */ onPhonebookContactsReceived(PbapPhonebook contacts)346 private void onPhonebookContactsReceived(PbapPhonebook contacts) { 347 obtainMessage(MSG_PHONEBOOK_CONTACTS_RECEIVED, contacts).sendToTarget(); 348 } 349 350 /** Notify this device state machine that a download contacts request failed */ onPhonebookContactsDownloadFailed(String phonebook)351 private void onPhonebookContactsDownloadFailed(String phonebook) { 352 PbapPhonebook emptyContacts = new PbapPhonebook(phonebook); 353 obtainMessage(MSG_PHONEBOOK_CONTACTS_RECEIVED, emptyContacts).sendToTarget(); 354 } 355 356 /** Get the current connection state */ getConnectionState()357 public int getConnectionState() { 358 return mCurrentState; 359 } 360 361 class Disconnected extends State { 362 @Override enter()363 public void enter() { 364 debug("Disconnected: Enter, from=" + eventToString(getCurrentMessage().what)); 365 if (mCurrentState != STATE_DISCONNECTED) { 366 // Only broadcast a state change that came from something other than disconnected 367 onConnectionStateChanged(STATE_DISCONNECTED); 368 369 // Quit processing on this handler. This makes this object one time use. The 370 // connection state changed callback event will trigger the service to clean up 371 // their state machine reference if they still have one. 372 quit(); 373 } 374 } 375 376 @Override processMessage(Message message)377 public boolean processMessage(Message message) { 378 debug("Disconnected: process message, what=" + eventToString(message.what)); 379 switch (message.what) { 380 case MSG_CONNECT: 381 transitionTo(mConnecting); 382 break; 383 default: 384 warn( 385 "Disconnected: Received unhandled message, what=" 386 + eventToString(message.what)); 387 return NOT_HANDLED; 388 } 389 return true; 390 } 391 } 392 393 class Connecting extends State { 394 @Override enter()395 public void enter() { 396 debug("Connecting: Enter from=" + eventToString(getCurrentMessage().what)); 397 onConnectionStateChanged(STATE_CONNECTING); 398 399 // We can't connect over OBEX until we known where/how to connect. We need the SDP 400 // record details to do this. Thus, being connected means we received a valid SDP record 401 // and properly connected our OBEX Client afterwards. 402 mDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 403 404 // Wait up to CONNECT_TIMEOUT for SDP to complete and our OBEX client to connect 405 sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS); 406 } 407 408 @Override processMessage(Message message)409 public boolean processMessage(Message message) { 410 debug("Connecting: process message, what=" + eventToString(message.what)); 411 switch (message.what) { 412 case MSG_DISCONNECT: 413 transitionTo(mDisconnecting); 414 break; 415 416 case MSG_OBEX_CLIENT_CONNECTED: 417 transitionTo(mConnected); 418 break; 419 420 case MSG_OBEX_CLIENT_DISCONNECTED: 421 case MSG_CONNECT_TIMEOUT: 422 transitionTo(mDisconnecting); 423 break; 424 425 case MSG_SDP_FAILED: 426 int failureCode = message.arg1; 427 info("Connecting: SDP unsuccessful, code=" + sdpCodeToString(failureCode)); 428 if (failureCode == SDP_BUSY) { 429 mDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 430 } else { 431 transitionTo(mDisconnecting); 432 } 433 break; 434 435 case MSG_SDP_COMPLETE: 436 mSdpRecord = (PbapSdpRecord) message.obj; 437 438 info("Connecting: received SDP record, record=" + mSdpRecord); 439 440 if (!mDevice.equals(mSdpRecord.getDevice())) { 441 warn("Connecting: received SDP record for improper device. Ignoring."); 442 return HANDLED; 443 } 444 445 // Use SDP contents to determine whether we connect on L2CAP or RFCOMM 446 if (mSdpRecord.getL2capPsm() != /* L2CAP_INVALID_PSM */ -1) { 447 mObexClient.connectL2cap(mSdpRecord.getL2capPsm()); 448 } else if (mSdpRecord.getRfcommChannelNumber() 449 != /* RFCOMM_INVALID_CHANNEL */ -1) { 450 mObexClient.connectRfcomm(mSdpRecord.getRfcommChannelNumber()); 451 } else { 452 error("Connecting: Record didn't contain a valid L2CAP PSM/RFCOMM channel"); 453 mDevice.sdpSearch(BluetoothUuid.PBAP_PSE); 454 } 455 456 if (mSdpRecord.isRepositorySupported(PbapSdpRecord.REPOSITORY_FAVORITES)) { 457 mPhonebooks.put( 458 PbapPhonebook.FAVORITES_PATH, 459 new Phonebook(PbapPhonebook.FAVORITES_PATH)); 460 } 461 if (mSdpRecord.isRepositorySupported( 462 PbapSdpRecord.REPOSITORY_LOCAL_PHONEBOOK)) { 463 mPhonebooks.put( 464 PbapPhonebook.LOCAL_PHONEBOOK_PATH, 465 new Phonebook(PbapPhonebook.LOCAL_PHONEBOOK_PATH)); 466 mPhonebooks.put( 467 PbapPhonebook.MCH_PATH, new Phonebook(PbapPhonebook.MCH_PATH)); 468 mPhonebooks.put( 469 PbapPhonebook.ICH_PATH, new Phonebook(PbapPhonebook.ICH_PATH)); 470 mPhonebooks.put( 471 PbapPhonebook.OCH_PATH, new Phonebook(PbapPhonebook.OCH_PATH)); 472 } 473 if (mSdpRecord.isRepositorySupported(PbapSdpRecord.REPOSITORY_SIM_CARD)) { 474 mPhonebooks.put( 475 PbapPhonebook.SIM_PHONEBOOK_PATH, 476 new Phonebook(PbapPhonebook.SIM_PHONEBOOK_PATH)); 477 mPhonebooks.put( 478 PbapPhonebook.SIM_MCH_PATH, 479 new Phonebook(PbapPhonebook.SIM_MCH_PATH)); 480 mPhonebooks.put( 481 PbapPhonebook.SIM_ICH_PATH, 482 new Phonebook(PbapPhonebook.SIM_ICH_PATH)); 483 mPhonebooks.put( 484 PbapPhonebook.SIM_OCH_PATH, 485 new Phonebook(PbapPhonebook.SIM_OCH_PATH)); 486 } 487 break; 488 489 default: 490 warn( 491 "Connecting: Received unhandled message, what=" 492 + eventToString(message.what)); 493 return NOT_HANDLED; 494 } 495 return HANDLED; 496 } 497 498 @Override exit()499 public void exit() { 500 removeMessages(MSG_CONNECT_TIMEOUT); 501 } 502 } 503 504 class Connected extends State { 505 private boolean mHasDownloaded = false; 506 507 @Override enter()508 public void enter() { 509 debug("Connected: Enter, from=" + eventToString(getCurrentMessage().what)); 510 if (mCurrentState != STATE_CONNECTING) { 511 return; 512 } 513 514 onConnectionStateChanged(STATE_CONNECTED); 515 516 mHasDownloaded = false; 517 518 mContactsStorage.registerCallback(mStorageCallback); 519 if (mContactsStorage.isStorageReady()) { 520 onPbapClientStorageReady(); 521 } else { 522 Log.i(TAG, "Awaiting storage to be ready"); 523 } 524 } 525 526 @Override processMessage(Message message)527 public boolean processMessage(Message message) { 528 debug("Connected: process message, what=" + eventToString(message.what)); 529 switch (message.what) { 530 case MSG_OBEX_CLIENT_DISCONNECTED: 531 case MSG_DISCONNECT: 532 transitionTo(mDisconnecting); 533 break; 534 535 case MSG_STORAGE_READY: 536 if (mContactsStorage.getStorageAccounts().contains(mAccount)) { 537 info("Connected: Account already exists, time to download"); 538 if (!mHasDownloaded) { 539 download(); 540 mHasDownloaded = true; 541 } 542 } else { 543 info("Connected: Account not found. Requesting to add it."); 544 mContactsStorage.addAccount(mAccount); 545 } 546 break; 547 548 case MSG_ACCOUNT_ADDED: 549 info("Connected: account was added, time to download"); 550 if (!mHasDownloaded) { 551 download(); 552 mHasDownloaded = true; 553 } 554 break; 555 556 case MSG_ACCOUNT_REMOVED: 557 info("Connected: account was removed, time to disconnect"); 558 transitionTo(mDisconnecting); 559 break; 560 561 case MSG_DOWNLOAD: 562 transitionTo(mDownloading); 563 break; 564 565 default: 566 warn( 567 "Connected: received unhandled message, what=" 568 + eventToString(message.what)); 569 return NOT_HANDLED; 570 } 571 return HANDLED; 572 } 573 } 574 575 class Downloading extends State { 576 List<String> mPhonebooksToDownload = new ArrayList<String>(); 577 578 @Override enter()579 public void enter() { 580 581 info("Downloading: Start download process"); 582 583 // Initialize our list of phonebooks to download based on supported repositories 584 initializePhonebooksToDownload(); 585 586 String currentPhonebook = getCurrentPhonebook(); 587 if (currentPhonebook != null) { 588 downloadPhonebookMetadata(currentPhonebook); 589 } else { 590 warn("Downloading: no supported repositories to download"); 591 transitionTo(mConnected); 592 } 593 } 594 595 @Override processMessage(Message message)596 public boolean processMessage(Message message) { 597 String currentPhonebook = getCurrentPhonebook(); 598 String phonebook = null; 599 debug("Downloading: process message, what=" + eventToString(message.what)); 600 switch (message.what) { 601 case MSG_DISCONNECT: 602 transitionTo(mDisconnecting); 603 break; 604 605 case MSG_PHONEBOOK_METADATA_RECEIVED: 606 PbapPhonebookMetadata metadata = (PbapPhonebookMetadata) message.obj; 607 phonebook = metadata.phonebook(); 608 if (currentPhonebook != null && currentPhonebook.equals(phonebook)) { 609 info("Downloading: received metadata=" + metadata); 610 611 // Process Metadata 612 mPhonebooks.get(phonebook).setMetadata(metadata); 613 614 // If phonebook has contacts, begin downloading them 615 if (metadata.size() > 0) { 616 downloadPhonebook(currentPhonebook, 0, CONTACT_DOWNLOAD_BATCH_SIZE); 617 } else { 618 warn( 619 "Downloading: no contacts for phonebook=" 620 + currentPhonebook 621 + ", skipping"); 622 setNextPhonebookOrComplete(); 623 break; 624 } 625 } else { 626 warn( 627 "Downloading: dropped metadata event for phonebook=" 628 + phonebook 629 + ", current=" 630 + currentPhonebook); 631 } 632 break; 633 634 case MSG_PHONEBOOK_CONTACTS_RECEIVED: 635 PbapPhonebook contacts = (PbapPhonebook) message.obj; 636 phonebook = contacts.getPhonebook(); 637 if (currentPhonebook != null && currentPhonebook.equals(phonebook)) { 638 int numReceived = contacts.getCount(); 639 mPhonebooks.get(phonebook).onContactsDownloaded(numReceived); 640 int totalContactDownloaded = 641 mPhonebooks.get(phonebook).getNumberOfContactsDownloaded(); 642 int totalContactsExpected = 643 mPhonebooks.get(phonebook).getTotalNumberOfContacts(); 644 645 info( 646 "Downloading: received contacts, phonebook=" 647 + phonebook 648 + ", entries=" 649 + numReceived 650 + ", total=" 651 + totalContactDownloaded 652 + "/" 653 + totalContactsExpected); 654 if (numReceived != 0) { 655 storeDownloadedContacts(phonebook, contacts); 656 } else { 657 warn( 658 "Downloading: contacts empty for phonebook=" 659 + phonebook 660 + ", proceed to next phonebook"); 661 setNextPhonebookOrComplete(); 662 break; 663 } 664 665 if (totalContactDownloaded >= totalContactsExpected) { 666 info("Downloading: download complete, phonebook=" + phonebook); 667 setNextPhonebookOrComplete(); 668 } else { 669 downloadPhonebook( 670 currentPhonebook, 671 totalContactDownloaded, 672 CONTACT_DOWNLOAD_BATCH_SIZE); 673 } 674 } else { 675 warn("Downloading: dropped received contacts, phonebook=" + phonebook); 676 } 677 break; 678 679 default: 680 debug( 681 "Downloading: passing message to parent state, type=" 682 + eventToString(message.what)); 683 return NOT_HANDLED; 684 } 685 return HANDLED; 686 } 687 688 /* Initialize our prioritized list of phonebooks we want to download */ initializePhonebooksToDownload()689 private void initializePhonebooksToDownload() { 690 mPhonebooksToDownload.clear(); 691 692 if (mPhonebooks.containsKey(PbapPhonebook.FAVORITES_PATH)) { 693 mPhonebooksToDownload.add(PbapPhonebook.FAVORITES_PATH); 694 } 695 if (mPhonebooks.containsKey(PbapPhonebook.LOCAL_PHONEBOOK_PATH)) { 696 mPhonebooksToDownload.add(PbapPhonebook.LOCAL_PHONEBOOK_PATH); 697 } 698 if (mPhonebooks.containsKey(PbapPhonebook.SIM_PHONEBOOK_PATH)) { 699 mPhonebooksToDownload.add(PbapPhonebook.SIM_PHONEBOOK_PATH); 700 } 701 if (mPhonebooks.containsKey(PbapPhonebook.MCH_PATH)) { 702 mPhonebooksToDownload.add(PbapPhonebook.MCH_PATH); 703 } 704 if (mPhonebooks.containsKey(PbapPhonebook.ICH_PATH)) { 705 mPhonebooksToDownload.add(PbapPhonebook.ICH_PATH); 706 } 707 if (mPhonebooks.containsKey(PbapPhonebook.OCH_PATH)) { 708 mPhonebooksToDownload.add(PbapPhonebook.OCH_PATH); 709 } 710 if (mPhonebooks.containsKey(PbapPhonebook.SIM_MCH_PATH)) { 711 mPhonebooksToDownload.add(PbapPhonebook.SIM_MCH_PATH); 712 } 713 if (mPhonebooks.containsKey(PbapPhonebook.SIM_ICH_PATH)) { 714 mPhonebooksToDownload.add(PbapPhonebook.SIM_ICH_PATH); 715 } 716 if (mPhonebooks.containsKey(PbapPhonebook.SIM_OCH_PATH)) { 717 mPhonebooksToDownload.add(PbapPhonebook.SIM_OCH_PATH); 718 } 719 720 info("Downloading: initialized download process, phonebooks=" + mPhonebooksToDownload); 721 } 722 723 /* Get the currently downloading/processing phonebook path */ getCurrentPhonebook()724 private String getCurrentPhonebook() { 725 return mPhonebooksToDownload.size() != 0 ? mPhonebooksToDownload.get(0) : null; 726 } 727 728 /* 729 * Complete operation on one phonebook and update to the next one, if available. 730 * 731 * <p>If there's further phonebooks to download, this will trigger the process to download 732 * the next phonebook. If there are no more phonebooks to download, this will return us to 733 * the Connected state. 734 */ setNextPhonebookOrComplete()735 private void setNextPhonebookOrComplete() { 736 String currentPhonebook = getCurrentPhonebook(); 737 if (currentPhonebook == null) { 738 warn("Downloading: No phonebooks left to download"); 739 transitionTo(mConnected); 740 return; 741 } 742 743 mPhonebooksToDownload.remove(0); 744 if (mPhonebooksToDownload.size() != 0) { 745 String nextPhonebook = getCurrentPhonebook(); 746 debug( 747 "Downloading: Phonebook changed, old=" 748 + currentPhonebook 749 + ", new=" 750 + nextPhonebook); 751 downloadPhonebookMetadata(nextPhonebook); 752 } else { 753 info("Downloading: All phonebooks downloaded"); 754 transitionTo(mConnected); 755 } 756 } 757 758 /* 759 * Request the size and version counters for a specific phonebook, by path. 760 * 761 * <p>Downloads are in two parts. First we get the metadata and then we use that to create 762 * batches to download. Downloaded contacts are handed to the Contacts Client for storage 763 */ downloadPhonebookMetadata(String path)764 private void downloadPhonebookMetadata(String path) { 765 info("Downloading: Request metadata, phonebook=" + path); 766 mObexClient.requestPhonebookMetadata( 767 path, 768 new PbapApplicationParameters( 769 DEFAULT_PROPERTIES, 770 DEFAULT_VCARD_VERSION, 771 PbapApplicationParameters.RETURN_SIZE_ONLY, 772 0)); 773 } 774 775 /* 776 * Download a specific phonebook, by path, using the given batching parameters 777 * 778 * <p>Downloads are in two parts. First we get the metadata and then we use that to create 779 * batches to download. Downloaded contacts are handed to the Contacts Client for storage 780 */ downloadPhonebook(String path, int batchStart, int numToFetch)781 private void downloadPhonebook(String path, int batchStart, int numToFetch) { 782 int batchEnd = (batchStart + numToFetch - 1); 783 info( 784 "Downloading: Download contents, phonebook=" 785 + path 786 + ", start=" 787 + batchStart 788 + ", end=" 789 + batchEnd); 790 791 PbapApplicationParameters params = 792 new PbapApplicationParameters( 793 DEFAULT_PROPERTIES, DEFAULT_VCARD_VERSION, numToFetch, batchStart); 794 mObexClient.requestDownloadPhonebook(mPhonebooksToDownload.get(0), params); 795 } 796 } 797 798 class Disconnecting extends State { 799 @Override enter()800 public void enter() { 801 debug("Disconnecting: Enter, from=" + eventToString(getCurrentMessage().what)); 802 onConnectionStateChanged(STATE_DISCONNECTING); 803 804 // Disconnect 805 if (mObexClient.getConnectionState() != STATE_DISCONNECTED) { 806 mObexClient.disconnect(); 807 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT_MS); 808 } else { 809 transitionTo(mDisconnected); 810 } 811 } 812 813 @Override processMessage(Message message)814 public boolean processMessage(Message message) { 815 debug("Disconnecting: process message, what=" + eventToString(message.what)); 816 switch (message.what) { 817 case MSG_OBEX_CLIENT_DISCONNECTED: 818 removeMessages(MSG_DISCONNECT_TIMEOUT); 819 transitionTo(mDisconnected); 820 break; 821 822 case MSG_DISCONNECT: 823 deferMessage(message); 824 break; 825 826 case MSG_DISCONNECT_TIMEOUT: 827 warn("Disconnecting: Timeout, Forcing"); 828 mObexClient.close(); 829 transitionTo(mDisconnected); 830 break; 831 832 default: 833 warn( 834 "Disconnecting: Received unhandled message, what=" 835 + eventToString(message.what)); 836 return NOT_HANDLED; 837 } 838 return HANDLED; 839 } 840 841 @Override exit()842 public void exit() { 843 mContactsStorage.unregisterCallback(mStorageCallback); 844 845 // Always remove data as a last step 846 cleanup(); 847 } 848 } 849 850 /* 851 * Force this state machine to stop immediately 852 * 853 * <p>This function quits the state machine operation by broadcasting the proper connection 854 * state changes and properly cleaning up data that may be exist. 855 */ 856 @Override onQuitting()857 protected void onQuitting() { 858 Log.d(TAG, "State machine is force quitting"); 859 switch (mCurrentState) { 860 case STATE_CONNECTED: 861 onConnectionStateChanged(STATE_DISCONNECTING); 862 // intentional fallthrough-- we want to broadcast both state changes 863 case STATE_CONNECTING: 864 case STATE_DISCONNECTING: 865 onConnectionStateChanged(STATE_DISCONNECTED); 866 cleanup(); 867 break; 868 default: 869 Log.i(TAG, "Force quit a disconnected state machine. No state to broadcast"); 870 } 871 } 872 cleanup()873 private void cleanup() { 874 info("cleanup: evaluate data to cleanup"); 875 cleanupContactsDataAndAccounts(); 876 } 877 cleanupContactsDataAndAccounts()878 private void cleanupContactsDataAndAccounts() { 879 info("cleanupContactsDataAndAccounts: clear saved contacts, call history and account"); 880 mContactsStorage.removeAllContacts(mAccount); 881 mContactsStorage.removeCallHistory(mAccount); 882 mContactsStorage.removeAccount(mAccount); 883 } 884 885 /* Request to insert downloaded contacts into storage */ storeDownloadedContacts(String phonebook, PbapPhonebook contacts)886 private void storeDownloadedContacts(String phonebook, PbapPhonebook contacts) { 887 info("Request to store contacts for phonebook=" + phonebook); 888 if (phonebook.equals(PbapPhonebook.FAVORITES_PATH)) { 889 mContactsStorage.insertFavorites(mAccount, contacts.getList()); 890 } else if (phonebook.equals(PbapPhonebook.LOCAL_PHONEBOOK_PATH)) { 891 mContactsStorage.insertLocalContacts(mAccount, contacts.getList()); 892 } else if (phonebook.equals(PbapPhonebook.SIM_PHONEBOOK_PATH)) { 893 mContactsStorage.insertSimContacts(mAccount, contacts.getList()); 894 } else if (phonebook.equals(PbapPhonebook.MCH_PATH) 895 || phonebook.equals(PbapPhonebook.SIM_MCH_PATH)) { 896 mContactsStorage.insertMissedCallHistory(mAccount, contacts.getList()); 897 } else if (phonebook.equals(PbapPhonebook.ICH_PATH) 898 || phonebook.equals(PbapPhonebook.SIM_ICH_PATH)) { 899 mContactsStorage.insertIncomingCallHistory(mAccount, contacts.getList()); 900 } else if (phonebook.equals(PbapPhonebook.OCH_PATH) 901 || phonebook.equals(PbapPhonebook.SIM_OCH_PATH)) { 902 mContactsStorage.insertOutgoingCallHistory(mAccount, contacts.getList()); 903 } else { 904 warn("Received unknown phonebook to store, phonebook=" + phonebook); 905 } 906 } 907 onConnectionStateChanged(int state)908 private void onConnectionStateChanged(int state) { 909 int prevState = mCurrentState; 910 911 Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 912 intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); 913 intent.putExtra(BluetoothProfile.EXTRA_STATE, state); 914 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); 915 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 916 917 // Update the state, notify our service, AdapterService, and send the broadcast all at once 918 mCurrentState = state; 919 920 info("Connection state changed, prev=" + prevState + ", new=" + state); 921 922 AdapterService adapterService = AdapterService.getAdapterService(); 923 mCallback.onConnectionStateChanged(prevState, state); 924 if (adapterService != null) { 925 adapterService.updateProfileConnectionAdapterProperties( 926 mDevice, BluetoothProfile.PBAP_CLIENT, state, prevState); 927 } 928 mContext.sendBroadcastMultiplePermissions( 929 intent, 930 new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}, 931 Utils.getTempBroadcastOptions()); 932 } 933 934 /* Callback for getting events back from our OBEX Client */ 935 class PbapClientObexClientCallback implements PbapClientObexClient.Callback { 936 PbapClientObexClientCallback()937 PbapClientObexClientCallback() {} 938 939 @Override onConnectionStateChanged(int oldState, int newState)940 public void onConnectionStateChanged(int oldState, int newState) { 941 info("Obex client connection state changed: " + oldState + " -> " + newState); 942 if (newState == STATE_DISCONNECTED) { 943 obtainMessage(MSG_OBEX_CLIENT_DISCONNECTED).sendToTarget(); 944 } else if (newState == STATE_CONNECTED) { 945 obtainMessage(MSG_OBEX_CLIENT_CONNECTED).sendToTarget(); 946 } 947 } 948 949 @Override onGetPhonebookMetadataComplete( int responseCode, String phonebook, PbapPhonebookMetadata metadata)950 public void onGetPhonebookMetadataComplete( 951 int responseCode, String phonebook, PbapPhonebookMetadata metadata) { 952 if (responseCode != ResponseCodes.OBEX_HTTP_OK) { 953 warn( 954 "PullPhonebook for metadata failed, phonebook=" 955 + phonebook 956 + ", code=" 957 + responseCode); 958 onPhonebookMetadataDownloadFailed(phonebook); 959 return; 960 } 961 debug("Received phonebook metadata, phonebook=" + phonebook + ", metadata=" + metadata); 962 onPhonebookMetadataReceived(metadata); 963 } 964 965 @Override onPhonebookContactsDownloaded( int responseCode, String phonebook, PbapPhonebook contacts)966 public void onPhonebookContactsDownloaded( 967 int responseCode, String phonebook, PbapPhonebook contacts) { 968 if (responseCode != ResponseCodes.OBEX_HTTP_OK) { 969 warn("PullPhonebook failed, phonebook=" + phonebook + ", code=" + responseCode); 970 onPhonebookContactsDownloadFailed(phonebook); 971 return; 972 } 973 debug("Received contacts, phonebook=" + phonebook + ", count=" + contacts.getCount()); 974 onPhonebookContactsReceived(contacts); 975 } 976 } 977 eventToString(int message)978 private static String eventToString(int message) { 979 switch (message) { 980 case -2 /* Special, from StateMachine.java */: 981 return "SM_INIT_CMD"; 982 case -1 /* Special, from StateMachine.java */: 983 return "SM_QUIT_CMD"; 984 case MSG_CONNECT: 985 return "MSG_CONNECT"; 986 case MSG_DISCONNECT: 987 return "MSG_DISCONNECT"; 988 case MSG_SDP_COMPLETE: 989 return "MSG_SDP_COMPLETE"; 990 case MSG_SDP_FAILED: 991 return "MSG_SDP_FAILED"; 992 case MSG_OBEX_CLIENT_CONNECTED: 993 return "MSG_OBEX_CLIENT_CONNECTED"; 994 case MSG_OBEX_CLIENT_DISCONNECTED: 995 return "MSG_OBEX_CLIENT_DISCONNECTED"; 996 case MSG_STORAGE_READY: 997 return "MSG_STORAGE_READY"; 998 case MSG_ACCOUNT_ADDED: 999 return "MSG_ACCOUNT_ADDED"; 1000 case MSG_ACCOUNT_REMOVED: 1001 return "MSG_ACCOUNT_REMOVED"; 1002 case MSG_DOWNLOAD: 1003 return "MSG_DOWNLOAD"; 1004 case MSG_PHONEBOOK_METADATA_RECEIVED: 1005 return "MSG_PHONEBOOK_METADATA_RECEIVED"; 1006 case MSG_PHONEBOOK_CONTACTS_RECEIVED: 1007 return "MSG_PHONEBOOK_CONTACTS_RECEIVED"; 1008 case MSG_CONNECT_TIMEOUT: 1009 return "MSG_CONNECT_TIMEOUT"; 1010 case MSG_DISCONNECT_TIMEOUT: 1011 return "MSG_DISCONNECT_TIMEOUT"; 1012 default: 1013 return "Unknown (" + message + ")"; 1014 } 1015 } 1016 sdpCodeToString(int code)1017 private static String sdpCodeToString(int code) { 1018 switch (code) { 1019 case SDP_SUCCESS: 1020 return "SDP_SUCCESS"; 1021 case SDP_FAILED: 1022 return "SDP_FAILED"; 1023 case SDP_BUSY: 1024 return "SDP_BUSY"; 1025 default: 1026 return "Unknown (" + code + ")"; 1027 } 1028 } 1029 debug(String message)1030 private void debug(String message) { 1031 Log.d(TAG, "[" + mDevice + "] " + message); 1032 } 1033 info(String message)1034 private void info(String message) { 1035 Log.i(TAG, "[" + mDevice + "] " + message); 1036 } 1037 warn(String message)1038 private void warn(String message) { 1039 Log.w(TAG, "[" + mDevice + "] " + message); 1040 } 1041 error(String message)1042 private void error(String message) { 1043 Log.e(TAG, "[" + mDevice + "] " + message); 1044 } 1045 dump(StringBuilder sb)1046 public void dump(StringBuilder sb) { 1047 ProfileService.println( 1048 sb, 1049 " mDevice: " 1050 + mDevice.getAddress() 1051 + "(" 1052 + Utils.getName(mDevice) 1053 + ") " 1054 + this.toString()); 1055 1056 if (mSdpRecord != null) { 1057 ProfileService.println( 1058 sb, 1059 " Server Version: " 1060 + PbapSdpRecord.versionToString(mSdpRecord.getProfileVersion())); 1061 } else { 1062 ProfileService.println(sb, " Server Version: Unknown, no SDP record"); 1063 } 1064 1065 ProfileService.println(sb, " OBEX Client: " + mObexClient); 1066 1067 ProfileService.println(sb, " Download Batch Size: " + CONTACT_DOWNLOAD_BATCH_SIZE); 1068 1069 int totalContacts = 0; 1070 int totalContactDownloaded = 0; 1071 ProfileService.println(sb, " Supported Repositories:"); 1072 for (Phonebook pb : mPhonebooks.values()) { 1073 ProfileService.println(sb, " " + pb); 1074 totalContacts += pb.getTotalNumberOfContacts(); 1075 totalContactDownloaded += pb.getNumberOfContactsDownloaded(); 1076 } 1077 ProfileService.println(sb, " Total Contacts: " + totalContacts); 1078 ProfileService.println( 1079 sb, " Download Progress: " + totalContactDownloaded + "/" + totalContacts); 1080 } 1081 } 1082