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