blob: 45f2e210be69a0179b0c880f388dd457a301ae7c [file] [log] [blame]
/************************************************************************************
*
* Copyright (C) 2009-2012 Broadcom Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
************************************************************************************/
package com.android.bluetooth.pbap;
import android.content.Context;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.RawContactsEntity;
import android.util.Log;
import com.android.vcard.VCardComposer;
import com.android.vcard.VCardConfig;
import com.android.bluetooth.Utils;
import com.android.bluetooth.pbap.BluetoothPbapService;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.Math;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
public class BluetoothPbapUtils {
private static final String TAG = "BluetoothPbapUtils";
private static final boolean V = BluetoothPbapService.VERBOSE;
public static int FILTER_PHOTO = 3;
public static int FILTER_TEL = 7;
public static int FILTER_NICKNAME = 23;
private static final long QUERY_CONTACT_RETRY_INTERVAL = 4000;
protected static AtomicLong mDbIdentifier = new AtomicLong();
protected static long primaryVersionCounter = 0;
protected static long secondaryVersionCounter = 0;
public static long totalContacts = 0;
/* totalFields and totalSvcFields used to update primary/secondary version
* counter between pbap sessions*/
public static long totalFields = 0;
public static long totalSvcFields = 0;
public static long contactsLastUpdated = 0;
public static boolean contactsLoaded = false;
private static class ContactData {
private String name;
private ArrayList<String> email;
private ArrayList<String> phone;
private ArrayList<String> address;
public ContactData() {
phone = new ArrayList<String>();
email = new ArrayList<String>();
address = new ArrayList<String>();
}
public ContactData(String name, ArrayList<String> phone, ArrayList<String> email,
ArrayList<String> address) {
this.name = name;
this.phone = phone;
this.email = email;
this.address = address;
}
}
private static HashMap<String, ContactData> contactDataset = new HashMap<String, ContactData>();
private static HashSet<String> ContactSet = new HashSet<String>();
private static final String TYPE_NAME = "name";
private static final String TYPE_PHONE = "phone";
private static final String TYPE_EMAIL = "email";
private static final String TYPE_ADDRESS = "address";
public static boolean hasFilter(byte[] filter) {
return filter != null && filter.length > 0;
}
public static boolean isNameAndNumberOnly(byte[] filter) {
// For vcard 2.0: VERSION,N,TEL is mandatory
// For vcard 3.0, VERSION,N,FN,TEL is mandatory
// So we only need to make sure that no other fields except optionally
// NICKNAME is set
// Check that an explicit filter is not set. If not, this means
// return everything
if (!hasFilter(filter)) {
Log.v(TAG, "No filter set. isNameAndNumberOnly=false");
return false;
}
// Check bytes 0-4 are all 0
for (int i = 0; i <= 4; i++) {
if (filter[i] != 0) {
return false;
}
}
// On byte 5, only BIT_NICKNAME can be set, so make sure
// rest of bits are not set
if ((filter[5] & 0x7F) > 0) {
return false;
}
// Check byte 6 is not set
if (filter[6] != 0) {
return false;
}
// Check if bit#3-6 is set. Return false if so.
if ((filter[7] & 0x78) > 0) {
return false;
}
return true;
}
public static boolean isFilterBitSet(byte[] filter, int filterBit) {
if (hasFilter(filter)) {
int byteNumber = 7 - filterBit / 8;
int bitNumber = filterBit % 8;
if (byteNumber < filter.length) {
return (filter[byteNumber] & (1 << bitNumber)) > 0;
}
}
return false;
}
public static VCardComposer createFilteredVCardComposer(final Context ctx,
final int vcardType, final byte[] filter) {
int vType = vcardType;
boolean includePhoto = BluetoothPbapConfig.includePhotosInVcard()
&& (!hasFilter(filter) || isFilterBitSet(filter, FILTER_PHOTO));
if (!includePhoto) {
if (V) Log.v(TAG, "Excluding images from VCardComposer...");
vType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
}
return new VCardComposer(ctx, vType, true);
}
public static boolean isProfileSet(Context context) {
Cursor c = context.getContentResolver().query(
Profile.CONTENT_VCARD_URI, new String[] { Profile._ID }, null,
null, null);
boolean isSet = (c != null && c.getCount() > 0);
if (c != null) {
c.close();
c = null;
}
return isSet;
}
public static String getProfileName(Context context) {
Cursor c = context.getContentResolver().query(
Profile.CONTENT_URI, new String[] { Profile.DISPLAY_NAME}, null,
null, null);
String ownerName =null;
if (c!= null && c.moveToFirst()) {
ownerName = c.getString(0);
}
if (c != null) {
c.close();
c = null;
}
return ownerName;
}
public static final String createProfileVCard(Context ctx, final int vcardType,final byte[] filter) {
VCardComposer composer = null;
String vcard = null;
try {
composer = createFilteredVCardComposer(ctx, vcardType, filter);
if (composer
.init(Profile.CONTENT_URI, null, null, null, null, Uri
.withAppendedPath(Profile.CONTENT_URI,
RawContactsEntity.CONTENT_URI
.getLastPathSegment()))) {
vcard = composer.createOneEntry();
} else {
Log.e(TAG,
"Unable to create profile vcard. Error initializing composer: "
+ composer.getErrorReason());
}
} catch (Throwable t) {
Log.e(TAG, "Unable to create profile vcard.", t);
}
if (composer != null) {
try {
composer.terminate();
} catch (Throwable t) {
}
}
return vcard;
}
public static boolean createProfileVCardFile(File file, Context context) {
FileInputStream is = null;
FileOutputStream os = null;
boolean success = true;
try {
AssetFileDescriptor fd = context.getContentResolver()
.openAssetFileDescriptor(Profile.CONTENT_VCARD_URI, "r");
if(fd == null)
{
return false;
}
is = fd.createInputStream();
os = new FileOutputStream(file);
Utils.copyStream(is, os, 200);
} catch (Throwable t) {
Log.e(TAG, "Unable to create default contact vcard file", t);
success = false;
}
Utils.safeCloseStream(is);
Utils.safeCloseStream(os);
return success;
}
protected static void savePbapParams(Context ctx, long primaryCounter, long secondaryCounter,
long dbIdentifier, long lastUpdatedTimestamp, long totalFields, long totalSvcFields,
long totalContacts) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx);
Editor edit = pref.edit();
edit.putLong("primary", primaryCounter);
edit.putLong("secondary", secondaryCounter);
edit.putLong("dbIdentifier", dbIdentifier);
edit.putLong("totalContacts", totalContacts);
edit.putLong("lastUpdatedTimestamp", lastUpdatedTimestamp);
edit.putLong("totalFields", totalFields);
edit.putLong("totalSvcFields", totalSvcFields);
edit.apply();
if (V)
Log.v(TAG, "Saved Primary:" + primaryCounter + ", Secondary:" + secondaryCounter
+ ", Database Identifier: " + dbIdentifier);
}
/* fetchPbapParams() loads preserved value of Database Identifiers and folder
* version counters. Servers using a database identifier 0 or regenerating
* one at each connection will not benefit from the resulting performance and
* user experience improvements. So database identifier is set with current
* timestamp and updated on rollover of folder version counter.*/
protected static void fetchPbapParams(Context ctx) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ctx);
long timeStamp = Calendar.getInstance().getTimeInMillis();
BluetoothPbapUtils.mDbIdentifier.set(pref.getLong("mDbIdentifier", timeStamp));
BluetoothPbapUtils.primaryVersionCounter = pref.getLong("primary", 0);
BluetoothPbapUtils.secondaryVersionCounter = pref.getLong("secondary", 0);
BluetoothPbapUtils.totalFields = pref.getLong("totalContacts", 0);
BluetoothPbapUtils.contactsLastUpdated = pref.getLong("lastUpdatedTimestamp", timeStamp);
BluetoothPbapUtils.totalFields = pref.getLong("totalFields", 0);
BluetoothPbapUtils.totalSvcFields = pref.getLong("totalSvcFields", 0);
if (V) Log.v(TAG, " fetchPbapParams " + pref.getAll());
}
/* loadAllContacts() fetches data like name,phone,email or addrees related to
* all contacts. It is required to determine which field of the contact is
* added/updated/deleted to increment secondary version counter accordingly.*/
protected static void loadAllContacts(Context mContext, Handler mHandler) {
if (V) Log.v(TAG, "Loading Contacts ...");
try {
String[] projection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE};
int contactCount = 0;
if ((contactCount = fetchAndSetContacts(
mContext, mHandler, projection, null, null, true))
< 0)
return;
totalContacts = contactCount; // to set total contacts count fetched on Connect
contactsLoaded = true;
} catch (Exception e) {
Log.e(TAG, "Exception occurred in load contacts: " + e);
}
}
protected static void updateSecondaryVersionCounter(Context mContext, Handler mHandler) {
try {
/* updated_list stores list of contacts which are added/updated after
* the time when contacts were last updated. (contactsLastUpdated
* indicates the time when contact/contacts were last updated and
* corresponding changes were reflected in Folder Version Counters).*/
ArrayList<String> updated_list = new ArrayList<String>();
HashSet<String> currentContactSet = new HashSet<String>();
int currentContactCount = 0;
String[] projection = {Contacts._ID, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP};
Cursor c = mContext.getContentResolver().query(
Contacts.CONTENT_URI, projection, null, null, null);
if (c == null) {
Log.d(TAG, "Failed to fetch data from contact database");
return;
}
while (c.moveToNext()) {
String contactId = c.getString(0);
long lastUpdatedTime = c.getLong(1);
if (lastUpdatedTime > contactsLastUpdated) {
updated_list.add(contactId);
}
currentContactSet.add(contactId);
}
currentContactCount = c.getCount();
c.close();
if (V) Log.v(TAG, "updated list =" + updated_list);
String[] dataProjection = {Data.CONTACT_ID, Data.DATA1, Data.MIMETYPE};
String whereClause = Data.CONTACT_ID + "=?";
/* code to check if new contact/contacts are added */
if (currentContactCount > totalContacts) {
for (int i = 0; i < updated_list.size(); i++) {
String[] selectionArgs = {updated_list.get(i)};
fetchAndSetContacts(
mContext, mHandler, dataProjection, whereClause, selectionArgs, false);
secondaryVersionCounter++;
primaryVersionCounter++;
totalContacts = currentContactCount;
}
/* When contact/contacts are deleted */
} else if (currentContactCount < totalContacts) {
totalContacts = currentContactCount;
ArrayList<String> svcFields = new ArrayList<String>(
Arrays.asList(StructuredName.CONTENT_ITEM_TYPE, Phone.CONTENT_ITEM_TYPE,
Email.CONTENT_ITEM_TYPE, StructuredPostal.CONTENT_ITEM_TYPE));
HashSet<String> deletedContacts = new HashSet<String>(ContactSet);
deletedContacts.removeAll(currentContactSet);
primaryVersionCounter += deletedContacts.size();
secondaryVersionCounter += deletedContacts.size();
if (V) Log.v(TAG, "Deleted Contacts : " + deletedContacts);
// to decrement totalFields and totalSvcFields count
for (String deletedContact : deletedContacts) {
ContactSet.remove(deletedContact);
String[] selectionArgs = {deletedContact};
Cursor dataCursor = mContext.getContentResolver().query(
Data.CONTENT_URI, dataProjection, whereClause, selectionArgs, null);
if (dataCursor == null) {
Log.d(TAG, "Failed to fetch data from contact database");
return;
}
while (dataCursor.moveToNext()) {
if (svcFields.contains(
dataCursor.getString(dataCursor.getColumnIndex(Data.MIMETYPE))))
totalSvcFields--;
totalFields--;
}
dataCursor.close();
}
/* When contacts are updated. i.e. Fields of existing contacts are
* added/updated/deleted */
} else {
for (int i = 0; i < updated_list.size(); i++) {
primaryVersionCounter++;
ArrayList<String> phone_tmp = new ArrayList<String>();
ArrayList<String> email_tmp = new ArrayList<String>();
ArrayList<String> address_tmp = new ArrayList<String>();
String name_tmp = null, updatedCID = updated_list.get(i);
boolean updated = false;
String[] selectionArgs = {updated_list.get(i)};
Cursor dataCursor = mContext.getContentResolver().query(
Data.CONTENT_URI, dataProjection, whereClause, selectionArgs, null);
if (dataCursor == null) {
Log.d(TAG, "Failed to fetch data from contact database");
return;
}
// fetch all updated contacts and compare with cached copy of contacts
int indexData = dataCursor.getColumnIndex(Data.DATA1);
int indexMimeType = dataCursor.getColumnIndex(Data.MIMETYPE);
String data;
String mimeType;
while (dataCursor.moveToNext()) {
data = dataCursor.getString(indexData);
mimeType = dataCursor.getString(indexMimeType);
switch (mimeType) {
case Email.CONTENT_ITEM_TYPE:
email_tmp.add(data);
break;
case Phone.CONTENT_ITEM_TYPE:
phone_tmp.add(data);
break;
case StructuredPostal.CONTENT_ITEM_TYPE:
address_tmp.add(data);
break;
case StructuredName.CONTENT_ITEM_TYPE:
name_tmp = new String(data);
break;
}
}
ContactData cData =
new ContactData(name_tmp, phone_tmp, email_tmp, address_tmp);
dataCursor.close();
if ((name_tmp == null && contactDataset.get(updatedCID).name != null)
|| (name_tmp != null && contactDataset.get(updatedCID).name == null)
|| (!(name_tmp == null && contactDataset.get(updatedCID).name == null)
&& !name_tmp.equals(contactDataset.get(updatedCID).name))) {
updated = true;
} else if (checkFieldUpdates(contactDataset.get(updatedCID).phone, phone_tmp)) {
updated = true;
} else if (checkFieldUpdates(contactDataset.get(updatedCID).email, email_tmp)) {
updated = true;
} else if (checkFieldUpdates(
contactDataset.get(updatedCID).address, address_tmp)) {
updated = true;
}
if (updated) {
secondaryVersionCounter++;
contactDataset.put(updatedCID, cData);
}
}
}
Log.d(TAG, "primaryVersionCounter = " + primaryVersionCounter
+ ", secondaryVersionCounter=" + secondaryVersionCounter);
// check if Primary/Secondary version Counter has rolled over
if (secondaryVersionCounter < 0 || primaryVersionCounter < 0)
mHandler.sendMessage(
mHandler.obtainMessage(BluetoothPbapService.ROLLOVER_COUNTERS));
} catch (Exception e) {
Log.e(TAG, "Exception while updating secondary version counter:" + e);
}
}
/* checkFieldUpdates checks update contact fields of a particular contact.
* Field update can be a field updated/added/deleted in an existing contact.
* Returns true if any contact field is updated else return false. */
protected static boolean checkFieldUpdates(
ArrayList<String> oldFields, ArrayList<String> newFields) {
if (newFields != null && oldFields != null) {
if (newFields.size() != oldFields.size()) {
totalSvcFields += Math.abs(newFields.size() - oldFields.size());
totalFields += Math.abs(newFields.size() - oldFields.size());
return true;
}
for (int i = 0; i < newFields.size(); i++) {
if (!oldFields.contains(newFields.get(i))) {
return true;
}
}
/* when all fields of type(phone/email/address) are deleted in a given contact*/
} else if (newFields == null && oldFields != null && oldFields.size() > 0) {
totalSvcFields += oldFields.size();
totalFields += oldFields.size();
return true;
/* when new fields are added for a type(phone/email/address) in a contact
* for which there were no fields of this type earliar.*/
} else if (oldFields == null && newFields != null && newFields.size() > 0) {
totalSvcFields += newFields.size();
totalFields += newFields.size();
return true;
}
return false;
}
/* fetchAndSetContacts reads contacts and caches them
* isLoad = true indicates its loading all contacts
* isLoad = false indiacates its caching recently added contact in database*/
protected static int fetchAndSetContacts(Context mContext, Handler mHandler,
String[] projection, String whereClause, String[] selectionArgs, boolean isLoad) {
long currentTotalFields = 0, currentSvcFieldCount = 0;
Cursor c = mContext.getContentResolver().query(
Data.CONTENT_URI, projection, whereClause, selectionArgs, null);
/* send delayed message to loadContact when ContentResolver is unable
* to fetch data from contact database using the specified URI at that
* moment (Case: immediate Pbap connect on system boot with BT ON)*/
if (c == null) {
Log.d(TAG, "Failed to fetch contacts data from database..");
if (isLoad)
mHandler.sendMessageDelayed(
mHandler.obtainMessage(BluetoothPbapService.LOAD_CONTACTS),
QUERY_CONTACT_RETRY_INTERVAL);
return -1;
}
int indexCId = c.getColumnIndex(Data.CONTACT_ID);
int indexData = c.getColumnIndex(Data.DATA1);
int indexMimeType = c.getColumnIndex(Data.MIMETYPE);
String contactId, data, mimeType;
while (c.moveToNext()) {
contactId = c.getString(indexCId);
data = c.getString(indexData);
mimeType = c.getString(indexMimeType);
/* fetch phone/email/address/name information of the contact */
switch (mimeType) {
case Phone.CONTENT_ITEM_TYPE:
setContactFields(TYPE_PHONE, contactId, data);
currentSvcFieldCount++;
break;
case Email.CONTENT_ITEM_TYPE:
setContactFields(TYPE_EMAIL, contactId, data);
currentSvcFieldCount++;
break;
case StructuredPostal.CONTENT_ITEM_TYPE:
setContactFields(TYPE_ADDRESS, contactId, data);
currentSvcFieldCount++;
break;
case StructuredName.CONTENT_ITEM_TYPE:
setContactFields(TYPE_NAME, contactId, data);
currentSvcFieldCount++;
break;
}
ContactSet.add(contactId);
currentTotalFields++;
}
c.close();
/* This code checks if there is any update in contacts after last pbap
* disconnect has happenned (even if BT is turned OFF during this time)*/
if (isLoad && currentTotalFields != totalFields) {
primaryVersionCounter += Math.abs(totalContacts - ContactSet.size());
if (currentSvcFieldCount != totalSvcFields)
if (totalContacts != ContactSet.size())
secondaryVersionCounter += Math.abs(totalContacts - ContactSet.size());
else
secondaryVersionCounter++;
if (primaryVersionCounter < 0 || secondaryVersionCounter < 0) rolloverCounters();
totalFields = currentTotalFields;
totalSvcFields = currentSvcFieldCount;
contactsLastUpdated = System.currentTimeMillis();
Log.d(TAG, "Contacts updated between last BT OFF and current"
+ "Pbap Connect, primaryVersionCounter=" + primaryVersionCounter
+ ", secondaryVersionCounter=" + secondaryVersionCounter);
} else if (!isLoad) {
totalFields++;
totalSvcFields++;
}
return ContactSet.size();
}
/* setContactFields() is used to store contacts data in local cache (phone,
* email or address which is required for updating Secondary Version counter).
* contactsFieldData - List of field data for phone/email/address.
* contactId - Contact ID, data1 - field value from data table for phone/email/address*/
protected static void setContactFields(String fieldType, String contactId, String data) {
ContactData cData = null;
if (contactDataset.containsKey(contactId))
cData = contactDataset.get(contactId);
else
cData = new ContactData();
switch (fieldType) {
case TYPE_NAME:
cData.name = data;
break;
case TYPE_PHONE:
cData.phone.add(data);
break;
case TYPE_EMAIL:
cData.email.add(data);
break;
case TYPE_ADDRESS:
cData.address.add(data);
break;
}
contactDataset.put(contactId, cData);
}
/* As per Pbap 1.2 specification, Database Identifies shall be
* re-generated when a Folder Version Counter rolls over or starts over.*/
protected static void rolloverCounters() {
mDbIdentifier.set(Calendar.getInstance().getTimeInMillis());
primaryVersionCounter = (primaryVersionCounter < 0) ? 0 : primaryVersionCounter;
secondaryVersionCounter = (secondaryVersionCounter < 0) ? 0 : secondaryVersionCounter;
if (V) Log.v(TAG, "mDbIdentifier rolled over to:" + mDbIdentifier);
}
}