• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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