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.pbap; 18 19 import android.bluetooth.BluetoothProfile; 20 import android.bluetooth.BluetoothProtoEnums; 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.content.SharedPreferences.Editor; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.preference.PreferenceManager; 28 import android.provider.ContactsContract.CommonDataKinds.Email; 29 import android.provider.ContactsContract.CommonDataKinds.Phone; 30 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 31 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 32 import android.provider.ContactsContract.Contacts; 33 import android.provider.ContactsContract.Data; 34 import android.provider.ContactsContract.Profile; 35 import android.provider.ContactsContract.RawContactsEntity; 36 import android.util.Log; 37 38 import com.android.bluetooth.BluetoothMethodProxy; 39 import com.android.bluetooth.BluetoothStatsLog; 40 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.vcard.VCardComposer; 43 import com.android.vcard.VCardConfig; 44 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.Calendar; 48 import java.util.HashMap; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Objects; 52 import java.util.concurrent.atomic.AtomicLong; 53 54 // Next tag value for ContentProfileErrorReportUtils.report(): 4 55 class BluetoothPbapUtils { 56 private static final String TAG = BluetoothPbapUtils.class.getSimpleName(); 57 58 // Filter constants from Bluetooth PBAP specification 59 private static final int FILTER_PHOTO = 3; 60 private static final int FILTER_BDAY = 4; 61 private static final int FILTER_ADDRESS = 5; 62 private static final int FILTER_LABEL = 6; 63 private static final int FILTER_EMAIL = 8; 64 private static final int FILTER_MAILER = 9; 65 private static final int FILTER_ORG = 16; 66 private static final int FILTER_NOTE = 17; 67 private static final int FILTER_SOUND = 19; 68 private static final int FILTER_URL = 20; 69 private static final int FILTER_NICKNAME = 23; 70 71 private static final long QUERY_CONTACT_RETRY_INTERVAL = 4000; 72 73 static AtomicLong sDbIdentifier = new AtomicLong(); 74 75 static long sPrimaryVersionCounter = 0; 76 static long sSecondaryVersionCounter = 0; 77 @VisibleForTesting static long sTotalContacts = 0; 78 79 /* totalFields and totalSvcFields used to update primary/secondary version 80 * counter between pbap sessions*/ 81 @VisibleForTesting static long sTotalFields = 0; 82 @VisibleForTesting static long sTotalSvcFields = 0; 83 @VisibleForTesting static long sContactsLastUpdated = 0; 84 85 private static class ContactData { 86 private String mName; 87 private final List<String> mEmail; 88 private final List<String> mPhone; 89 private final List<String> mAddress; 90 ContactData()91 ContactData() { 92 mPhone = new ArrayList<>(); 93 mEmail = new ArrayList<>(); 94 mAddress = new ArrayList<>(); 95 } 96 ContactData(String name, List<String> phone, List<String> email, List<String> address)97 ContactData(String name, List<String> phone, List<String> email, List<String> address) { 98 this.mName = name; 99 this.mPhone = phone; 100 this.mEmail = email; 101 this.mAddress = address; 102 } 103 } 104 105 @VisibleForTesting static HashMap<String, ContactData> sContactDataset = new HashMap<>(); 106 107 @VisibleForTesting static HashSet<String> sContactSet = new HashSet<>(); 108 109 @VisibleForTesting static final String TYPE_NAME = "name"; 110 @VisibleForTesting static final String TYPE_PHONE = "phone"; 111 @VisibleForTesting static final String TYPE_EMAIL = "email"; 112 @VisibleForTesting static final String TYPE_ADDRESS = "address"; 113 hasFilter(byte[] filter)114 private static boolean hasFilter(byte[] filter) { 115 return filter != null && filter.length > 0; 116 } 117 isFilterBitSet(byte[] filter, int filterBit)118 private static boolean isFilterBitSet(byte[] filter, int filterBit) { 119 if (hasFilter(filter)) { 120 int byteNumber = 7 - filterBit / 8; 121 int bitNumber = filterBit % 8; 122 if (byteNumber < filter.length) { 123 return (filter[byteNumber] & (1 << bitNumber)) > 0; 124 } 125 } 126 return false; 127 } 128 createFilteredVCardComposer( final Context ctx, final int vcardType, final byte[] filter)129 static VCardComposer createFilteredVCardComposer( 130 final Context ctx, final int vcardType, final byte[] filter) { 131 int vType = vcardType; 132 boolean includePhoto = 133 BluetoothPbapConfig.includePhotosInVcard() 134 && (!hasFilter(filter) || isFilterBitSet(filter, FILTER_PHOTO)); 135 if (!includePhoto) { 136 Log.v(TAG, "Excluding images from VCardComposer..."); 137 vType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 138 } 139 if (hasFilter(filter)) { 140 if (!isFilterBitSet(filter, FILTER_ADDRESS) && !isFilterBitSet(filter, FILTER_LABEL)) { 141 Log.i(TAG, "Excluding addresses from VCardComposer..."); 142 vType |= VCardConfig.FLAG_REFRAIN_ADDRESS_EXPORT; 143 } 144 if (!isFilterBitSet(filter, FILTER_EMAIL) && !isFilterBitSet(filter, FILTER_MAILER)) { 145 Log.i(TAG, "Excluding email addresses from VCardComposer..."); 146 vType |= VCardConfig.FLAG_REFRAIN_EMAIL_EXPORT; 147 } 148 if (!isFilterBitSet(filter, FILTER_ORG)) { 149 Log.i(TAG, "Excluding organization from VCardComposer..."); 150 vType |= VCardConfig.FLAG_REFRAIN_ORGANIZATION_EXPORT; 151 } 152 if (!isFilterBitSet(filter, FILTER_URL)) { 153 Log.i(TAG, "Excluding URLS from VCardComposer..."); 154 vType |= VCardConfig.FLAG_REFRAIN_WEBSITES_EXPORT; 155 } 156 if (!isFilterBitSet(filter, FILTER_NOTE)) { 157 Log.i(TAG, "Excluding notes from VCardComposer..."); 158 vType |= VCardConfig.FLAG_REFRAIN_NOTES_EXPORT; 159 } 160 if (!isFilterBitSet(filter, FILTER_NICKNAME)) { 161 Log.i(TAG, "Excluding nickname from VCardComposer..."); 162 vType |= VCardConfig.FLAG_REFRAIN_NICKNAME_EXPORT; 163 } 164 if (!isFilterBitSet(filter, FILTER_SOUND)) { 165 Log.i(TAG, "Excluding phonetic name from VCardComposer..."); 166 vType |= VCardConfig.FLAG_REFRAIN_PHONETIC_NAME_EXPORT; 167 } 168 if (!isFilterBitSet(filter, FILTER_BDAY)) { 169 Log.i(TAG, "Excluding birthday from VCardComposer..."); 170 vType |= VCardConfig.FLAG_REFRAIN_EVENTS_EXPORT; 171 } 172 } 173 return new VCardComposer(ctx, vType, true); 174 } 175 getProfileName(Context context)176 public static synchronized String getProfileName(Context context) { 177 try (Cursor c = 178 BluetoothMethodProxy.getInstance() 179 .contentResolverQuery( 180 context.getContentResolver(), 181 Profile.CONTENT_URI, 182 new String[] {Profile.DISPLAY_NAME}, 183 null, 184 null, 185 null)) { 186 String ownerName = null; 187 if (c != null && c.moveToFirst()) { 188 ownerName = c.getString(0); 189 } 190 return ownerName; 191 } 192 } 193 createProfileVCard(Context ctx, final int vcardType, final byte[] filter)194 static String createProfileVCard(Context ctx, final int vcardType, final byte[] filter) { 195 VCardComposer composer = null; 196 String vcard = null; 197 try { 198 composer = createFilteredVCardComposer(ctx, vcardType, filter); 199 if (composer.init( 200 Profile.CONTENT_URI, 201 null, 202 null, 203 null, 204 null, 205 Uri.withAppendedPath( 206 Profile.CONTENT_URI, 207 RawContactsEntity.CONTENT_URI.getLastPathSegment()))) { 208 vcard = composer.createOneEntry(); 209 } else { 210 Log.e( 211 TAG, 212 "Unable to create profile vcard. Error initializing composer: " 213 + composer.getErrorReason()); 214 ContentProfileErrorReportUtils.report( 215 BluetoothProfile.PBAP, 216 BluetoothProtoEnums.BLUETOOTH_PBAP_UTILS, 217 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 218 0); 219 } 220 } catch (Throwable t) { 221 ContentProfileErrorReportUtils.report( 222 BluetoothProfile.PBAP, 223 BluetoothProtoEnums.BLUETOOTH_PBAP_UTILS, 224 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 225 1); 226 Log.e(TAG, "Unable to create profile vcard.", t); 227 } 228 if (composer != null) { 229 composer.terminate(); 230 } 231 return vcard; 232 } 233 savePbapParams(Context ctx)234 static void savePbapParams(Context ctx) { 235 SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx); 236 long dbIdentifier = sDbIdentifier.get(); 237 Editor edit = pref.edit(); 238 edit.putLong("primary", sPrimaryVersionCounter); 239 edit.putLong("secondary", sSecondaryVersionCounter); 240 edit.putLong("dbIdentifier", dbIdentifier); 241 edit.putLong("totalContacts", sTotalContacts); 242 edit.putLong("lastUpdatedTimestamp", sContactsLastUpdated); 243 edit.putLong("totalFields", sTotalFields); 244 edit.putLong("totalSvcFields", sTotalSvcFields); 245 edit.apply(); 246 247 Log.v( 248 TAG, 249 "Saved Primary:" 250 + sPrimaryVersionCounter 251 + ", Secondary:" 252 + sSecondaryVersionCounter 253 + ", Database Identifier: " 254 + dbIdentifier); 255 } 256 257 /* fetchPbapParams() loads preserved value of Database Identifiers and folder 258 * version counters. Servers using a database identifier 0 or regenerating 259 * one at each connection will not benefit from the resulting performance and 260 * user experience improvements. So database identifier is set with current 261 * timestamp and updated on rollover of folder version counter.*/ fetchPbapParams(Context ctx)262 static void fetchPbapParams(Context ctx) { 263 SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx); 264 long timeStamp = Calendar.getInstance().getTimeInMillis(); 265 BluetoothPbapUtils.sDbIdentifier.set(pref.getLong("DbIdentifier", timeStamp)); 266 BluetoothPbapUtils.sPrimaryVersionCounter = pref.getLong("primary", 0); 267 BluetoothPbapUtils.sSecondaryVersionCounter = pref.getLong("secondary", 0); 268 BluetoothPbapUtils.sTotalFields = pref.getLong("totalContacts", 0); 269 BluetoothPbapUtils.sContactsLastUpdated = pref.getLong("lastUpdatedTimestamp", timeStamp); 270 BluetoothPbapUtils.sTotalFields = pref.getLong("totalFields", 0); 271 BluetoothPbapUtils.sTotalSvcFields = pref.getLong("totalSvcFields", 0); 272 Log.v(TAG, " fetchPbapParams " + pref.getAll()); 273 } 274 loadAllContacts(Context context, Handler handler)275 static void loadAllContacts(Context context, Handler handler) { 276 Log.v(TAG, "Loading Contacts ..."); 277 278 String[] projection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE}; 279 sTotalContacts = fetchAndSetContacts(context, handler, projection, null, null, true); 280 if (sTotalContacts < 0) { 281 sTotalContacts = 0; 282 return; 283 } 284 handler.sendMessage(handler.obtainMessage(BluetoothPbapService.CONTACTS_LOADED)); 285 } 286 updateSecondaryVersionCounter(Context context, Handler handler)287 static synchronized void updateSecondaryVersionCounter(Context context, Handler handler) { 288 /* updatedList stores list of contacts which are added/updated after 289 * the time when contacts were last updated. (contactsLastUpdated 290 * indicates the time when contact/contacts were last updated and 291 * corresponding changes were reflected in Folder Version Counters).*/ 292 ArrayList<String> updatedList = new ArrayList<>(); 293 HashSet<String> currentContactSet = new HashSet<>(); 294 295 String[] projection = {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP}; 296 int currentContactCount = 0; 297 try (Cursor c = 298 BluetoothMethodProxy.getInstance() 299 .contentResolverQuery( 300 context.getContentResolver(), 301 Contacts.CONTENT_URI, 302 projection, 303 null, 304 null, 305 null)) { 306 307 if (c == null) { 308 Log.d(TAG, "Failed to fetch data from contact database"); 309 return; 310 } 311 while (c.moveToNext()) { 312 String contactId = c.getString(0); 313 long lastUpdatedTime = c.getLong(1); 314 if (lastUpdatedTime > sContactsLastUpdated) { 315 updatedList.add(contactId); 316 } 317 currentContactSet.add(contactId); 318 } 319 currentContactCount = c.getCount(); 320 } 321 322 Log.v(TAG, "updated list =" + updatedList); 323 String[] dataProjection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE}; 324 325 String whereClause = Data.CONTACT_ID + "=?"; 326 327 /* code to check if new contact/contacts are added */ 328 if (currentContactCount > sTotalContacts) { 329 for (String contact : updatedList) { 330 String[] selectionArgs = {contact}; 331 fetchAndSetContacts( 332 context, handler, dataProjection, whereClause, selectionArgs, false); 333 sSecondaryVersionCounter++; 334 sPrimaryVersionCounter++; 335 sTotalContacts = currentContactCount; 336 } 337 /* When contact/contacts are deleted */ 338 } else if (currentContactCount < sTotalContacts) { 339 sTotalContacts = currentContactCount; 340 ArrayList<String> svcFields = 341 new ArrayList<>( 342 Arrays.asList( 343 StructuredName.CONTENT_ITEM_TYPE, 344 Phone.CONTENT_ITEM_TYPE, 345 Email.CONTENT_ITEM_TYPE, 346 StructuredPostal.CONTENT_ITEM_TYPE)); 347 HashSet<String> deletedContacts = new HashSet<>(sContactSet); 348 deletedContacts.removeAll(currentContactSet); 349 sPrimaryVersionCounter += deletedContacts.size(); 350 sSecondaryVersionCounter += deletedContacts.size(); 351 Log.v(TAG, "Deleted Contacts : " + deletedContacts); 352 353 // to decrement totalFields and totalSvcFields count 354 for (String deletedContact : deletedContacts) { 355 sContactSet.remove(deletedContact); 356 String[] selectionArgs = {deletedContact}; 357 try (Cursor dataCursor = 358 BluetoothMethodProxy.getInstance() 359 .contentResolverQuery( 360 context.getContentResolver(), 361 Data.CONTENT_URI, 362 dataProjection, 363 whereClause, 364 selectionArgs, 365 null)) { 366 367 if (dataCursor == null) { 368 Log.d(TAG, "Failed to fetch data from contact database"); 369 return; 370 } 371 372 while (dataCursor.moveToNext()) { 373 if (svcFields.contains( 374 dataCursor.getString(dataCursor.getColumnIndex(Data.MIMETYPE)))) { 375 sTotalSvcFields--; 376 } 377 sTotalFields--; 378 } 379 } 380 } 381 382 /* When contacts are updated. i.e. Fields of existing contacts are 383 * added/updated/deleted */ 384 } else { 385 for (String contact : updatedList) { 386 sPrimaryVersionCounter++; 387 List<String> phoneTmp = new ArrayList<>(); 388 List<String> emailTmp = new ArrayList<>(); 389 List<String> addressTmp = new ArrayList<>(); 390 String nameTmp = null; 391 boolean updated = false; 392 393 String[] selectionArgs = {contact}; 394 try (Cursor dataCursor = 395 BluetoothMethodProxy.getInstance() 396 .contentResolverQuery( 397 context.getContentResolver(), 398 Data.CONTENT_URI, 399 dataProjection, 400 whereClause, 401 selectionArgs, 402 null)) { 403 404 if (dataCursor == null) { 405 Log.d(TAG, "Failed to fetch data from contact database"); 406 return; 407 } 408 // fetch all updated contacts and compare with cached copy of contacts 409 int indexData = dataCursor.getColumnIndex(Data.DATA1); 410 int indexMimeType = dataCursor.getColumnIndex(Data.MIMETYPE); 411 String data; 412 String mimeType; 413 while (dataCursor.moveToNext()) { 414 data = dataCursor.getString(indexData); 415 mimeType = dataCursor.getString(indexMimeType); 416 switch (mimeType) { 417 case Email.CONTENT_ITEM_TYPE: 418 emailTmp.add(data); 419 break; 420 case Phone.CONTENT_ITEM_TYPE: 421 phoneTmp.add(data); 422 break; 423 case StructuredPostal.CONTENT_ITEM_TYPE: 424 addressTmp.add(data); 425 break; 426 case StructuredName.CONTENT_ITEM_TYPE: 427 nameTmp = data; 428 break; 429 } 430 } 431 } 432 ContactData cData = new ContactData(nameTmp, phoneTmp, emailTmp, addressTmp); 433 434 ContactData currentContactData = sContactDataset.get(contact); 435 if (currentContactData == null) { 436 Log.e(TAG, "Null contact in the updateList: " + contact); 437 ContentProfileErrorReportUtils.report( 438 BluetoothProfile.PBAP, 439 BluetoothProtoEnums.BLUETOOTH_PBAP_UTILS, 440 BluetoothStatsLog 441 .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 442 2); 443 continue; 444 } 445 446 if (!Objects.equals(nameTmp, currentContactData.mName)) { 447 updated = true; 448 } else if (checkFieldUpdates(currentContactData.mPhone, phoneTmp)) { 449 updated = true; 450 } else if (checkFieldUpdates(currentContactData.mEmail, emailTmp)) { 451 updated = true; 452 } else if (checkFieldUpdates(currentContactData.mAddress, addressTmp)) { 453 updated = true; 454 } 455 456 if (updated) { 457 sSecondaryVersionCounter++; 458 sContactDataset.put(contact, cData); 459 } 460 } 461 } 462 463 Log.d( 464 TAG, 465 "primaryVersionCounter = " 466 + sPrimaryVersionCounter 467 + ", secondaryVersionCounter=" 468 + sSecondaryVersionCounter); 469 470 // check if Primary/Secondary version Counter has rolled over 471 if (sSecondaryVersionCounter < 0 || sPrimaryVersionCounter < 0) { 472 handler.sendMessage(handler.obtainMessage(BluetoothPbapService.ROLLOVER_COUNTERS)); 473 } 474 } 475 476 /* checkFieldUpdates checks update contact fields of a particular contact. 477 * Field update can be a field updated/added/deleted in an existing contact. 478 * Returns true if any contact field is updated else return false. */ 479 @VisibleForTesting checkFieldUpdates(List<String> oldFields, List<String> newFields)480 static boolean checkFieldUpdates(List<String> oldFields, List<String> newFields) { 481 if (newFields != null && oldFields != null) { 482 if (newFields.size() != oldFields.size()) { 483 sTotalSvcFields += Math.abs(newFields.size() - oldFields.size()); 484 sTotalFields += Math.abs(newFields.size() - oldFields.size()); 485 return true; 486 } 487 for (String newField : newFields) { 488 if (!oldFields.contains(newField)) { 489 return true; 490 } 491 } 492 /* when all fields of type(phone/email/address) are deleted in a given contact*/ 493 } else if (newFields == null && oldFields != null && oldFields.size() > 0) { 494 sTotalSvcFields += oldFields.size(); 495 sTotalFields += oldFields.size(); 496 return true; 497 498 /* when new fields are added for a type(phone/email/address) in a contact 499 * for which there were no fields of this type earlier.*/ 500 } else if (oldFields == null && newFields != null && newFields.size() > 0) { 501 sTotalSvcFields += newFields.size(); 502 sTotalFields += newFields.size(); 503 return true; 504 } 505 return false; 506 } 507 508 /* fetchAndSetContacts reads contacts and caches them 509 * isLoad = true indicates its loading all contacts 510 * isLoad = false indicates its caching recently added contact in database*/ 511 @VisibleForTesting fetchAndSetContacts( Context context, Handler handler, String[] projection, String whereClause, String[] selectionArgs, boolean isLoad)512 static synchronized int fetchAndSetContacts( 513 Context context, 514 Handler handler, 515 String[] projection, 516 String whereClause, 517 String[] selectionArgs, 518 boolean isLoad) { 519 long currentTotalFields = 0, currentSvcFieldCount = 0; 520 try (Cursor c = 521 BluetoothMethodProxy.getInstance() 522 .contentResolverQuery( 523 context.getContentResolver(), 524 Data.CONTENT_URI, 525 projection, 526 whereClause, 527 selectionArgs, 528 null)) { 529 530 /* send delayed message to loadContact when ContentResolver is unable 531 * to fetch data from contact database using the specified URI at that 532 * moment (Case: immediate Pbap connect on system boot with BT ON)*/ 533 if (c == null) { 534 Log.d(TAG, "Failed to fetch contacts data from database.."); 535 if (isLoad) { 536 handler.sendMessageDelayed( 537 handler.obtainMessage(BluetoothPbapService.LOAD_CONTACTS), 538 QUERY_CONTACT_RETRY_INTERVAL); 539 } 540 return -1; 541 } 542 543 int indexCId = c.getColumnIndex(Data.CONTACT_ID); 544 int indexData = c.getColumnIndex(Data.DATA1); 545 int indexMimeType = c.getColumnIndex(Data.MIMETYPE); 546 String contactId, data, mimeType; 547 548 while (c.moveToNext()) { 549 if (c.isNull(indexCId)) { 550 Log.w(TAG, "_id column is null. Row was deleted during iteration, skipping"); 551 ContentProfileErrorReportUtils.report( 552 BluetoothProfile.PBAP, 553 BluetoothProtoEnums.BLUETOOTH_PBAP_UTILS, 554 BluetoothStatsLog 555 .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN, 556 3); 557 continue; 558 } 559 contactId = c.getString(indexCId); 560 data = c.getString(indexData); 561 mimeType = c.getString(indexMimeType); 562 /* fetch phone/email/address/name information of the contact */ 563 switch (mimeType) { 564 case Phone.CONTENT_ITEM_TYPE: 565 setContactFields(TYPE_PHONE, contactId, data); 566 currentSvcFieldCount++; 567 break; 568 case Email.CONTENT_ITEM_TYPE: 569 setContactFields(TYPE_EMAIL, contactId, data); 570 currentSvcFieldCount++; 571 break; 572 case StructuredPostal.CONTENT_ITEM_TYPE: 573 setContactFields(TYPE_ADDRESS, contactId, data); 574 currentSvcFieldCount++; 575 break; 576 case StructuredName.CONTENT_ITEM_TYPE: 577 setContactFields(TYPE_NAME, contactId, data); 578 currentSvcFieldCount++; 579 break; 580 } 581 sContactSet.add(contactId); 582 currentTotalFields++; 583 } 584 } 585 586 /* This code checks if there is any update in contacts after last pbap 587 * disconnect has happened (even if BT is turned OFF during this time)*/ 588 if (isLoad && currentTotalFields != sTotalFields) { 589 sPrimaryVersionCounter += Math.abs(sTotalContacts - sContactSet.size()); 590 591 if (currentSvcFieldCount != sTotalSvcFields) { 592 if (sTotalContacts != sContactSet.size()) { 593 sSecondaryVersionCounter += Math.abs(sTotalContacts - sContactSet.size()); 594 } else { 595 sSecondaryVersionCounter++; 596 } 597 } 598 if (sPrimaryVersionCounter < 0 || sSecondaryVersionCounter < 0) { 599 rolloverCounters(); 600 } 601 602 sTotalFields = currentTotalFields; 603 sTotalSvcFields = currentSvcFieldCount; 604 sContactsLastUpdated = System.currentTimeMillis(); 605 Log.d( 606 TAG, 607 "Contacts updated between last BT OFF and current" 608 + "Pbap Connect, primaryVersionCounter=" 609 + sPrimaryVersionCounter 610 + ", secondaryVersionCounter=" 611 + sSecondaryVersionCounter); 612 } else if (!isLoad) { 613 sTotalFields++; 614 sTotalSvcFields++; 615 } 616 return sContactSet.size(); 617 } 618 619 /* setContactFields() is used to store contacts data in local cache (phone, 620 * email or address which is required for updating Secondary Version counter). 621 * contactsFieldData - List of field data for phone/email/address. 622 * contactId - Contact ID, data1 - field value from data table for phone/email/address*/ 623 @VisibleForTesting setContactFields(String fieldType, String contactId, String data)624 static void setContactFields(String fieldType, String contactId, String data) { 625 ContactData cData; 626 if (sContactDataset.containsKey(contactId)) { 627 cData = sContactDataset.get(contactId); 628 } else { 629 cData = new ContactData(); 630 } 631 632 switch (fieldType) { 633 case TYPE_NAME: 634 cData.mName = data; 635 break; 636 case TYPE_PHONE: 637 cData.mPhone.add(data); 638 break; 639 case TYPE_EMAIL: 640 cData.mEmail.add(data); 641 break; 642 case TYPE_ADDRESS: 643 cData.mAddress.add(data); 644 break; 645 } 646 sContactDataset.put(contactId, cData); 647 } 648 649 /* As per Pbap 1.2 specification, Database Identifies shall be 650 * re-generated when a Folder Version Counter rolls over or starts over.*/ 651 rolloverCounters()652 static void rolloverCounters() { 653 sDbIdentifier.set(Calendar.getInstance().getTimeInMillis()); 654 sPrimaryVersionCounter = (sPrimaryVersionCounter < 0) ? 0 : sPrimaryVersionCounter; 655 sSecondaryVersionCounter = (sSecondaryVersionCounter < 0) ? 0 : sSecondaryVersionCounter; 656 Log.v(TAG, "DbIdentifier rolled over to:" + sDbIdentifier); 657 } 658 } 659