• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
PhonebookPullRequest(Context context, Account account)39     public PhonebookPullRequest(Context context, Account account) {
40         mContext = context;
41         mAccount = account;
42         path = BluetoothPbapClient.PB_PATH;
43     }
44 
fetchContact(String id)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 
fetchExistingContacts()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 
addContacts(List<PhonebookEntry> entries)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 
deleteContacts(List<PhonebookEntry> entries)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
onPullComplete()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