1package com.android.bluetooth.pbapclient;
2
3import com.android.vcard.VCardEntry;
4
5import android.accounts.Account;
6import com.android.bluetooth.pbapclient.BluetoothPbapClient;
7import android.content.ContentProviderOperation;
8import android.content.Context;
9import android.content.OperationApplicationException;
10import android.provider.ContactsContract;
11import android.database.Cursor;
12import android.net.Uri;
13import android.os.Bundle;
14import android.os.RemoteException;
15import android.provider.ContactsContract.Data;
16import android.provider.ContactsContract.RawContacts;
17import android.provider.ContactsContract.RawContactsEntity;
18import android.provider.ContactsContract.Contacts.Entity;
19import android.provider.ContactsContract.CommonDataKinds.Phone;
20import android.provider.ContactsContract.CommonDataKinds.StructuredName;
21import android.util.Log;
22
23import com.android.vcard.VCardEntry;
24
25import java.lang.InterruptedException;
26import java.util.ArrayList;
27import java.util.HashMap;
28import java.util.List;
29
30public 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