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