BluetoothPbapUtils.java revision 65a6b0e556d87a4f749105b4e0bc9e16c23b74fd
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.database.Cursor;
24import android.net.Uri;
25import android.os.Handler;
26import android.preference.PreferenceManager;
27import android.provider.ContactsContract.CommonDataKinds.Email;
28import android.provider.ContactsContract.CommonDataKinds.Phone;
29import android.provider.ContactsContract.CommonDataKinds.StructuredName;
30import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
31import android.provider.ContactsContract.Contacts;
32import android.provider.ContactsContract.Data;
33import android.provider.ContactsContract.Profile;
34import android.provider.ContactsContract.RawContactsEntity;
35import android.util.Log;
36
37import com.android.vcard.VCardComposer;
38import com.android.vcard.VCardConfig;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Calendar;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.Objects;
46import java.util.concurrent.atomic.AtomicLong;
47
48class 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
74        ContactData() {
75            mPhone = new ArrayList<>();
76            mEmail = new ArrayList<>();
77            mAddress = new ArrayList<>();
78        }
79
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
98    private static boolean hasFilter(byte[] filter) {
99        return filter != null && filter.length > 0;
100    }
101
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
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
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
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
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.*/
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
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
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. */
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*/
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        while (c.moveToNext()) {
433            contactId = c.getString(indexCId);
434            data = c.getString(indexData);
435            mimeType = c.getString(indexMimeType);
436            /* fetch phone/email/address/name information of the contact */
437            switch (mimeType) {
438                case Phone.CONTENT_ITEM_TYPE:
439                    setContactFields(TYPE_PHONE, contactId, data);
440                    currentSvcFieldCount++;
441                    break;
442                case Email.CONTENT_ITEM_TYPE:
443                    setContactFields(TYPE_EMAIL, contactId, data);
444                    currentSvcFieldCount++;
445                    break;
446                case StructuredPostal.CONTENT_ITEM_TYPE:
447                    setContactFields(TYPE_ADDRESS, contactId, data);
448                    currentSvcFieldCount++;
449                    break;
450                case StructuredName.CONTENT_ITEM_TYPE:
451                    setContactFields(TYPE_NAME, contactId, data);
452                    currentSvcFieldCount++;
453                    break;
454            }
455            sContactSet.add(contactId);
456            currentTotalFields++;
457        }
458        c.close();
459
460        /* This code checks if there is any update in contacts after last pbap
461         * disconnect has happenned (even if BT is turned OFF during this time)*/
462        if (isLoad && currentTotalFields != sTotalFields) {
463            sPrimaryVersionCounter += Math.abs(sTotalContacts - sContactSet.size());
464
465            if (currentSvcFieldCount != sTotalSvcFields) {
466                if (sTotalContacts != sContactSet.size()) {
467                    sSecondaryVersionCounter += Math.abs(sTotalContacts - sContactSet.size());
468                } else {
469                    sSecondaryVersionCounter++;
470                }
471            }
472            if (sPrimaryVersionCounter < 0 || sSecondaryVersionCounter < 0) {
473                rolloverCounters();
474            }
475
476            sTotalFields = currentTotalFields;
477            sTotalSvcFields = currentSvcFieldCount;
478            sContactsLastUpdated = System.currentTimeMillis();
479            Log.d(TAG, "Contacts updated between last BT OFF and current"
480                    + "Pbap Connect, primaryVersionCounter=" + sPrimaryVersionCounter
481                    + ", secondaryVersionCounter=" + sSecondaryVersionCounter);
482        } else if (!isLoad) {
483            sTotalFields++;
484            sTotalSvcFields++;
485        }
486        return sContactSet.size();
487    }
488
489    /* setContactFields() is used to store contacts data in local cache (phone,
490     * email or address which is required for updating Secondary Version counter).
491     * contactsFieldData - List of field data for phone/email/address.
492     * contactId - Contact ID, data1 - field value from data table for phone/email/address*/
493
494    private static void setContactFields(String fieldType, String contactId, String data) {
495        ContactData cData;
496        if (sContactDataset.containsKey(contactId)) {
497            cData = sContactDataset.get(contactId);
498        } else {
499            cData = new ContactData();
500        }
501
502        switch (fieldType) {
503            case TYPE_NAME:
504                cData.mName = data;
505                break;
506            case TYPE_PHONE:
507                cData.mPhone.add(data);
508                break;
509            case TYPE_EMAIL:
510                cData.mEmail.add(data);
511                break;
512            case TYPE_ADDRESS:
513                cData.mAddress.add(data);
514                break;
515        }
516        sContactDataset.put(contactId, cData);
517    }
518
519    /* As per Pbap 1.2 specification, Database Identifies shall be
520     * re-generated when a Folder Version Counter rolls over or starts over.*/
521
522    static void rolloverCounters() {
523        sDbIdentifier.set(Calendar.getInstance().getTimeInMillis());
524        sPrimaryVersionCounter = (sPrimaryVersionCounter < 0) ? 0 : sPrimaryVersionCounter;
525        sSecondaryVersionCounter = (sSecondaryVersionCounter < 0) ? 0 : sSecondaryVersionCounter;
526        if (V) {
527            Log.v(TAG, "DbIdentifier rolled over to:" + sDbIdentifier);
528        }
529    }
530}
531