Home | History | Annotate | Download | only in pbapclient
      1 package com.android.bluetooth.pbapclient;
      2 
      3 import com.android.vcard.VCardEntry;
      4 
      5 import android.accounts.Account;
      6 import com.android.bluetooth.pbapclient.BluetoothPbapClient;
      7 import android.content.ContentProviderOperation;
      8 import android.content.Context;
      9 import android.content.OperationApplicationException;
     10 import android.provider.ContactsContract;
     11 import android.database.Cursor;
     12 import android.net.Uri;
     13 import android.os.Bundle;
     14 import android.os.RemoteException;
     15 import android.provider.ContactsContract.Data;
     16 import android.provider.ContactsContract.RawContacts;
     17 import android.provider.ContactsContract.RawContactsEntity;
     18 import android.provider.ContactsContract.Contacts.Entity;
     19 import android.provider.ContactsContract.CommonDataKinds.Phone;
     20 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     21 import android.util.Log;
     22 
     23 import com.android.vcard.VCardEntry;
     24 
     25 import java.lang.InterruptedException;
     26 import java.util.ArrayList;
     27 import java.util.HashMap;
     28 import java.util.List;
     29 
     30 public class PhonebookPullRequest extends PullRequest {
     31     private static final int MAX_OPS = 200;
     32     private static final boolean DBG = true;
     33     private static final String TAG = "PbapPhonebookPullRequest";
     34 
     35     private final Account mAccount;
     36     private final Context mContext;
     37     public boolean complete = false;
     38 
     39     public PhonebookPullRequest(Context context, Account account) {
     40         mContext = context;
     41         mAccount = account;
     42         path = BluetoothPbapClient.PB_PATH;
     43     }
     44 
     45     private PhonebookEntry fetchContact(String id) {
     46         PhonebookEntry entry = new PhonebookEntry();
     47         entry.id = id;
     48         Cursor c = null;
     49         try {
     50             c = mContext.getContentResolver().query(
     51                     Data.CONTENT_URI,
     52                     null,
     53                     Data.RAW_CONTACT_ID + " = ?",
     54                     new String[] { id },
     55                     null);
     56             if (c != null) {
     57                 int mimeTypeIndex = c.getColumnIndex(Data.MIMETYPE);
     58                 int familyNameIndex = c.getColumnIndex(StructuredName.FAMILY_NAME);
     59                 int givenNameIndex = c.getColumnIndex(StructuredName.GIVEN_NAME);
     60                 int middleNameIndex = c.getColumnIndex(StructuredName.MIDDLE_NAME);
     61                 int prefixIndex = c.getColumnIndex(StructuredName.PREFIX);
     62                 int suffixIndex = c.getColumnIndex(StructuredName.SUFFIX);
     63 
     64                 int phoneTypeIndex = c.getColumnIndex(Phone.TYPE);
     65                 int phoneNumberIndex = c.getColumnIndex(Phone.NUMBER);
     66 
     67                 while (c.moveToNext()) {
     68                     String mimeType = c.getString(mimeTypeIndex);
     69                     if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
     70                         entry.name.family = c.getString(familyNameIndex);
     71                         entry.name.given = c.getString(givenNameIndex);
     72                         entry.name.middle = c.getString(middleNameIndex);
     73                         entry.name.prefix = c.getString(prefixIndex);
     74                         entry.name.suffix = c.getString(suffixIndex);
     75                     } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
     76                         PhonebookEntry.Phone p = new PhonebookEntry.Phone();
     77                         p.type = c.getInt(phoneTypeIndex);
     78                         p.number = c.getString(phoneNumberIndex);
     79                         entry.phones.add(p);
     80                     }
     81                 }
     82             }
     83         } finally {
     84             if (c != null) {
     85                 c.close();
     86             }
     87         }
     88         return entry;
     89     }
     90 
     91     private HashMap<PhonebookEntry.Name, PhonebookEntry> fetchExistingContacts() {
     92         HashMap<PhonebookEntry.Name, PhonebookEntry> entries = new HashMap<>();
     93 
     94         Cursor c = null;
     95         try {
     96             // First find all the contacts present. Fetch all rows.
     97             Uri uri = RawContacts.CONTENT_URI.buildUpon()
     98                     .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.name)
     99                     .appendQueryParameter(RawContacts.ACCOUNT_TYPE, mAccount.type)
    100                     .build();
    101             // First get all the raw contact ids.
    102             c = mContext.getContentResolver().query(uri,
    103                     new String[]  { RawContacts._ID },
    104                     null, null, null);
    105 
    106             if (c != null) {
    107                 while (c.moveToNext()) {
    108                     // For each raw contact id, fetch all the data.
    109                     PhonebookEntry e = fetchContact(c.getString(0));
    110                     entries.put(e.name, e);
    111                 }
    112             }
    113         } finally {
    114             if (c != null) {
    115                 c.close();
    116             }
    117         }
    118 
    119         return entries;
    120     }
    121 
    122     private void addContacts(List<PhonebookEntry> entries)
    123             throws RemoteException, OperationApplicationException, InterruptedException {
    124         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
    125         for (PhonebookEntry e : entries) {
    126             if (Thread.currentThread().isInterrupted()) {
    127                 throw new InterruptedException();
    128             }
    129             int index = ops.size();
    130             // Add an entry.
    131             ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
    132                     .withValue(RawContacts.ACCOUNT_TYPE, mAccount.type)
    133                     .withValue(RawContacts.ACCOUNT_NAME, mAccount.name)
    134                     .build());
    135 
    136             // Populate the name.
    137             ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
    138                     .withValueBackReference(Data.RAW_CONTACT_ID, index)
    139                     .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
    140                     .withValue(StructuredName.FAMILY_NAME , e.name.family)
    141                     .withValue(StructuredName.GIVEN_NAME , e.name.given)
    142                     .withValue(StructuredName.MIDDLE_NAME , e.name.middle)
    143                     .withValue(StructuredName.PREFIX , e.name.prefix)
    144                     .withValue(StructuredName.SUFFIX , e.name.suffix)
    145                     .build());
    146 
    147             // Populate the phone number(s) if any.
    148             for (PhonebookEntry.Phone p : e.phones) {
    149                 ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
    150                         .withValueBackReference(Data.RAW_CONTACT_ID, index)
    151                         .withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
    152                         .withValue(Phone.NUMBER, p.number)
    153                         .withValue(Phone.TYPE, p.type)
    154                         .build());
    155             }
    156 
    157             // Commit MAX_OPS at a time so that the binder transaction doesn't get too large.
    158             if (ops.size() > MAX_OPS) {
    159                 mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
    160                 ops.clear();
    161             }
    162         }
    163 
    164         if (ops.size() > 0) {
    165             // Commit remaining entries.
    166             mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
    167         }
    168     }
    169 
    170     private void deleteContacts(List<PhonebookEntry> entries)
    171             throws RemoteException, OperationApplicationException {
    172         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
    173         for (PhonebookEntry e : entries) {
    174             ops.add(ContentProviderOperation.newDelete(RawContacts.CONTENT_URI.buildUpon()
    175                         .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    176                         .build())
    177                 .withSelection(RawContacts._ID + "=?", new String[] { e.id })
    178                 .build());
    179         }
    180         mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
    181     }
    182 
    183     @Override
    184     public void onPullComplete() {
    185         if (mEntries == null) {
    186             Log.e(TAG, "onPullComplete entries is null.");
    187             return;
    188         }
    189 
    190         if (DBG) {
    191             Log.d(TAG, "onPullComplete with " + mEntries.size() + " count.");
    192         }
    193         try {
    194 
    195             HashMap<PhonebookEntry.Name, PhonebookEntry> contacts = fetchExistingContacts();
    196 
    197             List<PhonebookEntry> contactsToAdd = new ArrayList<PhonebookEntry>();
    198             List<PhonebookEntry> contactsToDelete = new ArrayList<PhonebookEntry>();
    199 
    200             for (VCardEntry e : mEntries) {
    201                 PhonebookEntry current = new PhonebookEntry(e);
    202                 PhonebookEntry.Name key = current.name;
    203 
    204                 PhonebookEntry contact = contacts.get(key);
    205                 if (contact == null) {
    206                     contactsToAdd.add(current);
    207                 } else if (!contact.equals(current)) {
    208                     // Instead of trying to figure out what changed on an update, do a delete
    209                     // and an add. Sure, it churns contact ids but a contact being updated
    210                     // while someone is connected is a low enough frequency event that the
    211                     // complexity of doing an update is just not worth it.
    212                     contactsToAdd.add(current);
    213                     // Don't remove it from the hashmap so it will get deleted.
    214                 } else {
    215                     contacts.remove(key);
    216                 }
    217             }
    218             contactsToDelete.addAll(contacts.values());
    219 
    220             if (!contactsToDelete.isEmpty()) {
    221                 deleteContacts(contactsToDelete);
    222             }
    223 
    224             if (!contactsToAdd.isEmpty()) {
    225                 addContacts(contactsToAdd);
    226             }
    227 
    228             Log.d(TAG, "Sync complete: add=" + contactsToAdd.size()
    229                     + " delete=" + contactsToDelete.size());
    230         } catch (OperationApplicationException | RemoteException | NumberFormatException e) {
    231             Log.d(TAG, "Got exception: ", e);
    232         } catch (InterruptedException e) {
    233             Log.d(TAG, "Interrupted durring insert.");
    234         } finally {
    235             complete = true;
    236         }
    237     }
    238 }
    239