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