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