1 package com
.morphoss
.acal
.contacts
;
3 import java
.util
.ArrayList
;
5 import java
.util
.HashMap
;
6 import java
.util
.HashSet
;
7 import java
.util
.Locale
;
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
;
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
);
69 sourceCard
.setPersistentOn();
71 catch ( YouMustSurroundThisMethodInTryCatchOrIllEatYouException e
) { }
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
);
86 uid
= new AcalProperty("UID", vCardRow
.getAsString(DavResources
._ID
));
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();
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]);
112 s
= new HashSet
<AcalProperty
>();
113 typeMap
.put(nameSplit
[0], s
);
118 s
= typeMap
.get(nameSplit
[1]);
120 s
= new HashSet
<AcalProperty
>();
121 typeMap
.put(nameSplit
[1], s
);
125 s
= groupMap
.get(nameSplit
[0]);
127 s
= new HashSet
<AcalProperty
>();
128 groupMap
.put(nameSplit
[0], s
);
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() {
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())
161 this.writeContactDetails(ops
, true, 0);
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())
173 this.writeContactDetails(ops
, false, androidContactId
);
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
) {
193 AcalProperty
[] vCardProperties
= sourceCard
.getAllProperties();
194 for (AcalProperty prop
: vCardProperties
) {
197 op
= ContentProviderOperation
.newInsert(ContactsContract
.Data
.CONTENT_URI
);
198 op
.withValueBackReference(ContactsContract
.Data
.RAW_CONTACT_ID
, 0);
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
);
216 Log
.println(Constants
.LOGD
,TAG
,"Applying "+propertyName
+" change for:"+op
.build().toString());
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());
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 )
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
);
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();
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());
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));
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
);
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);
337 if ( cur
.moveToFirst() ) {
338 ContentValues result
= new ContentValues();
339 DatabaseUtils
.cursorRowToContentValues(cur
, result
);
344 catch( Exception e
) {
345 Log
.w(TAG
,"Could not retrieve Android contact",e
);
348 if ( cur
!= null ) cur
.close();
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()) );