Updated CardDAV contacts should now update in the contact list.
[acal.git] / src / com / morphoss / acal / contacts / VCardContact.java
blob25aefbb24c945705957b0ceae63f6873bd4d9c8e
1 package com.morphoss.acal.contacts;
3 import java.util.ArrayList;
4 import java.util.Date;
5 import java.util.HashMap;
6 import java.util.HashSet;
7 import java.util.Locale;
8 import java.util.Map;
9 import java.util.Set;
10 import java.util.TimeZone;
11 import java.util.regex.Matcher;
12 import java.util.regex.Pattern;
14 import android.accounts.Account;
15 import android.content.ContentProviderOperation;
16 import android.content.ContentProviderOperation.Builder;
17 import android.content.ContentUris;
18 import android.content.ContentValues;
19 import android.content.Context;
20 import android.content.OperationApplicationException;
21 import android.database.Cursor;
22 import android.database.DatabaseUtils;
23 import android.net.Uri;
24 import android.os.RemoteException;
25 import android.provider.ContactsContract;
26 import android.provider.ContactsContract.CommonDataKinds;
27 import android.provider.ContactsContract.RawContacts;
28 import android.provider.ContactsContract.RawContacts.Data;
29 import android.util.Log;
31 import com.morphoss.acal.Constants;
32 import com.morphoss.acal.acaltime.AcalDateTime;
33 import com.morphoss.acal.davacal.AcalCollection;
34 import com.morphoss.acal.davacal.AcalProperty;
35 import com.morphoss.acal.davacal.PropertyName;
36 import com.morphoss.acal.davacal.VCard;
37 import com.morphoss.acal.davacal.VComponent;
38 import com.morphoss.acal.davacal.VComponentCreationException;
39 import com.morphoss.acal.davacal.YouMustSurroundThisMethodInTryCatchOrIllEatYouException;
40 import com.morphoss.acal.providers.DavResources;
41 import com.morphoss.acal.service.connector.Base64Coder;
43 public class VCardContact {
45 public final static String TAG = "aCal VCardContact";
47 private static final Pattern structuredAddressMatcher = Pattern.compile("^(.*);(.*);(.*);(.*);(.*);(.*);(.*)$");
48 private static final Pattern structuredNameMatcher = Pattern.compile("^(.*);(.*);(.*);(.*);(.*)$");
49 private static final Pattern simpleSplit = Pattern.compile("[.]");
51 private final ContentValues vCardRow;
52 private final VCard sourceCard;
53 private Map<String,Set<AcalProperty>> typeMap = null;
54 private Map<String,Set<AcalProperty>> groupMap = null;
55 private AcalProperty uid = null;
56 private int sequence = 0;
58 public VCardContact( ContentValues resourceRow, AcalCollection collectionObject ) throws VComponentCreationException {
59 vCardRow = resourceRow;
60 try {
61 sourceCard = (VCard) VComponent.createComponentFromResource(resourceRow, collectionObject);
63 catch ( Exception e ) {
64 Log.w(TAG,"Could not build VCard from resource", e);
65 throw new VComponentCreationException("Could not build VCard from resource", e);
68 try {
69 sourceCard.setPersistentOn();
71 catch ( YouMustSurroundThisMethodInTryCatchOrIllEatYouException e ) { }
72 finally {
73 // We don't sourceCard.setPersistentOff();
74 // We want the contents to be expanded until we're done with this object
77 AcalDateTime revisionTime = AcalDateTime.fromAcalProperty(sourceCard.getProperty(PropertyName.REV));
78 if ( revisionTime == null ) {
79 String modTime = vCardRow.getAsString(DavResources.LAST_MODIFIED);
80 revisionTime = AcalDateTime.fromMillis(Date.parse(modTime)).setTimeZone(AcalDateTime.UTC.getID());
82 sequence = (int) ((revisionTime.getEpoch() - 1000000000L) % 2000000000L);
84 uid = sourceCard.getProperty(PropertyName.UID);
85 if ( uid == null ) {
86 uid = new AcalProperty("UID", vCardRow.getAsString(DavResources._ID));
88 buildTypeMap();
93 /**
94 * Traverses the properties, building an index by type and another by association.
96 * VCARD properties may be either like "PROPERTY:VALUE" or possibly as "aname.property:VALUE" (case is irrelevant) and
97 * this is building an index so we can get all "PROPERTY" properties from typeMap and all "aname" properties from groupMap
100 private void buildTypeMap() {
101 typeMap = new HashMap<String,Set<AcalProperty>>();
102 groupMap = new HashMap<String,Set<AcalProperty>>();
104 AcalProperty[] vCardProperties = sourceCard.getAllProperties();
105 String[] nameSplit;
106 Set<AcalProperty> s;
107 for( AcalProperty prop : vCardProperties ) {
108 nameSplit = simpleSplit.split(prop.getName().toUpperCase(Locale.US),2);
109 if ( nameSplit.length == 1 ) {
110 s = typeMap.get(nameSplit[0]);
111 if ( s == null ) {
112 s = new HashSet<AcalProperty>();
113 typeMap.put(nameSplit[0], s);
115 s.add(prop);
117 else {
118 s = typeMap.get(nameSplit[1]);
119 if ( s == null ) {
120 s = new HashSet<AcalProperty>();
121 typeMap.put(nameSplit[1], s);
123 s.add(prop);
125 s = groupMap.get(nameSplit[0]);
126 if ( s == null ) {
127 s = new HashSet<AcalProperty>();
128 groupMap.put(nameSplit[0], s);
130 s.add(prop);
135 public String getUid() {
136 return uid.getValue();
139 public String getFullName() {
140 if ( sourceCard == null ) return null;
141 AcalProperty fnProp = sourceCard.getProperty(PropertyName.FN);
142 if ( fnProp == null ) return null;
143 return fnProp.getValue();
146 public int getSequence() {
147 return sequence;
151 public void writeToContact(Context context, Account account, Integer androidContactId) {
152 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
153 if ( androidContactId < 0 ) {
154 Log.println(Constants.LOGD,TAG,"Inserting data for '"+sourceCard.getProperty(PropertyName.FN).getValue()+"'");
155 ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
156 .withValue(RawContacts.ACCOUNT_TYPE, account.type)
157 .withValue(RawContacts.ACCOUNT_NAME, account.name)
158 .withValue(RawContacts.SYNC1, this.getUid())
159 .build());
161 this.writeContactDetails(ops, true, 0);
163 else {
164 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, androidContactId);
165 Log.println(Constants.LOGD,TAG,"Updating data for '"+sourceCard.getProperty(PropertyName.FN).getValue()+"'");
166 ops.add(ContentProviderOperation.newUpdate(rawContactUri)
167 .withValue(RawContacts.ACCOUNT_TYPE, account.type)
168 .withValue(RawContacts.ACCOUNT_NAME, account.name)
169 .withValue(RawContacts.SYNC1, this.getUid())
170 .withValue(RawContacts.VERSION,this.getSequence())
171 .build());
173 this.writeContactDetails(ops, false, androidContactId);
176 try {
177 Log.println(Constants.LOGD,TAG,"Applying update batch: "+ops.toString());
178 context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
180 catch (RemoteException e) {
181 // TODO Auto-generated catch block
182 Log.e(TAG,Log.getStackTraceString(e));
184 catch (OperationApplicationException e) {
185 // TODO Auto-generated catch block
186 Log.e(TAG,Log.getStackTraceString(e));
191 private void writeContactDetails(ArrayList<ContentProviderOperation> ops, boolean isInsert, int rawContactId) {
192 String propertyName;
193 AcalProperty[] vCardProperties = sourceCard.getAllProperties();
194 for (AcalProperty prop : vCardProperties) {
195 Builder op;
196 if ( isInsert ) {
197 op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
198 op.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0);
200 else {
201 op = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, rawContactId));
203 propertyName = prop.getName();
204 String nameSplit[] = simpleSplit.split(prop.getName().toUpperCase(Locale.US),2);
205 propertyName = (nameSplit.length == 2 ? nameSplit[1] : nameSplit[0]);
207 if ( propertyName.equals("FN") ) doStructuredName(op, prop, sourceCard.getProperty("N"));
208 else if ( propertyName.equals("TEL") ) doPhone(op, prop);
209 else if ( propertyName.equals("ADR") ) doStructuredAddress(op, prop);
210 else if ( propertyName.equals("EMAIL")) doEmail(op, prop);
211 else if ( propertyName.equals("PHOTO")) doPhoto(op, prop);
212 else
213 continue;
216 Log.println(Constants.LOGD,TAG,"Applying "+propertyName+" change for:"+op.build().toString());
217 ops.add(op.build());
222 private void doStructuredName(Builder op, AcalProperty fnProp, AcalProperty nProp) {
223 Log.v(TAG,"Processing field FN:"+fnProp.getValue());
225 op.withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
226 if ( nProp != null ) {
227 Matcher m = structuredNameMatcher.matcher(nProp.getValue());
228 if ( m.matches() ) {
230 * The structured property value corresponds, in
231 * sequence, to the Surname (also known as family name), Given Names,
232 * Honorific Prefixes, and Honorific Suffixes.
234 op.withValue(CommonDataKinds.StructuredName.FAMILY_NAME, m.group(1));
235 op.withValue(CommonDataKinds.StructuredName.GIVEN_NAME, m.group(2));
236 op.withValue(CommonDataKinds.StructuredName.PREFIX, m.group(3));
237 op.withValue(CommonDataKinds.StructuredName.SUFFIX, m.group(4));
238 Log.v(TAG,"Processing 'N' field: '"+nProp.getValue()+"' prefix>"
239 + m.group(3) + "< firstname> " + m.group(2) + "< lastname>" + m.group(1) + "< suffix>" + m.group(4));
243 op.withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, fnProp.getValue());
247 private void doPhone(Builder op, AcalProperty telProp ) {
248 op.withValue(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
249 op.withValue(CommonDataKinds.Phone.NUMBER,telProp.getValue());
250 String phoneType = telProp.getParam("TYPE");
251 if ( phoneType == null )
252 phoneType = "OTHER";
253 else
254 phoneType = phoneType.toUpperCase();
256 if ( phoneType.contains("HOME") ) {
257 op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_HOME);
259 else if ( phoneType.contains("WORK") ) {
260 op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_WORK);
262 else if ( phoneType.contains("CELL") ) {
263 op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_MOBILE);
265 else {
266 op.withValue(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_OTHER);
268 Log.v(TAG,"Processing field TEL:"+phoneType+":"+telProp.getValue());
272 private void doStructuredAddress(Builder op, AcalProperty adrProp) {
273 op.withValue(Data.MIMETYPE, CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
274 String addressType = adrProp.getParam("TYPE").toUpperCase();
276 int opTYpe;
277 if ( addressType.contains("HOME") ) opTYpe = CommonDataKinds.StructuredPostal.TYPE_HOME;
278 else if ( addressType.contains("WORK") ) opTYpe = CommonDataKinds.StructuredPostal.TYPE_WORK;
279 else opTYpe = CommonDataKinds.StructuredPostal.TYPE_OTHER;
280 op.withValue(CommonDataKinds.StructuredPostal.TYPE, opTYpe);
282 Log.v(TAG,"Processing field ADR:"+addressType+":"+adrProp.getValue());
284 Matcher m = structuredAddressMatcher.matcher(adrProp.getValue());
285 if ( m.matches() ) {
287 * The structured type value
288 * corresponds, in sequence, to the post office box; the extended
289 * address (e.g. apartment or suite number); the street address; the
290 * locality (e.g., city); the region (e.g., state or province); the
291 * postal code; the country name.
293 op.withValue(CommonDataKinds.StructuredPostal.POBOX, m.group(1));
294 if ( m.group(2) == null || m.group(2).equals("") )
295 op.withValue(CommonDataKinds.StructuredPostal.STREET, m.group(3));
296 else
297 op.withValue(CommonDataKinds.StructuredPostal.STREET, m.group(2) + " / " + m.group(3));
299 op.withValue(CommonDataKinds.StructuredPostal.CITY, m.group(4));
300 op.withValue(CommonDataKinds.StructuredPostal.REGION, m.group(5));
301 op.withValue(CommonDataKinds.StructuredPostal.POSTCODE, m.group(6));
302 op.withValue(CommonDataKinds.StructuredPostal.COUNTRY, m.group(7));
307 private void doEmail(Builder op, AcalProperty emailProp) {
308 op.withValue(Data.MIMETYPE, CommonDataKinds.Email.CONTENT_ITEM_TYPE);
309 op.withValue(CommonDataKinds.Email.DATA,emailProp.getValue());
310 String emailType = emailProp.getParam("TYPE").toUpperCase();
311 if ( emailType.contains("HOME") ) {
312 op.withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_HOME);
314 else if ( emailType.contains("WORK") ) {
315 op.withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_WORK);
317 else {
318 op.withValue(CommonDataKinds.Email.TYPE, CommonDataKinds.Email.TYPE_OTHER);
320 Log.v(TAG,"Processing field EMAIL:"+emailType+":"+emailProp.getValue());
325 private void doPhoto(Builder op, AcalProperty prop) {
326 byte[] decodedString = Base64Coder.decode(prop.getValue().replaceAll(" ",""));
327 op.withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
328 op.withValue(ContactsContract.CommonDataKinds.Photo.PHOTO,decodedString);
329 Log.v(TAG,"Processing field PHOTO:"+prop.getValue());
333 public static ContentValues getAndroidContact(Context context, Integer rawContactId) {
334 Uri contactDataUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
335 Cursor cur = context.getContentResolver().query(contactDataUri, null, null, null, null);
336 try {
337 if ( cur.moveToFirst() ) {
338 ContentValues result = new ContentValues();
339 DatabaseUtils.cursorRowToContentValues(cur, result);
340 cur.close();
341 return result;
344 catch( Exception e ) {
345 Log.w(TAG,"Could not retrieve Android contact",e);
347 finally {
348 if ( cur != null ) cur.close();
350 return null;
354 public void writeToVCard(Context context, ContentValues androidContact) {
355 sourceCard.setEditable();
357 Log.println( Constants.LOGD, TAG, "I should write this to a VCard!" );
358 for( Map.Entry<String,Object> androidValue : androidContact.valueSet() ) {
359 String key = androidValue.getKey();
360 Object value = androidValue.getValue();
361 Log.println( Constants.LOGD, TAG, key+"="+(value == null ? "null" : value.toString()) );