• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008-2009 Marc Blank
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.exchange.adapter;
19 
20 import android.content.ContentProviderClient;
21 import android.content.ContentProviderOperation;
22 import android.content.ContentProviderOperation.Builder;
23 import android.content.ContentProviderResult;
24 import android.content.ContentResolver;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Entity;
28 import android.content.Entity.NamedContentValues;
29 import android.content.EntityIterator;
30 import android.content.OperationApplicationException;
31 import android.database.Cursor;
32 import android.net.Uri;
33 import android.os.RemoteException;
34 import android.provider.ContactsContract;
35 import android.provider.ContactsContract.CommonDataKinds.Email;
36 import android.provider.ContactsContract.CommonDataKinds.Event;
37 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
38 import android.provider.ContactsContract.CommonDataKinds.Im;
39 import android.provider.ContactsContract.CommonDataKinds.Nickname;
40 import android.provider.ContactsContract.CommonDataKinds.Note;
41 import android.provider.ContactsContract.CommonDataKinds.Organization;
42 import android.provider.ContactsContract.CommonDataKinds.Phone;
43 import android.provider.ContactsContract.CommonDataKinds.Photo;
44 import android.provider.ContactsContract.CommonDataKinds.Relation;
45 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
46 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
47 import android.provider.ContactsContract.CommonDataKinds.Website;
48 import android.provider.ContactsContract.Data;
49 import android.provider.ContactsContract.Groups;
50 import android.provider.ContactsContract.RawContacts;
51 import android.provider.ContactsContract.RawContactsEntity;
52 import android.provider.ContactsContract.Settings;
53 import android.provider.ContactsContract.SyncState;
54 import android.provider.SyncStateContract;
55 import android.text.TextUtils;
56 import android.text.util.Rfc822Token;
57 import android.text.util.Rfc822Tokenizer;
58 import android.util.Base64;
59 import android.util.Log;
60 
61 import com.android.emailcommon.utility.Utility;
62 import com.android.exchange.CommandStatusException;
63 import com.android.exchange.Eas;
64 import com.android.exchange.EasSyncService;
65 import com.android.exchange.utility.CalendarUtilities;
66 
67 import java.io.IOException;
68 import java.io.InputStream;
69 import java.util.ArrayList;
70 import java.util.GregorianCalendar;
71 import java.util.TimeZone;
72 
73 /**
74  * Sync adapter for EAS Contacts
75  *
76  */
77 public class ContactsSyncAdapter extends AbstractSyncAdapter {
78 
79     private static final String TAG = "EasContactsSyncAdapter";
80     private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
81     private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
82     private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
83     private static final String[] GROUP_TITLE_PROJECTION = new String[] {Groups.TITLE};
84     private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = Data.MIMETYPE + "='" +
85         GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=?";
86     private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID};
87 
88     private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
89         = new ArrayList<NamedContentValues>();
90 
91     private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
92 
93     private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
94         Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
95         Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
96         Tags.CONTACTS_HOME_ADDRESS_STATE,
97         Tags.CONTACTS_HOME_ADDRESS_STREET};
98 
99     private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
100         Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
101         Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
102         Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
103         Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
104 
105     private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
106         Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
107         Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
108         Tags.CONTACTS_OTHER_ADDRESS_STATE,
109         Tags.CONTACTS_OTHER_ADDRESS_STREET};
110 
111     private static final int MAX_IM_ROWS = 3;
112     private static final int MAX_EMAIL_ROWS = 3;
113     private static final int MAX_PHONE_ROWS = 2;
114     private static final String COMMON_DATA_ROW = Im.DATA;  // Could have been Email.DATA, etc.
115     private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
116 
117     private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
118         Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
119 
120     private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
121         Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
122 
123     private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
124         Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
125 
126     private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
127         Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
128 
129     private static final Object sSyncKeyLock = new Object();
130 
131     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
132     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
133 
134     private final Uri mAccountUri;
135     private final ContentResolver mContentResolver;
136     private boolean mGroupsUsed = false;
137 
ContactsSyncAdapter(EasSyncService service)138     public ContactsSyncAdapter(EasSyncService service) {
139         super(service);
140         mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
141         mContentResolver = mContext.getContentResolver();
142     }
143 
addCallerIsSyncAdapterParameter(Uri uri)144     static Uri addCallerIsSyncAdapterParameter(Uri uri) {
145         return uri.buildUpon()
146                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
147                 .build();
148     }
149 
150     @Override
sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)151     public void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)
152             throws IOException  {
153         if (initialSync) {
154             // These are the tags we support for upload; whenever we add/remove support
155             // (in addData), we need to update this list
156             s.start(Tags.SYNC_SUPPORTED);
157             s.tag(Tags.CONTACTS_FIRST_NAME);
158             s.tag(Tags.CONTACTS_LAST_NAME);
159             s.tag(Tags.CONTACTS_MIDDLE_NAME);
160             s.tag(Tags.CONTACTS_SUFFIX);
161             s.tag(Tags.CONTACTS_COMPANY_NAME);
162             s.tag(Tags.CONTACTS_JOB_TITLE);
163             s.tag(Tags.CONTACTS_EMAIL1_ADDRESS);
164             s.tag(Tags.CONTACTS_EMAIL2_ADDRESS);
165             s.tag(Tags.CONTACTS_EMAIL3_ADDRESS);
166             s.tag(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER);
167             s.tag(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER);
168             s.tag(Tags.CONTACTS2_MMS);
169             s.tag(Tags.CONTACTS_BUSINESS_FAX_NUMBER);
170             s.tag(Tags.CONTACTS2_COMPANY_MAIN_PHONE);
171             s.tag(Tags.CONTACTS_HOME_FAX_NUMBER);
172             s.tag(Tags.CONTACTS_HOME_TELEPHONE_NUMBER);
173             s.tag(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER);
174             s.tag(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER);
175             s.tag(Tags.CONTACTS_CAR_TELEPHONE_NUMBER);
176             s.tag(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER);
177             s.tag(Tags.CONTACTS_PAGER_NUMBER);
178             s.tag(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER);
179             s.tag(Tags.CONTACTS2_IM_ADDRESS);
180             s.tag(Tags.CONTACTS2_IM_ADDRESS_2);
181             s.tag(Tags.CONTACTS2_IM_ADDRESS_3);
182             s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_CITY);
183             s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY);
184             s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE);
185             s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STATE);
186             s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STREET);
187             s.tag(Tags.CONTACTS_HOME_ADDRESS_CITY);
188             s.tag(Tags.CONTACTS_HOME_ADDRESS_COUNTRY);
189             s.tag(Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE);
190             s.tag(Tags.CONTACTS_HOME_ADDRESS_STATE);
191             s.tag(Tags.CONTACTS_HOME_ADDRESS_STREET);
192             s.tag(Tags.CONTACTS_OTHER_ADDRESS_CITY);
193             s.tag(Tags.CONTACTS_OTHER_ADDRESS_COUNTRY);
194             s.tag(Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE);
195             s.tag(Tags.CONTACTS_OTHER_ADDRESS_STATE);
196             s.tag(Tags.CONTACTS_OTHER_ADDRESS_STREET);
197             s.tag(Tags.CONTACTS_YOMI_COMPANY_NAME);
198             s.tag(Tags.CONTACTS_YOMI_FIRST_NAME);
199             s.tag(Tags.CONTACTS_YOMI_LAST_NAME);
200             s.tag(Tags.CONTACTS2_NICKNAME);
201             s.tag(Tags.CONTACTS_ASSISTANT_NAME);
202             s.tag(Tags.CONTACTS2_MANAGER_NAME);
203             s.tag(Tags.CONTACTS_SPOUSE);
204             s.tag(Tags.CONTACTS_DEPARTMENT);
205             s.tag(Tags.CONTACTS_TITLE);
206             s.tag(Tags.CONTACTS_OFFICE_LOCATION);
207             s.tag(Tags.CONTACTS2_CUSTOMER_ID);
208             s.tag(Tags.CONTACTS2_GOVERNMENT_ID);
209             s.tag(Tags.CONTACTS2_ACCOUNT_NAME);
210             s.tag(Tags.CONTACTS_ANNIVERSARY);
211             s.tag(Tags.CONTACTS_BIRTHDAY);
212             s.tag(Tags.CONTACTS_WEBPAGE);
213             s.tag(Tags.CONTACTS_PICTURE);
214             s.end(); // SYNC_SUPPORTED
215         } else {
216             setPimSyncOptions(protocolVersion, null, s);
217         }
218     }
219 
220     @Override
isSyncable()221     public boolean isSyncable() {
222         return ContentResolver.getSyncAutomatically(
223                 mAccountManagerAccount, ContactsContract.AUTHORITY);
224     }
225 
226     @Override
parse(InputStream is)227     public boolean parse(InputStream is) throws IOException, CommandStatusException {
228         EasContactsSyncParser p = new EasContactsSyncParser(is, this);
229         return p.parse();
230     }
231 
232 
233     @Override
wipe()234     public void wipe() {
235         mContentResolver.delete(mAccountUri, null, null);
236     }
237 
238     interface UntypedRow {
addValues(RowBuilder builder)239         public void addValues(RowBuilder builder);
isSameAs(int type, String value)240         public boolean isSameAs(int type, String value);
241     }
242 
243     /**
244      * We get our SyncKey from ContactsProvider.  If there's not one, we set it to "0" (the reset
245      * state) and save that away.
246      */
247     @Override
getSyncKey()248     public String getSyncKey() throws IOException {
249         synchronized (sSyncKeyLock) {
250             ContentProviderClient client = mService.mContentResolver
251                     .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
252             try {
253                 byte[] data = SyncStateContract.Helpers.get(client,
254                         ContactsContract.SyncState.CONTENT_URI, mAccountManagerAccount);
255                 if (data == null || data.length == 0) {
256                     // Initialize the SyncKey
257                     setSyncKey("0", false);
258                     // Make sure ungrouped contacts for Exchange are defaultly visible
259                     ContentValues cv = new ContentValues();
260                     cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
261                     cv.put(Groups.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
262                     cv.put(Settings.UNGROUPED_VISIBLE, true);
263                     client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
264                     return "0";
265                 } else {
266                     return new String(data);
267                 }
268             } catch (RemoteException e) {
269                 throw new IOException("Can't get SyncKey from ContactsProvider");
270             }
271         }
272     }
273 
274     /**
275      * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
276      * cases, the SyncKey is set within ContactOperations
277      */
278     @Override
setSyncKey(String syncKey, boolean inCommands)279     public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
280         synchronized (sSyncKeyLock) {
281             if ("0".equals(syncKey) || !inCommands) {
282                 ContentProviderClient client = mService.mContentResolver
283                         .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
284                 try {
285                     SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
286                             mAccountManagerAccount, syncKey.getBytes());
287                     userLog("SyncKey set to ", syncKey, " in ContactsProvider");
288                 } catch (RemoteException e) {
289                     throw new IOException("Can't set SyncKey in ContactsProvider");
290                 }
291             }
292             mMailbox.mSyncKey = syncKey;
293         }
294     }
295 
296     public static final class EasChildren {
EasChildren()297         private EasChildren() {}
298 
299         /** MIME type used when storing this in data table. */
300         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
301         public static final int MAX_CHILDREN = 8;
302         public static final String[] ROWS =
303             new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
304     }
305 
306     public static final class EasPersonal {
307         String anniversary;
308         String fileAs;
309 
310             /** MIME type used when storing this in data table. */
311         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
312         public static final String ANNIVERSARY = "data2";
313         public static final String FILE_AS = "data4";
314 
hasData()315         boolean hasData() {
316             return anniversary != null || fileAs != null;
317         }
318     }
319 
320     public static final class EasBusiness {
321         String customerId;
322         String governmentId;
323         String accountName;
324 
325         /** MIME type used when storing this in data table. */
326         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
327         public static final String CUSTOMER_ID = "data6";
328         public static final String GOVERNMENT_ID = "data7";
329         public static final String ACCOUNT_NAME = "data8";
330 
hasData()331         boolean hasData() {
332             return customerId != null || governmentId != null || accountName != null;
333         }
334     }
335 
336     public static final class Address {
337         String city;
338         String country;
339         String code;
340         String street;
341         String state;
342 
hasData()343         boolean hasData() {
344             return city != null || country != null || code != null || state != null
345                 || street != null;
346         }
347     }
348 
349     class EmailRow implements UntypedRow {
350         String email;
351         String displayName;
352 
EmailRow(String _email)353         public EmailRow(String _email) {
354             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
355             // Can't happen, but belt & suspenders
356             if (tokens.length == 0) {
357                 email = "";
358                 displayName = "";
359             } else {
360                 Rfc822Token token = tokens[0];
361                 email = token.getAddress();
362                 displayName = token.getName();
363             }
364         }
365 
366         @Override
addValues(RowBuilder builder)367         public void addValues(RowBuilder builder) {
368             builder.withValue(Email.DATA, email);
369             builder.withValue(Email.DISPLAY_NAME, displayName);
370         }
371 
372         @Override
isSameAs(int type, String value)373         public boolean isSameAs(int type, String value) {
374             return email.equalsIgnoreCase(value);
375         }
376     }
377 
378     class ImRow implements UntypedRow {
379         String im;
380 
ImRow(String _im)381         public ImRow(String _im) {
382             im = _im;
383         }
384 
385         @Override
addValues(RowBuilder builder)386         public void addValues(RowBuilder builder) {
387             builder.withValue(Im.DATA, im);
388         }
389 
390         @Override
isSameAs(int type, String value)391         public boolean isSameAs(int type, String value) {
392             return im.equalsIgnoreCase(value);
393         }
394     }
395 
396     class PhoneRow implements UntypedRow {
397         String phone;
398         int type;
399 
PhoneRow(String _phone, int _type)400         public PhoneRow(String _phone, int _type) {
401             phone = _phone;
402             type = _type;
403         }
404 
405         @Override
addValues(RowBuilder builder)406         public void addValues(RowBuilder builder) {
407             builder.withValue(Im.DATA, phone);
408             builder.withValue(Phone.TYPE, type);
409         }
410 
411         @Override
isSameAs(int _type, String value)412         public boolean isSameAs(int _type, String value) {
413             return type == _type && phone.equalsIgnoreCase(value);
414         }
415     }
416 
417    class EasContactsSyncParser extends AbstractSyncParser {
418 
419         String[] mBindArgument = new String[1];
420         String mMailboxIdAsString;
421         ContactOperations ops = new ContactOperations();
422 
EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)423         public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)
424                 throws IOException {
425             super(in, adapter);
426         }
427 
addData(String serverId, ContactOperations ops, Entity entity)428         public void addData(String serverId, ContactOperations ops, Entity entity)
429                 throws IOException {
430             String prefix = null;
431             String firstName = null;
432             String lastName = null;
433             String middleName = null;
434             String suffix = null;
435             String companyName = null;
436             String yomiFirstName = null;
437             String yomiLastName = null;
438             String yomiCompanyName = null;
439             String title = null;
440             String department = null;
441             String officeLocation = null;
442             Address home = new Address();
443             Address work = new Address();
444             Address other = new Address();
445             EasBusiness business = new EasBusiness();
446             EasPersonal personal = new EasPersonal();
447             ArrayList<String> children = new ArrayList<String>();
448             ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
449             ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
450             ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
451             ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
452             if (entity == null) {
453                 ops.newContact(serverId);
454             }
455 
456             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
457                 switch (tag) {
458                     case Tags.CONTACTS_FIRST_NAME:
459                         firstName = getValue();
460                         break;
461                     case Tags.CONTACTS_LAST_NAME:
462                         lastName = getValue();
463                         break;
464                     case Tags.CONTACTS_MIDDLE_NAME:
465                         middleName = getValue();
466                         break;
467                     case Tags.CONTACTS_SUFFIX:
468                         suffix = getValue();
469                         break;
470                     case Tags.CONTACTS_COMPANY_NAME:
471                         companyName = getValue();
472                         break;
473                     case Tags.CONTACTS_JOB_TITLE:
474                         title = getValue();
475                         break;
476                     case Tags.CONTACTS_EMAIL1_ADDRESS:
477                     case Tags.CONTACTS_EMAIL2_ADDRESS:
478                     case Tags.CONTACTS_EMAIL3_ADDRESS:
479                         emails.add(new EmailRow(getValue()));
480                         break;
481                     case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
482                     case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
483                         workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
484                         break;
485                     case Tags.CONTACTS2_MMS:
486                         ops.addPhone(entity, Phone.TYPE_MMS, getValue());
487                         break;
488                     case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
489                         ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
490                         break;
491                     case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
492                         ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
493                         break;
494                     case Tags.CONTACTS_HOME_FAX_NUMBER:
495                         ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
496                         break;
497                     case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
498                     case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
499                         homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
500                         break;
501                     case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
502                         ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
503                         break;
504                     case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
505                         ops.addPhone(entity, Phone.TYPE_CAR, getValue());
506                         break;
507                     case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
508                         ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
509                         break;
510                     case Tags.CONTACTS_PAGER_NUMBER:
511                         ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
512                         break;
513                     case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
514                         ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
515                         break;
516                     case Tags.CONTACTS2_IM_ADDRESS:
517                     case Tags.CONTACTS2_IM_ADDRESS_2:
518                     case Tags.CONTACTS2_IM_ADDRESS_3:
519                         ims.add(new ImRow(getValue()));
520                         break;
521                     case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
522                         work.city = getValue();
523                         break;
524                     case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
525                         work.country = getValue();
526                         break;
527                     case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
528                         work.code = getValue();
529                         break;
530                     case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
531                         work.state = getValue();
532                         break;
533                     case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
534                         work.street = getValue();
535                         break;
536                     case Tags.CONTACTS_HOME_ADDRESS_CITY:
537                         home.city = getValue();
538                         break;
539                     case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
540                         home.country = getValue();
541                         break;
542                     case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
543                         home.code = getValue();
544                         break;
545                     case Tags.CONTACTS_HOME_ADDRESS_STATE:
546                         home.state = getValue();
547                         break;
548                     case Tags.CONTACTS_HOME_ADDRESS_STREET:
549                         home.street = getValue();
550                         break;
551                     case Tags.CONTACTS_OTHER_ADDRESS_CITY:
552                         other.city = getValue();
553                         break;
554                     case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
555                         other.country = getValue();
556                         break;
557                     case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
558                         other.code = getValue();
559                         break;
560                     case Tags.CONTACTS_OTHER_ADDRESS_STATE:
561                         other.state = getValue();
562                         break;
563                     case Tags.CONTACTS_OTHER_ADDRESS_STREET:
564                         other.street = getValue();
565                         break;
566 
567                     case Tags.CONTACTS_CHILDREN:
568                         childrenParser(children);
569                         break;
570 
571                     case Tags.CONTACTS_YOMI_COMPANY_NAME:
572                         yomiCompanyName = getValue();
573                         break;
574                     case Tags.CONTACTS_YOMI_FIRST_NAME:
575                         yomiFirstName = getValue();
576                         break;
577                     case Tags.CONTACTS_YOMI_LAST_NAME:
578                         yomiLastName = getValue();
579                         break;
580 
581                     case Tags.CONTACTS2_NICKNAME:
582                         ops.addNickname(entity, getValue());
583                         break;
584 
585                     case Tags.CONTACTS_ASSISTANT_NAME:
586                         ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
587                         break;
588                     case Tags.CONTACTS2_MANAGER_NAME:
589                         ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
590                         break;
591                     case Tags.CONTACTS_SPOUSE:
592                         ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
593                         break;
594                     case Tags.CONTACTS_DEPARTMENT:
595                         department = getValue();
596                         break;
597                     case Tags.CONTACTS_TITLE:
598                         prefix = getValue();
599                         break;
600 
601                     // EAS Business
602                     case Tags.CONTACTS_OFFICE_LOCATION:
603                         officeLocation = getValue();
604                         break;
605                     case Tags.CONTACTS2_CUSTOMER_ID:
606                         business.customerId = getValue();
607                         break;
608                     case Tags.CONTACTS2_GOVERNMENT_ID:
609                         business.governmentId = getValue();
610                         break;
611                     case Tags.CONTACTS2_ACCOUNT_NAME:
612                         business.accountName = getValue();
613                         break;
614 
615                     // EAS Personal
616                     case Tags.CONTACTS_ANNIVERSARY:
617                         personal.anniversary = getValue();
618                         break;
619                     case Tags.CONTACTS_BIRTHDAY:
620                         ops.addBirthday(entity, getValue());
621                         break;
622                     case Tags.CONTACTS_WEBPAGE:
623                         ops.addWebpage(entity, getValue());
624                         break;
625 
626                     case Tags.CONTACTS_PICTURE:
627                         ops.addPhoto(entity, getValue());
628                         break;
629 
630                     case Tags.BASE_BODY:
631                         ops.addNote(entity, bodyParser());
632                         break;
633                     case Tags.CONTACTS_BODY:
634                         ops.addNote(entity, getValue());
635                         break;
636 
637                     case Tags.CONTACTS_CATEGORIES:
638                         mGroupsUsed = true;
639                         categoriesParser(ops, entity);
640                         break;
641 
642                     default:
643                         skipTag();
644                 }
645             }
646 
647             // We must have first name, last name, or company name
648             String name = null;
649             if (firstName != null || lastName != null) {
650                 if (firstName == null) {
651                     name = lastName;
652                 } else if (lastName == null) {
653                     name = firstName;
654                 } else {
655                     name = firstName + ' ' + lastName;
656                 }
657             } else if (companyName != null) {
658                 name = companyName;
659             }
660 
661             ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name,
662                     yomiFirstName, yomiLastName);
663             ops.addBusiness(entity, business);
664             ops.addPersonal(entity, personal);
665 
666             ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
667             ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
668             ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
669                     MAX_PHONE_ROWS);
670             ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
671                     MAX_PHONE_ROWS);
672 
673             if (!children.isEmpty()) {
674                 ops.addChildren(entity, children);
675             }
676 
677             if (work.hasData()) {
678                 ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
679                         work.state, work.country, work.code);
680             }
681             if (home.hasData()) {
682                 ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
683                         home.state, home.country, home.code);
684             }
685             if (other.hasData()) {
686                 ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
687                         other.state, other.country, other.code);
688             }
689 
690             if (companyName != null) {
691                 ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
692                         yomiCompanyName, officeLocation);
693             }
694 
695             if (entity != null) {
696                 // We've been removing rows from the list as they've been found in the xml
697                 // Any that are left must have been deleted on the server
698                 ArrayList<NamedContentValues> ncvList = entity.getSubValues();
699                 for (NamedContentValues ncv: ncvList) {
700                     // These rows need to be deleted...
701                     Uri u = dataUriFromNamedContentValues(ncv);
702                     ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
703                             .build());
704                 }
705             }
706         }
707 
categoriesParser(ContactOperations ops, Entity entity)708         private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
709             while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
710                 switch (tag) {
711                     case Tags.CONTACTS_CATEGORY:
712                         ops.addGroup(entity, getValue());
713                         break;
714                     default:
715                         skipTag();
716                 }
717             }
718         }
719 
childrenParser(ArrayList<String> children)720         private void childrenParser(ArrayList<String> children) throws IOException {
721             while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
722                 switch (tag) {
723                     case Tags.CONTACTS_CHILD:
724                         if (children.size() < EasChildren.MAX_CHILDREN) {
725                             children.add(getValue());
726                         }
727                         break;
728                     default:
729                         skipTag();
730                 }
731             }
732         }
733 
bodyParser()734         private String bodyParser() throws IOException {
735             String body = null;
736             while (nextTag(Tags.BASE_BODY) != END) {
737                 switch (tag) {
738                     case Tags.BASE_DATA:
739                         body = getValue();
740                         break;
741                     default:
742                         skipTag();
743                 }
744             }
745             return body;
746         }
747 
addParser(ContactOperations ops)748         public void addParser(ContactOperations ops) throws IOException {
749             String serverId = null;
750             while (nextTag(Tags.SYNC_ADD) != END) {
751                 switch (tag) {
752                     case Tags.SYNC_SERVER_ID: // same as
753                         serverId = getValue();
754                         break;
755                     case Tags.SYNC_APPLICATION_DATA:
756                         addData(serverId, ops, null);
757                         break;
758                     default:
759                         skipTag();
760                 }
761             }
762         }
763 
getServerIdCursor(String serverId)764         private Cursor getServerIdCursor(String serverId) {
765             mBindArgument[0] = serverId;
766             return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
767                     mBindArgument, null);
768         }
769 
getClientIdCursor(String clientId)770         private Cursor getClientIdCursor(String clientId) {
771             mBindArgument[0] = clientId;
772             return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
773                     mBindArgument, null);
774         }
775 
deleteParser(ContactOperations ops)776         public void deleteParser(ContactOperations ops) throws IOException {
777             while (nextTag(Tags.SYNC_DELETE) != END) {
778                 switch (tag) {
779                     case Tags.SYNC_SERVER_ID:
780                         String serverId = getValue();
781                         // Find the message in this mailbox with the given serverId
782                         Cursor c = getServerIdCursor(serverId);
783                         try {
784                             if (c.moveToFirst()) {
785                                 userLog("Deleting ", serverId);
786                                 ops.delete(c.getLong(0));
787                             }
788                         } finally {
789                             c.close();
790                         }
791                         break;
792                     default:
793                         skipTag();
794                 }
795             }
796         }
797 
798         class ServerChange {
799             long id;
800             boolean read;
801 
ServerChange(long _id, boolean _read)802             ServerChange(long _id, boolean _read) {
803                 id = _id;
804                 read = _read;
805             }
806         }
807 
808         /**
809          * Changes are handled row by row, and only changed/new rows are acted upon
810          * @param ops the array of pending ContactProviderOperations.
811          * @throws IOException
812          */
changeParser(ContactOperations ops)813         public void changeParser(ContactOperations ops) throws IOException {
814             String serverId = null;
815             Entity entity = null;
816             while (nextTag(Tags.SYNC_CHANGE) != END) {
817                 switch (tag) {
818                     case Tags.SYNC_SERVER_ID:
819                         serverId = getValue();
820                         Cursor c = getServerIdCursor(serverId);
821                         try {
822                             if (c.moveToFirst()) {
823                                 // TODO Handle deleted individual rows...
824                                 Uri uri = ContentUris.withAppendedId(
825                                         RawContacts.CONTENT_URI, c.getLong(0));
826                                 uri = Uri.withAppendedPath(
827                                         uri, RawContacts.Entity.CONTENT_DIRECTORY);
828                                 EntityIterator entityIterator = RawContacts.newEntityIterator(
829                                     mContentResolver.query(uri, null, null, null, null));
830                                 if (entityIterator.hasNext()) {
831                                     entity = entityIterator.next();
832                                 }
833                                 userLog("Changing contact ", serverId);
834                             }
835                         } finally {
836                             c.close();
837                         }
838                         break;
839                     case Tags.SYNC_APPLICATION_DATA:
840                         addData(serverId, ops, entity);
841                         break;
842                     default:
843                         skipTag();
844                 }
845             }
846         }
847 
848         @Override
commandsParser()849         public void commandsParser() throws IOException {
850             while (nextTag(Tags.SYNC_COMMANDS) != END) {
851                 if (tag == Tags.SYNC_ADD) {
852                     addParser(ops);
853                     incrementChangeCount();
854                 } else if (tag == Tags.SYNC_DELETE) {
855                     deleteParser(ops);
856                     incrementChangeCount();
857                 } else if (tag == Tags.SYNC_CHANGE) {
858                     changeParser(ops);
859                     incrementChangeCount();
860                 } else
861                     skipTag();
862             }
863         }
864 
865         @Override
commit()866         public void commit() throws IOException {
867            // Save the syncKey here, using the Helper provider by Contacts provider
868             userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
869             ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
870                     mAccountManagerAccount, mMailbox.mSyncKey.getBytes()));
871 
872             // Execute these all at once...
873             ops.execute();
874 
875             if (ops.mResults != null) {
876                 ContentValues cv = new ContentValues();
877                 cv.put(RawContacts.DIRTY, 0);
878                 for (int i = 0; i < ops.mContactIndexCount; i++) {
879                     int index = ops.mContactIndexArray[i];
880                     Uri u = ops.mResults[index].uri;
881                     if (u != null) {
882                         String idString = u.getLastPathSegment();
883                         mContentResolver.update(
884                                 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
885                                 RawContacts._ID + "=" + idString, null);
886                     }
887                 }
888             }
889         }
890 
addResponsesParser()891         public void addResponsesParser() throws IOException {
892             String serverId = null;
893             String clientId = null;
894             ContentValues cv = new ContentValues();
895             while (nextTag(Tags.SYNC_ADD) != END) {
896                 switch (tag) {
897                     case Tags.SYNC_SERVER_ID:
898                         serverId = getValue();
899                         break;
900                     case Tags.SYNC_CLIENT_ID:
901                         clientId = getValue();
902                         break;
903                     case Tags.SYNC_STATUS:
904                         getValue();
905                         break;
906                     default:
907                         skipTag();
908                 }
909             }
910 
911             // This is theoretically impossible, but...
912             if (clientId == null || serverId == null) return;
913 
914             Cursor c = getClientIdCursor(clientId);
915             try {
916                 if (c.moveToFirst()) {
917                     cv.put(RawContacts.SOURCE_ID, serverId);
918                     cv.put(RawContacts.DIRTY, 0);
919                     ops.add(ContentProviderOperation.newUpdate(
920                             ContentUris.withAppendedId(
921                                     addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
922                                     c.getLong(0)))
923                             .withValues(cv)
924                             .build());
925                     userLog("New contact " + clientId + " was given serverId: " + serverId);
926                 }
927             } finally {
928                 c.close();
929             }
930         }
931 
changeResponsesParser()932         public void changeResponsesParser() throws IOException {
933             String serverId = null;
934             String status = null;
935             while (nextTag(Tags.SYNC_CHANGE) != END) {
936                 switch (tag) {
937                     case Tags.SYNC_SERVER_ID:
938                         serverId = getValue();
939                         break;
940                     case Tags.SYNC_STATUS:
941                         status = getValue();
942                         break;
943                     default:
944                         skipTag();
945                 }
946             }
947             if (serverId != null && status != null) {
948                 userLog("Changed contact " + serverId + " failed with status: " + status);
949             }
950         }
951 
952 
953         @Override
responsesParser()954         public void responsesParser() throws IOException {
955             // Handle server responses here (for Add and Change)
956             while (nextTag(Tags.SYNC_RESPONSES) != END) {
957                 if (tag == Tags.SYNC_ADD) {
958                     addResponsesParser();
959                 } else if (tag == Tags.SYNC_CHANGE) {
960                     changeResponsesParser();
961                 } else
962                     skipTag();
963             }
964         }
965     }
966 
967 
uriWithAccountAndIsSyncAdapter(Uri uri)968     private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
969         return uri.buildUpon()
970             .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
971             .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
972             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
973             .build();
974     }
975 
976     /**
977      * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
978      * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
979      * represent the current values of that row, that can be compared against current values to
980      * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
981      * the Builder.
982      */
983     private class RowBuilder {
984         Builder builder;
985         ContentValues cv;
986 
RowBuilder(Builder _builder)987         public RowBuilder(Builder _builder) {
988             builder = _builder;
989         }
990 
RowBuilder(Builder _builder, NamedContentValues _ncv)991         public RowBuilder(Builder _builder, NamedContentValues _ncv) {
992             builder = _builder;
993             cv = _ncv.values;
994         }
995 
withValues(ContentValues values)996         RowBuilder withValues(ContentValues values) {
997             builder.withValues(values);
998             return this;
999         }
1000 
withValueBackReference(String key, int previousResult)1001         RowBuilder withValueBackReference(String key, int previousResult) {
1002             builder.withValueBackReference(key, previousResult);
1003             return this;
1004         }
1005 
build()1006         ContentProviderOperation build() {
1007             return builder.build();
1008         }
1009 
withValue(String key, Object value)1010         RowBuilder withValue(String key, Object value) {
1011             builder.withValue(key, value);
1012             return this;
1013         }
1014     }
1015 
1016     private class ContactOperations extends ArrayList<ContentProviderOperation> {
1017         private static final long serialVersionUID = 1L;
1018         private int mCount = 0;
1019         private int mContactBackValue = mCount;
1020         // Make an array big enough for the PIM window (max items we can get)
1021         private int[] mContactIndexArray =
1022             new int[Integer.parseInt(AbstractSyncAdapter.PIM_WINDOW_SIZE)];
1023         private int mContactIndexCount = 0;
1024         private ContentProviderResult[] mResults = null;
1025 
1026         @Override
add(ContentProviderOperation op)1027         public boolean add(ContentProviderOperation op) {
1028             super.add(op);
1029             mCount++;
1030             return true;
1031         }
1032 
newContact(String serverId)1033         public void newContact(String serverId) {
1034             Builder builder = ContentProviderOperation
1035                 .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
1036             ContentValues values = new ContentValues();
1037             values.put(RawContacts.SOURCE_ID, serverId);
1038             builder.withValues(values);
1039             mContactBackValue = mCount;
1040             mContactIndexArray[mContactIndexCount++] = mCount;
1041             add(builder.build());
1042         }
1043 
delete(long id)1044         public void delete(long id) {
1045             add(ContentProviderOperation
1046                     .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
1047                             .buildUpon()
1048                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1049                             .build())
1050                     .build());
1051         }
1052 
execute()1053         public void execute() {
1054             synchronized (mService.getSynchronizer()) {
1055                 if (!mService.isStopped()) {
1056                     try {
1057                         if (!isEmpty()) {
1058                             mService.userLog("Executing ", size(), " CPO's");
1059                             mResults = mContext.getContentResolver().applyBatch(
1060                                     ContactsContract.AUTHORITY, this);
1061                         }
1062                     } catch (RemoteException e) {
1063                         // There is nothing sensible to be done here
1064                         Log.e(TAG, "problem inserting contact during server update", e);
1065                     } catch (OperationApplicationException e) {
1066                         // There is nothing sensible to be done here
1067                         Log.e(TAG, "problem inserting contact during server update", e);
1068                     }
1069                 }
1070             }
1071         }
1072 
1073         /**
1074          * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
1075          * tries to find a match, returning it
1076          * @param list the list of NCV's from the contact entity
1077          * @param contentItemType the mime type we're looking for
1078          * @param type the subtype (e.g. HOME, WORK, etc.)
1079          * @return the matching NCV or null if not found
1080          */
findTypedData(ArrayList<NamedContentValues> list, String contentItemType, int type, String stringType)1081         private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
1082                 String contentItemType, int type, String stringType) {
1083             NamedContentValues result = null;
1084 
1085             // Loop through the ncv's, looking for an existing row
1086             for (NamedContentValues namedContentValues: list) {
1087                 Uri uri = namedContentValues.uri;
1088                 ContentValues cv = namedContentValues.values;
1089                 if (Data.CONTENT_URI.equals(uri)) {
1090                     String mimeType = cv.getAsString(Data.MIMETYPE);
1091                     if (mimeType.equals(contentItemType)) {
1092                         if (stringType != null) {
1093                             if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
1094                                 result = namedContentValues;
1095                             }
1096                         // Note Email.TYPE could be ANY type column; they are all defined in
1097                         // the private CommonColumns class in ContactsContract
1098                         // We'll accept either type < 0 (don't care), cv doesn't have a type,
1099                         // or the types are equal
1100                         } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
1101                                 cv.getAsInteger(Email.TYPE) == type) {
1102                             result = namedContentValues;
1103                         }
1104                     }
1105                 }
1106             }
1107 
1108             // If we've found an existing data row, we'll delete it.  Any rows left at the
1109             // end should be deleted...
1110             if (result != null) {
1111                 list.remove(result);
1112             }
1113 
1114             // Return the row found (or null)
1115             return result;
1116         }
1117 
1118         /**
1119          * Given the list of NamedContentValues for an entity and a mime type
1120          * gather all of the matching NCV's, returning them
1121          * @param list the list of NCV's from the contact entity
1122          * @param contentItemType the mime type we're looking for
1123          * @param type the subtype (e.g. HOME, WORK, etc.)
1124          * @return the matching NCVs
1125          */
findUntypedData(ArrayList<NamedContentValues> list, int type, String contentItemType)1126         private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
1127                 int type, String contentItemType) {
1128             ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
1129 
1130             // Loop through the ncv's, looking for an existing row
1131             for (NamedContentValues namedContentValues: list) {
1132                 Uri uri = namedContentValues.uri;
1133                 ContentValues cv = namedContentValues.values;
1134                 if (Data.CONTENT_URI.equals(uri)) {
1135                     String mimeType = cv.getAsString(Data.MIMETYPE);
1136                     if (mimeType.equals(contentItemType)) {
1137                         if (type != -1) {
1138                             int subtype = cv.getAsInteger(Phone.TYPE);
1139                             if (type != subtype) {
1140                                 continue;
1141                             }
1142                         }
1143                         result.add(namedContentValues);
1144                     }
1145                 }
1146             }
1147 
1148             // If we've found an existing data row, we'll delete it.  Any rows left at the
1149             // end should be deleted...
1150             for (NamedContentValues values : result) {
1151                 list.remove(values);
1152             }
1153 
1154             // Return the row found (or null)
1155             return result;
1156         }
1157 
1158         /**
1159          * Create a wrapper for a builder (insert or update) that also includes the NCV for
1160          * an existing row of this type.   If the SmartBuilder's cv field is not null, then
1161          * it represents the current (old) values of this field.  The caller can then check
1162          * whether the field is now different and needs to be updated; if it's not different,
1163          * the caller will simply return and not generate a new CPO.  Otherwise, the builder
1164          * should have its content values set, and the built CPO should be added to the
1165          * ContactOperations list.
1166          *
1167          * @param entity the contact entity (or null if this is a new contact)
1168          * @param mimeType the mime type of this row
1169          * @param type the subtype of this row
1170          * @param stringType for groups, the name of the group (type will be ignored), or null
1171          * @return the created SmartBuilder
1172          */
createBuilder(Entity entity, String mimeType, int type, String stringType)1173         public RowBuilder createBuilder(Entity entity, String mimeType, int type,
1174                 String stringType) {
1175             RowBuilder builder = null;
1176 
1177             if (entity != null) {
1178                 NamedContentValues ncv =
1179                     findTypedData(entity.getSubValues(), mimeType, type, stringType);
1180                 if (ncv != null) {
1181                     builder = new RowBuilder(
1182                             ContentProviderOperation
1183                                 .newUpdate(addCallerIsSyncAdapterParameter(
1184                                     dataUriFromNamedContentValues(ncv))),
1185                             ncv);
1186                 }
1187             }
1188 
1189             if (builder == null) {
1190                 builder = newRowBuilder(entity, mimeType);
1191             }
1192 
1193             // Return the appropriate builder (insert or update)
1194             // Caller will fill in the appropriate values; 4 MIMETYPE is already set
1195             return builder;
1196         }
1197 
typedRowBuilder(Entity entity, String mimeType, int type)1198         private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
1199             return createBuilder(entity, mimeType, type, null);
1200         }
1201 
untypedRowBuilder(Entity entity, String mimeType)1202         private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
1203             return createBuilder(entity, mimeType, -1, null);
1204         }
1205 
newRowBuilder(Entity entity, String mimeType)1206         private RowBuilder newRowBuilder(Entity entity, String mimeType) {
1207             // This is a new row; first get the contactId
1208             // If the Contact is new, use the saved back value; otherwise the value in the entity
1209             int contactId = mContactBackValue;
1210             if (entity != null) {
1211                 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
1212             }
1213 
1214             // Create an insert operation with the proper contactId reference
1215             RowBuilder builder =
1216                 new RowBuilder(ContentProviderOperation.newInsert(
1217                         addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
1218             if (entity == null) {
1219                 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
1220             } else {
1221                 builder.withValue(Data.RAW_CONTACT_ID, contactId);
1222             }
1223 
1224             // Set the mime type of the row
1225             builder.withValue(Data.MIMETYPE, mimeType);
1226             return builder;
1227         }
1228 
1229         /**
1230          * Compare a column in a ContentValues with an (old) value, and see if they are the
1231          * same.  For this purpose, null and an empty string are considered the same.
1232          * @param cv a ContentValues object, from a NamedContentValues
1233          * @param column a column that might be in the ContentValues
1234          * @param oldValue an old value (or null) to check against
1235          * @return whether the column's value in the ContentValues matches oldValue
1236          */
cvCompareString(ContentValues cv, String column, String oldValue)1237         private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
1238             if (cv.containsKey(column)) {
1239                 if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
1240                     return true;
1241                 }
1242             } else if (oldValue == null || oldValue.length() == 0) {
1243                 return true;
1244             }
1245             return false;
1246         }
1247 
addChildren(Entity entity, ArrayList<String> children)1248         public void addChildren(Entity entity, ArrayList<String> children) {
1249             RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
1250             int i = 0;
1251             for (String child: children) {
1252                 builder.withValue(EasChildren.ROWS[i++], child);
1253             }
1254             add(builder.build());
1255         }
1256 
addGroup(Entity entity, String group)1257         public void addGroup(Entity entity, String group) {
1258             RowBuilder builder =
1259                 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
1260             builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
1261             add(builder.build());
1262         }
1263 
addBirthday(Entity entity, String birthday)1264         public void addBirthday(Entity entity, String birthday) {
1265             RowBuilder builder =
1266                     typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
1267             ContentValues cv = builder.cv;
1268             if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
1269                 return;
1270             }
1271             long millis = Utility.parseEmailDateTimeToMillis(birthday);
1272             GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
1273             cal.setTimeInMillis(millis);
1274             if (cal.get(GregorianCalendar.HOUR_OF_DAY) >= 12) {
1275                 cal.add(GregorianCalendar.DATE, 1);
1276             }
1277             String realBirthday = CalendarUtilities.calendarToBirthdayString(cal);
1278             builder.withValue(Event.START_DATE, realBirthday);
1279             builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
1280             add(builder.build());
1281         }
1282 
addName(Entity entity, String prefix, String givenName, String familyName, String middleName, String suffix, String displayName, String yomiFirstName, String yomiLastName)1283         public void addName(Entity entity, String prefix, String givenName, String familyName,
1284                 String middleName, String suffix, String displayName, String yomiFirstName,
1285                 String yomiLastName) {
1286             RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
1287             ContentValues cv = builder.cv;
1288             if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
1289                     cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
1290                     cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
1291                     cvCompareString(cv, StructuredName.PREFIX, prefix) &&
1292                     cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
1293                     cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
1294                     cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
1295                 return;
1296             }
1297             builder.withValue(StructuredName.GIVEN_NAME, givenName);
1298             builder.withValue(StructuredName.FAMILY_NAME, familyName);
1299             builder.withValue(StructuredName.MIDDLE_NAME, middleName);
1300             builder.withValue(StructuredName.SUFFIX, suffix);
1301             builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
1302             builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
1303             builder.withValue(StructuredName.PREFIX, prefix);
1304             add(builder.build());
1305         }
1306 
addPersonal(Entity entity, EasPersonal personal)1307         public void addPersonal(Entity entity, EasPersonal personal) {
1308             RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
1309             ContentValues cv = builder.cv;
1310             if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
1311                     cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
1312                 return;
1313             }
1314             if (!personal.hasData()) {
1315                 return;
1316             }
1317             builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
1318             builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
1319             add(builder.build());
1320         }
1321 
addBusiness(Entity entity, EasBusiness business)1322         public void addBusiness(Entity entity, EasBusiness business) {
1323             RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
1324             ContentValues cv = builder.cv;
1325             if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
1326                     cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
1327                     cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
1328                 return;
1329             }
1330             if (!business.hasData()) {
1331                 return;
1332             }
1333             builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
1334             builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
1335             builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
1336             add(builder.build());
1337         }
1338 
addPhoto(Entity entity, String photo)1339         public void addPhoto(Entity entity, String photo) {
1340             RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
1341             // We're always going to add this; it's not worth trying to figure out whether the
1342             // picture is the same as the one stored.
1343             byte[] pic = Base64.decode(photo, Base64.DEFAULT);
1344             builder.withValue(Photo.PHOTO, pic);
1345             add(builder.build());
1346         }
1347 
addPhone(Entity entity, int type, String phone)1348         public void addPhone(Entity entity, int type, String phone) {
1349             RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
1350             ContentValues cv = builder.cv;
1351             if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
1352                 return;
1353             }
1354             builder.withValue(Phone.TYPE, type);
1355             builder.withValue(Phone.NUMBER, phone);
1356             add(builder.build());
1357         }
1358 
addWebpage(Entity entity, String url)1359         public void addWebpage(Entity entity, String url) {
1360             RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
1361             ContentValues cv = builder.cv;
1362             if (cv != null && cvCompareString(cv, Website.URL, url)) {
1363                 return;
1364             }
1365             builder.withValue(Website.TYPE, Website.TYPE_WORK);
1366             builder.withValue(Website.URL, url);
1367             add(builder.build());
1368         }
1369 
addRelation(Entity entity, int type, String value)1370         public void addRelation(Entity entity, int type, String value) {
1371             RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
1372             ContentValues cv = builder.cv;
1373             if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
1374                 return;
1375             }
1376             builder.withValue(Relation.TYPE, type);
1377             builder.withValue(Relation.DATA, value);
1378             add(builder.build());
1379         }
1380 
addNickname(Entity entity, String name)1381         public void addNickname(Entity entity, String name) {
1382             RowBuilder builder =
1383                 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
1384             ContentValues cv = builder.cv;
1385             if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
1386                 return;
1387             }
1388             builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1389             builder.withValue(Nickname.NAME, name);
1390             add(builder.build());
1391         }
1392 
addPostal(Entity entity, int type, String street, String city, String state, String country, String code)1393         public void addPostal(Entity entity, int type, String street, String city, String state,
1394                 String country, String code) {
1395             RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
1396                     type);
1397             ContentValues cv = builder.cv;
1398             if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
1399                     cvCompareString(cv, StructuredPostal.STREET, street) &&
1400                     cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
1401                     cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
1402                     cvCompareString(cv, StructuredPostal.REGION, state)) {
1403                 return;
1404             }
1405             builder.withValue(StructuredPostal.TYPE, type);
1406             builder.withValue(StructuredPostal.CITY, city);
1407             builder.withValue(StructuredPostal.STREET, street);
1408             builder.withValue(StructuredPostal.COUNTRY, country);
1409             builder.withValue(StructuredPostal.POSTCODE, code);
1410             builder.withValue(StructuredPostal.REGION, state);
1411             add(builder.build());
1412         }
1413 
1414        /**
1415          * We now are dealing with up to maxRows typeless rows of mimeType data.  We need to try to
1416          * match them with existing rows; if there's a match, everything's great.  Otherwise, we
1417          * either need to add a new row for the data, or we have to replace an existing one
1418          * that no longer matches.  This is similar to the way Emails are handled.
1419          */
addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, int type, int maxRows)1420         public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
1421                 int type, int maxRows) {
1422             // Make a list of all same type rows in the existing entity
1423             ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
1424             ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
1425             if (entity != null) {
1426                 oldValues = findUntypedData(entityValues, type, mimeType);
1427                 entityValues = entity.getSubValues();
1428             }
1429 
1430             // These will be rows needing replacement with new values
1431             ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
1432 
1433             // The count of existing rows
1434             int numRows = oldValues.size();
1435             for (UntypedRow row: rows) {
1436                 boolean found = false;
1437                 // If we already have this row, mark it
1438                 for (NamedContentValues ncv: oldValues) {
1439                     ContentValues cv = ncv.values;
1440                     String data = cv.getAsString(COMMON_DATA_ROW);
1441                     int rowType = -1;
1442                     if (cv.containsKey(COMMON_TYPE_ROW)) {
1443                         rowType = cv.getAsInteger(COMMON_TYPE_ROW);
1444                     }
1445                     if (row.isSameAs(rowType, data)) {
1446                         cv.put(FOUND_DATA_ROW, true);
1447                         // Remove this to indicate it's still being used
1448                         entityValues.remove(ncv);
1449                         found = true;
1450                         break;
1451                     }
1452                 }
1453                 if (!found) {
1454                     // If we don't, there are two possibilities
1455                     if (numRows < maxRows) {
1456                         // If there are available rows, add a new one
1457                         RowBuilder builder = newRowBuilder(entity, mimeType);
1458                         row.addValues(builder);
1459                         add(builder.build());
1460                         numRows++;
1461                     } else {
1462                         // Otherwise, say we need to replace a row with this
1463                         rowsToReplace.add(row);
1464                     }
1465                 }
1466             }
1467 
1468             // Go through rows needing replacement
1469             for (UntypedRow row: rowsToReplace) {
1470                 for (NamedContentValues ncv: oldValues) {
1471                     ContentValues cv = ncv.values;
1472                     // Find a row that hasn't been used (i.e. doesn't match current rows)
1473                     if (!cv.containsKey(FOUND_DATA_ROW)) {
1474                         // And update it
1475                         RowBuilder builder = new RowBuilder(
1476                                 ContentProviderOperation
1477                                     .newUpdate(addCallerIsSyncAdapterParameter(
1478                                         dataUriFromNamedContentValues(ncv))),
1479                                 ncv);
1480                         row.addValues(builder);
1481                         add(builder.build());
1482                     }
1483                 }
1484             }
1485         }
1486 
addOrganization(Entity entity, int type, String company, String title, String department, String yomiCompanyName, String officeLocation)1487         public void addOrganization(Entity entity, int type, String company, String title,
1488                 String department, String yomiCompanyName, String officeLocation) {
1489             RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
1490             ContentValues cv = builder.cv;
1491             if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
1492                     cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
1493                     cvCompareString(cv, Organization.DEPARTMENT, department) &&
1494                     cvCompareString(cv, Organization.TITLE, title) &&
1495                     cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
1496                 return;
1497             }
1498             builder.withValue(Organization.TYPE, type);
1499             builder.withValue(Organization.COMPANY, company);
1500             builder.withValue(Organization.TITLE, title);
1501             builder.withValue(Organization.DEPARTMENT, department);
1502             builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
1503             builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
1504             add(builder.build());
1505         }
1506 
addNote(Entity entity, String note)1507         public void addNote(Entity entity, String note) {
1508             RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
1509             ContentValues cv = builder.cv;
1510             if (note == null) return;
1511             note = note.replaceAll("\r\n", "\n");
1512             if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
1513                 return;
1514             }
1515 
1516             // Reject notes with nothing in them.  Often, we get something from Outlook when
1517             // nothing was ever entered.  Sigh.
1518             int len = note.length();
1519             int i = 0;
1520             for (; i < len; i++) {
1521                 char c = note.charAt(i);
1522                 if (!Character.isWhitespace(c)) {
1523                     break;
1524                 }
1525             }
1526             if (i == len) return;
1527 
1528             builder.withValue(Note.NOTE, note);
1529             add(builder.build());
1530         }
1531     }
1532 
1533     /**
1534      * Generate the uri for the data row associated with this NamedContentValues object
1535      * @param ncv the NamedContentValues object
1536      * @return a uri that can be used to refer to this row
1537      */
dataUriFromNamedContentValues(NamedContentValues ncv)1538     public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
1539         long id = ncv.values.getAsLong(RawContacts._ID);
1540         Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
1541         return dataUri;
1542     }
1543 
1544     @Override
cleanup()1545     public void cleanup() {
1546         // Mark the changed contacts dirty = 0
1547         // Permanently delete the user deletions
1548         ContactOperations ops = new ContactOperations();
1549         for (Long id: mUpdatedIdList) {
1550             ops.add(ContentProviderOperation
1551                     .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
1552                             .buildUpon()
1553                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1554                             .build())
1555                     .withValue(RawContacts.DIRTY, 0).build());
1556         }
1557         for (Long id: mDeletedIdList) {
1558             ops.add(ContentProviderOperation
1559                     .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
1560                             .buildUpon()
1561                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1562                             .build())
1563                     .build());
1564         }
1565         ops.execute();
1566         ContentResolver cr = mContext.getContentResolver();
1567         if (mGroupsUsed) {
1568             // Make sure the title column is set for all of our groups
1569             // And that all of our groups are visible
1570             // TODO Perhaps the visible part should only happen when the group is created, but
1571             // this is fine for now.
1572             Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
1573             Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
1574                     Groups.TITLE + " IS NULL", null, null);
1575             ContentValues values = new ContentValues();
1576             values.put(Groups.GROUP_VISIBLE, 1);
1577             try {
1578                 while (c.moveToNext()) {
1579                     String sourceId = c.getString(0);
1580                     values.put(Groups.TITLE, sourceId);
1581                     cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
1582                             Groups.SOURCE_ID + "=?", new String[] {sourceId});
1583                 }
1584             } finally {
1585                 c.close();
1586             }
1587         }
1588     }
1589 
1590     @Override
getCollectionName()1591     public String getCollectionName() {
1592         return "Contacts";
1593     }
1594 
sendEmail(Serializer s, ContentValues cv, int count, String displayName)1595     private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
1596             throws IOException {
1597         // Get both parts of the email address (a newly created one in the UI won't have a name)
1598         String addr = cv.getAsString(Email.DATA);
1599         String name = cv.getAsString(Email.DISPLAY_NAME);
1600         if (name == null) {
1601             if (displayName != null) {
1602                 name = displayName;
1603             } else {
1604                 name = addr;
1605             }
1606         }
1607         // Compose address from name and addr
1608         if (addr != null) {
1609             String value;
1610             // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on
1611             // an RFC822 address)
1612             if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
1613                 value = addr;
1614             } else {
1615                 value = '\"' + name + "\" <" + addr + '>';
1616             }
1617             if (count < MAX_EMAIL_ROWS) {
1618                 s.data(EMAIL_TAGS[count], value);
1619             }
1620         }
1621     }
1622 
sendIm(Serializer s, ContentValues cv, int count)1623     private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
1624         String value = cv.getAsString(Im.DATA);
1625         if (value == null) return;
1626         if (count < MAX_IM_ROWS) {
1627             s.data(IM_TAGS[count], value);
1628         }
1629     }
1630 
sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)1631     private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
1632             throws IOException{
1633         sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]);
1634         sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]);
1635         sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]);
1636         sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]);
1637         sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]);
1638     }
1639 
sendStructuredPostal(Serializer s, ContentValues cv)1640     private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
1641         switch (cv.getAsInteger(StructuredPostal.TYPE)) {
1642             case StructuredPostal.TYPE_HOME:
1643                 sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
1644                 break;
1645             case StructuredPostal.TYPE_WORK:
1646                 sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
1647                 break;
1648             case StructuredPostal.TYPE_OTHER:
1649                 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
1650                 break;
1651             default:
1652                 break;
1653         }
1654     }
1655 
sendStringData(Serializer s, ContentValues cv, String column, int tag)1656     private void sendStringData(Serializer s, ContentValues cv, String column, int tag)
1657             throws IOException {
1658         if (cv.containsKey(column)) {
1659             String value = cv.getAsString(column);
1660             if (!TextUtils.isEmpty(value)) {
1661                 s.data(tag, value);
1662             }
1663         }
1664     }
1665 
sendStructuredName(Serializer s, ContentValues cv)1666     private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
1667         String displayName = null;
1668         sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME);
1669         sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME);
1670         sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME);
1671         sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX);
1672         sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME);
1673         sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME);
1674         sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE);
1675         return displayName;
1676     }
1677 
sendBusiness(Serializer s, ContentValues cv)1678     private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
1679         sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME);
1680         sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID);
1681         sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID);
1682     }
1683 
sendPersonal(Serializer s, ContentValues cv)1684     private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
1685         sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
1686         sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
1687     }
1688 
sendBirthday(Serializer s, ContentValues cv)1689     private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
1690         sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY);
1691     }
1692 
sendPhoto(Serializer s, ContentValues cv)1693     private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
1694         if (cv.containsKey(Photo.PHOTO)) {
1695             byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
1696             String pic = Base64.encodeToString(bytes, Base64.NO_WRAP);
1697             s.data(Tags.CONTACTS_PICTURE, pic);
1698         } else {
1699             // Send an empty tag, which signals the server to delete any pre-existing photo
1700             s.tag(Tags.CONTACTS_PICTURE);
1701         }
1702     }
1703 
sendOrganization(Serializer s, ContentValues cv)1704     private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
1705         sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE);
1706         sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME);
1707         sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT);
1708         sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION);
1709     }
1710 
sendNickname(Serializer s, ContentValues cv)1711     private void sendNickname(Serializer s, ContentValues cv) throws IOException {
1712         sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME);
1713     }
1714 
sendWebpage(Serializer s, ContentValues cv)1715     private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
1716         sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE);
1717     }
1718 
sendNote(Serializer s, ContentValues cv)1719     private void sendNote(Serializer s, ContentValues cv) throws IOException {
1720         // Even when there is no local note, we must explicitly upsync an empty note,
1721         // which is the only way to force the server to delete any pre-existing note.
1722         String note = "";
1723         if (cv.containsKey(Note.NOTE)) {
1724             // EAS won't accept note data with raw newline characters
1725             note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
1726         }
1727         // Format of upsync data depends on protocol version
1728         if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
1729             s.start(Tags.BASE_BODY);
1730             s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
1731             s.end();
1732         } else {
1733             s.data(Tags.CONTACTS_BODY, note);
1734         }
1735     }
1736 
sendChildren(Serializer s, ContentValues cv)1737     private void sendChildren(Serializer s, ContentValues cv) throws IOException {
1738         boolean first = true;
1739         for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
1740             String row = EasChildren.ROWS[i];
1741             if (cv.containsKey(row)) {
1742                 if (first) {
1743                     s.start(Tags.CONTACTS_CHILDREN);
1744                     first = false;
1745                 }
1746                 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
1747             }
1748         }
1749         if (!first) {
1750             s.end();
1751         }
1752     }
1753 
sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)1754     private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
1755             throws IOException {
1756         String value = cv.getAsString(Phone.NUMBER);
1757         if (value == null) return;
1758         switch (cv.getAsInteger(Phone.TYPE)) {
1759             case Phone.TYPE_WORK:
1760                 if (workCount < MAX_PHONE_ROWS) {
1761                     s.data(WORK_PHONE_TAGS[workCount], value);
1762                 }
1763                 break;
1764             case Phone.TYPE_MMS:
1765                 s.data(Tags.CONTACTS2_MMS, value);
1766                 break;
1767             case Phone.TYPE_ASSISTANT:
1768                 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
1769                 break;
1770             case Phone.TYPE_FAX_WORK:
1771                 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
1772                 break;
1773             case Phone.TYPE_COMPANY_MAIN:
1774                 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
1775                 break;
1776             case Phone.TYPE_HOME:
1777                 if (homeCount < MAX_PHONE_ROWS) {
1778                     s.data(HOME_PHONE_TAGS[homeCount], value);
1779                 }
1780                 break;
1781             case Phone.TYPE_MOBILE:
1782                 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
1783                 break;
1784             case Phone.TYPE_CAR:
1785                 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
1786                 break;
1787             case Phone.TYPE_PAGER:
1788                 s.data(Tags.CONTACTS_PAGER_NUMBER, value);
1789                 break;
1790             case Phone.TYPE_RADIO:
1791                 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
1792                 break;
1793             case Phone.TYPE_FAX_HOME:
1794                 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
1795                 break;
1796             default:
1797                 break;
1798         }
1799     }
1800 
sendRelation(Serializer s, ContentValues cv)1801     private void sendRelation(Serializer s, ContentValues cv) throws IOException {
1802         String value = cv.getAsString(Relation.DATA);
1803         if (value == null) return;
1804         switch (cv.getAsInteger(Relation.TYPE)) {
1805             case Relation.TYPE_ASSISTANT:
1806                 s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
1807                 break;
1808             case Relation.TYPE_MANAGER:
1809                 s.data(Tags.CONTACTS2_MANAGER_NAME, value);
1810                 break;
1811             case Relation.TYPE_SPOUSE:
1812                 s.data(Tags.CONTACTS_SPOUSE, value);
1813                 break;
1814             default:
1815                 break;
1816         }
1817     }
1818 
dirtyContactsWithinDirtyGroups()1819     private void dirtyContactsWithinDirtyGroups() {
1820         ContentResolver cr = mService.mContentResolver;
1821         Cursor c = cr.query(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI),
1822                 GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null);
1823         try {
1824             if (c.getCount() > 0) {
1825                 String[] updateArgs = new String[1];
1826                 ContentValues updateValues = new ContentValues();
1827                 while (c.moveToNext()) {
1828                     // For each, "touch" all data rows with this group id; this will mark contacts
1829                     // in this group as dirty (per ContactsContract).  We will then know to upload
1830                     // them to the server with the modified group information
1831                     long id = c.getLong(0);
1832                     updateValues.put(GroupMembership.GROUP_ROW_ID, id);
1833                     updateArgs[0] = Long.toString(id);
1834                     cr.update(Data.CONTENT_URI, updateValues,
1835                             MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs);
1836                 }
1837                 // Really delete groups that are marked deleted
1838                 cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), Groups.DELETED + "=1",
1839                         null);
1840                 // Clear the dirty flag for all of our groups
1841                 updateValues.clear();
1842                 updateValues.put(Groups.DIRTY, 0);
1843                 cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), updateValues, null,
1844                         null);
1845             }
1846         } finally {
1847             c.close();
1848         }
1849     }
1850 
1851     @Override
sendLocalChanges(Serializer s)1852     public boolean sendLocalChanges(Serializer s) throws IOException {
1853         ContentResolver cr = mService.mContentResolver;
1854 
1855         // Find any groups of ours that are dirty and dirty those groups' members
1856         dirtyContactsWithinDirtyGroups();
1857 
1858         // First, let's find Contacts that have changed.
1859         Uri uri = uriWithAccountAndIsSyncAdapter(RawContactsEntity.CONTENT_URI);
1860         if (getSyncKey().equals("0")) {
1861             return false;
1862         }
1863 
1864         // Get them all atomically
1865         EntityIterator ei = RawContacts.newEntityIterator(
1866                 cr.query(uri, null, RawContacts.DIRTY + "=1", null, null));
1867         ContentValues cidValues = new ContentValues();
1868         try {
1869             boolean first = true;
1870             final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
1871             while (ei.hasNext()) {
1872                 Entity entity = ei.next();
1873                 // For each of these entities, create the change commands
1874                 ContentValues entityValues = entity.getEntityValues();
1875                 String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
1876                 ArrayList<Integer> groupIds = new ArrayList<Integer>();
1877                 if (first) {
1878                     s.start(Tags.SYNC_COMMANDS);
1879                     userLog("Sending Contacts changes to the server");
1880                     first = false;
1881                 }
1882                 if (serverId == null) {
1883                     // This is a new contact; create a clientId
1884                     String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
1885                     userLog("Creating new contact with clientId: ", clientId);
1886                     s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
1887                     // And save it in the raw contact
1888                     cidValues.put(RawContacts.SYNC1, clientId);
1889                     cr.update(ContentUris.
1890                             withAppendedId(rawContactUri,
1891                                     entityValues.getAsLong(RawContacts._ID)),
1892                                     cidValues, null, null);
1893                 } else {
1894                     if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
1895                         userLog("Deleting contact with serverId: ", serverId);
1896                         s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
1897                         mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
1898                         continue;
1899                     }
1900                     userLog("Upsync change to contact with serverId: " + serverId);
1901                     s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
1902                 }
1903                 s.start(Tags.SYNC_APPLICATION_DATA);
1904                 // Write out the data here
1905                 int imCount = 0;
1906                 int emailCount = 0;
1907                 int homePhoneCount = 0;
1908                 int workPhoneCount = 0;
1909                 String displayName = null;
1910                 ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
1911                 for (NamedContentValues ncv: entity.getSubValues()) {
1912                     ContentValues cv = ncv.values;
1913                     String mimeType = cv.getAsString(Data.MIMETYPE);
1914                     if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
1915                         emailValues.add(cv);
1916                     } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
1917                         sendNickname(s, cv);
1918                     } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
1919                         sendChildren(s, cv);
1920                     } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
1921                         sendBusiness(s, cv);
1922                     } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
1923                         sendWebpage(s, cv);
1924                     } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
1925                         sendPersonal(s, cv);
1926                     } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
1927                         sendPhone(s, cv, workPhoneCount, homePhoneCount);
1928                         int type = cv.getAsInteger(Phone.TYPE);
1929                         if (type == Phone.TYPE_HOME) homePhoneCount++;
1930                         if (type == Phone.TYPE_WORK) workPhoneCount++;
1931                     } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
1932                         sendRelation(s, cv);
1933                     } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
1934                         displayName = sendStructuredName(s, cv);
1935                     } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
1936                         sendStructuredPostal(s, cv);
1937                     } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
1938                         sendOrganization(s, cv);
1939                     } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
1940                         sendIm(s, cv, imCount++);
1941                     } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
1942                         Integer eventType = cv.getAsInteger(Event.TYPE);
1943                         if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
1944                             sendBirthday(s, cv);
1945                         }
1946                     } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
1947                         // We must gather these, and send them together (below)
1948                         groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
1949                     } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
1950                         sendNote(s, cv);
1951                     } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
1952                         sendPhoto(s, cv);
1953                     } else {
1954                         userLog("Contacts upsync, unknown data: ", mimeType);
1955                     }
1956                 }
1957 
1958                 // We do the email rows last, because we need to make sure we've found the
1959                 // displayName (if one exists); this would be in a StructuredName rnow
1960                 for (ContentValues cv: emailValues) {
1961                     sendEmail(s, cv, emailCount++, displayName);
1962                 }
1963 
1964                 // Now, we'll send up groups, if any
1965                 if (!groupIds.isEmpty()) {
1966                     boolean groupFirst = true;
1967                     for (int id: groupIds) {
1968                         // Since we get id's from the provider, we need to find their names
1969                         Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
1970                                 GROUP_TITLE_PROJECTION, null, null, null);
1971                         try {
1972                             // Presumably, this should always succeed, but ...
1973                             if (c.moveToFirst()) {
1974                                 if (groupFirst) {
1975                                     s.start(Tags.CONTACTS_CATEGORIES);
1976                                     groupFirst = false;
1977                                 }
1978                                 s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
1979                             }
1980                         } finally {
1981                             c.close();
1982                         }
1983                     }
1984                     if (!groupFirst) {
1985                         s.end();
1986                     }
1987                 }
1988                 s.end().end(); // ApplicationData & Change
1989                 mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
1990             }
1991             if (!first) {
1992                 s.end(); // Commands
1993             }
1994         } finally {
1995             ei.close();
1996         }
1997 
1998         return false;
1999     }
2000 }
2001