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