1 /************************************************************************************
2  *
3  *  Copyright (C) 2009-2012 Broadcom Corporation
4  *
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  ************************************************************************************/
18 package com.android.bluetooth.pbap;
19 
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.content.SharedPreferences.Editor;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.Handler;
26 import android.preference.PreferenceManager;
27 import android.provider.ContactsContract.CommonDataKinds.Email;
28 import android.provider.ContactsContract.CommonDataKinds.Phone;
29 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
30 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
31 import android.provider.ContactsContract.Contacts;
32 import android.provider.ContactsContract.Data;
33 import android.provider.ContactsContract.Profile;
34 import android.provider.ContactsContract.RawContactsEntity;
35 import android.util.Log;
36 
37 import com.android.vcard.VCardComposer;
38 import com.android.vcard.VCardConfig;
39 
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Calendar;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.Objects;
46 import java.util.concurrent.atomic.AtomicLong;
47 
48 class BluetoothPbapUtils {
49     private static final String TAG = "BluetoothPbapUtils";
50     private static final boolean V = BluetoothPbapService.VERBOSE;
51 
52     private static final int FILTER_PHOTO = 3;
53 
54     private static final long QUERY_CONTACT_RETRY_INTERVAL = 4000;
55 
56     static AtomicLong sDbIdentifier = new AtomicLong();
57 
58     static long sPrimaryVersionCounter = 0;
59     static long sSecondaryVersionCounter = 0;
60     private static long sTotalContacts = 0;
61 
62     /* totalFields and totalSvcFields used to update primary/secondary version
63      * counter between pbap sessions*/
64     private static long sTotalFields = 0;
65     private static long sTotalSvcFields = 0;
66     private static long sContactsLastUpdated = 0;
67 
68     private static class ContactData {
69         private String mName;
70         private ArrayList<String> mEmail;
71         private ArrayList<String> mPhone;
72         private ArrayList<String> mAddress;
73 
ContactData()74         ContactData() {
75             mPhone = new ArrayList<>();
76             mEmail = new ArrayList<>();
77             mAddress = new ArrayList<>();
78         }
79 
ContactData(String name, ArrayList<String> phone, ArrayList<String> email, ArrayList<String> address)80         ContactData(String name, ArrayList<String> phone, ArrayList<String> email,
81                 ArrayList<String> address) {
82             this.mName = name;
83             this.mPhone = phone;
84             this.mEmail = email;
85             this.mAddress = address;
86         }
87     }
88 
89     private static HashMap<String, ContactData> sContactDataset = new HashMap<>();
90 
91     private static HashSet<String> sContactSet = new HashSet<>();
92 
93     private static final String TYPE_NAME = "name";
94     private static final String TYPE_PHONE = "phone";
95     private static final String TYPE_EMAIL = "email";
96     private static final String TYPE_ADDRESS = "address";
97 
hasFilter(byte[] filter)98     private static boolean hasFilter(byte[] filter) {
99         return filter != null && filter.length > 0;
100     }
101 
isFilterBitSet(byte[] filter, int filterBit)102     private static boolean isFilterBitSet(byte[] filter, int filterBit) {
103         if (hasFilter(filter)) {
104             int byteNumber = 7 - filterBit / 8;
105             int bitNumber = filterBit % 8;
106             if (byteNumber < filter.length) {
107                 return (filter[byteNumber] & (1 << bitNumber)) > 0;
108             }
109         }
110         return false;
111     }
112 
createFilteredVCardComposer(final Context ctx, final int vcardType, final byte[] filter)113     static VCardComposer createFilteredVCardComposer(final Context ctx, final int vcardType,
114             final byte[] filter) {
115         int vType = vcardType;
116         boolean includePhoto =
117                 BluetoothPbapConfig.includePhotosInVcard() && (!hasFilter(filter) || isFilterBitSet(
118                         filter, FILTER_PHOTO));
119         if (!includePhoto) {
120             if (V) {
121                 Log.v(TAG, "Excluding images from VCardComposer...");
122             }
123             vType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
124         }
125         return new VCardComposer(ctx, vType, true);
126     }
127 
getProfileName(Context context)128     public static String getProfileName(Context context) {
129         Cursor c = context.getContentResolver()
130                 .query(Profile.CONTENT_URI, new String[]{Profile.DISPLAY_NAME}, null, null, null);
131         String ownerName = null;
132         if (c != null && c.moveToFirst()) {
133             ownerName = c.getString(0);
134         }
135         if (c != null) {
136             c.close();
137         }
138         return ownerName;
139     }
140 
createProfileVCard(Context ctx, final int vcardType, final byte[] filter)141     static String createProfileVCard(Context ctx, final int vcardType, final byte[] filter) {
142         VCardComposer composer = null;
143         String vcard = null;
144         try {
145             composer = createFilteredVCardComposer(ctx, vcardType, filter);
146             if (composer.init(Profile.CONTENT_URI, null, null, null, null,
147                     Uri.withAppendedPath(Profile.CONTENT_URI,
148                             RawContactsEntity.CONTENT_URI.getLastPathSegment()))) {
149                 vcard = composer.createOneEntry();
150             } else {
151                 Log.e(TAG, "Unable to create profile vcard. Error initializing composer: "
152                         + composer.getErrorReason());
153             }
154         } catch (Throwable t) {
155             Log.e(TAG, "Unable to create profile vcard.", t);
156         }
157         if (composer != null) {
158             composer.terminate();
159         }
160         return vcard;
161     }
162 
savePbapParams(Context ctx)163     static void savePbapParams(Context ctx) {
164         SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx);
165         long dbIdentifier = sDbIdentifier.get();
166         Editor edit = pref.edit();
167         edit.putLong("primary", sPrimaryVersionCounter);
168         edit.putLong("secondary", sSecondaryVersionCounter);
169         edit.putLong("dbIdentifier", dbIdentifier);
170         edit.putLong("totalContacts", sTotalContacts);
171         edit.putLong("lastUpdatedTimestamp", sContactsLastUpdated);
172         edit.putLong("totalFields", sTotalFields);
173         edit.putLong("totalSvcFields", sTotalSvcFields);
174         edit.apply();
175 
176         if (V) {
177             Log.v(TAG, "Saved Primary:" + sPrimaryVersionCounter + ", Secondary:"
178                     + sSecondaryVersionCounter + ", Database Identifier: " + dbIdentifier);
179         }
180     }
181 
182     /* fetchPbapParams() loads preserved value of Database Identifiers and folder
183      * version counters. Servers using a database identifier 0 or regenerating
184      * one at each connection will not benefit from the resulting performance and
185      * user experience improvements. So database identifier is set with current
186      * timestamp and updated on rollover of folder version counter.*/
fetchPbapParams(Context ctx)187     static void fetchPbapParams(Context ctx) {
188         SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx);
189         long timeStamp = Calendar.getInstance().getTimeInMillis();
190         BluetoothPbapUtils.sDbIdentifier.set(pref.getLong("DbIdentifier", timeStamp));
191         BluetoothPbapUtils.sPrimaryVersionCounter = pref.getLong("primary", 0);
192         BluetoothPbapUtils.sSecondaryVersionCounter = pref.getLong("secondary", 0);
193         BluetoothPbapUtils.sTotalFields = pref.getLong("totalContacts", 0);
194         BluetoothPbapUtils.sContactsLastUpdated = pref.getLong("lastUpdatedTimestamp", timeStamp);
195         BluetoothPbapUtils.sTotalFields = pref.getLong("totalFields", 0);
196         BluetoothPbapUtils.sTotalSvcFields = pref.getLong("totalSvcFields", 0);
197         if (V) {
198             Log.v(TAG, " fetchPbapParams " + pref.getAll());
199         }
200     }
201 
loadAllContacts(Context context, Handler handler)202     static void loadAllContacts(Context context, Handler handler) {
203         if (V) {
204             Log.v(TAG, "Loading Contacts ...");
205         }
206 
207         String[] projection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE};
208         sTotalContacts = fetchAndSetContacts(context, handler, projection, null, null, true);
209         if (sTotalContacts < 0) {
210             sTotalContacts = 0;
211             return;
212         }
213         handler.sendMessage(handler.obtainMessage(BluetoothPbapService.CONTACTS_LOADED));
214     }
215 
updateSecondaryVersionCounter(Context context, Handler handler)216     static void updateSecondaryVersionCounter(Context context, Handler handler) {
217             /* updatedList stores list of contacts which are added/updated after
218              * the time when contacts were last updated. (contactsLastUpdated
219              * indicates the time when contact/contacts were last updated and
220              * corresponding changes were reflected in Folder Version Counters).*/
221         ArrayList<String> updatedList = new ArrayList<>();
222         HashSet<String> currentContactSet = new HashSet<>();
223 
224         String[] projection = {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP};
225         Cursor c = context.getContentResolver()
226                 .query(Contacts.CONTENT_URI, projection, null, null, null);
227 
228         if (c == null) {
229             Log.d(TAG, "Failed to fetch data from contact database");
230             return;
231         }
232         while (c.moveToNext()) {
233             String contactId = c.getString(0);
234             long lastUpdatedTime = c.getLong(1);
235             if (lastUpdatedTime > sContactsLastUpdated) {
236                 updatedList.add(contactId);
237             }
238             currentContactSet.add(contactId);
239         }
240         int currentContactCount = c.getCount();
241         c.close();
242 
243         if (V) {
244             Log.v(TAG, "updated list =" + updatedList);
245         }
246         String[] dataProjection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE};
247 
248         String whereClause = Data.CONTACT_ID + "=?";
249 
250             /* code to check if new contact/contacts are added */
251         if (currentContactCount > sTotalContacts) {
252             for (String contact : updatedList) {
253                 String[] selectionArgs = {contact};
254                 fetchAndSetContacts(context, handler, dataProjection, whereClause, selectionArgs,
255                         false);
256                 sSecondaryVersionCounter++;
257                 sPrimaryVersionCounter++;
258                 sTotalContacts = currentContactCount;
259             }
260                 /* When contact/contacts are deleted */
261         } else if (currentContactCount < sTotalContacts) {
262             sTotalContacts = currentContactCount;
263             ArrayList<String> svcFields = new ArrayList<>(
264                     Arrays.asList(StructuredName.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE,
265                             Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE));
266             HashSet<String> deletedContacts = new HashSet<>(sContactSet);
267             deletedContacts.removeAll(currentContactSet);
268             sPrimaryVersionCounter += deletedContacts.size();
269             sSecondaryVersionCounter += deletedContacts.size();
270             if (V) {
271                 Log.v(TAG, "Deleted Contacts : " + deletedContacts);
272             }
273 
274             // to decrement totalFields and totalSvcFields count
275             for (String deletedContact : deletedContacts) {
276                 sContactSet.remove(deletedContact);
277                 String[] selectionArgs = {deletedContact};
278                 Cursor dataCursor = context.getContentResolver()
279                         .query(Data.CONTENT_URI, dataProjection, whereClause, selectionArgs, null);
280 
281                 if (dataCursor == null) {
282                     Log.d(TAG, "Failed to fetch data from contact database");
283                     return;
284                 }
285 
286                 while (dataCursor.moveToNext()) {
287                     if (svcFields.contains(
288                             dataCursor.getString(dataCursor.getColumnIndex(Data.MIMETYPE)))) {
289                         sTotalSvcFields--;
290                     }
291                     sTotalFields--;
292                 }
293                 dataCursor.close();
294             }
295 
296                 /* When contacts are updated. i.e. Fields of existing contacts are
297                  * added/updated/deleted */
298         } else {
299             for (String contact : updatedList) {
300                 sPrimaryVersionCounter++;
301                 ArrayList<String> phoneTmp = new ArrayList<>();
302                 ArrayList<String> emailTmp = new ArrayList<>();
303                 ArrayList<String> addressTmp = new ArrayList<>();
304                 String nameTmp = null;
305                 boolean updated = false;
306 
307                 String[] selectionArgs = {contact};
308                 Cursor dataCursor = context.getContentResolver()
309                         .query(Data.CONTENT_URI, dataProjection, whereClause, selectionArgs, null);
310 
311                 if (dataCursor == null) {
312                     Log.d(TAG, "Failed to fetch data from contact database");
313                     return;
314                 }
315                 // fetch all updated contacts and compare with cached copy of contacts
316                 int indexData = dataCursor.getColumnIndex(Data.DATA1);
317                 int indexMimeType = dataCursor.getColumnIndex(Data.MIMETYPE);
318                 String data;
319                 String mimeType;
320                 while (dataCursor.moveToNext()) {
321                     data = dataCursor.getString(indexData);
322                     mimeType = dataCursor.getString(indexMimeType);
323                     switch (mimeType) {
324                         case Email.CONTENT_ITEM_TYPE:
325                             emailTmp.add(data);
326                             break;
327                         case Phone.CONTENT_ITEM_TYPE:
328                             phoneTmp.add(data);
329                             break;
330                         case StructuredPostal.CONTENT_ITEM_TYPE:
331                             addressTmp.add(data);
332                             break;
333                         case StructuredName.CONTENT_ITEM_TYPE:
334                             nameTmp = data;
335                             break;
336                     }
337                 }
338                 ContactData cData = new ContactData(nameTmp, phoneTmp, emailTmp, addressTmp);
339                 dataCursor.close();
340 
341                 ContactData currentContactData = sContactDataset.get(contact);
342                 if (currentContactData == null) {
343                     Log.e(TAG, "Null contact in the updateList: " + contact);
344                     continue;
345                 }
346 
347                 if (!Objects.equals(nameTmp, currentContactData.mName)) {
348                     updated = true;
349                 } else if (checkFieldUpdates(currentContactData.mPhone, phoneTmp)) {
350                     updated = true;
351                 } else if (checkFieldUpdates(currentContactData.mEmail, emailTmp)) {
352                     updated = true;
353                 } else if (checkFieldUpdates(currentContactData.mAddress, addressTmp)) {
354                     updated = true;
355                 }
356 
357                 if (updated) {
358                     sSecondaryVersionCounter++;
359                     sContactDataset.put(contact, cData);
360                 }
361             }
362         }
363 
364         Log.d(TAG,
365                 "primaryVersionCounter = " + sPrimaryVersionCounter + ", secondaryVersionCounter="
366                         + sSecondaryVersionCounter);
367 
368         // check if Primary/Secondary version Counter has rolled over
369         if (sSecondaryVersionCounter < 0 || sPrimaryVersionCounter < 0) {
370             handler.sendMessage(handler.obtainMessage(BluetoothPbapService.ROLLOVER_COUNTERS));
371         }
372     }
373 
374     /* checkFieldUpdates checks update contact fields of a particular contact.
375      * Field update can be a field updated/added/deleted in an existing contact.
376      * Returns true if any contact field is updated else return false. */
checkFieldUpdates(ArrayList<String> oldFields, ArrayList<String> newFields)377     private static boolean checkFieldUpdates(ArrayList<String> oldFields,
378             ArrayList<String> newFields) {
379         if (newFields != null && oldFields != null) {
380             if (newFields.size() != oldFields.size()) {
381                 sTotalSvcFields += Math.abs(newFields.size() - oldFields.size());
382                 sTotalFields += Math.abs(newFields.size() - oldFields.size());
383                 return true;
384             }
385             for (String newField : newFields) {
386                 if (!oldFields.contains(newField)) {
387                     return true;
388                 }
389             }
390             /* when all fields of type(phone/email/address) are deleted in a given contact*/
391         } else if (newFields == null && oldFields != null && oldFields.size() > 0) {
392             sTotalSvcFields += oldFields.size();
393             sTotalFields += oldFields.size();
394             return true;
395 
396             /* when new fields are added for a type(phone/email/address) in a contact
397              * for which there were no fields of this type earliar.*/
398         } else if (oldFields == null && newFields != null && newFields.size() > 0) {
399             sTotalSvcFields += newFields.size();
400             sTotalFields += newFields.size();
401             return true;
402         }
403         return false;
404     }
405 
406     /* fetchAndSetContacts reads contacts and caches them
407      * isLoad = true indicates its loading all contacts
408      * isLoad = false indiacates its caching recently added contact in database*/
fetchAndSetContacts(Context context, Handler handler, String[] projection, String whereClause, String[] selectionArgs, boolean isLoad)409     private static int fetchAndSetContacts(Context context, Handler handler, String[] projection,
410             String whereClause, String[] selectionArgs, boolean isLoad) {
411         long currentTotalFields = 0, currentSvcFieldCount = 0;
412         Cursor c = context.getContentResolver()
413                 .query(Data.CONTENT_URI, projection, whereClause, selectionArgs, null);
414 
415         /* send delayed message to loadContact when ContentResolver is unable
416          * to fetch data from contact database using the specified URI at that
417          * moment (Case: immediate Pbap connect on system boot with BT ON)*/
418         if (c == null) {
419             Log.d(TAG, "Failed to fetch contacts data from database..");
420             if (isLoad) {
421                 handler.sendMessageDelayed(
422                         handler.obtainMessage(BluetoothPbapService.LOAD_CONTACTS),
423                         QUERY_CONTACT_RETRY_INTERVAL);
424             }
425             return -1;
426         }
427 
428         int indexCId = c.getColumnIndex(Data.CONTACT_ID);
429         int indexData = c.getColumnIndex(Data.DATA1);
430         int indexMimeType = c.getColumnIndex(Data.MIMETYPE);
431         String contactId, data, mimeType;
432 
433         while (c.moveToNext()) {
434             if (c.isNull(indexCId)) {
435                 Log.w(TAG, "_id column is null. Row was deleted during iteration, skipping");
436                 continue;
437             }
438             contactId = c.getString(indexCId);
439             data = c.getString(indexData);
440             mimeType = c.getString(indexMimeType);
441             /* fetch phone/email/address/name information of the contact */
442             switch (mimeType) {
443                 case Phone.CONTENT_ITEM_TYPE:
444                     setContactFields(TYPE_PHONE, contactId, data);
445                     currentSvcFieldCount++;
446                     break;
447                 case Email.CONTENT_ITEM_TYPE:
448                     setContactFields(TYPE_EMAIL, contactId, data);
449                     currentSvcFieldCount++;
450                     break;
451                 case StructuredPostal.CONTENT_ITEM_TYPE:
452                     setContactFields(TYPE_ADDRESS, contactId, data);
453                     currentSvcFieldCount++;
454                     break;
455                 case StructuredName.CONTENT_ITEM_TYPE:
456                     setContactFields(TYPE_NAME, contactId, data);
457                     currentSvcFieldCount++;
458                     break;
459             }
460             sContactSet.add(contactId);
461             currentTotalFields++;
462         }
463         c.close();
464 
465         /* This code checks if there is any update in contacts after last pbap
466          * disconnect has happenned (even if BT is turned OFF during this time)*/
467         if (isLoad && currentTotalFields != sTotalFields) {
468             sPrimaryVersionCounter += Math.abs(sTotalContacts - sContactSet.size());
469 
470             if (currentSvcFieldCount != sTotalSvcFields) {
471                 if (sTotalContacts != sContactSet.size()) {
472                     sSecondaryVersionCounter += Math.abs(sTotalContacts - sContactSet.size());
473                 } else {
474                     sSecondaryVersionCounter++;
475                 }
476             }
477             if (sPrimaryVersionCounter < 0 || sSecondaryVersionCounter < 0) {
478                 rolloverCounters();
479             }
480 
481             sTotalFields = currentTotalFields;
482             sTotalSvcFields = currentSvcFieldCount;
483             sContactsLastUpdated = System.currentTimeMillis();
484             Log.d(TAG, "Contacts updated between last BT OFF and current"
485                     + "Pbap Connect, primaryVersionCounter=" + sPrimaryVersionCounter
486                     + ", secondaryVersionCounter=" + sSecondaryVersionCounter);
487         } else if (!isLoad) {
488             sTotalFields++;
489             sTotalSvcFields++;
490         }
491         return sContactSet.size();
492     }
493 
494     /* setContactFields() is used to store contacts data in local cache (phone,
495      * email or address which is required for updating Secondary Version counter).
496      * contactsFieldData - List of field data for phone/email/address.
497      * contactId - Contact ID, data1 - field value from data table for phone/email/address*/
498 
setContactFields(String fieldType, String contactId, String data)499     private static void setContactFields(String fieldType, String contactId, String data) {
500         ContactData cData;
501         if (sContactDataset.containsKey(contactId)) {
502             cData = sContactDataset.get(contactId);
503         } else {
504             cData = new ContactData();
505         }
506 
507         switch (fieldType) {
508             case TYPE_NAME:
509                 cData.mName = data;
510                 break;
511             case TYPE_PHONE:
512                 cData.mPhone.add(data);
513                 break;
514             case TYPE_EMAIL:
515                 cData.mEmail.add(data);
516                 break;
517             case TYPE_ADDRESS:
518                 cData.mAddress.add(data);
519                 break;
520         }
521         sContactDataset.put(contactId, cData);
522     }
523 
524     /* As per Pbap 1.2 specification, Database Identifies shall be
525      * re-generated when a Folder Version Counter rolls over or starts over.*/
526 
rolloverCounters()527     static void rolloverCounters() {
528         sDbIdentifier.set(Calendar.getInstance().getTimeInMillis());
529         sPrimaryVersionCounter = (sPrimaryVersionCounter < 0) ? 0 : sPrimaryVersionCounter;
530         sSecondaryVersionCounter = (sSecondaryVersionCounter < 0) ? 0 : sSecondaryVersionCounter;
531         if (V) {
532             Log.v(TAG, "DbIdentifier rolled over to:" + sDbIdentifier);
533         }
534     }
535 }
536