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