| /* |
| * Copyright (C) 2008-2009 Marc Blank |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.exchange.adapter; |
| |
| import android.content.ContentProviderClient; |
| import android.content.ContentProviderOperation; |
| import android.content.ContentProviderOperation.Builder; |
| import android.content.ContentProviderResult; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Entity; |
| import android.content.Entity.NamedContentValues; |
| import android.content.EntityIterator; |
| import android.content.OperationApplicationException; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.RemoteException; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Event; |
| import android.provider.ContactsContract.CommonDataKinds.GroupMembership; |
| import android.provider.ContactsContract.CommonDataKinds.Im; |
| import android.provider.ContactsContract.CommonDataKinds.Nickname; |
| import android.provider.ContactsContract.CommonDataKinds.Note; |
| import android.provider.ContactsContract.CommonDataKinds.Organization; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.ContactsContract.CommonDataKinds.Relation; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredName; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; |
| import android.provider.ContactsContract.CommonDataKinds.Website; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.Groups; |
| import android.provider.ContactsContract.RawContacts; |
| import android.provider.ContactsContract.RawContactsEntity; |
| import android.provider.ContactsContract.Settings; |
| import android.provider.ContactsContract.SyncState; |
| import android.provider.SyncStateContract; |
| import android.text.TextUtils; |
| import android.text.util.Rfc822Token; |
| import android.text.util.Rfc822Tokenizer; |
| import android.util.Base64; |
| import android.util.Log; |
| |
| import com.android.emailcommon.utility.Utility; |
| import com.android.exchange.CommandStatusException; |
| import com.android.exchange.Eas; |
| import com.android.exchange.EasSyncService; |
| import com.android.exchange.utility.CalendarUtilities; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.GregorianCalendar; |
| import java.util.TimeZone; |
| |
| /** |
| * Sync adapter for EAS Contacts |
| * |
| */ |
| public class ContactsSyncAdapter extends AbstractSyncAdapter { |
| |
| private static final String TAG = "EasContactsSyncAdapter"; |
| private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?"; |
| private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?"; |
| private static final String[] ID_PROJECTION = new String[] {RawContacts._ID}; |
| private static final String[] GROUP_TITLE_PROJECTION = new String[] {Groups.TITLE}; |
| private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = Data.MIMETYPE + "='" + |
| GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=?"; |
| private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID}; |
| |
| private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES |
| = new ArrayList<NamedContentValues>(); |
| |
| private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW"; |
| |
| private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, |
| Tags.CONTACTS_HOME_ADDRESS_COUNTRY, |
| Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE, |
| Tags.CONTACTS_HOME_ADDRESS_STATE, |
| Tags.CONTACTS_HOME_ADDRESS_STREET}; |
| |
| private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY, |
| Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY, |
| Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE, |
| Tags.CONTACTS_BUSINESS_ADDRESS_STATE, |
| Tags.CONTACTS_BUSINESS_ADDRESS_STREET}; |
| |
| private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY, |
| Tags.CONTACTS_OTHER_ADDRESS_COUNTRY, |
| Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE, |
| Tags.CONTACTS_OTHER_ADDRESS_STATE, |
| Tags.CONTACTS_OTHER_ADDRESS_STREET}; |
| |
| private static final int MAX_IM_ROWS = 3; |
| private static final int MAX_EMAIL_ROWS = 3; |
| private static final int MAX_PHONE_ROWS = 2; |
| private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc. |
| private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row |
| |
| private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS, |
| Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3}; |
| |
| private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS, |
| Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS}; |
| |
| private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, |
| Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER}; |
| |
| private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER, |
| Tags.CONTACTS_HOME2_TELEPHONE_NUMBER}; |
| |
| private static final Object sSyncKeyLock = new Object(); |
| |
| ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); |
| ArrayList<Long> mUpdatedIdList = new ArrayList<Long>(); |
| |
| private final Uri mAccountUri; |
| private final ContentResolver mContentResolver; |
| private boolean mGroupsUsed = false; |
| |
| public ContactsSyncAdapter(EasSyncService service) { |
| super(service); |
| mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI); |
| mContentResolver = mContext.getContentResolver(); |
| } |
| |
| static Uri addCallerIsSyncAdapterParameter(Uri uri) { |
| return uri.buildUpon() |
| .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") |
| .build(); |
| } |
| |
| @Override |
| public void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync) |
| throws IOException { |
| if (initialSync) { |
| // These are the tags we support for upload; whenever we add/remove support |
| // (in addData), we need to update this list |
| s.start(Tags.SYNC_SUPPORTED); |
| s.tag(Tags.CONTACTS_FIRST_NAME); |
| s.tag(Tags.CONTACTS_LAST_NAME); |
| s.tag(Tags.CONTACTS_MIDDLE_NAME); |
| s.tag(Tags.CONTACTS_SUFFIX); |
| s.tag(Tags.CONTACTS_COMPANY_NAME); |
| s.tag(Tags.CONTACTS_JOB_TITLE); |
| s.tag(Tags.CONTACTS_EMAIL1_ADDRESS); |
| s.tag(Tags.CONTACTS_EMAIL2_ADDRESS); |
| s.tag(Tags.CONTACTS_EMAIL3_ADDRESS); |
| s.tag(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS2_MMS); |
| s.tag(Tags.CONTACTS_BUSINESS_FAX_NUMBER); |
| s.tag(Tags.CONTACTS2_COMPANY_MAIN_PHONE); |
| s.tag(Tags.CONTACTS_HOME_FAX_NUMBER); |
| s.tag(Tags.CONTACTS_HOME_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS_CAR_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS_PAGER_NUMBER); |
| s.tag(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER); |
| s.tag(Tags.CONTACTS2_IM_ADDRESS); |
| s.tag(Tags.CONTACTS2_IM_ADDRESS_2); |
| s.tag(Tags.CONTACTS2_IM_ADDRESS_3); |
| s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_CITY); |
| s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY); |
| s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE); |
| s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STATE); |
| s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STREET); |
| s.tag(Tags.CONTACTS_HOME_ADDRESS_CITY); |
| s.tag(Tags.CONTACTS_HOME_ADDRESS_COUNTRY); |
| s.tag(Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE); |
| s.tag(Tags.CONTACTS_HOME_ADDRESS_STATE); |
| s.tag(Tags.CONTACTS_HOME_ADDRESS_STREET); |
| s.tag(Tags.CONTACTS_OTHER_ADDRESS_CITY); |
| s.tag(Tags.CONTACTS_OTHER_ADDRESS_COUNTRY); |
| s.tag(Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE); |
| s.tag(Tags.CONTACTS_OTHER_ADDRESS_STATE); |
| s.tag(Tags.CONTACTS_OTHER_ADDRESS_STREET); |
| s.tag(Tags.CONTACTS_YOMI_COMPANY_NAME); |
| s.tag(Tags.CONTACTS_YOMI_FIRST_NAME); |
| s.tag(Tags.CONTACTS_YOMI_LAST_NAME); |
| s.tag(Tags.CONTACTS2_NICKNAME); |
| s.tag(Tags.CONTACTS_ASSISTANT_NAME); |
| s.tag(Tags.CONTACTS2_MANAGER_NAME); |
| s.tag(Tags.CONTACTS_SPOUSE); |
| s.tag(Tags.CONTACTS_DEPARTMENT); |
| s.tag(Tags.CONTACTS_TITLE); |
| s.tag(Tags.CONTACTS_OFFICE_LOCATION); |
| s.tag(Tags.CONTACTS2_CUSTOMER_ID); |
| s.tag(Tags.CONTACTS2_GOVERNMENT_ID); |
| s.tag(Tags.CONTACTS2_ACCOUNT_NAME); |
| s.tag(Tags.CONTACTS_ANNIVERSARY); |
| s.tag(Tags.CONTACTS_BIRTHDAY); |
| s.tag(Tags.CONTACTS_WEBPAGE); |
| s.tag(Tags.CONTACTS_PICTURE); |
| s.end(); // SYNC_SUPPORTED |
| } else { |
| setPimSyncOptions(protocolVersion, null, s); |
| } |
| } |
| |
| @Override |
| public boolean isSyncable() { |
| return ContentResolver.getSyncAutomatically( |
| mAccountManagerAccount, ContactsContract.AUTHORITY); |
| } |
| |
| @Override |
| public boolean parse(InputStream is) throws IOException, CommandStatusException { |
| EasContactsSyncParser p = new EasContactsSyncParser(is, this); |
| return p.parse(); |
| } |
| |
| |
| @Override |
| public void wipe() { |
| mContentResolver.delete(mAccountUri, null, null); |
| } |
| |
| interface UntypedRow { |
| public void addValues(RowBuilder builder); |
| public boolean isSameAs(int type, String value); |
| } |
| |
| /** |
| * We get our SyncKey from ContactsProvider. If there's not one, we set it to "0" (the reset |
| * state) and save that away. |
| */ |
| @Override |
| public String getSyncKey() throws IOException { |
| synchronized (sSyncKeyLock) { |
| ContentProviderClient client = mService.mContentResolver |
| .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); |
| try { |
| byte[] data = SyncStateContract.Helpers.get(client, |
| ContactsContract.SyncState.CONTENT_URI, mAccountManagerAccount); |
| if (data == null || data.length == 0) { |
| // Initialize the SyncKey |
| setSyncKey("0", false); |
| // Make sure ungrouped contacts for Exchange are defaultly visible |
| ContentValues cv = new ContentValues(); |
| cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress); |
| cv.put(Groups.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| cv.put(Settings.UNGROUPED_VISIBLE, true); |
| client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv); |
| return "0"; |
| } else { |
| return new String(data); |
| } |
| } catch (RemoteException e) { |
| throw new IOException("Can't get SyncKey from ContactsProvider"); |
| } |
| } |
| } |
| |
| /** |
| * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other |
| * cases, the SyncKey is set within ContactOperations |
| */ |
| @Override |
| public void setSyncKey(String syncKey, boolean inCommands) throws IOException { |
| synchronized (sSyncKeyLock) { |
| if ("0".equals(syncKey) || !inCommands) { |
| ContentProviderClient client = mService.mContentResolver |
| .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); |
| try { |
| SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI, |
| mAccountManagerAccount, syncKey.getBytes()); |
| userLog("SyncKey set to ", syncKey, " in ContactsProvider"); |
| } catch (RemoteException e) { |
| throw new IOException("Can't set SyncKey in ContactsProvider"); |
| } |
| } |
| mMailbox.mSyncKey = syncKey; |
| } |
| } |
| |
| public static final class EasChildren { |
| private EasChildren() {} |
| |
| /** MIME type used when storing this in data table. */ |
| public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children"; |
| public static final int MAX_CHILDREN = 8; |
| public static final String[] ROWS = |
| new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"}; |
| } |
| |
| public static final class EasPersonal { |
| String anniversary; |
| String fileAs; |
| |
| /** MIME type used when storing this in data table. */ |
| public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal"; |
| public static final String ANNIVERSARY = "data2"; |
| public static final String FILE_AS = "data4"; |
| |
| boolean hasData() { |
| return anniversary != null || fileAs != null; |
| } |
| } |
| |
| public static final class EasBusiness { |
| String customerId; |
| String governmentId; |
| String accountName; |
| |
| /** MIME type used when storing this in data table. */ |
| public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business"; |
| public static final String CUSTOMER_ID = "data6"; |
| public static final String GOVERNMENT_ID = "data7"; |
| public static final String ACCOUNT_NAME = "data8"; |
| |
| boolean hasData() { |
| return customerId != null || governmentId != null || accountName != null; |
| } |
| } |
| |
| public static final class Address { |
| String city; |
| String country; |
| String code; |
| String street; |
| String state; |
| |
| boolean hasData() { |
| return city != null || country != null || code != null || state != null |
| || street != null; |
| } |
| } |
| |
| class EmailRow implements UntypedRow { |
| String email; |
| String displayName; |
| |
| public EmailRow(String _email) { |
| Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email); |
| // Can't happen, but belt & suspenders |
| if (tokens.length == 0) { |
| email = ""; |
| displayName = ""; |
| } else { |
| Rfc822Token token = tokens[0]; |
| email = token.getAddress(); |
| displayName = token.getName(); |
| } |
| } |
| |
| @Override |
| public void addValues(RowBuilder builder) { |
| builder.withValue(Email.DATA, email); |
| builder.withValue(Email.DISPLAY_NAME, displayName); |
| } |
| |
| @Override |
| public boolean isSameAs(int type, String value) { |
| return email.equalsIgnoreCase(value); |
| } |
| } |
| |
| class ImRow implements UntypedRow { |
| String im; |
| |
| public ImRow(String _im) { |
| im = _im; |
| } |
| |
| @Override |
| public void addValues(RowBuilder builder) { |
| builder.withValue(Im.DATA, im); |
| } |
| |
| @Override |
| public boolean isSameAs(int type, String value) { |
| return im.equalsIgnoreCase(value); |
| } |
| } |
| |
| class PhoneRow implements UntypedRow { |
| String phone; |
| int type; |
| |
| public PhoneRow(String _phone, int _type) { |
| phone = _phone; |
| type = _type; |
| } |
| |
| @Override |
| public void addValues(RowBuilder builder) { |
| builder.withValue(Im.DATA, phone); |
| builder.withValue(Phone.TYPE, type); |
| } |
| |
| @Override |
| public boolean isSameAs(int _type, String value) { |
| return type == _type && phone.equalsIgnoreCase(value); |
| } |
| } |
| |
| class EasContactsSyncParser extends AbstractSyncParser { |
| |
| String[] mBindArgument = new String[1]; |
| String mMailboxIdAsString; |
| ContactOperations ops = new ContactOperations(); |
| |
| public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter) |
| throws IOException { |
| super(in, adapter); |
| } |
| |
| public void addData(String serverId, ContactOperations ops, Entity entity) |
| throws IOException { |
| String prefix = null; |
| String firstName = null; |
| String lastName = null; |
| String middleName = null; |
| String suffix = null; |
| String companyName = null; |
| String yomiFirstName = null; |
| String yomiLastName = null; |
| String yomiCompanyName = null; |
| String title = null; |
| String department = null; |
| String officeLocation = null; |
| Address home = new Address(); |
| Address work = new Address(); |
| Address other = new Address(); |
| EasBusiness business = new EasBusiness(); |
| EasPersonal personal = new EasPersonal(); |
| ArrayList<String> children = new ArrayList<String>(); |
| ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>(); |
| ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>(); |
| ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>(); |
| ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>(); |
| if (entity == null) { |
| ops.newContact(serverId); |
| } |
| |
| while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { |
| switch (tag) { |
| case Tags.CONTACTS_FIRST_NAME: |
| firstName = getValue(); |
| break; |
| case Tags.CONTACTS_LAST_NAME: |
| lastName = getValue(); |
| break; |
| case Tags.CONTACTS_MIDDLE_NAME: |
| middleName = getValue(); |
| break; |
| case Tags.CONTACTS_SUFFIX: |
| suffix = getValue(); |
| break; |
| case Tags.CONTACTS_COMPANY_NAME: |
| companyName = getValue(); |
| break; |
| case Tags.CONTACTS_JOB_TITLE: |
| title = getValue(); |
| break; |
| case Tags.CONTACTS_EMAIL1_ADDRESS: |
| case Tags.CONTACTS_EMAIL2_ADDRESS: |
| case Tags.CONTACTS_EMAIL3_ADDRESS: |
| emails.add(new EmailRow(getValue())); |
| break; |
| case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER: |
| case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER: |
| workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK)); |
| break; |
| case Tags.CONTACTS2_MMS: |
| ops.addPhone(entity, Phone.TYPE_MMS, getValue()); |
| break; |
| case Tags.CONTACTS_BUSINESS_FAX_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue()); |
| break; |
| case Tags.CONTACTS2_COMPANY_MAIN_PHONE: |
| ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue()); |
| break; |
| case Tags.CONTACTS_HOME_FAX_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue()); |
| break; |
| case Tags.CONTACTS_HOME_TELEPHONE_NUMBER: |
| case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER: |
| homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME)); |
| break; |
| case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_MOBILE, getValue()); |
| break; |
| case Tags.CONTACTS_CAR_TELEPHONE_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_CAR, getValue()); |
| break; |
| case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_RADIO, getValue()); |
| break; |
| case Tags.CONTACTS_PAGER_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_PAGER, getValue()); |
| break; |
| case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER: |
| ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue()); |
| break; |
| case Tags.CONTACTS2_IM_ADDRESS: |
| case Tags.CONTACTS2_IM_ADDRESS_2: |
| case Tags.CONTACTS2_IM_ADDRESS_3: |
| ims.add(new ImRow(getValue())); |
| break; |
| case Tags.CONTACTS_BUSINESS_ADDRESS_CITY: |
| work.city = getValue(); |
| break; |
| case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY: |
| work.country = getValue(); |
| break; |
| case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE: |
| work.code = getValue(); |
| break; |
| case Tags.CONTACTS_BUSINESS_ADDRESS_STATE: |
| work.state = getValue(); |
| break; |
| case Tags.CONTACTS_BUSINESS_ADDRESS_STREET: |
| work.street = getValue(); |
| break; |
| case Tags.CONTACTS_HOME_ADDRESS_CITY: |
| home.city = getValue(); |
| break; |
| case Tags.CONTACTS_HOME_ADDRESS_COUNTRY: |
| home.country = getValue(); |
| break; |
| case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE: |
| home.code = getValue(); |
| break; |
| case Tags.CONTACTS_HOME_ADDRESS_STATE: |
| home.state = getValue(); |
| break; |
| case Tags.CONTACTS_HOME_ADDRESS_STREET: |
| home.street = getValue(); |
| break; |
| case Tags.CONTACTS_OTHER_ADDRESS_CITY: |
| other.city = getValue(); |
| break; |
| case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY: |
| other.country = getValue(); |
| break; |
| case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE: |
| other.code = getValue(); |
| break; |
| case Tags.CONTACTS_OTHER_ADDRESS_STATE: |
| other.state = getValue(); |
| break; |
| case Tags.CONTACTS_OTHER_ADDRESS_STREET: |
| other.street = getValue(); |
| break; |
| |
| case Tags.CONTACTS_CHILDREN: |
| childrenParser(children); |
| break; |
| |
| case Tags.CONTACTS_YOMI_COMPANY_NAME: |
| yomiCompanyName = getValue(); |
| break; |
| case Tags.CONTACTS_YOMI_FIRST_NAME: |
| yomiFirstName = getValue(); |
| break; |
| case Tags.CONTACTS_YOMI_LAST_NAME: |
| yomiLastName = getValue(); |
| break; |
| |
| case Tags.CONTACTS2_NICKNAME: |
| ops.addNickname(entity, getValue()); |
| break; |
| |
| case Tags.CONTACTS_ASSISTANT_NAME: |
| ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue()); |
| break; |
| case Tags.CONTACTS2_MANAGER_NAME: |
| ops.addRelation(entity, Relation.TYPE_MANAGER, getValue()); |
| break; |
| case Tags.CONTACTS_SPOUSE: |
| ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue()); |
| break; |
| case Tags.CONTACTS_DEPARTMENT: |
| department = getValue(); |
| break; |
| case Tags.CONTACTS_TITLE: |
| prefix = getValue(); |
| break; |
| |
| // EAS Business |
| case Tags.CONTACTS_OFFICE_LOCATION: |
| officeLocation = getValue(); |
| break; |
| case Tags.CONTACTS2_CUSTOMER_ID: |
| business.customerId = getValue(); |
| break; |
| case Tags.CONTACTS2_GOVERNMENT_ID: |
| business.governmentId = getValue(); |
| break; |
| case Tags.CONTACTS2_ACCOUNT_NAME: |
| business.accountName = getValue(); |
| break; |
| |
| // EAS Personal |
| case Tags.CONTACTS_ANNIVERSARY: |
| personal.anniversary = getValue(); |
| break; |
| case Tags.CONTACTS_BIRTHDAY: |
| ops.addBirthday(entity, getValue()); |
| break; |
| case Tags.CONTACTS_WEBPAGE: |
| ops.addWebpage(entity, getValue()); |
| break; |
| |
| case Tags.CONTACTS_PICTURE: |
| ops.addPhoto(entity, getValue()); |
| break; |
| |
| case Tags.BASE_BODY: |
| ops.addNote(entity, bodyParser()); |
| break; |
| case Tags.CONTACTS_BODY: |
| ops.addNote(entity, getValue()); |
| break; |
| |
| case Tags.CONTACTS_CATEGORIES: |
| mGroupsUsed = true; |
| categoriesParser(ops, entity); |
| break; |
| |
| default: |
| skipTag(); |
| } |
| } |
| |
| // We must have first name, last name, or company name |
| String name = null; |
| if (firstName != null || lastName != null) { |
| if (firstName == null) { |
| name = lastName; |
| } else if (lastName == null) { |
| name = firstName; |
| } else { |
| name = firstName + ' ' + lastName; |
| } |
| } else if (companyName != null) { |
| name = companyName; |
| } |
| |
| ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name, |
| yomiFirstName, yomiLastName); |
| ops.addBusiness(entity, business); |
| ops.addPersonal(entity, personal); |
| |
| ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS); |
| ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS); |
| ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME, |
| MAX_PHONE_ROWS); |
| ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK, |
| MAX_PHONE_ROWS); |
| |
| if (!children.isEmpty()) { |
| ops.addChildren(entity, children); |
| } |
| |
| if (work.hasData()) { |
| ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city, |
| work.state, work.country, work.code); |
| } |
| if (home.hasData()) { |
| ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city, |
| home.state, home.country, home.code); |
| } |
| if (other.hasData()) { |
| ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city, |
| other.state, other.country, other.code); |
| } |
| |
| if (companyName != null) { |
| ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department, |
| yomiCompanyName, officeLocation); |
| } |
| |
| if (entity != null) { |
| // We've been removing rows from the list as they've been found in the xml |
| // Any that are left must have been deleted on the server |
| ArrayList<NamedContentValues> ncvList = entity.getSubValues(); |
| for (NamedContentValues ncv: ncvList) { |
| // These rows need to be deleted... |
| Uri u = dataUriFromNamedContentValues(ncv); |
| ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u)) |
| .build()); |
| } |
| } |
| } |
| |
| private void categoriesParser(ContactOperations ops, Entity entity) throws IOException { |
| while (nextTag(Tags.CONTACTS_CATEGORIES) != END) { |
| switch (tag) { |
| case Tags.CONTACTS_CATEGORY: |
| ops.addGroup(entity, getValue()); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| private void childrenParser(ArrayList<String> children) throws IOException { |
| while (nextTag(Tags.CONTACTS_CHILDREN) != END) { |
| switch (tag) { |
| case Tags.CONTACTS_CHILD: |
| if (children.size() < EasChildren.MAX_CHILDREN) { |
| children.add(getValue()); |
| } |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| private String bodyParser() throws IOException { |
| String body = null; |
| while (nextTag(Tags.BASE_BODY) != END) { |
| switch (tag) { |
| case Tags.BASE_DATA: |
| body = getValue(); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| return body; |
| } |
| |
| public void addParser(ContactOperations ops) throws IOException { |
| String serverId = null; |
| while (nextTag(Tags.SYNC_ADD) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: // same as |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_APPLICATION_DATA: |
| addData(serverId, ops, null); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| private Cursor getServerIdCursor(String serverId) { |
| mBindArgument[0] = serverId; |
| return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION, |
| mBindArgument, null); |
| } |
| |
| private Cursor getClientIdCursor(String clientId) { |
| mBindArgument[0] = clientId; |
| return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION, |
| mBindArgument, null); |
| } |
| |
| public void deleteParser(ContactOperations ops) throws IOException { |
| while (nextTag(Tags.SYNC_DELETE) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| String serverId = getValue(); |
| // Find the message in this mailbox with the given serverId |
| Cursor c = getServerIdCursor(serverId); |
| try { |
| if (c.moveToFirst()) { |
| userLog("Deleting ", serverId); |
| ops.delete(c.getLong(0)); |
| } |
| } finally { |
| c.close(); |
| } |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| class ServerChange { |
| long id; |
| boolean read; |
| |
| ServerChange(long _id, boolean _read) { |
| id = _id; |
| read = _read; |
| } |
| } |
| |
| /** |
| * Changes are handled row by row, and only changed/new rows are acted upon |
| * @param ops the array of pending ContactProviderOperations. |
| * @throws IOException |
| */ |
| public void changeParser(ContactOperations ops) throws IOException { |
| String serverId = null; |
| Entity entity = null; |
| while (nextTag(Tags.SYNC_CHANGE) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| serverId = getValue(); |
| Cursor c = getServerIdCursor(serverId); |
| try { |
| if (c.moveToFirst()) { |
| // TODO Handle deleted individual rows... |
| Uri uri = ContentUris.withAppendedId( |
| RawContacts.CONTENT_URI, c.getLong(0)); |
| uri = Uri.withAppendedPath( |
| uri, RawContacts.Entity.CONTENT_DIRECTORY); |
| EntityIterator entityIterator = RawContacts.newEntityIterator( |
| mContentResolver.query(uri, null, null, null, null)); |
| if (entityIterator.hasNext()) { |
| entity = entityIterator.next(); |
| } |
| userLog("Changing contact ", serverId); |
| } |
| } finally { |
| c.close(); |
| } |
| break; |
| case Tags.SYNC_APPLICATION_DATA: |
| addData(serverId, ops, entity); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| @Override |
| public void commandsParser() throws IOException { |
| while (nextTag(Tags.SYNC_COMMANDS) != END) { |
| if (tag == Tags.SYNC_ADD) { |
| addParser(ops); |
| incrementChangeCount(); |
| } else if (tag == Tags.SYNC_DELETE) { |
| deleteParser(ops); |
| incrementChangeCount(); |
| } else if (tag == Tags.SYNC_CHANGE) { |
| changeParser(ops); |
| incrementChangeCount(); |
| } else |
| skipTag(); |
| } |
| } |
| |
| @Override |
| public void commit() throws IOException { |
| // Save the syncKey here, using the Helper provider by Contacts provider |
| userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey); |
| ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI, |
| mAccountManagerAccount, mMailbox.mSyncKey.getBytes())); |
| |
| // Execute these all at once... |
| ops.execute(); |
| |
| if (ops.mResults != null) { |
| ContentValues cv = new ContentValues(); |
| cv.put(RawContacts.DIRTY, 0); |
| for (int i = 0; i < ops.mContactIndexCount; i++) { |
| int index = ops.mContactIndexArray[i]; |
| Uri u = ops.mResults[index].uri; |
| if (u != null) { |
| String idString = u.getLastPathSegment(); |
| mContentResolver.update( |
| addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv, |
| RawContacts._ID + "=" + idString, null); |
| } |
| } |
| } |
| } |
| |
| public void addResponsesParser() throws IOException { |
| String serverId = null; |
| String clientId = null; |
| ContentValues cv = new ContentValues(); |
| while (nextTag(Tags.SYNC_ADD) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_CLIENT_ID: |
| clientId = getValue(); |
| break; |
| case Tags.SYNC_STATUS: |
| getValue(); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| |
| // This is theoretically impossible, but... |
| if (clientId == null || serverId == null) return; |
| |
| Cursor c = getClientIdCursor(clientId); |
| try { |
| if (c.moveToFirst()) { |
| cv.put(RawContacts.SOURCE_ID, serverId); |
| cv.put(RawContacts.DIRTY, 0); |
| ops.add(ContentProviderOperation.newUpdate( |
| ContentUris.withAppendedId( |
| addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), |
| c.getLong(0))) |
| .withValues(cv) |
| .build()); |
| userLog("New contact " + clientId + " was given serverId: " + serverId); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| |
| public void changeResponsesParser() throws IOException { |
| String serverId = null; |
| String status = null; |
| while (nextTag(Tags.SYNC_CHANGE) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_STATUS: |
| status = getValue(); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| if (serverId != null && status != null) { |
| userLog("Changed contact " + serverId + " failed with status: " + status); |
| } |
| } |
| |
| |
| @Override |
| public void responsesParser() throws IOException { |
| // Handle server responses here (for Add and Change) |
| while (nextTag(Tags.SYNC_RESPONSES) != END) { |
| if (tag == Tags.SYNC_ADD) { |
| addResponsesParser(); |
| } else if (tag == Tags.SYNC_CHANGE) { |
| changeResponsesParser(); |
| } else |
| skipTag(); |
| } |
| } |
| } |
| |
| |
| private Uri uriWithAccountAndIsSyncAdapter(Uri uri) { |
| return uri.buildUpon() |
| .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress) |
| .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE) |
| .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") |
| .build(); |
| } |
| |
| /** |
| * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a |
| * ContentProvider. It has, in addition to the Builder, ContentValues which, if present, |
| * represent the current values of that row, that can be compared against current values to |
| * see whether an update is even necessary. The methods on SmartBuilder are delegated to |
| * the Builder. |
| */ |
| private class RowBuilder { |
| Builder builder; |
| ContentValues cv; |
| |
| public RowBuilder(Builder _builder) { |
| builder = _builder; |
| } |
| |
| public RowBuilder(Builder _builder, NamedContentValues _ncv) { |
| builder = _builder; |
| cv = _ncv.values; |
| } |
| |
| RowBuilder withValueBackReference(String key, int previousResult) { |
| builder.withValueBackReference(key, previousResult); |
| return this; |
| } |
| |
| ContentProviderOperation build() { |
| return builder.build(); |
| } |
| |
| RowBuilder withValue(String key, Object value) { |
| builder.withValue(key, value); |
| return this; |
| } |
| } |
| |
| private class ContactOperations extends ArrayList<ContentProviderOperation> { |
| private static final long serialVersionUID = 1L; |
| private int mCount = 0; |
| private int mContactBackValue = mCount; |
| // Make an array big enough for the PIM window (max items we can get) |
| private int[] mContactIndexArray = |
| new int[Integer.parseInt(AbstractSyncAdapter.PIM_WINDOW_SIZE)]; |
| private int mContactIndexCount = 0; |
| private ContentProviderResult[] mResults = null; |
| |
| @Override |
| public boolean add(ContentProviderOperation op) { |
| super.add(op); |
| mCount++; |
| return true; |
| } |
| |
| public void newContact(String serverId) { |
| Builder builder = ContentProviderOperation |
| .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI)); |
| ContentValues values = new ContentValues(); |
| values.put(RawContacts.SOURCE_ID, serverId); |
| builder.withValues(values); |
| mContactBackValue = mCount; |
| mContactIndexArray[mContactIndexCount++] = mCount; |
| add(builder.build()); |
| } |
| |
| public void delete(long id) { |
| add(ContentProviderOperation |
| .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) |
| .buildUpon() |
| .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") |
| .build()) |
| .build()); |
| } |
| |
| public void execute() { |
| synchronized (mService.getSynchronizer()) { |
| if (!mService.isStopped()) { |
| try { |
| if (!isEmpty()) { |
| mService.userLog("Executing ", size(), " CPO's"); |
| mResults = mContext.getContentResolver().applyBatch( |
| ContactsContract.AUTHORITY, this); |
| } |
| } catch (RemoteException e) { |
| // There is nothing sensible to be done here |
| Log.e(TAG, "problem inserting contact during server update", e); |
| } catch (OperationApplicationException e) { |
| // There is nothing sensible to be done here |
| Log.e(TAG, "problem inserting contact during server update", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Given the list of NamedContentValues for an entity, a mime type, and a subtype, |
| * tries to find a match, returning it |
| * @param list the list of NCV's from the contact entity |
| * @param contentItemType the mime type we're looking for |
| * @param type the subtype (e.g. HOME, WORK, etc.) |
| * @return the matching NCV or null if not found |
| */ |
| private NamedContentValues findTypedData(ArrayList<NamedContentValues> list, |
| String contentItemType, int type, String stringType) { |
| NamedContentValues result = null; |
| |
| // Loop through the ncv's, looking for an existing row |
| for (NamedContentValues namedContentValues: list) { |
| Uri uri = namedContentValues.uri; |
| ContentValues cv = namedContentValues.values; |
| if (Data.CONTENT_URI.equals(uri)) { |
| String mimeType = cv.getAsString(Data.MIMETYPE); |
| if (mimeType.equals(contentItemType)) { |
| if (stringType != null) { |
| if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) { |
| result = namedContentValues; |
| } |
| // Note Email.TYPE could be ANY type column; they are all defined in |
| // the private CommonColumns class in ContactsContract |
| // We'll accept either type < 0 (don't care), cv doesn't have a type, |
| // or the types are equal |
| } else if (type < 0 || !cv.containsKey(Email.TYPE) || |
| cv.getAsInteger(Email.TYPE) == type) { |
| result = namedContentValues; |
| } |
| } |
| } |
| } |
| |
| // If we've found an existing data row, we'll delete it. Any rows left at the |
| // end should be deleted... |
| if (result != null) { |
| list.remove(result); |
| } |
| |
| // Return the row found (or null) |
| return result; |
| } |
| |
| /** |
| * Given the list of NamedContentValues for an entity and a mime type |
| * gather all of the matching NCV's, returning them |
| * @param list the list of NCV's from the contact entity |
| * @param contentItemType the mime type we're looking for |
| * @param type the subtype (e.g. HOME, WORK, etc.) |
| * @return the matching NCVs |
| */ |
| private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list, |
| int type, String contentItemType) { |
| ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>(); |
| |
| // Loop through the ncv's, looking for an existing row |
| for (NamedContentValues namedContentValues: list) { |
| Uri uri = namedContentValues.uri; |
| ContentValues cv = namedContentValues.values; |
| if (Data.CONTENT_URI.equals(uri)) { |
| String mimeType = cv.getAsString(Data.MIMETYPE); |
| if (mimeType.equals(contentItemType)) { |
| if (type != -1) { |
| int subtype = cv.getAsInteger(Phone.TYPE); |
| if (type != subtype) { |
| continue; |
| } |
| } |
| result.add(namedContentValues); |
| } |
| } |
| } |
| |
| // If we've found an existing data row, we'll delete it. Any rows left at the |
| // end should be deleted... |
| for (NamedContentValues values : result) { |
| list.remove(values); |
| } |
| |
| // Return the row found (or null) |
| return result; |
| } |
| |
| /** |
| * Create a wrapper for a builder (insert or update) that also includes the NCV for |
| * an existing row of this type. If the SmartBuilder's cv field is not null, then |
| * it represents the current (old) values of this field. The caller can then check |
| * whether the field is now different and needs to be updated; if it's not different, |
| * the caller will simply return and not generate a new CPO. Otherwise, the builder |
| * should have its content values set, and the built CPO should be added to the |
| * ContactOperations list. |
| * |
| * @param entity the contact entity (or null if this is a new contact) |
| * @param mimeType the mime type of this row |
| * @param type the subtype of this row |
| * @param stringType for groups, the name of the group (type will be ignored), or null |
| * @return the created SmartBuilder |
| */ |
| public RowBuilder createBuilder(Entity entity, String mimeType, int type, |
| String stringType) { |
| RowBuilder builder = null; |
| |
| if (entity != null) { |
| NamedContentValues ncv = |
| findTypedData(entity.getSubValues(), mimeType, type, stringType); |
| if (ncv != null) { |
| builder = new RowBuilder( |
| ContentProviderOperation |
| .newUpdate(addCallerIsSyncAdapterParameter( |
| dataUriFromNamedContentValues(ncv))), |
| ncv); |
| } |
| } |
| |
| if (builder == null) { |
| builder = newRowBuilder(entity, mimeType); |
| } |
| |
| // Return the appropriate builder (insert or update) |
| // Caller will fill in the appropriate values; 4 MIMETYPE is already set |
| return builder; |
| } |
| |
| private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) { |
| return createBuilder(entity, mimeType, type, null); |
| } |
| |
| private RowBuilder untypedRowBuilder(Entity entity, String mimeType) { |
| return createBuilder(entity, mimeType, -1, null); |
| } |
| |
| private RowBuilder newRowBuilder(Entity entity, String mimeType) { |
| // This is a new row; first get the contactId |
| // If the Contact is new, use the saved back value; otherwise the value in the entity |
| int contactId = mContactBackValue; |
| if (entity != null) { |
| contactId = entity.getEntityValues().getAsInteger(RawContacts._ID); |
| } |
| |
| // Create an insert operation with the proper contactId reference |
| RowBuilder builder = |
| new RowBuilder(ContentProviderOperation.newInsert( |
| addCallerIsSyncAdapterParameter(Data.CONTENT_URI))); |
| if (entity == null) { |
| builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId); |
| } else { |
| builder.withValue(Data.RAW_CONTACT_ID, contactId); |
| } |
| |
| // Set the mime type of the row |
| builder.withValue(Data.MIMETYPE, mimeType); |
| return builder; |
| } |
| |
| /** |
| * Compare a column in a ContentValues with an (old) value, and see if they are the |
| * same. For this purpose, null and an empty string are considered the same. |
| * @param cv a ContentValues object, from a NamedContentValues |
| * @param column a column that might be in the ContentValues |
| * @param oldValue an old value (or null) to check against |
| * @return whether the column's value in the ContentValues matches oldValue |
| */ |
| private boolean cvCompareString(ContentValues cv, String column, String oldValue) { |
| if (cv.containsKey(column)) { |
| if (oldValue != null && cv.getAsString(column).equals(oldValue)) { |
| return true; |
| } |
| } else if (oldValue == null || oldValue.length() == 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| public void addChildren(Entity entity, ArrayList<String> children) { |
| RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE); |
| int i = 0; |
| for (String child: children) { |
| builder.withValue(EasChildren.ROWS[i++], child); |
| } |
| add(builder.build()); |
| } |
| |
| public void addGroup(Entity entity, String group) { |
| RowBuilder builder = |
| createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group); |
| builder.withValue(GroupMembership.GROUP_SOURCE_ID, group); |
| add(builder.build()); |
| } |
| |
| public void addBirthday(Entity entity, String birthday) { |
| RowBuilder builder = |
| typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) { |
| return; |
| } |
| long millis = Utility.parseEmailDateTimeToMillis(birthday); |
| GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); |
| cal.setTimeInMillis(millis); |
| if (cal.get(GregorianCalendar.HOUR_OF_DAY) >= 12) { |
| cal.add(GregorianCalendar.DATE, 1); |
| } |
| String realBirthday = CalendarUtilities.calendarToBirthdayString(cal); |
| builder.withValue(Event.START_DATE, realBirthday); |
| builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); |
| add(builder.build()); |
| } |
| |
| public void addName(Entity entity, String prefix, String givenName, String familyName, |
| String middleName, String suffix, String displayName, String yomiFirstName, |
| String yomiLastName) { |
| RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) && |
| cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) && |
| cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) && |
| cvCompareString(cv, StructuredName.PREFIX, prefix) && |
| cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) && |
| cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) && |
| cvCompareString(cv, StructuredName.SUFFIX, suffix)) { |
| return; |
| } |
| builder.withValue(StructuredName.GIVEN_NAME, givenName); |
| builder.withValue(StructuredName.FAMILY_NAME, familyName); |
| builder.withValue(StructuredName.MIDDLE_NAME, middleName); |
| builder.withValue(StructuredName.SUFFIX, suffix); |
| builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName); |
| builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName); |
| builder.withValue(StructuredName.PREFIX, prefix); |
| add(builder.build()); |
| } |
| |
| public void addPersonal(Entity entity, EasPersonal personal) { |
| RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) && |
| cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) { |
| return; |
| } |
| if (!personal.hasData()) { |
| return; |
| } |
| builder.withValue(EasPersonal.FILE_AS, personal.fileAs); |
| builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary); |
| add(builder.build()); |
| } |
| |
| public void addBusiness(Entity entity, EasBusiness business) { |
| RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) && |
| cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) && |
| cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) { |
| return; |
| } |
| if (!business.hasData()) { |
| return; |
| } |
| builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName); |
| builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId); |
| builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId); |
| add(builder.build()); |
| } |
| |
| public void addPhoto(Entity entity, String photo) { |
| RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE); |
| // We're always going to add this; it's not worth trying to figure out whether the |
| // picture is the same as the one stored. |
| byte[] pic = Base64.decode(photo, Base64.DEFAULT); |
| builder.withValue(Photo.PHOTO, pic); |
| add(builder.build()); |
| } |
| |
| public void addPhone(Entity entity, int type, String phone) { |
| RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) { |
| return; |
| } |
| builder.withValue(Phone.TYPE, type); |
| builder.withValue(Phone.NUMBER, phone); |
| add(builder.build()); |
| } |
| |
| public void addWebpage(Entity entity, String url) { |
| RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, Website.URL, url)) { |
| return; |
| } |
| builder.withValue(Website.TYPE, Website.TYPE_WORK); |
| builder.withValue(Website.URL, url); |
| add(builder.build()); |
| } |
| |
| public void addRelation(Entity entity, int type, String value) { |
| RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, Relation.DATA, value)) { |
| return; |
| } |
| builder.withValue(Relation.TYPE, type); |
| builder.withValue(Relation.DATA, value); |
| add(builder.build()); |
| } |
| |
| public void addNickname(Entity entity, String name) { |
| RowBuilder builder = |
| typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, Nickname.NAME, name)) { |
| return; |
| } |
| builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); |
| builder.withValue(Nickname.NAME, name); |
| add(builder.build()); |
| } |
| |
| public void addPostal(Entity entity, int type, String street, String city, String state, |
| String country, String code) { |
| RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE, |
| type); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) && |
| cvCompareString(cv, StructuredPostal.STREET, street) && |
| cvCompareString(cv, StructuredPostal.COUNTRY, country) && |
| cvCompareString(cv, StructuredPostal.POSTCODE, code) && |
| cvCompareString(cv, StructuredPostal.REGION, state)) { |
| return; |
| } |
| builder.withValue(StructuredPostal.TYPE, type); |
| builder.withValue(StructuredPostal.CITY, city); |
| builder.withValue(StructuredPostal.STREET, street); |
| builder.withValue(StructuredPostal.COUNTRY, country); |
| builder.withValue(StructuredPostal.POSTCODE, code); |
| builder.withValue(StructuredPostal.REGION, state); |
| add(builder.build()); |
| } |
| |
| /** |
| * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to |
| * match them with existing rows; if there's a match, everything's great. Otherwise, we |
| * either need to add a new row for the data, or we have to replace an existing one |
| * that no longer matches. This is similar to the way Emails are handled. |
| */ |
| public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType, |
| int type, int maxRows) { |
| // Make a list of all same type rows in the existing entity |
| ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES; |
| ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES; |
| if (entity != null) { |
| oldValues = findUntypedData(entityValues, type, mimeType); |
| entityValues = entity.getSubValues(); |
| } |
| |
| // These will be rows needing replacement with new values |
| ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>(); |
| |
| // The count of existing rows |
| int numRows = oldValues.size(); |
| for (UntypedRow row: rows) { |
| boolean found = false; |
| // If we already have this row, mark it |
| for (NamedContentValues ncv: oldValues) { |
| ContentValues cv = ncv.values; |
| String data = cv.getAsString(COMMON_DATA_ROW); |
| int rowType = -1; |
| if (cv.containsKey(COMMON_TYPE_ROW)) { |
| rowType = cv.getAsInteger(COMMON_TYPE_ROW); |
| } |
| if (row.isSameAs(rowType, data)) { |
| cv.put(FOUND_DATA_ROW, true); |
| // Remove this to indicate it's still being used |
| entityValues.remove(ncv); |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| // If we don't, there are two possibilities |
| if (numRows < maxRows) { |
| // If there are available rows, add a new one |
| RowBuilder builder = newRowBuilder(entity, mimeType); |
| row.addValues(builder); |
| add(builder.build()); |
| numRows++; |
| } else { |
| // Otherwise, say we need to replace a row with this |
| rowsToReplace.add(row); |
| } |
| } |
| } |
| |
| // Go through rows needing replacement |
| for (UntypedRow row: rowsToReplace) { |
| for (NamedContentValues ncv: oldValues) { |
| ContentValues cv = ncv.values; |
| // Find a row that hasn't been used (i.e. doesn't match current rows) |
| if (!cv.containsKey(FOUND_DATA_ROW)) { |
| // And update it |
| RowBuilder builder = new RowBuilder( |
| ContentProviderOperation |
| .newUpdate(addCallerIsSyncAdapterParameter( |
| dataUriFromNamedContentValues(ncv))), |
| ncv); |
| row.addValues(builder); |
| add(builder.build()); |
| } |
| } |
| } |
| } |
| |
| public void addOrganization(Entity entity, int type, String company, String title, |
| String department, String yomiCompanyName, String officeLocation) { |
| RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type); |
| ContentValues cv = builder.cv; |
| if (cv != null && cvCompareString(cv, Organization.COMPANY, company) && |
| cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) && |
| cvCompareString(cv, Organization.DEPARTMENT, department) && |
| cvCompareString(cv, Organization.TITLE, title) && |
| cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) { |
| return; |
| } |
| builder.withValue(Organization.TYPE, type); |
| builder.withValue(Organization.COMPANY, company); |
| builder.withValue(Organization.TITLE, title); |
| builder.withValue(Organization.DEPARTMENT, department); |
| builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName); |
| builder.withValue(Organization.OFFICE_LOCATION, officeLocation); |
| add(builder.build()); |
| } |
| |
| public void addNote(Entity entity, String note) { |
| RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1); |
| ContentValues cv = builder.cv; |
| if (note == null) return; |
| note = note.replaceAll("\r\n", "\n"); |
| if (cv != null && cvCompareString(cv, Note.NOTE, note)) { |
| return; |
| } |
| |
| // Reject notes with nothing in them. Often, we get something from Outlook when |
| // nothing was ever entered. Sigh. |
| int len = note.length(); |
| int i = 0; |
| for (; i < len; i++) { |
| char c = note.charAt(i); |
| if (!Character.isWhitespace(c)) { |
| break; |
| } |
| } |
| if (i == len) return; |
| |
| builder.withValue(Note.NOTE, note); |
| add(builder.build()); |
| } |
| } |
| |
| /** |
| * Generate the uri for the data row associated with this NamedContentValues object |
| * @param ncv the NamedContentValues object |
| * @return a uri that can be used to refer to this row |
| */ |
| public Uri dataUriFromNamedContentValues(NamedContentValues ncv) { |
| long id = ncv.values.getAsLong(RawContacts._ID); |
| Uri dataUri = ContentUris.withAppendedId(ncv.uri, id); |
| return dataUri; |
| } |
| |
| @Override |
| public void cleanup() { |
| // Mark the changed contacts dirty = 0 |
| // Permanently delete the user deletions |
| ContactOperations ops = new ContactOperations(); |
| for (Long id: mUpdatedIdList) { |
| ops.add(ContentProviderOperation |
| .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) |
| .buildUpon() |
| .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") |
| .build()) |
| .withValue(RawContacts.DIRTY, 0).build()); |
| } |
| for (Long id: mDeletedIdList) { |
| ops.add(ContentProviderOperation |
| .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id) |
| .buildUpon() |
| .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") |
| .build()) |
| .build()); |
| } |
| ops.execute(); |
| ContentResolver cr = mContext.getContentResolver(); |
| if (mGroupsUsed) { |
| // Make sure the title column is set for all of our groups |
| // And that all of our groups are visible |
| // TODO Perhaps the visible part should only happen when the group is created, but |
| // this is fine for now. |
| Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI); |
| Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE}, |
| Groups.TITLE + " IS NULL", null, null); |
| ContentValues values = new ContentValues(); |
| values.put(Groups.GROUP_VISIBLE, 1); |
| try { |
| while (c.moveToNext()) { |
| String sourceId = c.getString(0); |
| values.put(Groups.TITLE, sourceId); |
| cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values, |
| Groups.SOURCE_ID + "=?", new String[] {sourceId}); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| } |
| |
| @Override |
| public String getCollectionName() { |
| return "Contacts"; |
| } |
| |
| private void sendEmail(Serializer s, ContentValues cv, int count, String displayName) |
| throws IOException { |
| // Get both parts of the email address (a newly created one in the UI won't have a name) |
| String addr = cv.getAsString(Email.DATA); |
| String name = cv.getAsString(Email.DISPLAY_NAME); |
| if (name == null) { |
| if (displayName != null) { |
| name = displayName; |
| } else { |
| name = addr; |
| } |
| } |
| // Compose address from name and addr |
| if (addr != null) { |
| String value; |
| // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on |
| // an RFC822 address) |
| if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { |
| value = addr; |
| } else { |
| value = '\"' + name + "\" <" + addr + '>'; |
| } |
| if (count < MAX_EMAIL_ROWS) { |
| s.data(EMAIL_TAGS[count], value); |
| } |
| } |
| } |
| |
| private void sendIm(Serializer s, ContentValues cv, int count) throws IOException { |
| String value = cv.getAsString(Im.DATA); |
| if (value == null) return; |
| if (count < MAX_IM_ROWS) { |
| s.data(IM_TAGS[count], value); |
| } |
| } |
| |
| private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames) |
| throws IOException{ |
| sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]); |
| sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]); |
| sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]); |
| sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]); |
| sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]); |
| } |
| |
| private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException { |
| switch (cv.getAsInteger(StructuredPostal.TYPE)) { |
| case StructuredPostal.TYPE_HOME: |
| sendOnePostal(s, cv, HOME_ADDRESS_TAGS); |
| break; |
| case StructuredPostal.TYPE_WORK: |
| sendOnePostal(s, cv, WORK_ADDRESS_TAGS); |
| break; |
| case StructuredPostal.TYPE_OTHER: |
| sendOnePostal(s, cv, OTHER_ADDRESS_TAGS); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| private void sendStringData(Serializer s, ContentValues cv, String column, int tag) |
| throws IOException { |
| if (cv.containsKey(column)) { |
| String value = cv.getAsString(column); |
| if (!TextUtils.isEmpty(value)) { |
| s.data(tag, value); |
| } |
| } |
| } |
| |
| private String sendStructuredName(Serializer s, ContentValues cv) throws IOException { |
| String displayName = null; |
| sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME); |
| sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME); |
| sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME); |
| sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX); |
| sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME); |
| sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME); |
| sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE); |
| return displayName; |
| } |
| |
| private void sendBusiness(Serializer s, ContentValues cv) throws IOException { |
| sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME); |
| sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID); |
| sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID); |
| } |
| |
| private void sendPersonal(Serializer s, ContentValues cv) throws IOException { |
| sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY); |
| sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS); |
| } |
| |
| private void sendBirthday(Serializer s, ContentValues cv) throws IOException { |
| sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY); |
| } |
| |
| private void sendPhoto(Serializer s, ContentValues cv) throws IOException { |
| if (cv.containsKey(Photo.PHOTO)) { |
| byte[] bytes = cv.getAsByteArray(Photo.PHOTO); |
| String pic = Base64.encodeToString(bytes, Base64.NO_WRAP); |
| s.data(Tags.CONTACTS_PICTURE, pic); |
| } else { |
| // Send an empty tag, which signals the server to delete any pre-existing photo |
| s.tag(Tags.CONTACTS_PICTURE); |
| } |
| } |
| |
| private void sendOrganization(Serializer s, ContentValues cv) throws IOException { |
| sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE); |
| sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME); |
| sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT); |
| sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION); |
| } |
| |
| private void sendNickname(Serializer s, ContentValues cv) throws IOException { |
| sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME); |
| } |
| |
| private void sendWebpage(Serializer s, ContentValues cv) throws IOException { |
| sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE); |
| } |
| |
| private void sendNote(Serializer s, ContentValues cv) throws IOException { |
| // Even when there is no local note, we must explicitly upsync an empty note, |
| // which is the only way to force the server to delete any pre-existing note. |
| String note = ""; |
| if (cv.containsKey(Note.NOTE)) { |
| // EAS won't accept note data with raw newline characters |
| note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n"); |
| } |
| // Format of upsync data depends on protocol version |
| if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { |
| s.start(Tags.BASE_BODY); |
| s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note); |
| s.end(); |
| } else { |
| s.data(Tags.CONTACTS_BODY, note); |
| } |
| } |
| |
| private void sendChildren(Serializer s, ContentValues cv) throws IOException { |
| boolean first = true; |
| for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) { |
| String row = EasChildren.ROWS[i]; |
| if (cv.containsKey(row)) { |
| if (first) { |
| s.start(Tags.CONTACTS_CHILDREN); |
| first = false; |
| } |
| s.data(Tags.CONTACTS_CHILD, cv.getAsString(row)); |
| } |
| } |
| if (!first) { |
| s.end(); |
| } |
| } |
| |
| private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount) |
| throws IOException { |
| String value = cv.getAsString(Phone.NUMBER); |
| if (value == null) return; |
| switch (cv.getAsInteger(Phone.TYPE)) { |
| case Phone.TYPE_WORK: |
| if (workCount < MAX_PHONE_ROWS) { |
| s.data(WORK_PHONE_TAGS[workCount], value); |
| } |
| break; |
| case Phone.TYPE_MMS: |
| s.data(Tags.CONTACTS2_MMS, value); |
| break; |
| case Phone.TYPE_ASSISTANT: |
| s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value); |
| break; |
| case Phone.TYPE_FAX_WORK: |
| s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value); |
| break; |
| case Phone.TYPE_COMPANY_MAIN: |
| s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value); |
| break; |
| case Phone.TYPE_HOME: |
| if (homeCount < MAX_PHONE_ROWS) { |
| s.data(HOME_PHONE_TAGS[homeCount], value); |
| } |
| break; |
| case Phone.TYPE_MOBILE: |
| s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value); |
| break; |
| case Phone.TYPE_CAR: |
| s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value); |
| break; |
| case Phone.TYPE_PAGER: |
| s.data(Tags.CONTACTS_PAGER_NUMBER, value); |
| break; |
| case Phone.TYPE_RADIO: |
| s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value); |
| break; |
| case Phone.TYPE_FAX_HOME: |
| s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| private void sendRelation(Serializer s, ContentValues cv) throws IOException { |
| String value = cv.getAsString(Relation.DATA); |
| if (value == null) return; |
| switch (cv.getAsInteger(Relation.TYPE)) { |
| case Relation.TYPE_ASSISTANT: |
| s.data(Tags.CONTACTS_ASSISTANT_NAME, value); |
| break; |
| case Relation.TYPE_MANAGER: |
| s.data(Tags.CONTACTS2_MANAGER_NAME, value); |
| break; |
| case Relation.TYPE_SPOUSE: |
| s.data(Tags.CONTACTS_SPOUSE, value); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| private void dirtyContactsWithinDirtyGroups() { |
| ContentResolver cr = mService.mContentResolver; |
| Cursor c = cr.query(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), |
| GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null); |
| try { |
| if (c.getCount() > 0) { |
| String[] updateArgs = new String[1]; |
| ContentValues updateValues = new ContentValues(); |
| while (c.moveToNext()) { |
| // For each, "touch" all data rows with this group id; this will mark contacts |
| // in this group as dirty (per ContactsContract). We will then know to upload |
| // them to the server with the modified group information |
| long id = c.getLong(0); |
| updateValues.put(GroupMembership.GROUP_ROW_ID, id); |
| updateArgs[0] = Long.toString(id); |
| cr.update(Data.CONTENT_URI, updateValues, |
| MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs); |
| } |
| // Really delete groups that are marked deleted |
| cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), Groups.DELETED + "=1", |
| null); |
| // Clear the dirty flag for all of our groups |
| updateValues.clear(); |
| updateValues.put(Groups.DIRTY, 0); |
| cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), updateValues, null, |
| null); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| |
| @Override |
| public boolean sendLocalChanges(Serializer s) throws IOException { |
| ContentResolver cr = mService.mContentResolver; |
| |
| // Find any groups of ours that are dirty and dirty those groups' members |
| dirtyContactsWithinDirtyGroups(); |
| |
| // First, let's find Contacts that have changed. |
| Uri uri = uriWithAccountAndIsSyncAdapter(RawContactsEntity.CONTENT_URI); |
| if (getSyncKey().equals("0")) { |
| return false; |
| } |
| |
| // Get them all atomically |
| EntityIterator ei = RawContacts.newEntityIterator( |
| cr.query(uri, null, RawContacts.DIRTY + "=1", null, null)); |
| ContentValues cidValues = new ContentValues(); |
| try { |
| boolean first = true; |
| final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI); |
| while (ei.hasNext()) { |
| Entity entity = ei.next(); |
| // For each of these entities, create the change commands |
| ContentValues entityValues = entity.getEntityValues(); |
| String serverId = entityValues.getAsString(RawContacts.SOURCE_ID); |
| ArrayList<Integer> groupIds = new ArrayList<Integer>(); |
| if (first) { |
| s.start(Tags.SYNC_COMMANDS); |
| userLog("Sending Contacts changes to the server"); |
| first = false; |
| } |
| if (serverId == null) { |
| // This is a new contact; create a clientId |
| String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis(); |
| userLog("Creating new contact with clientId: ", clientId); |
| s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); |
| // And save it in the raw contact |
| cidValues.put(RawContacts.SYNC1, clientId); |
| cr.update(ContentUris. |
| withAppendedId(rawContactUri, |
| entityValues.getAsLong(RawContacts._ID)), |
| cidValues, null, null); |
| } else { |
| if (entityValues.getAsInteger(RawContacts.DELETED) == 1) { |
| userLog("Deleting contact with serverId: ", serverId); |
| s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); |
| mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID)); |
| continue; |
| } |
| userLog("Upsync change to contact with serverId: " + serverId); |
| s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); |
| } |
| s.start(Tags.SYNC_APPLICATION_DATA); |
| // Write out the data here |
| int imCount = 0; |
| int emailCount = 0; |
| int homePhoneCount = 0; |
| int workPhoneCount = 0; |
| String displayName = null; |
| ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>(); |
| for (NamedContentValues ncv: entity.getSubValues()) { |
| ContentValues cv = ncv.values; |
| String mimeType = cv.getAsString(Data.MIMETYPE); |
| if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) { |
| emailValues.add(cv); |
| } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) { |
| sendNickname(s, cv); |
| } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) { |
| sendChildren(s, cv); |
| } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) { |
| sendBusiness(s, cv); |
| } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) { |
| sendWebpage(s, cv); |
| } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) { |
| sendPersonal(s, cv); |
| } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) { |
| sendPhone(s, cv, workPhoneCount, homePhoneCount); |
| int type = cv.getAsInteger(Phone.TYPE); |
| if (type == Phone.TYPE_HOME) homePhoneCount++; |
| if (type == Phone.TYPE_WORK) workPhoneCount++; |
| } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) { |
| sendRelation(s, cv); |
| } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) { |
| displayName = sendStructuredName(s, cv); |
| } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) { |
| sendStructuredPostal(s, cv); |
| } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) { |
| sendOrganization(s, cv); |
| } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) { |
| sendIm(s, cv, imCount++); |
| } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) { |
| Integer eventType = cv.getAsInteger(Event.TYPE); |
| if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) { |
| sendBirthday(s, cv); |
| } |
| } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) { |
| // We must gather these, and send them together (below) |
| groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID)); |
| } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) { |
| sendNote(s, cv); |
| } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { |
| sendPhoto(s, cv); |
| } else { |
| userLog("Contacts upsync, unknown data: ", mimeType); |
| } |
| } |
| |
| // We do the email rows last, because we need to make sure we've found the |
| // displayName (if one exists); this would be in a StructuredName rnow |
| for (ContentValues cv: emailValues) { |
| sendEmail(s, cv, emailCount++, displayName); |
| } |
| |
| // Now, we'll send up groups, if any |
| if (!groupIds.isEmpty()) { |
| boolean groupFirst = true; |
| for (int id: groupIds) { |
| // Since we get id's from the provider, we need to find their names |
| Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id), |
| GROUP_TITLE_PROJECTION, null, null, null); |
| try { |
| // Presumably, this should always succeed, but ... |
| if (c.moveToFirst()) { |
| if (groupFirst) { |
| s.start(Tags.CONTACTS_CATEGORIES); |
| groupFirst = false; |
| } |
| s.data(Tags.CONTACTS_CATEGORY, c.getString(0)); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| if (!groupFirst) { |
| s.end(); |
| } |
| } |
| s.end().end(); // ApplicationData & Change |
| mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID)); |
| } |
| if (!first) { |
| s.end(); // Commands |
| } |
| } finally { |
| ei.close(); |
| } |
| |
| return false; |
| } |
| } |