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