diff options
Diffstat (limited to 'core/java/android/pim')
25 files changed, 7059 insertions, 67 deletions
diff --git a/core/java/android/pim/ContactsAsyncHelper.java b/core/java/android/pim/ContactsAsyncHelper.java index a21281e..7c78a81 100644 --- a/core/java/android/pim/ContactsAsyncHelper.java +++ b/core/java/android/pim/ContactsAsyncHelper.java @@ -27,8 +27,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.provider.Contacts; -import android.provider.Contacts.People; +import android.provider.ContactsContract.Contacts; import android.util.Log; import android.view.View; import android.widget.ImageView; @@ -39,35 +38,35 @@ import java.io.InputStream; * Helper class for async access of images. */ public class ContactsAsyncHelper extends Handler { - + private static final boolean DBG = false; private static final String LOG_TAG = "ContactsAsyncHelper"; - + /** * Interface for a WorkerHandler result return. */ public interface OnImageLoadCompleteListener { /** * Called when the image load is complete. - * + * * @param imagePresent true if an image was found - */ + */ public void onImageLoadComplete(int token, Object cookie, ImageView iView, boolean imagePresent); } - + // constants private static final int EVENT_LOAD_IMAGE = 1; private static final int DEFAULT_TOKEN = -1; - + // static objects private static Handler sThreadHandler; private static ContactsAsyncHelper sInstance; - + static { sInstance = new ContactsAsyncHelper(); } - + private static final class WorkerArgs { public Context context; public ImageView view; @@ -78,12 +77,12 @@ public class ContactsAsyncHelper extends Handler { public OnImageLoadCompleteListener listener; public CallerInfo info; } - + /** - * public inner class to help out the ContactsAsyncHelper callers - * with tracking the state of the CallerInfo Queries and image + * public inner class to help out the ContactsAsyncHelper callers + * with tracking the state of the CallerInfo Queries and image * loading. - * + * * Logic contained herein is used to remove the race conditions * that exist as the CallerInfo queries run and mix with the image * loads, which then mix with the Phone state changes. @@ -94,11 +93,11 @@ public class ContactsAsyncHelper extends Handler { public static final int DISPLAY_UNDEFINED = 0; public static final int DISPLAY_IMAGE = -1; public static final int DISPLAY_DEFAULT = -2; - + // State of the image on the imageview. private CallerInfo mCurrentCallerInfo; private int displayMode; - + public ImageTracker() { mCurrentCallerInfo = null; displayMode = DISPLAY_UNDEFINED; @@ -107,17 +106,17 @@ public class ContactsAsyncHelper extends Handler { /** * Used to see if the requested call / connection has a * different caller attached to it than the one we currently - * have in the CallCard. + * have in the CallCard. */ public boolean isDifferentImageRequest(CallerInfo ci) { // note, since the connections are around for the lifetime of the - // call, and the CallerInfo-related items as well, we can + // call, and the CallerInfo-related items as well, we can // definitely use a simple != comparison. return (mCurrentCallerInfo != ci); } - + public boolean isDifferentImageRequest(Connection connection) { - // if the connection does not exist, see if the + // if the connection does not exist, see if the // mCurrentCallerInfo is also null to match. if (connection == null) { if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null"); @@ -133,58 +132,65 @@ public class ContactsAsyncHelper extends Handler { } return runQuery; } - + /** - * Simple setter for the CallerInfo object. + * Simple setter for the CallerInfo object. */ public void setPhotoRequest(CallerInfo ci) { - mCurrentCallerInfo = ci; + mCurrentCallerInfo = ci; } - + /** - * Convenience method used to retrieve the URI - * representing the Photo file recorded in the attached - * CallerInfo Object. + * Convenience method used to retrieve the URI + * representing the Photo file recorded in the attached + * CallerInfo Object. */ public Uri getPhotoUri() { if (mCurrentCallerInfo != null) { - return ContentUris.withAppendedId(People.CONTENT_URI, + return ContentUris.withAppendedId(Contacts.CONTENT_URI, mCurrentCallerInfo.person_id); } - return null; + return null; } - + /** - * Simple setter for the Photo state. + * Simple setter for the Photo state. */ public void setPhotoState(int state) { displayMode = state; } - + /** - * Simple getter for the Photo state. + * Simple getter for the Photo state. */ public int getPhotoState() { return displayMode; } } - + /** - * Thread worker class that handles the task of opening the stream and loading + * Thread worker class that handles the task of opening the stream and loading * the images. */ private class WorkerHandler extends Handler { public WorkerHandler(Looper looper) { super(looper); } - + + @Override public void handleMessage(Message msg) { WorkerArgs args = (WorkerArgs) msg.obj; - + switch (msg.arg1) { case EVENT_LOAD_IMAGE: - InputStream inputStream = Contacts.People.openContactPhotoInputStream( - args.context.getContentResolver(), args.uri); + InputStream inputStream = null; + try { + inputStream = Contacts.openContactPhotoInputStream( + args.context.getContentResolver(), args.uri); + } catch (Exception e) { + Log.e(LOG_TAG, "Error opening photo input stream", e); + } + if (inputStream != null) { args.result = Drawable.createFromStream(inputStream, args.uri.toString()); @@ -192,22 +198,22 @@ public class ContactsAsyncHelper extends Handler { " token: " + msg.what + " image URI: " + args.uri); } else { args.result = null; - if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.uri + + if (DBG) Log.d(LOG_TAG, "Problem with image: " + msg.arg1 + + " token: " + msg.what + " image URI: " + args.uri + ", using default image."); } break; default: } - - // send the reply to the enclosing class. + + // send the reply to the enclosing class. Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what); reply.arg1 = msg.arg1; reply.obj = msg.obj; reply.sendToTarget(); } } - + /** * Private constructor for static class */ @@ -216,14 +222,14 @@ public class ContactsAsyncHelper extends Handler { thread.start(); sThreadHandler = new WorkerHandler(thread.getLooper()); } - + /** * Convenience method for calls that do not want to deal with listeners and tokens. */ - public static final void updateImageViewWithContactPhotoAsync(Context context, + public static final void updateImageViewWithContactPhotoAsync(Context context, ImageView imageView, Uri person, int placeholderImageResource) { // Added additional Cookie field in the callee. - updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context, + updateImageViewWithContactPhotoAsync (null, DEFAULT_TOKEN, null, null, context, imageView, person, placeholderImageResource); } @@ -231,24 +237,24 @@ public class ContactsAsyncHelper extends Handler { * Convenience method for calls that do not want to deal with listeners and tokens, but have * a CallerInfo object to cache the image to. */ - public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, + public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, Context context, ImageView imageView, Uri person, int placeholderImageResource) { // Added additional Cookie field in the callee. - updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context, + updateImageViewWithContactPhotoAsync (info, DEFAULT_TOKEN, null, null, context, imageView, person, placeholderImageResource); } - + /** * Start an image load, attach the result to the specified CallerInfo object. * Note, when the query is started, we make the ImageView INVISIBLE if the * placeholderImageResource value is -1. When we're given a valid (!= -1) * placeholderImageResource value, we make sure the image is visible. */ - public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token, - OnImageLoadCompleteListener listener, Object cookie, Context context, + public static final void updateImageViewWithContactPhotoAsync(CallerInfo info, int token, + OnImageLoadCompleteListener listener, Object cookie, Context context, ImageView imageView, Uri person, int placeholderImageResource) { - + // in case the source caller info is null, the URI will be null as well. // just update using the placeholder image in this case. if (person == null) { @@ -257,10 +263,10 @@ public class ContactsAsyncHelper extends Handler { imageView.setImageResource(placeholderImageResource); return; } - + // Added additional Cookie field in the callee to handle arguments // sent to the callback function. - + // setup arguments WorkerArgs args = new WorkerArgs(); args.cookie = cookie; @@ -270,15 +276,15 @@ public class ContactsAsyncHelper extends Handler { args.defaultResource = placeholderImageResource; args.listener = listener; args.info = info; - + // setup message arguments Message msg = sThreadHandler.obtainMessage(token); msg.arg1 = EVENT_LOAD_IMAGE; msg.obj = args; - - if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + + + if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri + ", displaying default image for now."); - + // set the default image first, when the query is complete, we will // replace the image with the correct one. if (placeholderImageResource != -1) { @@ -287,11 +293,11 @@ public class ContactsAsyncHelper extends Handler { } else { imageView.setVisibility(View.INVISIBLE); } - + // notify the thread to begin working sThreadHandler.sendMessage(msg); } - + /** * Called when loading is done. */ @@ -316,21 +322,21 @@ public class ContactsAsyncHelper extends Handler { args.view.setVisibility(View.VISIBLE); args.view.setImageResource(args.defaultResource); } - + // Note that the data is cached. if (args.info != null) { args.info.isCachedPhotoCurrent = true; } - + // notify the listener if it is there. if (args.listener != null) { - if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + + if (DBG) Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() + " image: " + args.uri + " completed"); args.listener.onImageLoadComplete(msg.what, args.cookie, args.view, imagePresent); } break; - default: + default: } } } diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index 1a287c8..bd7924a 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -223,6 +223,7 @@ public class RecurrenceSet { return true; } + // This can be removed when the old CalendarSyncAdapter is removed. public static boolean populateComponent(Cursor cursor, ICalendar.Component component) { @@ -292,6 +293,64 @@ public class RecurrenceSet { return true; } +public static boolean populateComponent(ContentValues values, + ICalendar.Component component) { + long dtstart = -1; + if (values.containsKey(Calendar.Events.DTSTART)) { + dtstart = values.getAsLong(Calendar.Events.DTSTART); + } + String duration = values.getAsString(Calendar.Events.DURATION); + String tzid = values.getAsString(Calendar.Events.EVENT_TIMEZONE); + String rruleStr = values.getAsString(Calendar.Events.RRULE); + String rdateStr = values.getAsString(Calendar.Events.RDATE); + String exruleStr = values.getAsString(Calendar.Events.EXRULE); + String exdateStr = values.getAsString(Calendar.Events.EXDATE); + boolean allDay = values.getAsInteger(Calendar.Events.ALL_DAY) == 1; + + if ((dtstart == -1) || + (TextUtils.isEmpty(duration))|| + ((TextUtils.isEmpty(rruleStr))&& + (TextUtils.isEmpty(rdateStr)))) { + // no recurrence. + return false; + } + + ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART"); + Time dtstartTime = null; + if (!TextUtils.isEmpty(tzid)) { + if (!allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid)); + } + dtstartTime = new Time(tzid); + } else { + // use the "floating" timezone + dtstartTime = new Time(Time.TIMEZONE_UTC); + } + + dtstartTime.set(dtstart); + // make sure the time is printed just as a date, if all day. + // TODO: android.pim.Time really should take care of this for us. + if (allDay) { + dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE")); + dtstartTime.allDay = true; + dtstartTime.hour = 0; + dtstartTime.minute = 0; + dtstartTime.second = 0; + } + + dtstartProp.setValue(dtstartTime.format2445()); + component.addProperty(dtstartProp); + ICalendar.Property durationProp = new ICalendar.Property("DURATION"); + durationProp.setValue(duration); + component.addProperty(durationProp); + + addPropertiesForRuleStr(component, "RRULE", rruleStr); + addPropertyForDateStr(component, "RDATE", rdateStr); + addPropertiesForRuleStr(component, "EXRULE", exruleStr); + addPropertyForDateStr(component, "EXDATE", exdateStr); + return true; + } + private static void addPropertiesForRuleStr(ICalendar.Component component, String propertyName, String ruleStr) { @@ -351,10 +410,14 @@ public class RecurrenceSet { Time end = new Time(endTzid); end.parse(dtendProperty.getValue()); - long durationMillis = end.toMillis(false /* use isDst */) + long durationMillis = end.toMillis(false /* use isDst */) - start.toMillis(false /* use isDst */); long durationSeconds = (durationMillis / 1000); - return "P" + durationSeconds + "S"; + if (start.allDay && (durationSeconds % 86400) == 0) { + return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S + } else { + return "P" + durationSeconds + "S"; + } } private static String flattenProperties(ICalendar.Component component, diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java new file mode 100644 index 0000000..ca41ce5 --- /dev/null +++ b/core/java/android/pim/vcard/Constants.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +/** + * Constants used in both composer and parser. + */ +/* package */ class Constants { + + public static final String ATTR_TYPE = "TYPE"; + + public static final String VERSION_V21 = "2.1"; + public static final String VERSION_V30 = "3.0"; + + // Properties both the current (as of 2009-08-17) ContactsStruct and de-fact vCard extensions + // shown in http://en.wikipedia.org/wiki/VCard support are defined here. + public static final String PROPERTY_X_AIM = "X-AIM"; + public static final String PROPERTY_X_MSN = "X-MSN"; + public static final String PROPERTY_X_YAHOO = "X-YAHOO"; + public static final String PROPERTY_X_ICQ = "X-ICQ"; + public static final String PROPERTY_X_JABBER = "X-JABBER"; + public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; + public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; + // Phone number for Skype, available as usual phone. + public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; + // Some device emits this "X-" attribute, which is specifically invalid but should be + // always properly accepted, and emitted in some special case (for that device/application). + public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; + + // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 + // + // e.g. + // 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..." + // 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..." + // 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..." + // + // 2) has been the default of VCard exporter/importer in Android, but we can see the other + // formats in vCard data emitted by the other softwares/devices. + // + // So we are currently not sure which type is the best; probably we will have to change which + // type should be emitted depending on the device. + public static final String ATTR_TYPE_HOME = "HOME"; + public static final String ATTR_TYPE_WORK = "WORK"; + public static final String ATTR_TYPE_FAX = "FAX"; + public static final String ATTR_TYPE_CELL = "CELL"; + public static final String ATTR_TYPE_VOICE = "VOICE"; + public static final String ATTR_TYPE_INTERNET = "INTERNET"; + + public static final String ATTR_TYPE_PREF = "PREF"; + + // Phone types valid in vCard and known to ContactsContract, but not so common. + public static final String ATTR_TYPE_CAR = "CAR"; + public static final String ATTR_TYPE_ISDN = "ISDN"; + public static final String ATTR_TYPE_PAGER = "PAGER"; + + // Phone types existing in vCard 2.1 but not known to ContactsContract. + // TODO: should make parser make these TYPE_CUSTOM. + public static final String ATTR_TYPE_MODEM = "MODEM"; + public static final String ATTR_TYPE_MSG = "MSG"; + public static final String ATTR_TYPE_BBS = "BBS"; + public static final String ATTR_TYPE_VIDEO = "VIDEO"; + + // Phone types existing in the current Contacts structure but not valid in vCard (at least 2.1) + // These types are encoded to "X-" attributes when composing vCard for now. + // Parser passes these even if "X-" is added to the attribute. + public static final String ATTR_TYPE_PHONE_EXTRA_OTHER = "OTHER"; + public static final String ATTR_TYPE_PHONE_EXTRA_CALLBACK = "CALLBACK"; + // TODO: may be "TYPE=COMPANY,PREF", not "COMPANY-MAIN". + public static final String ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN = "COMPANY-MAIN"; + public static final String ATTR_TYPE_PHONE_EXTRA_RADIO = "RADIO"; + public static final String ATTR_TYPE_PHONE_EXTRA_TELEX = "TELEX"; + public static final String ATTR_TYPE_PHONE_EXTRA_TTY_TDD = "TTY-TDD"; + public static final String ATTR_TYPE_PHONE_EXTRA_ASSISTANT = "ASSISTANT"; + + // DoCoMo specific attribute. Used with "SOUND" property, which is alternate of SORT-STRING in + // vCard 3.0. + public static final String ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; + + private Constants() { + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/ContactStruct.java new file mode 100644 index 0000000..36e5e23 --- /dev/null +++ b/core/java/android/pim/vcard/ContactStruct.java @@ -0,0 +1,1367 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.accounts.Account; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Groups; +import android.provider.ContactsContract.RawContacts; +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.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * This class bridges between data structure of Contact app and VCard data. + */ +public class ContactStruct { + private static final String LOG_TAG = "vcard.ContactStruct"; + + // Key: the name shown in VCard. e.g. "X-AIM", "X-ICQ" + // Value: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} + private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); + + static { + sImMap.put(Constants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); + sImMap.put(Constants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); + sImMap.put(Constants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); + sImMap.put(Constants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); + sImMap.put(Constants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); + sImMap.put(Constants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); + sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); + } + + /** + * @hide only for testing + */ + static public class PhoneData { + public final int type; + public final String data; + public final String label; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public PhoneData(int type, String data, String label, boolean isPrimary) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PhoneData) { + return false; + } + PhoneData phoneData = (PhoneData)obj; + return (type == phoneData.type && data.equals(phoneData.data) && + label.equals(phoneData.label) && isPrimary == phoneData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + /** + * @hide only for testing + */ + static public class EmailData { + public final int type; + public final String data; + // Used only when TYPE is TYPE_CUSTOM. + public final String label; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public EmailData(int type, String data, String label, boolean isPrimary) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof EmailData) { + return false; + } + EmailData emailData = (EmailData)obj; + return (type == emailData.type && data.equals(emailData.data) && + label.equals(emailData.label) && isPrimary == emailData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + static public class PostalData { + // Determined by vCard spec. + // PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name + public static final int ADDR_MAX_DATA_SIZE = 7; + private final String[] dataArray; + public final String pobox; + public final String extendedAddress; + public final String street; + public final String localty; + public final String region; + public final String postalCode; + public final String country; + + public final int type; + + // Used only when type variable is TYPE_CUSTOM. + public final String label; + + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public PostalData(int type, List<String> propValueList, + String label, boolean isPrimary) { + this.type = type; + dataArray = new String[ADDR_MAX_DATA_SIZE]; + + int size = propValueList.size(); + if (size > ADDR_MAX_DATA_SIZE) { + size = ADDR_MAX_DATA_SIZE; + } + + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + // + // Use Iterator assuming List may be LinkedList, though actually it is + // always ArrayList in the current implementation. + int i = 0; + for (String addressElement : propValueList) { + dataArray[i] = addressElement; + if (++i >= size) { + break; + } + } + while (i < ADDR_MAX_DATA_SIZE) { + dataArray[i++] = null; + } + + this.pobox = dataArray[0]; + this.extendedAddress = dataArray[1]; + this.street = dataArray[2]; + this.localty = dataArray[3]; + this.region = dataArray[4]; + this.postalCode = dataArray[5]; + this.country = dataArray[6]; + + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PostalData) { + return false; + } + PostalData postalData = (PostalData)obj; + return (Arrays.equals(dataArray, postalData.dataArray) && + (type == postalData.type && + (type == StructuredPostal.TYPE_CUSTOM ? + (label == postalData.label) : true)) && + (isPrimary == postalData.isPrimary)); + } + + public String getFormattedAddress(int vcardType) { + StringBuilder builder = new StringBuilder(); + boolean empty = true; + if (VCardConfig.isJapaneseDevice(vcardType)) { + // In Japan, the order is reversed. + for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) { + String addressPart = dataArray[i]; + if (!TextUtils.isEmpty(addressPart)) { + if (!empty) { + builder.append(' '); + } + builder.append(addressPart); + empty = false; + } + } + } else { + for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { + String addressPart = dataArray[i]; + if (!TextUtils.isEmpty(addressPart)) { + if (!empty) { + builder.append(' '); + } + builder.append(addressPart); + empty = false; + } + } + } + + return builder.toString().trim(); + } + + @Override + public String toString() { + return String.format("type: %d, label: %s, isPrimary: %s", + type, label, isPrimary); + } + } + + /** + * @hide only for testing. + */ + static public class OrganizationData { + public final int type; + public final String companyName; + // can be changed in some VCard format. + public String positionName; + // isPrimary is changable only when there's no appropriate one existing in + // the original VCard. + public boolean isPrimary; + public OrganizationData(int type, String companyName, String positionName, + boolean isPrimary) { + this.type = type; + this.companyName = companyName; + this.positionName = positionName; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof OrganizationData) { + return false; + } + OrganizationData organization = (OrganizationData)obj; + return (type == organization.type && companyName.equals(organization.companyName) && + positionName.equals(organization.positionName) && + isPrimary == organization.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, company: %s, position: %s, isPrimary: %s", + type, companyName, positionName, isPrimary); + } + } + + static public class ImData { + public final int type; + public final String data; + public final String label; + public final boolean isPrimary; + + // TODO: ContactsConstant#PROTOCOL, ContactsConstant#CUSTOM_PROTOCOL should be used? + public ImData(int type, String data, String label, boolean isPrimary) { + this.type = type; + this.data = data; + this.label = label; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ImData) { + return false; + } + ImData imData = (ImData)obj; + return (type == imData.type && data.equals(imData.data) && + label.equals(imData.label) && isPrimary == imData.isPrimary); + } + + @Override + public String toString() { + return String.format("type: %d, data: %s, label: %s, isPrimary: %s", + type, data, label, isPrimary); + } + } + + /** + * @hide only for testing. + */ + static public class PhotoData { + public static final String FORMAT_FLASH = "SWF"; + public final int type; + public final String formatName; // used when type is not defined in ContactsContract. + public final byte[] photoBytes; + + public PhotoData(int type, String formatName, byte[] photoBytes) { + this.type = type; + this.formatName = formatName; + this.photoBytes = photoBytes; + } + } + + static /* package */ class Property { + private String mPropertyName; + private Map<String, Collection<String>> mParameterMap = + new HashMap<String, Collection<String>>(); + private List<String> mPropertyValueList = new ArrayList<String>(); + private byte[] mPropertyBytes; + + public Property() { + clear(); + } + + public void setPropertyName(final String propertyName) { + mPropertyName = propertyName; + } + + public void addParameter(final String paramName, final String paramValue) { + Collection<String> values; + if (!mParameterMap.containsKey(paramName)) { + if (paramName.equals("TYPE")) { + values = new HashSet<String>(); + } else { + values = new ArrayList<String>(); + } + mParameterMap.put(paramName, values); + } else { + values = mParameterMap.get(paramName); + } + values.add(paramValue); + } + + public void addToPropertyValueList(final String propertyValue) { + mPropertyValueList.add(propertyValue); + } + + public void setPropertyBytes(final byte[] propertyBytes) { + mPropertyBytes = propertyBytes; + } + + public final Collection<String> getParameters(String type) { + return mParameterMap.get(type); + } + + public final List<String> getPropertyValueList() { + return mPropertyValueList; + } + + public void clear() { + mPropertyName = null; + mParameterMap.clear(); + mPropertyValueList.clear(); + } + } + + private String mFamilyName; + private String mGivenName; + private String mMiddleName; + private String mPrefix; + private String mSuffix; + + // Used only when no family nor given name is found. + private String mFullName; + + private String mPhoneticFamilyName; + private String mPhoneticGivenName; + private String mPhoneticMiddleName; + + private String mPhoneticFullName; + + private List<String> mNickNameList; + + private String mDisplayName; + + private String mBirthday; + + private List<String> mNoteList; + private List<PhoneData> mPhoneList; + private List<EmailData> mEmailList; + private List<PostalData> mPostalList; + private List<OrganizationData> mOrganizationList; + private List<ImData> mImList; + private List<PhotoData> mPhotoList; + private List<String> mWebsiteList; + + private final int mVCardType; + private final Account mAccount; + + // Each Column of four properties has ISPRIMARY field + // (See android.provider.Contacts) + // If false even after the parsing loop, we choose the first entry as a "primary" + // entry. + private boolean mPrefIsSet_Address; + private boolean mPrefIsSet_Phone; + private boolean mPrefIsSet_Email; + private boolean mPrefIsSet_Organization; + + public ContactStruct() { + this(VCardConfig.VCARD_TYPE_V21_GENERIC); + } + + public ContactStruct(int vcardType) { + this(vcardType, null); + } + + public ContactStruct(int vcardType, Account account) { + mVCardType = vcardType; + mAccount = account; + } + + /** + * @hide only for testing. + */ + public ContactStruct(String givenName, + String familyName, + String middleName, + String prefix, + String suffix, + String phoneticGivenName, + String pheneticFamilyName, + String phoneticMiddleName, + List<byte[]> photoBytesList, + List<String> notes, + List<PhoneData> phoneList, + List<EmailData> emailList, + List<PostalData> postalList, + List<OrganizationData> organizationList, + List<PhotoData> photoList, + List<String> websiteList) { + this(VCardConfig.VCARD_TYPE_DEFAULT); + mGivenName = givenName; + mFamilyName = familyName; + mPrefix = prefix; + mSuffix = suffix; + mPhoneticGivenName = givenName; + mPhoneticFamilyName = familyName; + mPhoneticMiddleName = middleName; + mEmailList = emailList; + mPostalList = postalList; + mOrganizationList = organizationList; + mPhotoList = photoList; + mWebsiteList = websiteList; + } + + // All getter methods should be used carefully, since they may change + // in the future as of 2009-09-24, on which I cannot be sure this structure + // is completely consolidated. + // When we are sure we will no longer change them, we'll be happy to + // make it complete public (withouth @hide tag) + // + // Also note that these getter methods should be used only after + // all properties being pushed into this object. If not, incorrect + // value will "be stored in the local cache and" be returned to you. + + /** + * @hide + */ + public String getFamilyName() { + return mFamilyName; + } + + /** + * @hide + */ + public String getGivenName() { + return mGivenName; + } + + /** + * @hide + */ + public String getMiddleName() { + return mMiddleName; + } + + /** + * @hide + */ + public String getPrefix() { + return mPrefix; + } + + /** + * @hide + */ + public String getSuffix() { + return mSuffix; + } + + /** + * @hide + */ + public String getFullName() { + return mFullName; + } + + /** + * @hide + */ + public String getPhoneticFamilyName() { + return mPhoneticFamilyName; + } + + /** + * @hide + */ + public String getPhoneticGivenName() { + return mPhoneticGivenName; + } + + /** + * @hide + */ + public String getPhoneticMiddleName() { + return mPhoneticMiddleName; + } + + /** + * @hide + */ + public String getPhoneticFullName() { + return mPhoneticFullName; + } + + /** + * @hide + */ + public final List<String> getNickNameList() { + return mNickNameList; + } + + /** + * @hide + */ + public String getDisplayName() { + if (mDisplayName == null) { + constructDisplayName(); + } + return mDisplayName; + } + + /** + * @hide + */ + public String getBirthday() { + return mBirthday; + } + + /** + * @hide + */ + public final List<PhotoData> getPhotoList() { + return mPhotoList; + } + + /** + * @hide + */ + public final List<String> getNotes() { + return mNoteList; + } + + /** + * @hide + */ + public final List<PhoneData> getPhoneList() { + return mPhoneList; + } + + /** + * @hide + */ + public final List<EmailData> getEmailList() { + return mEmailList; + } + + /** + * @hide + */ + public final List<PostalData> getPostalList() { + return mPostalList; + } + + /** + * @hide + */ + public final List<OrganizationData> getOrganizationList() { + return mOrganizationList; + } + + /** + * Add a phone info to phoneList. + * @param data phone number + * @param type type col of content://contacts/phones + * @param label lable col of content://contacts/phones + */ + private void addPhone(int type, String data, String label, boolean isPrimary){ + if (mPhoneList == null) { + mPhoneList = new ArrayList<PhoneData>(); + } + StringBuilder builder = new StringBuilder(); + String trimed = data.trim(); + int length = trimed.length(); + for (int i = 0; i < length; i++) { + char ch = trimed.charAt(i); + if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { + builder.append(ch); + } + } + + PhoneData phoneData = new PhoneData(type, + PhoneNumberUtils.formatNumber(builder.toString()), + label, isPrimary); + + mPhoneList.add(phoneData); + } + + private void addNickName(final String nickName) { + if (mNickNameList == null) { + mNickNameList = new ArrayList<String>(); + } + mNickNameList.add(nickName); + } + + private void addEmail(int type, String data, String label, boolean isPrimary){ + if (mEmailList == null) { + mEmailList = new ArrayList<EmailData>(); + } + mEmailList.add(new EmailData(type, data, label, isPrimary)); + } + + private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ + if (mPostalList == null) { + mPostalList = new ArrayList<PostalData>(); + } + mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); + } + + private void addOrganization(int type, final String companyName, + final String positionName, boolean isPrimary) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList<OrganizationData>(); + } + mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); + } + + private void addIm(int type, String data, String label, boolean isPrimary) { + if (mImList == null) { + mImList = new ArrayList<ImData>(); + } + mImList.add(new ImData(type, data, label, isPrimary)); + } + + private void addNote(final String note) { + if (mNoteList == null) { + mNoteList = new ArrayList<String>(1); + } + mNoteList.add(note); + } + + private void addPhotoBytes(String formatName, byte[] photoBytes) { + if (mPhotoList == null) { + mPhotoList = new ArrayList<PhotoData>(1); + } + final PhotoData photoData = new PhotoData(0, null, photoBytes); + mPhotoList.add(photoData); + } + + /** + * Set "position" value to the appropriate data. If there's more than one + * OrganizationData objects, the value is set to the last one. If there's no + * OrganizationData object, a new OrganizationData is created, whose company name is + * empty. + * + * TODO: incomplete logic. fix this: + * + * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not + * know how to handle it in general cases... + * ---- + * TITLE:Software Engineer + * ORG:Google + * ---- + */ + private void setPosition(String positionValue) { + if (mOrganizationList == null) { + mOrganizationList = new ArrayList<OrganizationData>(); + } + int size = mOrganizationList.size(); + if (size == 0) { + addOrganization(ContactsContract.CommonDataKinds.Organization.TYPE_OTHER, + "", null, false); + size = 1; + } + OrganizationData lastData = mOrganizationList.get(size - 1); + lastData.positionName = positionValue; + } + + @SuppressWarnings("fallthrough") + private void handleNProperty(List<String> elems) { + // Family, Given, Middle, Prefix, Suffix. (1 - 5) + int size; + if (elems == null || (size = elems.size()) < 1) { + return; + } + if (size > 5) { + size = 5; + } + + switch (size) { + // fallthrough + case 5: + mSuffix = elems.get(4); + case 4: + mPrefix = elems.get(3); + case 3: + mMiddleName = elems.get(2); + case 2: + mGivenName = elems.get(1); + default: + mFamilyName = elems.get(0); + } + } + + /** + * Some Japanese mobile phones use this field for phonetic name, + * since vCard 2.1 does not have "SORT-STRING" type. + * Also, in some cases, the field has some ';'s in it. + * Assume the ';' means the same meaning in N property + */ + @SuppressWarnings("fallthrough") + private void handlePhoneticNameFromSound(List<String> elems) { + // Family, Given, Middle. (1-3) + // This is not from specification but mere assumption. Some Japanese phones use this order. + int size; + if (elems == null || (size = elems.size()) < 1) { + return; + } + if (size > 3) { + size = 3; + } + + switch (size) { + // fallthrough + case 3: + mPhoneticMiddleName = elems.get(2); + case 2: + mPhoneticGivenName = elems.get(1); + default: + mPhoneticFamilyName = elems.get(0); + } + } + + public void addProperty(Property property) { + String propName = property.mPropertyName; + final Map<String, Collection<String>> paramMap = property.mParameterMap; + final List<String> propValueList = property.mPropertyValueList; + byte[] propBytes = property.mPropertyBytes; + + if (propValueList.size() == 0) { + return; + } + final String propValue = listToString(propValueList).trim(); + + if (propName.equals("VERSION")) { + // vCard version. Ignore this. + } else if (propName.equals("FN")) { + mFullName = propValue; + } else if (propName.equals("NAME") && mFullName == null) { + // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not + // actually exist in the real vCard data, does not exist. + mFullName = propValue; + } else if (propName.equals("N")) { + handleNProperty(propValueList); + } else if (propName.equals("SORT-STRING")) { + mPhoneticFullName = propValue; + } else if (propName.equals("NICKNAME") || propName.equals("X-NICKNAME")) { + addNickName(propValue); + } else if (propName.equals("SOUND")) { + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_X_IRMC_N)) { + handlePhoneticNameFromSound(propValueList); + } else { + // Ignore this field since Android cannot understand what it is. + } + } else if (propName.equals("ADR")) { + boolean valuesAreAllEmpty = true; + for (String value : propValueList) { + if (value.length() > 0) { + valuesAreAllEmpty = false; + break; + } + } + if (valuesAreAllEmpty) { + return; + } + + int type = -1; + String label = ""; + boolean isPrimary = false; + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Address) { + // Only first "PREF" is considered. + mPrefIsSet_Address = true; + isPrimary = true; + } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + type = StructuredPostal.TYPE_HOME; + label = ""; + } else if (typeString.equals(Constants.ATTR_TYPE_WORK) || + typeString.equalsIgnoreCase("COMPANY")) { + // "COMPANY" seems emitted by Windows Mobile, which is not + // specifically supported by vCard 2.1. We assume this is same + // as "WORK". + type = StructuredPostal.TYPE_WORK; + label = ""; + } else if (typeString.equals("PARCEL") || + typeString.equals("DOM") || + typeString.equals("INTL")) { + // We do not have any appropriate way to store this information. + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters + // emit non-standard types. We do not handle their values now. + type = StructuredPostal.TYPE_CUSTOM; + label = typeString; + } + } + } + // We use "HOME" as default + if (type < 0) { + type = StructuredPostal.TYPE_HOME; + } + + addPostal(type, propValueList, label, isPrimary); + } else if (propName.equals("EMAIL")) { + int type = -1; + String label = null; + boolean isPrimary = false; + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Email) { + // Only first "PREF" is considered. + mPrefIsSet_Email = true; + isPrimary = true; + } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + type = Email.TYPE_HOME; + } else if (typeString.equals(Constants.ATTR_TYPE_WORK)) { + type = Email.TYPE_WORK; + } else if (typeString.equals(Constants.ATTR_TYPE_CELL)) { + type = Email.TYPE_MOBILE; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + // vCard 3.0 allows iana-token. + // We may have INTERNET (specified in vCard spec), + // SCHOOL, etc. + type = Email.TYPE_CUSTOM; + label = typeString; + } + } + } + if (type < 0) { + type = Email.TYPE_OTHER; + } + addEmail(type, propValue, label, isPrimary); + } else if (propName.equals("ORG")) { + // vCard specification does not specify other types. + int type = Organization.TYPE_WORK; + boolean isPrimary = false; + + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Organization) { + // vCard specification officially does not have PREF in ORG. + // This is just for safety. + mPrefIsSet_Organization = true; + isPrimary = true; + } + } + } + + StringBuilder builder = new StringBuilder(); + for (Iterator<String> iter = propValueList.iterator(); iter.hasNext();) { + builder.append(iter.next()); + if (iter.hasNext()) { + builder.append(' '); + } + } + addOrganization(type, builder.toString(), "", isPrimary); + } else if (propName.equals("TITLE")) { + setPosition(propValue); + } else if (propName.equals("ROLE")) { + setPosition(propValue); + } else if (propName.equals("PHOTO") || propName.equals("LOGO")) { + String formatName = null; + Collection<String> typeCollection = paramMap.get("TYPE"); + if (typeCollection != null) { + formatName = typeCollection.iterator().next(); + } + Collection<String> paramMapValue = paramMap.get("VALUE"); + if (paramMapValue != null && paramMapValue.contains("URL")) { + // Currently we do not have appropriate example for testing this case. + } else { + addPhotoBytes(formatName, propBytes); + } + } else if (propName.equals("TEL")) { + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection); + final int type; + final String label; + if (typeObject instanceof Integer) { + type = (Integer)typeObject; + label = null; + } else { + type = Phone.TYPE_CUSTOM; + label = typeObject.toString(); + } + + final boolean isPrimary; + if (!mPrefIsSet_Phone && typeCollection != null && + typeCollection.contains(Constants.ATTR_TYPE_PREF)) { + mPrefIsSet_Phone = true; + isPrimary = true; + } else { + isPrimary = false; + } + addPhone(type, propValue, label, isPrimary); + } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { + // The phone number available via Skype. + Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + // XXX: should use TYPE_CUSTOM + the label "Skype"? (which may need localization) + int type = Phone.TYPE_OTHER; + final String label = null; + final boolean isPrimary; + if (!mPrefIsSet_Phone && typeCollection != null && + typeCollection.contains(Constants.ATTR_TYPE_PREF)) { + mPrefIsSet_Phone = true; + isPrimary = true; + } else { + isPrimary = false; + } + addPhone(type, propValue, label, isPrimary); + } else if (sImMap.containsKey(propName)){ + int type = sImMap.get(propName); + boolean isPrimary = false; + final Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + if (typeCollection != null) { + for (String typeString : typeCollection) { + if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + isPrimary = true; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { + type = Phone.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { + type = Phone.TYPE_WORK; + } + } + } + if (type < 0) { + type = Phone.TYPE_HOME; + } + addIm(type, propValue, null, isPrimary); + } else if (propName.equals("NOTE")) { + addNote(propValue); + } else if (propName.equals("URL")) { + if (mWebsiteList == null) { + mWebsiteList = new ArrayList<String>(1); + } + mWebsiteList.add(propValue); + } else if (propName.equals("X-PHONETIC-FIRST-NAME")) { + mPhoneticGivenName = propValue; + } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { + mPhoneticMiddleName = propValue; + } else if (propName.equals("X-PHONETIC-LAST-NAME")) { + mPhoneticFamilyName = propValue; + } else if (propName.equals("BDAY")) { + mBirthday = propValue; + /*} else if (propName.equals("REV")) { + // Revision of this VCard entry. I think we can ignore this. + } else if (propName.equals("UID")) { + } else if (propName.equals("KEY")) { + // Type is X509 or PGP? I don't know how to handle this... + } else if (propName.equals("MAILER")) { + } else if (propName.equals("TZ")) { + } else if (propName.equals("GEO")) { + } else if (propName.equals("CLASS")) { + // vCard 3.0 only. + // e.g. CLASS:CONFIDENTIAL + } else if (propName.equals("PROFILE")) { + // VCard 3.0 only. Must be "VCARD". I think we can ignore this. + } else if (propName.equals("CATEGORIES")) { + // VCard 3.0 only. + // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY + } else if (propName.equals("SOURCE")) { + // VCard 3.0 only. + } else if (propName.equals("PRODID")) { + // VCard 3.0 only. + // To specify the identifier for the product that created + // the vCard object.*/ + } else { + // Unknown X- words and IANA token. + } + } + + /** + * Construct the display name. The constructed data must not be null. + */ + private void constructDisplayName() { + if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { + StringBuilder builder = new StringBuilder(); + List<String> nameList; + switch (VCardConfig.getNameOrderType(mVCardType)) { + case VCardConfig.NAME_ORDER_JAPANESE: + if (VCardUtils.containsOnlyPrintableAscii(mFamilyName) && + VCardUtils.containsOnlyPrintableAscii(mGivenName)) { + nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); + } else { + nameList = Arrays.asList(mPrefix, mFamilyName, mMiddleName, mGivenName, mSuffix); + } + break; + case VCardConfig.NAME_ORDER_EUROPE: + nameList = Arrays.asList(mPrefix, mMiddleName, mGivenName, mFamilyName, mSuffix); + break; + default: + nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); + break; + } + boolean first = true; + for (String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); + } + } + mDisplayName = builder.toString(); + } else if (!TextUtils.isEmpty(mFullName)) { + mDisplayName = mFullName; + } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && + TextUtils.isEmpty(mPhoneticGivenName))) { + mDisplayName = VCardUtils.constructNameFromElements(mVCardType, + mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName); + } else if (mEmailList != null && mEmailList.size() > 0) { + mDisplayName = mEmailList.get(0).data; + } else if (mPhoneList != null && mPhoneList.size() > 0) { + mDisplayName = mPhoneList.get(0).data; + } else if (mPostalList != null && mPostalList.size() > 0) { + mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType); + } + + if (mDisplayName == null) { + mDisplayName = ""; + } + } + + /** + * Consolidate several fielsds (like mName) using name candidates, + */ + public void consolidateFields() { + constructDisplayName(); + + if (mPhoneticFullName != null) { + mPhoneticFullName = mPhoneticFullName.trim(); + } + + // If there is no "PREF", we choose the first entries as primary. + if (!mPrefIsSet_Phone && mPhoneList != null && mPhoneList.size() > 0) { + mPhoneList.get(0).isPrimary = true; + } + + if (!mPrefIsSet_Address && mPostalList != null && mPostalList.size() > 0) { + mPostalList.get(0).isPrimary = true; + } + if (!mPrefIsSet_Email && mEmailList != null && mEmailList.size() > 0) { + mEmailList.get(0).isPrimary = true; + } + if (!mPrefIsSet_Organization && mOrganizationList != null && mOrganizationList.size() > 0) { + mOrganizationList.get(0).isPrimary = true; + } + } + + // From GoogleSource.java in Contacts app. + private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; + private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; + + public void pushIntoContentResolver(ContentResolver resolver) { + ArrayList<ContentProviderOperation> operationList = + new ArrayList<ContentProviderOperation>(); + ContentProviderOperation.Builder builder = + ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); + String myGroupsId = null; + if (mAccount != null) { + builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); + builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); + + // Assume that caller side creates this group if it does not exist. + // TODO: refactor this code along with the change in GoogleSource.java + if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) { + final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { + Groups.SOURCE_ID }, + Groups.TITLE + "=?", new String[] { + GOOGLE_MY_CONTACTS_GROUP }, null); + try { + if (cursor != null && cursor.moveToFirst()) { + myGroupsId = cursor.getString(0); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + } else { + builder.withValue(RawContacts.ACCOUNT_NAME, null); + builder.withValue(RawContacts.ACCOUNT_TYPE, null); + } + operationList.add(builder.build()); + + { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); + + builder.withValue(StructuredName.GIVEN_NAME, mGivenName); + builder.withValue(StructuredName.FAMILY_NAME, mFamilyName); + builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName); + builder.withValue(StructuredName.PREFIX, mPrefix); + builder.withValue(StructuredName.SUFFIX, mSuffix); + + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); + builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); + builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + + builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); + operationList.add(builder.build()); + } + + if (mNickNameList != null && mNickNameList.size() > 0) { + boolean first = true; + for (String nickName : mNickNameList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); + + builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); + builder.withValue(Nickname.NAME, nickName); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + first = false; + } + operationList.add(builder.build()); + } + } + + if (mPhoneList != null) { + for (PhoneData phoneData : mPhoneList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); + + builder.withValue(Phone.TYPE, phoneData.type); + if (phoneData.type == Phone.TYPE_CUSTOM) { + builder.withValue(Phone.LABEL, phoneData.label); + } + builder.withValue(Phone.NUMBER, phoneData.data); + if (phoneData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + operationList.add(builder.build()); + } + } + + if (mOrganizationList != null) { + boolean first = true; + for (OrganizationData organizationData : mOrganizationList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); + + // Currently, we do not use TYPE_CUSTOM. + builder.withValue(Organization.TYPE, organizationData.type); + builder.withValue(Organization.COMPANY, organizationData.companyName); + builder.withValue(Organization.TITLE, organizationData.positionName); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + } + operationList.add(builder.build()); + } + } + + if (mEmailList != null) { + for (EmailData emailData : mEmailList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Email.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); + + builder.withValue(Email.TYPE, emailData.type); + if (emailData.type == Email.TYPE_CUSTOM) { + builder.withValue(Email.LABEL, emailData.label); + } + builder.withValue(Email.DATA, emailData.data); + if (emailData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + operationList.add(builder.build()); + } + } + + if (mPostalList != null) { + for (PostalData postalData : mPostalList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + VCardUtils.insertStructuredPostalDataUsingContactsStruct( + mVCardType, builder, postalData); + operationList.add(builder.build()); + } + } + + if (mImList != null) { + for (ImData imData : mImList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); + + builder.withValue(Im.TYPE, imData.type); + if (imData.type == Im.TYPE_CUSTOM) { + builder.withValue(Im.LABEL, imData.label); + } + builder.withValue(Im.DATA, imData.data); + if (imData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + } + } + + if (mNoteList != null) { + for (String note : mNoteList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); + + builder.withValue(Note.NOTE, note); + operationList.add(builder.build()); + } + } + + if (mPhotoList != null) { + boolean first = true; + for (PhotoData photoData : mPhotoList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); + builder.withValue(Photo.PHOTO, photoData.photoBytes); + if (first) { + builder.withValue(Data.IS_PRIMARY, 1); + first = false; + } + operationList.add(builder.build()); + } + } + + if (mWebsiteList != null) { + for (String website : mWebsiteList) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Website.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); + builder.withValue(Website.URL, website); + // There's no information about the type of URL in vCard. + // We use TYPE_HOME for safety. + builder.withValue(Website.TYPE, Website.TYPE_HOME); + operationList.add(builder.build()); + } + } + + if (!TextUtils.isEmpty(mBirthday)) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); + builder.withValue(Event.START_DATE, mBirthday); + builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); + operationList.add(builder.build()); + } + + if (myGroupsId != null) { + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); + builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId); + operationList.add(builder.build()); + } + + try { + resolver.applyBatch(ContactsContract.AUTHORITY, operationList); + } catch (RemoteException e) { + Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } catch (OperationApplicationException e) { + Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); + } + } + + public boolean isIgnorable() { + return getDisplayName().length() == 0; + } + + private String listToString(List<String> list){ + final int size = list.size(); + if (size > 1) { + StringBuilder builder = new StringBuilder(); + int i = 0; + for (String type : list) { + builder.append(type); + if (i < size - 1) { + builder.append(";"); + } + } + return builder.toString(); + } else if (size == 1) { + return list.get(0); + } else { + return ""; + } + } +} diff --git a/core/java/android/pim/vcard/EntryCommitter.java b/core/java/android/pim/vcard/EntryCommitter.java new file mode 100644 index 0000000..3f1655d --- /dev/null +++ b/core/java/android/pim/vcard/EntryCommitter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.content.ContentResolver; +import android.util.Log; + +/** + * EntryHandler implementation which commits the entry to Contacts Provider + */ +public class EntryCommitter implements EntryHandler { + public static String LOG_TAG = "vcard.EntryComitter"; + + private ContentResolver mContentResolver; + private long mTimeToCommit; + + public EntryCommitter(ContentResolver resolver) { + mContentResolver = resolver; + } + + public void onParsingStart() { + } + + public void onParsingEnd() { + if (VCardConfig.showPerformanceLog()) { + Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit)); + } + } + + public void onEntryCreated(final ContactStruct contactStruct) { + long start = System.currentTimeMillis(); + contactStruct.pushIntoContentResolver(mContentResolver); + mTimeToCommit += System.currentTimeMillis() - start; + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/EntryHandler.java b/core/java/android/pim/vcard/EntryHandler.java new file mode 100644 index 0000000..7fb8114 --- /dev/null +++ b/core/java/android/pim/vcard/EntryHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +/** + * Unlike {@link VCardBuilder}, this (and {@link VCardDataBuilder}) assumes + * "each VCard entry should be correctly parsed and passed to each EntryHandler object", + */ +public interface EntryHandler { + /** + * Called when the parsing started. + */ + public void onParsingStart(); + + /** + * The method called when one VCard entry is successfully created + */ + public void onEntryCreated(final ContactStruct entry); + + /** + * Called when the parsing ended. + * Able to be use this method for showing performance log, etc. + */ + public void onParsingEnd(); +} diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java new file mode 100644 index 0000000..e1c4b33 --- /dev/null +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import java.util.List; + +public interface VCardBuilder { + void start(); + + void end(); + + /** + * BEGIN:VCARD + */ + void startRecord(String type); + + /** END:VXX */ + void endRecord(); + + void startProperty(); + + void endProperty(); + + /** + * @param group + */ + void propertyGroup(String group); + + /** + * @param name + * N <br> + * N + */ + void propertyName(String name); + + /** + * @param type + * LANGUAGE \ ENCODING <br> + * ;LANGUage= \ ;ENCODING= + */ + void propertyParamType(String type); + + /** + * @param value + * FR-EN \ GBK <br> + * FR-EN \ GBK + */ + void propertyParamValue(String value); + + void propertyValues(List<String> values); +} diff --git a/core/java/android/pim/vcard/VCardBuilderCollection.java b/core/java/android/pim/vcard/VCardBuilderCollection.java new file mode 100644 index 0000000..e3985b6 --- /dev/null +++ b/core/java/android/pim/vcard/VCardBuilderCollection.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import java.util.Collection; +import java.util.List; + +public class VCardBuilderCollection implements VCardBuilder { + + private final Collection<VCardBuilder> mVCardBuilderCollection; + + public VCardBuilderCollection(Collection<VCardBuilder> vBuilderCollection) { + mVCardBuilderCollection = vBuilderCollection; + } + + public Collection<VCardBuilder> getVCardBuilderBaseCollection() { + return mVCardBuilderCollection; + } + + public void start() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.start(); + } + } + + public void end() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.end(); + } + } + + public void startRecord(String type) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.startRecord(type); + } + } + + public void endRecord() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.endRecord(); + } + } + + public void startProperty() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.startProperty(); + } + } + + + public void endProperty() { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.endProperty(); + } + } + + public void propertyGroup(String group) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyGroup(group); + } + } + + public void propertyName(String name) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyName(name); + } + } + + public void propertyParamType(String type) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyParamType(type); + } + } + + public void propertyParamValue(String value) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyParamValue(value); + } + } + + public void propertyValues(List<String> values) { + for (VCardBuilder builder : mVCardBuilderCollection) { + builder.propertyValues(values); + } + } +} diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java new file mode 100644 index 0000000..f9dce25 --- /dev/null +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -0,0 +1,2049 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.content.EntityIterator; +import android.content.Entity.NamedContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +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.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.format.Time; +import android.util.CharsetUtils; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * <p> + * The class for composing VCard from Contacts information. Note that this is + * completely differnt implementation from + * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. + * </p> + * + * <p> + * Usually, this class should be used like this. + * </p> + * + * <pre class="prettyprint"> VCardComposer composer = null; try { composer = new + * VCardComposer(context); composer.addHandler(composer.new + * HandlerForOutputStream(outputStream)); if (!composer.init()) { // Do + * something handling the situation. return; } while (!composer.isAfterLast()) { + * if (mCanceled) { // Assume a user may cancel this operation during the + * export. return; } if (!composer.createOneEntry()) { // Do something handling + * the error situation. return; } } } finally { if (composer != null) { + * composer.terminate(); } } </pre> + */ +public class VCardComposer { + private static final String LOG_TAG = "vcard.VCardComposer"; + + private static final String DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; + + public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = + "Failed to get database information"; + + public static final String FAILURE_REASON_NO_ENTRY = + "There's no exportable in the database"; + + public static final String FAILURE_REASON_NOT_INITIALIZED = + "The vCard composer object is not correctly initialized"; + + public static final String NO_ERROR = "No error"; + + private static final Uri sDataRequestUri; + + static { + Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); + builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); + sDataRequestUri = builder.build(); + } + + public static interface OneEntryHandler { + public boolean onInit(Context context); + + public boolean onEntryCreated(String vcard); + + public void onTerminate(); + } + + /** + * <p> + * An useful example handler, which emits VCard String to outputstream one + * by one. + * </p> + * <p> + * The input OutputStream object is closed() on {{@link #onTerminate()}. + * Must not close the stream outside. + * </p> + */ + public class HandlerForOutputStream implements OneEntryHandler { + @SuppressWarnings("hiding") + private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; + + final private OutputStream mOutputStream; // mWriter will close this. + private Writer mWriter; + + private boolean mOnTerminateIsCalled = false; + + /** + * Input stream will be closed on the detruction of this object. + */ + public HandlerForOutputStream(OutputStream outputStream) { + mOutputStream = outputStream; + } + + public boolean onInit(Context context) { + try { + mWriter = new BufferedWriter(new OutputStreamWriter( + mOutputStream, mCharsetString)); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); + mErrorReason = "Encoding is not supported (usually this does not happen!): " + + mCharsetString; + return false; + } + + if (mIsDoCoMo) { + try { + // Create one empty entry. + mWriter.write(createOneEntryInternal("-1")); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + } + return true; + } + + public boolean onEntryCreated(String vcard) { + try { + mWriter.write(vcard); + } catch (IOException e) { + Log.e(LOG_TAG, + "IOException occurred during exportOneContactData: " + + e.getMessage()); + mErrorReason = "IOException occurred: " + e.getMessage(); + return false; + } + return true; + } + + public void onTerminate() { + mOnTerminateIsCalled = true; + if (mWriter != null) { + try { + // Flush and sync the data so that a user is able to pull + // the SDCard just after + // the export. + mWriter.flush(); + if (mOutputStream != null + && mOutputStream instanceof FileOutputStream) { + ((FileOutputStream) mOutputStream).getFD().sync(); + } + } catch (IOException e) { + Log.d(LOG_TAG, + "IOException during closing the output stream: " + + e.getMessage()); + } finally { + try { + mWriter.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void finalize() { + if (!mOnTerminateIsCalled) { + onTerminate(); + } + } + } + + public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + + private static final String VCARD_PROPERTY_ADR = "ADR"; + private static final String VCARD_PROPERTY_BEGIN = "BEGIN"; + private static final String VCARD_PROPERTY_EMAIL = "EMAIL"; + private static final String VCARD_PROPERTY_END = "END"; + private static final String VCARD_PROPERTY_NAME = "N"; + private static final String VCARD_PROPERTY_FULL_NAME = "FN"; + private static final String VCARD_PROPERTY_NOTE = "NOTE"; + private static final String VCARD_PROPERTY_ORG = "ORG"; + private static final String VCARD_PROPERTY_SOUND = "SOUND"; + private static final String VCARD_PROPERTY_SORT_STRING = "SORT-STRING"; + private static final String VCARD_PROPERTY_NICKNAME = "NICKNAME"; + private static final String VCARD_PROPERTY_TEL = "TEL"; + private static final String VCARD_PROPERTY_TITLE = "TITLE"; + private static final String VCARD_PROPERTY_PHOTO = "PHOTO"; + private static final String VCARD_PROPERTY_VERSION = "VERSION"; + private static final String VCARD_PROPERTY_URL = "URL"; + private static final String VCARD_PROPERTY_BIRTHDAY = "BDAY"; + + private static final String VCARD_PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; + private static final String VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + private static final String VCARD_PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; + + // Android specific properties + // TODO: ues extra MIME-TYPE instead of adding this kind of inflexible fields + private static final String VCARD_PROPERTY_X_NICKNAME = "X-NICKNAME"; + + // Property for call log entry + private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; + private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; + private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; + private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; + + // Properties for DoCoMo vCard. + private static final String VCARD_PROPERTY_X_CLASS = "X-CLASS"; + private static final String VCARD_PROPERTY_X_REDUCTION = "X-REDUCTION"; + private static final String VCARD_PROPERTY_X_NO = "X-NO"; + private static final String VCARD_PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; + + private static final String VCARD_DATA_VCARD = "VCARD"; + private static final String VCARD_DATA_PUBLIC = "PUBLIC"; + + private static final String VCARD_ATTR_SEPARATOR = ";"; + private static final String VCARD_COL_SEPARATOR = "\r\n"; + private static final String VCARD_DATA_SEPARATOR = ":"; + private static final String VCARD_ITEM_SEPARATOR = ";"; + private static final String VCARD_WS = " "; + private static final String VCARD_ATTR_EQUAL = "="; + + // Type strings are now in VCardConstants.java. + + private static final String VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; + + private static final String VCARD_ATTR_ENCODING_BASE64_V21 = "ENCODING=BASE64"; + private static final String VCARD_ATTR_ENCODING_BASE64_V30 = "ENCODING=b"; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + + private final Context mContext; + private final int mVCardType; + private final boolean mCareHandlerErrors; + private final ContentResolver mContentResolver; + + // Convenient member variables about the restriction of the vCard format. + // Used for not calling the same methods returning same results. + private final boolean mIsV30; + private final boolean mIsJapaneseMobilePhone; + private final boolean mOnlyOneNoteFieldIsAvailable; + private final boolean mIsDoCoMo; + private final boolean mUsesQuotedPrintable; + private final boolean mUsesAndroidProperty; + private final boolean mUsesDefactProperty; + private final boolean mUsesUtf8; + private final boolean mUsesShiftJis; + private final boolean mUsesQPToPrimaryProperties; + + private Cursor mCursor; + private int mIdColumn; + + private final String mCharsetString; + private final String mVCardAttributeCharset; + private boolean mTerminateIsCalled; + final private List<OneEntryHandler> mHandlerList; + + private String mErrorReason = NO_ERROR; + + private static final Map<Integer, String> sImMap; + + static { + sImMap = new HashMap<Integer, String>(); + sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); + // Google talk is a special case. + } + + private boolean mIsCallLogComposer = false; + + private boolean mNeedPhotoForVCard = true; + + private static final String[] sContactsProjection = new String[] { + Contacts._ID, + }; + + /** The projection to use when querying the call log table */ + private static final String[] sCallLogProjection = new String[] { + Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE, + Calls.CACHED_NUMBER_LABEL + }; + private static final int NUMBER_COLUMN_INDEX = 0; + private static final int DATE_COLUMN_INDEX = 1; + private static final int CALL_TYPE_COLUMN_INDEX = 2; + private static final int CALLER_NAME_COLUMN_INDEX = 3; + private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4; + private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5; + + private static final String FLAG_TIMEZONE_UTC = "Z"; + + public VCardComposer(Context context) { + this(context, VCardConfig.VCARD_TYPE_DEFAULT, true, false, true); + } + + public VCardComposer(Context context, String vcardTypeStr, + boolean careHandlerErrors) { + this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), + careHandlerErrors, false, true); + } + + public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { + this(context, vcardType, careHandlerErrors, false, true); + } + + /** + * Construct for supporting call log entry vCard composing. + * + * @param isCallLogComposer true if this composer is for creating Call Log vCard. + */ + public VCardComposer(Context context, int vcardType, boolean careHandlerErrors, + boolean isCallLogComposer, boolean needPhotoInVCard) { + mContext = context; + mVCardType = vcardType; + mCareHandlerErrors = careHandlerErrors; + mIsCallLogComposer = isCallLogComposer; + mNeedPhotoForVCard = needPhotoInVCard; + mContentResolver = context.getContentResolver(); + + mIsV30 = VCardConfig.isV30(vcardType); + mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + mIsJapaneseMobilePhone = VCardConfig + .needsToConvertPhoneticString(vcardType); + mOnlyOneNoteFieldIsAvailable = VCardConfig + .onlyOneNoteFieldIsAvailable(vcardType); + mUsesAndroidProperty = VCardConfig + .usesAndroidSpecificProperty(vcardType); + mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); + mUsesUtf8 = VCardConfig.usesUtf8(vcardType); + mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); + mUsesQPToPrimaryProperties = VCardConfig.usesQPToPrimaryProperties(vcardType); + mHandlerList = new ArrayList<OneEntryHandler>(); + + if (mIsDoCoMo) { + mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but + // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in + // Android, not shown to the public). + mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + } else if (mUsesShiftJis) { + mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + } else { + mCharsetString = "UTF-8"; + mVCardAttributeCharset = "CHARSET=UTF-8"; + } + } + + /** + * This static function is to compose vCard for phone own number + */ + public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, + String phoneNumber, boolean vcardVer21) { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (!vcardVer21) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + + boolean needCharset = false; + if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { + needCharset = true; + } + // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, phoneName, needCharset, false); + appendVCardLine(builder, VCARD_PROPERTY_NAME, phoneName, needCharset, false); + + String label = Integer.toString(phonetype); + appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); + + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + /** + * Must call before {{@link #init()}. + */ + public void addHandler(OneEntryHandler handler) { + mHandlerList.add(handler); + } + + public boolean init() { + return init(null, null); + } + + /** + * @return Returns true when initialization is successful and all the other + * methods are available. Returns false otherwise. + */ + public boolean init(final String selection, final String[] selectionArgs) { + if (mCareHandlerErrors) { + List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onInit(mContext)) { + for (OneEntryHandler finished : finishedList) { + finished.onTerminate(); + } + return false; + } + } + } else { + // Just ignore the false returned from onInit(). + for (OneEntryHandler handler : mHandlerList) { + handler.onInit(mContext); + } + } + + if (mIsCallLogComposer) { + mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection, + selection, selectionArgs, null); + } else { + mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection, + selection, selectionArgs, null); + } + + if (mCursor == null) { + mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; + return false; + } + + if (getCount() == 0 || !mCursor.moveToFirst()) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); + } finally { + mCursor = null; + mErrorReason = FAILURE_REASON_NO_ENTRY; + } + return false; + } + + if (mIsCallLogComposer) { + mIdColumn = -1; + } else { + mIdColumn = mCursor.getColumnIndex(Contacts._ID); + } + + return true; + } + + public boolean createOneEntry() { + if (mCursor == null || mCursor.isAfterLast()) { + mErrorReason = FAILURE_REASON_NOT_INITIALIZED; + return false; + } + String name = null; + String vcard; + try { + if (mIsCallLogComposer) { + vcard = createOneCallLogEntryInternal(); + } else { + if (mIdColumn >= 0) { + vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); + } else { + Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); + return true; + } + } + } catch (OutOfMemoryError error) { + // Maybe some data (e.g. photo) is too big to have in memory. But it + // should be rare. + Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " + + name); + System.gc(); + // TODO: should tell users what happened? + return true; + } finally { + mCursor.moveToNext(); + } + + // This function does not care the OutOfMemoryError on the handler side + // :-P + if (mCareHandlerErrors) { + List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( + mHandlerList.size()); + for (OneEntryHandler handler : mHandlerList) { + if (!handler.onEntryCreated(vcard)) { + return false; + } + } + } else { + for (OneEntryHandler handler : mHandlerList) { + handler.onEntryCreated(vcard); + } + } + + return true; + } + + /** + * Format according to RFC 2445 DATETIME type. + * The format is: ("%Y%m%dT%H%M%SZ"). + */ + private final String toRfc2455Format(final long millSecs) { + Time startDate = new Time(); + startDate.set(millSecs); + String date = startDate.format2445(); + return date + FLAG_TIMEZONE_UTC; + } + + /** + * Try to append the property line for a call history time stamp field if possible. + * Do nothing if the call log type gotton from the database is invalid. + */ + private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { + // Extension for call history as defined in + // in the Specification for Ic Mobile Communcation - ver 1.1, + // Oct 2000. This is used to send the details of the call + // history - missed, incoming, outgoing along with date and time + // to the requesting device (For example, transferring phone book + // when connected over bluetooth) + // + // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" + final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); + final String callLogTypeStr; + switch (callLogType) { + case Calls.INCOMING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; + break; + } + case Calls.OUTGOING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; + break; + } + case Calls.MISSED_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; + break; + } + default: { + Log.w(LOG_TAG, "Call log type not correct."); + return; + } + } + + final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); + builder.append(VCARD_PROPERTY_X_TIMESTAMP); + builder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttribute(builder, callLogTypeStr); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(toRfc2455Format(dateAsLong)); + builder.append(VCARD_COL_SEPARATOR); + } + + private String createOneCallLogEntryInternal() { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); + if (TextUtils.isEmpty(name)) { + name = mCursor.getString(NUMBER_COLUMN_INDEX); + } + final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); + // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, name, needCharset, false); + appendVCardLine(builder, VCARD_PROPERTY_NAME, name, needCharset, false); + + String number = mCursor.getString(NUMBER_COLUMN_INDEX); + int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); + if (TextUtils.isEmpty(label)) { + label = Integer.toString(type); + } + appendVCardTelephoneLine(builder, type, label, number); + tryAppendCallHistoryTimeStampField(builder); + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + return builder.toString(); + } + + private String createOneEntryInternal(final String contactId) { + final Map<String, List<ContentValues>> contentValuesListMap = + new HashMap<String, List<ContentValues>>(); + final String selection = Data.CONTACT_ID + "=?"; + final String[] selectionArgs = new String[] {contactId}; + // The resolver may return the entity iterator with no data. It is possiible. + // e.g. If all the data in the contact of the given contact id are not exportable ones, + // they are hidden from the view of this method, though contact id itself exists. + boolean dataExists = false; + EntityIterator entityIterator = null; + try { + entityIterator = mContentResolver.queryEntities( + sDataRequestUri, selection, selectionArgs, null); + dataExists = entityIterator.hasNext(); + while (entityIterator.hasNext()) { + Entity entity = entityIterator.next(); + for (NamedContentValues namedContentValues : entity + .getSubValues()) { + ContentValues contentValues = namedContentValues.values; + String key = contentValues.getAsString(Data.MIMETYPE); + if (key != null) { + List<ContentValues> contentValuesList = + contentValuesListMap.get(key); + if (contentValuesList == null) { + contentValuesList = new ArrayList<ContentValues>(); + contentValuesListMap.put(key, contentValuesList); + } + contentValuesList.add(contentValues); + } + } + } + } catch (RemoteException e) { + Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)", + contactId, e.getMessage())); + return ""; + } finally { + if (entityIterator != null) { + entityIterator.close(); + } + } + + if (!dataExists) { + return ""; + } + + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + } + + appendStructuredNames(builder, contentValuesListMap); + appendNickNames(builder, contentValuesListMap); + appendPhones(builder, contentValuesListMap); + appendEmails(builder, contentValuesListMap); + appendPostals(builder, contentValuesListMap); + appendIms(builder, contentValuesListMap); + appendWebsites(builder, contentValuesListMap); + appendBirthday(builder, contentValuesListMap); + appendOrganizations(builder, contentValuesListMap); + if (mNeedPhotoForVCard) { + appendPhotos(builder, contentValuesListMap); + } + appendNotes(builder, contentValuesListMap); + // TODO: GroupMembership + + if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendVCardLine(builder, VCARD_PROPERTY_X_REDUCTION, ""); + appendVCardLine(builder, VCARD_PROPERTY_X_NO, ""); + appendVCardLine(builder, VCARD_PROPERTY_X_DCM_HMN_MODE, ""); + } + + appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + public void terminate() { + for (OneEntryHandler handler : mHandlerList) { + handler.onTerminate(); + } + + if (mCursor != null) { + try { + mCursor.close(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + + e.getMessage()); + } + mCursor = null; + } + + mTerminateIsCalled = true; + } + + @Override + public void finalize() { + if (!mTerminateIsCalled) { + terminate(); + } + } + + public int getCount() { + if (mCursor == null) { + return 0; + } + return mCursor.getCount(); + } + + public boolean isAfterLast() { + if (mCursor == null) { + return false; + } + return mCursor.isAfterLast(); + } + + /** + * @return Return the error reason if possible. + */ + public String getErrorReason() { + return mErrorReason; + } + + private void appendStructuredNames(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(StructuredName.CONTENT_ITEM_TYPE); + if (contentValuesList != null && contentValuesList.size() > 0) { + appendStructuredNamesInternal(builder, contentValuesList); + } else if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + } else if (mIsV30) { + // vCard 3.0 requires "N" and "FN" properties. + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + } + } + + private boolean containsNonEmptyName(ContentValues contentValues) { + final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); + final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); + final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); + final String prefix = contentValues.getAsString(StructuredName.PREFIX); + final String suffix = contentValues.getAsString(StructuredName.SUFFIX); + final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); + return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && + TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && + TextUtils.isEmpty(suffix) && TextUtils.isEmpty(displayName)); + } + + private void appendStructuredNamesInternal(final StringBuilder builder, + final List<ContentValues> contentValuesList) { + // For safety, we'll emit just one value around StructuredName, as external importers + // may get confused with multiple "N", "FN", etc. properties, though it is valid in + // vCard spec. + ContentValues primaryContentValues = null; + ContentValues subprimaryContentValues = null; + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null){ + continue; + } + Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); + if (isSuperPrimary != null && isSuperPrimary > 0) { + // We choose "super primary" ContentValues. + primaryContentValues = contentValues; + break; + } else if (primaryContentValues == null) { + // We choose the first "primary" ContentValues + // if "super primary" ContentValues does not exist. + Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); + if (isPrimary != null && isPrimary > 0 && + containsNonEmptyName(contentValues)) { + primaryContentValues = contentValues; + // Do not break, since there may be ContentValues with "super primary" + // afterword. + } else if (subprimaryContentValues == null && + containsNonEmptyName(contentValues)) { + subprimaryContentValues = contentValues; + } + } + } + + if (primaryContentValues == null) { + if (subprimaryContentValues != null) { + // We choose the first ContentValues if any "primary" ContentValues does not exist. + primaryContentValues = subprimaryContentValues; + } else { + Log.e(LOG_TAG, "All ContentValues given from database is empty."); + primaryContentValues = new ContentValues(); + } + } + + final String familyName = primaryContentValues + .getAsString(StructuredName.FAMILY_NAME); + final String middleName = primaryContentValues + .getAsString(StructuredName.MIDDLE_NAME); + final String givenName = primaryContentValues + .getAsString(StructuredName.GIVEN_NAME); + final String prefix = primaryContentValues + .getAsString(StructuredName.PREFIX); + final String suffix = primaryContentValues + .getAsString(StructuredName.SUFFIX); + final String displayName = primaryContentValues + .getAsString(StructuredName.DISPLAY_NAME); + + if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { + final String encodedFamily; + final String encodedGiven; + final String encodedMiddle; + final String encodedPrefix; + final String encodedSuffix; + + final boolean reallyUseQuotedPrintableToName = + (mUsesQPToPrimaryProperties && + !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); + + if (reallyUseQuotedPrintableToName) { + encodedFamily = encodeQuotedPrintable(familyName); + encodedGiven = encodeQuotedPrintable(givenName); + encodedMiddle = encodeQuotedPrintable(middleName); + encodedPrefix = encodeQuotedPrintable(prefix); + encodedSuffix = encodeQuotedPrintable(suffix); + } else { + encodedFamily = escapeCharacters(familyName); + encodedGiven = escapeCharacters(givenName); + encodedMiddle = escapeCharacters(middleName); + encodedPrefix = escapeCharacters(prefix); + encodedSuffix = escapeCharacters(suffix); + } + + // N property. This order is specified by vCard spec and does not depend on countries. + builder.append(VCARD_PROPERTY_NAME); + if (shouldAppendCharsetAttribute(Arrays.asList( + familyName, givenName, middleName, prefix, suffix))) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintableToName) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedFamily); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedGiven); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedMiddle); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPrefix); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedSuffix); + builder.append(VCARD_COL_SEPARATOR); + + final String fullname = VCardUtils.constructNameFromElements( + VCardConfig.getNameOrderType(mVCardType), + encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); + final boolean reallyUseQuotedPrintableToFullname = + mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); + + final String encodedFullname = + reallyUseQuotedPrintableToFullname ? + encodeQuotedPrintable(fullname) : + escapeCharacters(fullname); + + // FN property + builder.append(VCARD_PROPERTY_FULL_NAME); + if (shouldAppendCharsetAttribute(encodedFullname)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintableToFullname) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedFullname); + builder.append(VCARD_COL_SEPARATOR); + } else if (!TextUtils.isEmpty(displayName)) { + final boolean reallyUseQuotedPrintableToDisplayName = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); + final String encodedDisplayName = + reallyUseQuotedPrintableToDisplayName ? + encodeQuotedPrintable(displayName) : + escapeCharacters(displayName); + + builder.append(VCARD_PROPERTY_NAME); + if (shouldAppendCharsetAttribute(encodedDisplayName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintableToDisplayName) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedDisplayName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } else if (mIsDoCoMo) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + } else if (mIsV30) { + appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + } + + String phoneticFamilyName = primaryContentValues + .getAsString(StructuredName.PHONETIC_FAMILY_NAME); + String phoneticMiddleName = primaryContentValues + .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + String phoneticGivenName = primaryContentValues + .getAsString(StructuredName.PHONETIC_GIVEN_NAME); + if (!(TextUtils.isEmpty(phoneticFamilyName) + && TextUtils.isEmpty(phoneticMiddleName) && + TextUtils.isEmpty(phoneticGivenName))) { // if not empty + if (mIsJapaneseMobilePhone) { + phoneticFamilyName = VCardUtils + .toHalfWidthString(phoneticFamilyName); + phoneticMiddleName = VCardUtils + .toHalfWidthString(phoneticMiddleName); + phoneticGivenName = VCardUtils + .toHalfWidthString(phoneticGivenName); + } + + if (mIsV30) { + final String sortString = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, + phoneticMiddleName, + phoneticGivenName); + builder.append(VCARD_PROPERTY_SORT_STRING); + + // Do not need to care about QP, since vCard 3.0 does not allow it. + final String encodedSortString = escapeCharacters(sortString); + if (shouldAppendCharsetAttribute(encodedSortString)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedSortString); + builder.append(VCARD_COL_SEPARATOR); + } else { + // Note: There is no appropriate property for expressing + // phonetic name in vCard 2.1, while there is in + // vCard 3.0 (SORT-STRING). + // We chose to use DoCoMo's way since it is supported by + // a lot of Japanese mobile phones. This is "X-" property, so + // any parser hopefully would not get confused with this. + builder.append(VCARD_PROPERTY_SOUND); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_X_IRMC_N); + + boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !(VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticFamilyName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticMiddleName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticGivenName))); + + final String encodedPhoneticFamilyName; + final String encodedPhoneticMiddleName; + final String encodedPhoneticGivenName; + if (reallyUseQuotedPrintable) { + encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); + encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); + encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); + } else { + encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); + encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); + encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); + } + + if (shouldAppendCharsetAttribute(Arrays.asList( + encodedPhoneticFamilyName, encodedPhoneticMiddleName, + encodedPhoneticGivenName))) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticFamilyName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPhoneticGivenName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(encodedPhoneticMiddleName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + } else if (mIsDoCoMo) { + builder.append(VCARD_PROPERTY_SOUND); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + + if (mUsesDefactProperty) { + if (!TextUtils.isEmpty(phoneticGivenName)) { + final boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); + final String encodedPhoneticGivenName; + if (reallyUseQuotedPrintable) { + encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); + } else { + encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); + } + builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); + if (shouldAppendCharsetAttribute(encodedPhoneticGivenName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticGivenName); + builder.append(VCARD_COL_SEPARATOR); + } + if (!TextUtils.isEmpty(phoneticMiddleName)) { + final boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); + final String encodedPhoneticMiddleName; + if (reallyUseQuotedPrintable) { + encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); + } else { + encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); + } + builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); + if (shouldAppendCharsetAttribute(encodedPhoneticMiddleName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticMiddleName); + builder.append(VCARD_COL_SEPARATOR); + } + if (!TextUtils.isEmpty(phoneticFamilyName)) { + final boolean reallyUseQuotedPrintable = + (mUsesQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); + final String encodedPhoneticFamilyName; + if (reallyUseQuotedPrintable) { + encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); + } else { + encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); + } + builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); + if (shouldAppendCharsetAttribute(encodedPhoneticFamilyName)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedPhoneticFamilyName); + builder.append(VCARD_COL_SEPARATOR); + } + } + } + + private void appendNickNames(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Nickname.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + final String propertyNickname; + if (mIsV30) { + propertyNickname = VCARD_PROPERTY_NICKNAME; + } else if (mUsesAndroidProperty) { + propertyNickname = VCARD_PROPERTY_X_NICKNAME; + } else { + // There's no way to add this field. + return; + } + + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + + final String encodedNickname; + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); + if (reallyUseQuotedPrintable) { + encodedNickname = encodeQuotedPrintable(nickname); + } else { + encodedNickname = escapeCharacters(nickname); + } + + builder.append(propertyNickname); + if (shouldAppendCharsetAttribute(propertyNickname)) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + if (reallyUseQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedNickname); + builder.append(VCARD_COL_SEPARATOR); + } + } + } + + private void appendPhones(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Phone.CONTENT_ITEM_TYPE); + boolean phoneLineExists = false; + if (contentValuesList != null) { + Set<String> phoneSet = new HashSet<String>(); + for (ContentValues contentValues : contentValuesList) { + final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); + final String label = contentValues.getAsString(Phone.LABEL); + String phoneNumber = contentValues.getAsString(Phone.NUMBER); + if (phoneNumber != null) { + phoneNumber = phoneNumber.trim(); + } + if (TextUtils.isEmpty(phoneNumber)) { + continue; + } + int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); + + phoneLineExists = true; + if (type == Phone.TYPE_PAGER) { + phoneLineExists = true; + if (!phoneSet.contains(phoneNumber)) { + phoneSet.add(phoneNumber); + appendVCardTelephoneLine(builder, type, label, phoneNumber); + } + } else { + // The entry "may" have several phone numbers when the contact entry is + // corrupted because of its original source. + // + // e.g. I encountered the entry like the following. + // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." + // This kind of entry is not able to be inserted via Android devices, but + // possible if the source of the data is already corrupted. + List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); + if (phoneNumberList.isEmpty()) { + continue; + } + phoneLineExists = true; + for (String actualPhoneNumber : phoneNumberList) { + if (!phoneSet.contains(actualPhoneNumber)) { + final int format = VCardUtils.getPhoneNumberFormat(mVCardType); + SpannableStringBuilder tmpBuilder = + new SpannableStringBuilder(actualPhoneNumber); + PhoneNumberUtils.formatNumber(tmpBuilder, format); + final String formattedPhoneNumber = tmpBuilder.toString(); + phoneSet.add(actualPhoneNumber); + appendVCardTelephoneLine(builder, type, label, formattedPhoneNumber); + } + } + } + } + } + + if (!phoneLineExists && mIsDoCoMo) { + appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); + } + } + + private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) { + List<String> phoneList = new ArrayList<String>(); + + StringBuilder builder = new StringBuilder(); + final int length = phoneNumber.length(); + for (int i = 0; i < length; i++) { + final char ch = phoneNumber.charAt(i); + if (Character.isDigit(ch)) { + builder.append(ch); + } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { + phoneList.add(builder.toString()); + builder = new StringBuilder(); + } + } + if (builder.length() > 0) { + phoneList.add(builder.toString()); + } + + return phoneList; + } + + private void appendEmails(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Email.CONTENT_ITEM_TYPE); + boolean emailAddressExists = false; + if (contentValuesList != null) { + Set<String> addressSet = new HashSet<String>(); + for (ContentValues contentValues : contentValuesList) { + Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : Email.TYPE_OTHER); + final String label = contentValues.getAsString(Email.LABEL); + String emailAddress = contentValues.getAsString(Email.DATA); + if (emailAddress != null) { + emailAddress = emailAddress.trim(); + } + if (TextUtils.isEmpty(emailAddress)) { + continue; + } + emailAddressExists = true; + if (!addressSet.contains(emailAddress)) { + addressSet.add(emailAddress); + appendVCardEmailLine(builder, type, label, emailAddress); + } + } + } + + if (!emailAddressExists && mIsDoCoMo) { + appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); + } + } + + private void appendPostals(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(StructuredPostal.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + if (mIsDoCoMo) { + appendPostalsForDoCoMo(builder, contentValuesList); + } else { + appendPostalsForGeneric(builder, contentValuesList); + } + } else if (mIsDoCoMo) { + builder.append(VCARD_PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(Constants.ATTR_TYPE_HOME); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + } + + /** + * Tries to append just one line. If there's no appropriate address + * information, append an empty line. + */ + private void appendPostalsForDoCoMo(final StringBuilder builder, + final List<ContentValues> contentValuesList) { + // TODO: from old, inefficient code. fix this. + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_HOME)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_WORK)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_OTHER)) { + return; + } + if (appendPostalsForDoCoMoInternal(builder, contentValuesList, + StructuredPostal.TYPE_CUSTOM)) { + return; + } + + Log.w(LOG_TAG, + "Should not come here. Must have at least one postal data."); + } + + private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder, + final List<ContentValues> contentValuesList, Integer preferedType) { + for (ContentValues contentValues : contentValuesList) { + final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + if (type == preferedType) { + appendVCardPostalLine(builder, type, label, contentValues); + return true; + } + } + return false; + } + + private void appendPostalsForGeneric(final StringBuilder builder, + final List<ContentValues> contentValuesList) { + for (ContentValues contentValues : contentValuesList) { + final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + if (type != null) { + appendVCardPostalLine(builder, type, label, contentValues); + } + } + } + + private void appendIms(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Im.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + Integer protocol = contentValues.getAsInteger(Im.PROTOCOL); + String data = contentValues.getAsString(Im.DATA); + if (data != null) { + data = data.trim(); + } + if (TextUtils.isEmpty(data)) { + continue; + } + + if (protocol != null && protocol == Im.PROTOCOL_GOOGLE_TALK) { + if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { + appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); + } + // TODO: add "X-GOOGLE TALK" case... + } + } + } + } + + private void appendWebsites(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Website.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + String website = contentValues.getAsString(Website.URL); + if (website != null) { + website = website.trim(); + } + if (!TextUtils.isEmpty(website)) { + appendVCardLine(builder, VCARD_PROPERTY_URL, website); + } + } + } + } + + private void appendBirthday(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Event.CONTENT_ITEM_TYPE); + if (contentValuesList != null && contentValuesList.size() > 0) { + Integer eventType = contentValuesList.get(0).getAsInteger(Event.TYPE); + if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { + return; + } + // Theoretically, there must be only one birthday for each vCard data and + // we are afraid of some parse error occuring in some devices, so + // we emit only one birthday entry for now. + String birthday = contentValuesList.get(0).getAsString(Event.START_DATE); + if (birthday != null) { + birthday = birthday.trim(); + } + if (!TextUtils.isEmpty(birthday)) { + appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); + } + } + } + + private void appendOrganizations(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Organization.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + String company = contentValues + .getAsString(Organization.COMPANY); + if (company != null) { + company = company.trim(); + } + String title = contentValues + .getAsString(Organization.TITLE); + if (title != null) { + title = title.trim(); + } + + if (!TextUtils.isEmpty(company)) { + appendVCardLine(builder, VCARD_PROPERTY_ORG, company, + !VCardUtils.containsOnlyPrintableAscii(company), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(company))); + } + if (!TextUtils.isEmpty(title)) { + appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, + !VCardUtils.containsOnlyPrintableAscii(title), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); + } + } + } + } + + private void appendPhotos(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = contentValuesListMap + .get(Photo.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + byte[] data = contentValues.getAsByteArray(Photo.PHOTO); + if (data == null) { + continue; + } + final String photoType; + // Use some heuristics for guessing the format of the image. + // TODO: there should be some general API for detecting the file format. + if (data.length >= 3 && data[0] == 'G' && data[1] == 'I' + && data[2] == 'F') { + photoType = "GIF"; + } else if (data.length >= 4 && data[0] == (byte) 0x89 + && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { + // Note: vCard 2.1 officially does not support PNG, but we + // may have it + // and using X- word like "X-PNG" may not let importers know + // it is + // PNG. So we use the String "PNG" as is... + photoType = "PNG"; + } else if (data.length >= 2 && data[0] == (byte) 0xff + && data[1] == (byte) 0xd8) { + photoType = "JPEG"; + } else { + Log.d(LOG_TAG, "Unknown photo type. Ignore."); + continue; + } + final String photoString = VCardUtils.encodeBase64(data); + if (photoString.length() > 0) { + appendVCardPhotoLine(builder, photoString, photoType); + } + } + } + } + + private void appendNotes(final StringBuilder builder, + final Map<String, List<ContentValues>> contentValuesListMap) { + final List<ContentValues> contentValuesList = + contentValuesListMap.get(Note.CONTENT_ITEM_TYPE); + if (contentValuesList != null) { + if (mOnlyOneNoteFieldIsAvailable) { + StringBuilder noteBuilder = new StringBuilder(); + boolean first = true; + for (ContentValues contentValues : contentValuesList) { + String note = contentValues.getAsString(Note.NOTE); + if (note == null) { + note = ""; + } + if (note.length() > 0) { + if (first) { + first = false; + } else { + noteBuilder.append('\n'); + } + noteBuilder.append(note); + } + } + final String noteStr = noteBuilder.toString(); + // This means we scan noteStr completely twice, which is redundant. + // But for now, we assume this is not so time-consuming.. + final boolean shouldAppendCharsetInfo = + !VCardUtils.containsOnlyPrintableAscii(noteStr); + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); + appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } else { + for (ContentValues contentValues : contentValuesList) { + final String noteStr = contentValues.getAsString(Note.NOTE); + if (!TextUtils.isEmpty(noteStr)) { + final boolean shouldAppendCharsetInfo = + !VCardUtils.containsOnlyPrintableAscii(noteStr); + final boolean reallyUseQuotedPrintable = + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); + appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } + } + } + } + } + + /** + * Append '\' to the characters which should be escaped. The character set is different + * not only between vCard 2.1 and vCard 3.0 but also among each device. + * + * Note that Quoted-Printable string must not be input here. + */ + @SuppressWarnings("fallthrough") + private String escapeCharacters(final String unescaped) { + if (TextUtils.isEmpty(unescaped)) { + return ""; + } + + final StringBuilder tmpBuilder = new StringBuilder(); + final int length = unescaped.length(); + for (int i = 0; i < length; i++) { + char ch = unescaped.charAt(i); + switch (ch) { + case ';': { + tmpBuilder.append('\\'); + tmpBuilder.append(';'); + break; + } + case '\r': { + if (i + 1 < length) { + char nextChar = unescaped.charAt(i); + if (nextChar == '\n') { + continue; + } else { + // fall through + } + } else { + // fall through + } + } + case '\n': { + // In vCard 2.1, there's no specification about this, while + // vCard 3.0 explicitly requires this should be encoded to "\n". + tmpBuilder.append("\\n"); + break; + } + case '\\': { + if (mIsV30) { + tmpBuilder.append("\\\\"); + break; + } else { + // fall through + } + } + case '<': + case '>': { + if (mIsDoCoMo) { + tmpBuilder.append('\\'); + tmpBuilder.append(ch); + } else { + tmpBuilder.append(ch); + } + break; + } + case ',': { + if (mIsV30) { + tmpBuilder.append("\\,"); + } else { + tmpBuilder.append(ch); + } + break; + } + default: { + tmpBuilder.append(ch); + break; + } + } + } + return tmpBuilder.toString(); + } + + private void appendVCardPhotoLine(final StringBuilder builder, + final String encodedData, final String photoType) { + StringBuilder tmpBuilder = new StringBuilder(); + tmpBuilder.append(VCARD_PROPERTY_PHOTO); + tmpBuilder.append(VCARD_ATTR_SEPARATOR); + if (mIsV30) { + tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); + } else { + tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); + } + tmpBuilder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttribute(tmpBuilder, photoType); + tmpBuilder.append(VCARD_DATA_SEPARATOR); + tmpBuilder.append(encodedData); + + final String tmpStr = tmpBuilder.toString(); + tmpBuilder = new StringBuilder(); + int lineCount = 0; + int length = tmpStr.length(); + for (int i = 0; i < length; i++) { + tmpBuilder.append(tmpStr.charAt(i)); + lineCount++; + if (lineCount > 72) { + tmpBuilder.append(VCARD_COL_SEPARATOR); + tmpBuilder.append(VCARD_WS); + lineCount = 0; + } + } + builder.append(tmpBuilder.toString()); + builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardPostalLine(final StringBuilder builder, + final Integer typeAsObject, final String label, + final ContentValues contentValues) { + builder.append(VCARD_PROPERTY_ADR); + builder.append(VCARD_ATTR_SEPARATOR); + + // Note: Not sure why we need to emit "empty" line even when actual data does not exist. + // There may be some reason or may not be any. We keep safer side. + // TODO: investigate this. + boolean dataExists = false; + String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); + boolean actuallyUseQuotedPrintable = false; + boolean shouldAppendCharset = false; + for (String data : dataArray) { + if (!TextUtils.isEmpty(data)) { + dataExists = true; + if (!shouldAppendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { + shouldAppendCharset = true; + } + if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { + actuallyUseQuotedPrintable = true; + break; + } + } + } + + int length = dataArray.length; + for (int i = 0; i < length; i++) { + String data = dataArray[i]; + if (!TextUtils.isEmpty(data)) { + if (actuallyUseQuotedPrintable) { + dataArray[i] = encodeQuotedPrintable(data); + } else { + dataArray[i] = escapeCharacters(data); + } + } + } + + final int typeAsPrimitive; + if (typeAsObject == null) { + typeAsPrimitive = StructuredPostal.TYPE_OTHER; + } else { + typeAsPrimitive = typeAsObject; + } + + String typeAsString = null; + switch (typeAsPrimitive) { + case StructuredPostal.TYPE_HOME: { + typeAsString = Constants.ATTR_TYPE_HOME; + break; + } + case StructuredPostal.TYPE_WORK: { + typeAsString = Constants.ATTR_TYPE_WORK; + break; + } + case StructuredPostal.TYPE_CUSTOM: { + if (mUsesAndroidProperty && !TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + // We're not sure whether the label is valid in the spec + // ("IANA-token" in the vCard 3.0 is unclear...) + // Just for safety, we add "X-" at the beggining of each label. + // Also checks the label obeys with vCard 3.0 spec. + builder.append("X-"); + builder.append(label); + builder.append(VCARD_DATA_SEPARATOR); + } + break; + } + case StructuredPostal.TYPE_OTHER: { + break; + } + default: { + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); + break; + } + } + + // Attribute(s). + + { + boolean shouldAppendAttrSeparator = false; + if (typeAsString != null) { + appendTypeAttribute(builder, typeAsString); + shouldAppendAttrSeparator = true; + } + + if (dataExists) { + if (shouldAppendCharset) { + // Strictly, vCard 3.0 does not allow exporters to emit charset information, + // but we will add it since the information should be useful for importers, + // + // Assume no parser does not emit error with this attribute in vCard 3.0. + if (shouldAppendAttrSeparator) { + builder.append(VCARD_ATTR_SEPARATOR); + } + builder.append(mVCardAttributeCharset); + shouldAppendAttrSeparator = true; + } + + if (actuallyUseQuotedPrintable) { + if (shouldAppendAttrSeparator) { + builder.append(VCARD_ATTR_SEPARATOR); + } + builder.append(VCARD_ATTR_ENCODING_QP); + shouldAppendAttrSeparator = true; + } + } + } + + // Property values. + + builder.append(VCARD_DATA_SEPARATOR); + if (dataExists) { + // The elements in dataArray are already encoded to quoted printable + // if needed. + // See above. + // + // TODO: in vCard 3.0, one line may become too huge. Fix this. + builder.append(dataArray[0]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[1]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[2]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[3]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[4]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[5]); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(dataArray[6]); + } + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardEmailLine(final StringBuilder builder, + final Integer typeAsObject, final String label, final String data) { + builder.append(VCARD_PROPERTY_EMAIL); + + final int typeAsPrimitive; + if (typeAsObject == null) { + typeAsPrimitive = Email.TYPE_OTHER; + } else { + typeAsPrimitive = typeAsObject; + } + + final String typeAsString; + switch (typeAsPrimitive) { + case Email.TYPE_CUSTOM: { + // For backward compatibility. + // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. + // To support mobile type at that time, this custom label had been used. + if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME + .equals(label)) { + typeAsString = Constants.ATTR_TYPE_CELL; + } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + typeAsString = "X-" + label; + } else { + typeAsString = DEFAULT_EMAIL_TYPE; + } + break; + } + case Email.TYPE_HOME: { + typeAsString = Constants.ATTR_TYPE_HOME; + break; + } + case Email.TYPE_WORK: { + typeAsString = Constants.ATTR_TYPE_WORK; + break; + } + case Email.TYPE_OTHER: { + typeAsString = DEFAULT_EMAIL_TYPE; + break; + } + case Email.TYPE_MOBILE: { + typeAsString = Constants.ATTR_TYPE_CELL; + break; + } + default: { + Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); + typeAsString = DEFAULT_EMAIL_TYPE; + break; + } + } + + builder.append(VCARD_ATTR_SEPARATOR); + appendTypeAttribute(builder, typeAsString); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(data); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendVCardTelephoneLine(final StringBuilder builder, + final Integer typeAsObject, final String label, + String encodedData) { + builder.append(VCARD_PROPERTY_TEL); + builder.append(VCARD_ATTR_SEPARATOR); + + final int typeAsPrimitive; + if (typeAsObject == null) { + typeAsPrimitive = Phone.TYPE_OTHER; + } else { + typeAsPrimitive = typeAsObject; + } + + switch (typeAsPrimitive) { + case Phone.TYPE_HOME: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); + break; + case Phone.TYPE_WORK: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); + break; + case Phone.TYPE_FAX_HOME: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); + break; + case Phone.TYPE_FAX_WORK: + appendTypeAttributes(builder, Arrays.asList( + Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); + break; + case Phone.TYPE_MOBILE: + builder.append(Constants.ATTR_TYPE_CELL); + break; + case Phone.TYPE_PAGER: + if (mIsDoCoMo) { + // Not sure about the reason, but previous implementation had + // used "VOICE" instead of "PAGER" + // Also, refrain from using appendType() so that "TYPE=" is never be appended. + builder.append(Constants.ATTR_TYPE_VOICE); + } else { + appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); + } + break; + case Phone.TYPE_OTHER: + appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + break; + case Phone.TYPE_CUSTOM: + if (mUsesAndroidProperty && !TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + appendTypeAttribute(builder, "X-" + label); + } else { + // Just ignore the custom type. + appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + } + break; + default: + appendUncommonPhoneType(builder, typeAsPrimitive); + break; + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedData); + builder.append(VCARD_COL_SEPARATOR); + } + + /** + * Appends phone type string which may not be available in some devices. + */ + private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { + if (mIsDoCoMo) { + // The previous implementation for DoCoMo had been conservative + // about miscellaneous types. + builder.append(Constants.ATTR_TYPE_VOICE); + } else { + String phoneAttribute = VCardUtils.getPhoneAttributeString(type); + if (phoneAttribute != null) { + appendTypeAttribute(builder, phoneAttribute); + } else { + Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); + } + } + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final String rawData) { + appendVCardLine(builder, propertyName, rawData, false, false); + } + + private void appendVCardLine(final StringBuilder builder, + final String field, final String rawData, final boolean needCharset, + boolean needQuotedPrintable) { + builder.append(field); + if (needCharset) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(mVCardAttributeCharset); + } + + final String encodedData; + if (needQuotedPrintable) { + builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_ATTR_ENCODING_QP); + encodedData = encodeQuotedPrintable(rawData); + } else { + // TODO: one line may be too huge, which may be invalid in vCard spec, though + // several (even well-known) applications do not care this. + encodedData = escapeCharacters(rawData); + } + + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedData); + builder.append(VCARD_COL_SEPARATOR); + } + + private void appendTypeAttributes(final StringBuilder builder, + final List<String> types) { + // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, + // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. + boolean first = true; + for (String type : types) { + if (first) { + first = false; + } else { + builder.append(VCARD_ATTR_SEPARATOR); + } + appendTypeAttribute(builder, type); + } + } + + private void appendTypeAttribute(final StringBuilder builder, final String type) { + // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" + if (mIsV30) { + builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); + } + builder.append(type); + } + + /** + * Returns true when the property line should contain charset attribute + * information. This method may return true even when vCard version is 3.0. + * + * Strictly, adding charset information is invalid in VCard 3.0. + * However we'll add the info only when used charset is not UTF-8 + * in vCard 3.0 format, since parser side may be able to use the charset + * via this field, though we may encounter another problem by adding it... + * + * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 + * recommends UTF-8. By adding this field, parsers may be able + * to know this text is NOT UTF-8 but Shift_Jis. + */ + private boolean shouldAppendCharsetAttribute(final String propertyValue) { + return (!VCardUtils.containsOnlyPrintableAscii(propertyValue) && + (!mIsV30 || !mUsesUtf8)); + } + + private boolean shouldAppendCharsetAttribute(final List<String> propertyValueList) { + boolean shouldAppendBasically = false; + for (String propertyValue : propertyValueList) { + if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { + shouldAppendBasically = true; + break; + } + } + return shouldAppendBasically && (!mIsV30 || !mUsesUtf8); + } + + private String encodeQuotedPrintable(String str) { + if (TextUtils.isEmpty(str)) { + return ""; + } + { + // Replace "\n" and "\r" with "\r\n". + StringBuilder tmpBuilder = new StringBuilder(); + int length = str.length(); + for (int i = 0; i < length; i++) { + char ch = str.charAt(i); + if (ch == '\r') { + if (i + 1 < length && str.charAt(i + 1) == '\n') { + i++; + } + tmpBuilder.append("\r\n"); + } else if (ch == '\n') { + tmpBuilder.append("\r\n"); + } else { + tmpBuilder.append(ch); + } + } + str = tmpBuilder.toString(); + } + + final StringBuilder tmpBuilder = new StringBuilder(); + int index = 0; + int lineCount = 0; + byte[] strArray = null; + + try { + strArray = str.getBytes(mCharsetString); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " + + "Try default charset"); + strArray = str.getBytes(); + } + while (index < strArray.length) { + tmpBuilder.append(String.format("=%02X", strArray[index])); + index += 1; + lineCount += 3; + + if (lineCount >= 67) { + // Specification requires CRLF must be inserted before the + // length of the line + // becomes more than 76. + // Assuming that the next character is a multi-byte character, + // it will become + // 6 bytes. + // 76 - 6 - 3 = 67 + tmpBuilder.append("=\r\n"); + lineCount = 0; + } + } + + return tmpBuilder.toString(); + } +} diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java new file mode 100644 index 0000000..68cd0df --- /dev/null +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import java.util.HashMap; +import java.util.Map; + +/** + * The class representing VCard related configurations. Useful static methods are not in this class + * but in VCardUtils. + */ +public class VCardConfig { + // TODO: may be better to make the instance of this available and stop using static methods and + // one integer. + + /* package */ static final int LOG_LEVEL_NONE = 0; + /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1; + /* package */ static final int LOG_LEVEL_SHOW_WARNING = 0x2; + /* package */ static final int LOG_LEVEL_VERBOSE = + LOG_LEVEL_PERFORMANCE_MEASUREMENT | LOG_LEVEL_SHOW_WARNING; + + /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE; + + // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and + // decode the unicode to the original charset. If not, this setting will cause some bug. + public static final String DEFAULT_CHARSET = "iso-8859-1"; + + // TODO: make the other codes use this flag + public static final boolean IGNORE_CASE_EXCEPT_VALUE = true; + + private static final int FLAG_V21 = 0; + private static final int FLAG_V30 = 1; + + // 0x2 is reserved for the future use ... + + public static final int NAME_ORDER_DEFAULT = 0; + public static final int NAME_ORDER_EUROPE = 0x4; + public static final int NAME_ORDER_JAPANESE = 0x8; + private static final int NAME_ORDER_MASK = 0xC; + + // 0x10 is reserved for safety + + private static final int FLAG_CHARSET_UTF8 = 0; + private static final int FLAG_CHARSET_SHIFT_JIS = 0x20; + + /** + * The flag indicating the vCard composer will add some "X-" properties used only in Android + * when the formal vCard specification does not have appropriate fields for that data. + * + * For example, Android accepts nickname information while vCard 2.1 does not. + * When this flag is on, vCard composer emits alternative "X-" property (like "X-NICKNAME") + * instead of just dropping it. + * + * vCard parser code automatically parses the field emitted even when this flag is off. + * + * Note that this flag does not assure all the information must be hold in the emitted vCard. + */ + private static final int FLAG_USE_ANDROID_PROPERTY = 0x80000000; + + /** + * The flag indicating the vCard composer will add some "X-" properties seen in the + * vCard data emitted by the other softwares/devices when the formal vCard specification + * does not have appropriate field(s) for that data. + * + * One example is X-PHONETIC-FIRST-NAME/X-PHONETIC-MIDDLE-NAME/X-PHONETIC-LAST-NAME, which are + * for phonetic name (how the name is pronounced), seen in the vCard emitted by some other + * non-Android devices/softwares. We chose to enable the vCard composer to use those + * defact properties since they are also useful for Android devices. + * + * Note for developers: only "X-" properties should be added with this flag. vCard 2.1/3.0 + * allows any kind of "X-" properties but does not allow non-"X-" properties (except IANA tokens + * in vCard 3.0). Some external parsers may get confused with non-valid, non-"X-" properties. + */ + private static final int FLAG_USE_DEFACT_PROPERTY = 0x40000000; + + /** + * The flag indicating some specific dialect seen in vcard of DoCoMo (one of Japanese + * mobile careers) should be used. This flag does not include any other information like + * that "the vCard is for Japanese". So it is "possible" that "the vCard should have DoCoMo's + * dialect but the name order should be European", but it is not recommended. + */ + private static final int FLAG_DOCOMO = 0x20000000; + + /** + * The flag indicating the vCard composer use Quoted-Printable toward even "primary" types. + * In this context, "primary" types means "N", "FN", etc. which are usually "not" encoded + * into Quoted-Printable format in external exporters. + * This flag is useful when some target importer does not accept "primary" property values + * without Quoted-Printable encoding. + * + * @hide Temporaly made public. We don't strictly define "primary", so we may change the + * behavior around this flag in the future. Do not use this flag without any reason. + */ + public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; + + // VCard types + + /** + * General vCard format with the version 2.1. Uses UTF-8 for the charset. + * When composing a vCard entry, the US convension will be used. + * + * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", + * while in Japan, it should be "Prefix Family Middle Given Suffix". + */ + public static final int VCARD_TYPE_V21_GENERIC = + (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; + + /** + * General vCard format with the version 3.0. Uses UTF-8 for the charset. + * + * Note that this type is not fully implemented, so probably some bugs remain both in + * parsing and composing. + * + * TODO: implement this type correctly. + */ + public static final int VCARD_TYPE_V30_GENERIC = + (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; + + /** + * General vCard format with the version 2.1 with some Europe convension. Uses Utf-8. + * Currently, only name order is considered ("Prefix Middle Given Family Suffix") + */ + public static final int VCARD_TYPE_V21_EUROPE = + (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; + + /** + * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8 + */ + public static final int VCARD_TYPE_V30_EUROPE = + (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; + + /** + * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for + * parsing/composing the vCard data. + */ + public static final int VCARD_TYPE_V21_JAPANESE = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese"; + + /** + * vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + */ + public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + + /** + * vCard format for miscellaneous Japanese devices, using Shift_Jis for + * parsing/composing the vCard data. + */ + public static final int VCARD_TYPE_V30_JAPANESE = + (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese"; + + /** + * vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + */ + public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = + (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; + + /** + * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. + * No Android-specific property nor defact property is included. + */ + public static final int VCARD_TYPE_DOCOMO = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_DOCOMO); + + private static final String VCARD_TYPE_DOCOMO_STR = "docomo"; + + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; + + private static final Map<String, Integer> VCARD_TYPES_MAP; + + static { + VCARD_TYPES_MAP = new HashMap<String, Integer>(); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); + VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + VCARD_TYPES_MAP.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + } + + public static int getVCardTypeFromString(String vcardTypeString) { + String loweredKey = vcardTypeString.toLowerCase(); + if (VCARD_TYPES_MAP.containsKey(loweredKey)) { + return VCARD_TYPES_MAP.get(loweredKey); + } else { + // XXX: should return the value indicating the input is invalid? + return VCARD_TYPE_DEFAULT; + } + } + + public static boolean isV30(int vcardType) { + return ((vcardType & FLAG_V30) != 0); + } + + public static boolean usesQuotedPrintable(int vcardType) { + return !isV30(vcardType); + } + + public static boolean isDoCoMo(int vcardType) { + return ((vcardType & FLAG_DOCOMO) != 0); + } + + /** + * @return true if the device is Japanese and some Japanese convension is + * applied to creating "formatted" something like FORMATTED_ADDRESS. + */ + public static boolean isJapaneseDevice(int vcardType) { + return ((vcardType == VCARD_TYPE_V21_JAPANESE) || + (vcardType == VCARD_TYPE_V21_JAPANESE_UTF8) || + (vcardType == VCARD_TYPE_V30_JAPANESE) || + (vcardType == VCARD_TYPE_V30_JAPANESE_UTF8) || + (vcardType == VCARD_TYPE_DOCOMO)); + } + + public static boolean usesUtf8(int vcardType) { + return ((vcardType & FLAG_CHARSET_UTF8) != 0); + } + + public static boolean usesShiftJis(int vcardType) { + return ((vcardType & FLAG_CHARSET_SHIFT_JIS) != 0); + } + + /** + * @return true when Japanese phonetic string must be converted to a string + * containing only half-width katakana. This method exists since Japanese mobile + * phones usually use only half-width katakana for expressing phonetic names and + * some devices are not ready for parsing other phonetic strings like hiragana and + * full-width katakana. + */ + public static boolean needsToConvertPhoneticString(int vcardType) { + return (vcardType == VCARD_TYPE_DOCOMO); + } + + public static int getNameOrderType(int vcardType) { + return vcardType & NAME_ORDER_MASK; + } + + public static boolean usesAndroidSpecificProperty(int vcardType) { + return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0); + } + + public static boolean usesDefactProperty(int vcardType) { + return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); + } + + public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { + return vcardType == VCARD_TYPE_DOCOMO; + } + + public static boolean showPerformanceLog() { + return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; + } + + /** + * @hide + */ + public static boolean usesQPToPrimaryProperties(int vcardType) { + return (usesQuotedPrintable(vcardType) && + ((vcardType & FLAG_USE_QP_TO_PRIMARY_PROPERTIES) != 0)); + } + + private VCardConfig() { + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java new file mode 100644 index 0000000..d2026d0 --- /dev/null +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.accounts.Account; +import android.util.CharsetUtils; +import android.util.Log; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.net.QuotedPrintableCodec; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * VBuilder for VCard. VCard may contain big photo images encoded by BASE64, + * If we store all VNode entries in memory like VDataBuilder.java, + * OutOfMemoryError may be thrown. Thus, this class push each VCard entry into + * ContentResolver immediately. + */ +public class VCardDataBuilder implements VCardBuilder { + static private String LOG_TAG = "VCardDataBuilder"; + + /** + * If there's no other information available, this class uses this charset for encoding + * byte arrays. + */ + static public String TARGET_CHARSET = "UTF-8"; + + private ContactStruct.Property mCurrentProperty = new ContactStruct.Property(); + private ContactStruct mCurrentContactStruct; + private String mParamType; + + /** + * The charset using which VParser parses the text. + */ + private String mSourceCharset; + + /** + * The charset with which byte array is encoded to String. + */ + private String mTargetCharset; + private boolean mStrictLineBreakParsing; + + final private int mVCardType; + final private Account mAccount; + + // Just for testing. + private long mTimePushIntoContentResolver; + + private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); + + public VCardDataBuilder() { + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC, null); + } + + /** + * @hide + */ + public VCardDataBuilder(int vcardType) { + this(null, null, false, vcardType, null); + } + + /** + * @hide + */ + public VCardDataBuilder(String charset, + boolean strictLineBreakParsing, int vcardType, Account account) { + this(null, charset, strictLineBreakParsing, vcardType, account); + } + + /** + * @hide + */ + public VCardDataBuilder(String sourceCharset, + String targetCharset, + boolean strictLineBreakParsing, + int vcardType, + Account account) { + if (sourceCharset != null) { + mSourceCharset = sourceCharset; + } else { + mSourceCharset = VCardConfig.DEFAULT_CHARSET; + } + if (targetCharset != null) { + mTargetCharset = targetCharset; + } else { + mTargetCharset = TARGET_CHARSET; + } + mStrictLineBreakParsing = strictLineBreakParsing; + mVCardType = vcardType; + mAccount = account; + } + + public void addEntryHandler(EntryHandler entryHandler) { + mEntryHandlers.add(entryHandler); + } + + public void start() { + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onParsingStart(); + } + } + + public void end() { + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onParsingEnd(); + } + } + + /** + * Assume that VCard is not nested. In other words, this code does not accept + */ + public void startRecord(String type) { + // TODO: add the method clear() instead of using null for reducing GC? + if (mCurrentContactStruct != null) { + // This means startRecord() is called inside startRecord() - endRecord() block. + // TODO: should throw some Exception + Log.e(LOG_TAG, "Nested VCard code is not supported now."); + } + if (!type.equalsIgnoreCase("VCARD")) { + // TODO: add test case for this + Log.e(LOG_TAG, "This is not VCARD!"); + } + + mCurrentContactStruct = new ContactStruct(mVCardType, mAccount); + } + + public void endRecord() { + mCurrentContactStruct.consolidateFields(); + for (EntryHandler entryHandler : mEntryHandlers) { + entryHandler.onEntryCreated(mCurrentContactStruct); + } + mCurrentContactStruct = null; + } + + public void startProperty() { + mCurrentProperty.clear(); + } + + public void endProperty() { + mCurrentContactStruct.addProperty(mCurrentProperty); + } + + public void propertyName(String name) { + mCurrentProperty.setPropertyName(name); + } + + public void propertyGroup(String group) { + // ContactStruct does not support Group. + } + + public void propertyParamType(String type) { + if (mParamType != null) { + Log.e(LOG_TAG, "propertyParamType() is called more than once " + + "before propertyParamValue() is called"); + } + mParamType = type; + } + + public void propertyParamValue(String value) { + if (mParamType == null) { + // From vCard 2.1 specification. vCard 3.0 formally does not allow this case. + mParamType = "TYPE"; + } + mCurrentProperty.addParameter(mParamType, value); + mParamType = null; + } + + private String encodeString(String originalString, String targetCharset) { + if (mSourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + Charset charset = Charset.forName(mSourceCharset); + ByteBuffer byteBuffer = charset.encode(originalString); + // byteBuffer.array() "may" return byte array which is larger than + // byteBuffer.remaining(). Here, we keep on the safe side. + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return null; + } + } + + private String handleOneValue(String value, String targetCharset, String encoding) { + if (encoding != null) { + if (encoding.equals("BASE64") || encoding.equals("B")) { + mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); + return value; + } else if (encoding.equals("QUOTED-PRINTABLE")) { + // "= " -> " ", "=\t" -> "\t". + // Previous code had done this replacement. Keep on the safe side. + StringBuilder builder = new StringBuilder(); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '=' && i < length - 1) { + char nextCh = value.charAt(i + 1); + if (nextCh == ' ' || nextCh == '\t') { + + builder.append(nextCh); + i++; + continue; + } + } + builder.append(ch); + } + String quotedPrintable = builder.toString(); + + String[] lines; + if (mStrictLineBreakParsing) { + lines = quotedPrintable.split("\r\n"); + } else { + builder = new StringBuilder(); + length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } + } + } else { + builder.append(ch); + } + } + String finalLine = builder.toString(); + if (finalLine.length() > 0) { + list.add(finalLine); + } + lines = list.toArray(new String[0]); + } + + builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); + } + builder.append(line); + } + byte[] bytes; + try { + bytes = builder.toString().getBytes(mSourceCharset); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset); + bytes = builder.toString().getBytes(); + } + + try { + bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); + return ""; + } + + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); + } + } + // Unknown encoding. Fall back to default. + } + return encodeString(value, targetCharset); + } + + public void propertyValues(List<String> values) { + if (values == null || values.size() == 0) { + return; + } + + final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); + String charset = + ((charsetCollection != null) ? charsetCollection.iterator().next() : null); + String targetCharset = CharsetUtils.nameForDefaultVendor(charset); + + final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING"); + String encoding = + ((encodingCollection != null) ? encodingCollection.iterator().next() : null); + + if (targetCharset == null || targetCharset.length() == 0) { + targetCharset = mTargetCharset; + } + + for (String value : values) { + mCurrentProperty.addToPropertyValueList( + handleOneValue(value, targetCharset, encoding)); + } + } + + public void showPerformanceInfo() { + Log.d(LOG_TAG, "time for insert ContactStruct to database: " + + mTimePushIntoContentResolver + " ms"); + } +} diff --git a/core/java/android/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java new file mode 100644 index 0000000..f99b46c --- /dev/null +++ b/core/java/android/pim/vcard/VCardEntryCounter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import java.util.List; + +public class VCardEntryCounter implements VCardBuilder { + private int mCount; + + public int getCount() { + return mCount; + } + + public void start() { + } + + public void end() { + } + + public void startRecord(String type) { + } + + public void endRecord() { + mCount++; + } + + public void startProperty() { + } + + public void endProperty() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + } + + public void propertyParamType(String type) { + } + + public void propertyParamValue(String value) { + } + + public void propertyValues(List<String> values) { + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java new file mode 100644 index 0000000..b5e5049 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.pim.vcard.exception.VCardException; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class VCardParser { + + protected boolean mCanceled; + + /** + * Parses the given stream and send the VCard data into VCardBuilderBase object. + * + * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets + * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is + * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, + * In some exreme case, some VCard may have different charsets in one VCard (though + * we do not see any device which emits such kind of malicious data) + * + * In order to avoid "misunderstanding" charset as much as possible, this method + * use "ISO-8859-1" for reading the stream. When charset is specified in some property + * (with "CHARSET=..." attribute), the string is decoded to raw bytes and encoded to + * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit + * characters, which is not completely sure. In some cases, this "decoding-encoding" + * scheme may fail. To avoid the case, + * + * We recommend you to use VCardSourceDetector and detect which kind of source the + * VCard comes from and explicitly specify a charset using the result. + * + * @param is The source to parse. + * @param builder The VCardBuilderBase object which used to construct data. If you want to + * include multiple VCardBuilderBase objects in this field, consider using + * {#link VCardBuilderCollection} class. + * @return Returns true for success. Otherwise returns false. + * @throws IOException, VCardException + */ + public abstract boolean parse(InputStream is, VCardBuilder builder) + throws IOException, VCardException; + + /** + * The method variants which accept charset. + * + * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use + * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese + * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses + * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification + * (e.g. W53K). + * + * @param is The source to parse. + * @param charset Charset to be used. + * @param builder The VCardBuilderBase object. + * @return Returns true when successful. Otherwise returns false. + * @throws IOException, VCardException + */ + public abstract boolean parse(InputStream is, String charset, VCardBuilder builder) + throws IOException, VCardException; + + /** + * The method variants which tells this object the operation is already canceled. + * XXX: Is this really necessary? + * @hide + */ + public abstract void parse(InputStream is, String charset, + VCardBuilder builder, boolean canceled) + throws IOException, VCardException; + + /** + * Cancel parsing. + * Actual cancel is done after the end of the current one vcard entry parsing. + */ + public void cancel() { + mCanceled = true; + } +} diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java new file mode 100644 index 0000000..11b3888 --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -0,0 +1,939 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.pim.vcard.exception.VCardException; +import android.pim.vcard.exception.VCardInvalidCommentLineException; +import android.pim.vcard.exception.VCardInvalidLineException; +import android.pim.vcard.exception.VCardNestedException; +import android.pim.vcard.exception.VCardNotSupportedException; +import android.pim.vcard.exception.VCardVersionException; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +/** + * This class is used to parse vcard. Please refer to vCard Specification 2.1. + */ +public class VCardParser_V21 extends VCardParser { + private static final String LOG_TAG = "vcard.VCardParser_V21"; + + /** Store the known-type */ + private static final HashSet<String> sKnownTypeSet = new HashSet<String>( + Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", + "PREF", "VOICE", "FAX", "MSG", "CELL", "PAGER", "BBS", + "MODEM", "CAR", "ISDN", "VIDEO", "AOL", "APPLELINK", + "ATTMAIL", "CIS", "EWORLD", "INTERNET", "IBMMAIL", + "MCIMAIL", "POWERSHARE", "PRODIGY", "TLX", "X400", "GIF", + "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", + "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", + "WAVE", "AIFF", "PCM", "X509", "PGP")); + + /** Store the known-value */ + private static final HashSet<String> sKnownValueSet = new HashSet<String>( + Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); + + /** Store the property names available in vCard 2.1 */ + private static final HashSet<String> sAvailablePropertyNameSetV21 = + new HashSet<String>(Arrays.asList( + "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", + "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); + + /** + * Though vCard 2.1 specification does not allow "B" encoding, some data may have it. + * We allow it for safety... + */ + private static final HashSet<String> sAvailableEncodingV21 = + new HashSet<String>(Arrays.asList( + "7BIT", "8BIT", "QUOTED-PRINTABLE", "BASE64", "B")); + + // Used only for parsing END:VCARD. + private String mPreviousLine; + + /** The builder to build parsed data */ + protected VCardBuilder mBuilder = null; + + /** + * The encoding type. "Encoding" in vCard is different from "Charset". + * e.g. 7BIT, 8BIT, QUOTED-PRINTABLE. + */ + protected String mEncoding = null; + + protected final String sDefaultEncoding = "8BIT"; + + // Should not directly read a line from this. Use getLine() instead. + protected BufferedReader mReader; + + // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. + // See v21_foma_1.vcf in test directory for more information. + private int mNestCount; + + // In order to reduce warning message as much as possible, we hold the value which made Logger + // emit a warning message. + protected HashSet<String> mWarningValueMap = new HashSet<String>(); + + // Just for debugging + private long mTimeTotal; + private long mTimeReadStartRecord; + private long mTimeReadEndRecord; + private long mTimeStartProperty; + private long mTimeEndProperty; + private long mTimeParseItems; + private long mTimeParseLineAndHandleGroup; + private long mTimeParsePropertyValues; + private long mTimeParseAdrOrgN; + private long mTimeHandleMiscPropertyValue; + private long mTimeHandleQuotedPrintable; + private long mTimeHandleBase64; + + /** + * Create a new VCard parser. + */ + public VCardParser_V21() { + super(); + } + + public VCardParser_V21(VCardSourceDetector detector) { + super(); + if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { + mNestCount = 1; + } + } + + /** + * Parse the file at the given position + * vcard_file = [wsls] vcard [wsls] + */ + protected void parseVCardFile() throws IOException, VCardException { + boolean firstReading = true; + while (true) { + if (mCanceled) { + break; + } + if (!parseOneVCard(firstReading)) { + break; + } + firstReading = false; + } + + if (mNestCount > 0) { + boolean useCache = true; + for (int i = 0; i < mNestCount; i++) { + readEndVCard(useCache, true); + useCache = false; + } + } + } + + protected String getVersion() { + return "2.1"; + } + + /** + * @return true when the propertyName is a valid property name. + */ + protected boolean isValidPropertyName(String propertyName) { + if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || + propertyName.startsWith("X-")) && + !mWarningValueMap.contains(propertyName)) { + mWarningValueMap.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); + } + return true; + } + + /** + * @return true when the encoding is a valid encoding. + */ + protected boolean isValidEncoding(String encoding) { + return sAvailableEncodingV21.contains(encoding.toUpperCase()); + } + + /** + * @return String. It may be null, or its length may be 0 + * @throws IOException + */ + protected String getLine() throws IOException { + return mReader.readLine(); + } + + /** + * @return String with it's length > 0 + * @throws IOException + * @throws VCardException when the stream reached end of line + */ + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Reached end of buffer."); + } else if (line.trim().length() > 0) { + return line; + } + } + } + + /** + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF + * "END" [ws] ":" [ws] "VCARD" + */ + private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { + boolean allowGarbage = false; + if (firstReading) { + if (mNestCount > 0) { + for (int i = 0; i < mNestCount; i++) { + if (!readBeginVCard(allowGarbage)) { + return false; + } + allowGarbage = true; + } + } + } + + if (!readBeginVCard(allowGarbage)) { + return false; + } + long start; + if (mBuilder != null) { + start = System.currentTimeMillis(); + mBuilder.startRecord("VCARD"); + mTimeReadStartRecord += System.currentTimeMillis() - start; + } + start = System.currentTimeMillis(); + parseItems(); + mTimeParseItems += System.currentTimeMillis() - start; + readEndVCard(true, false); + if (mBuilder != null) { + start = System.currentTimeMillis(); + mBuilder.endRecord(); + mTimeReadEndRecord += System.currentTimeMillis() - start; + } + return true; + } + + /** + * @return True when successful. False when reaching the end of line + * @throws IOException + * @throws VCardException + */ + protected boolean readBeginVCard(boolean allowGarbage) + throws IOException, VCardException { + String line; + do { + while (true) { + line = getLine(); + if (line == null) { + return false; + } else if (line.trim().length() > 0) { + break; + } + } + String[] strArray = line.split(":", 2); + int length = strArray.length; + + // Though vCard 2.1/3.0 specification does not allow lower cases, + // some data may have them, so we allow it (Actually, previous code + // had explicitly allowed "BEGIN:vCard" though there's no example). + if (length == 2 && + strArray[0].trim().equalsIgnoreCase("BEGIN") && + strArray[1].trim().equalsIgnoreCase("VCARD")) { + return true; + } else if (!allowGarbage) { + if (mNestCount > 0) { + mPreviousLine = line; + return false; + } else { + throw new VCardException( + "Expected String \"BEGIN:VCARD\" did not come " + + "(Instead, \"" + line + "\" came)"); + } + } + } while(allowGarbage); + + throw new VCardException("Reached where must not be reached."); + } + + /** + * The arguments useCache and allowGarbase are usually true and false accordingly when + * this function is called outside this function itself. + * + * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() + * is used. + * @param allowGarbage When true, ignore non "END:VCARD" line. + * @throws IOException + * @throws VCardException + */ + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + String line; + do { + if (useCache) { + // Though vCard specification does not allow lower cases, + // some data may have them, so we allow it. + line = mPreviousLine; + } else { + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Expected END:VCARD was not found."); + } else if (line.trim().length() > 0) { + break; + } + } + } + + String[] strArray = line.split(":", 2); + if (strArray.length == 2 && + strArray[0].trim().equalsIgnoreCase("END") && + strArray[1].trim().equalsIgnoreCase("VCARD")) { + return; + } else if (!allowGarbage) { + throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); + } + useCache = false; + } while (allowGarbage); + } + + /** + * items = *CRLF item + * / item + */ + protected void parseItems() throws IOException, VCardException { + /* items *CRLF item / item */ + boolean ended = false; + + if (mBuilder != null) { + long start = System.currentTimeMillis(); + mBuilder.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + ended = parseItem(); + if (mBuilder != null && !ended) { + long start = System.currentTimeMillis(); + mBuilder.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + + while (!ended) { + // follow VCARD ,it wont reach endProperty + if (mBuilder != null) { + long start = System.currentTimeMillis(); + mBuilder.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; + } + try { + ended = parseItem(); + } catch (VCardInvalidCommentLineException e) { + Log.e(LOG_TAG, "Invalid line which looks like some comment was found. Ignored."); + ended = false; + } + if (mBuilder != null && !ended) { + long start = System.currentTimeMillis(); + mBuilder.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; + } + } + } + + /** + * item = [groups "."] name [params] ":" value CRLF + * / [groups "."] "ADR" [params] ":" addressparts CRLF + * / [groups "."] "ORG" [params] ":" orgparts CRLF + * / [groups "."] "N" [params] ":" nameparts CRLF + * / [groups "."] "AGENT" [params] ":" vcard CRLF + */ + protected boolean parseItem() throws IOException, VCardException { + mEncoding = sDefaultEncoding; + + String line = getNonEmptyLine(); + long start = System.currentTimeMillis(); + + String[] propertyNameAndValue = separateLineAndHandleGroup(line); + if (propertyNameAndValue == null) { + return true; + } + if (propertyNameAndValue.length != 2) { + throw new VCardInvalidLineException("Invalid line \"" + line + "\""); + } + String propertyName = propertyNameAndValue[0].toUpperCase(); + String propertyValue = propertyNameAndValue[1]; + + mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start; + + if (propertyName.equals("ADR") || propertyName.equals("ORG") || + propertyName.equals("N")) { + start = System.currentTimeMillis(); + handleMultiplePropertyValue(propertyName, propertyValue); + mTimeParseAdrOrgN += System.currentTimeMillis() - start; + return false; + } else if (propertyName.equals("AGENT")) { + handleAgent(propertyValue); + return false; + } else if (isValidPropertyName(propertyName)) { + if (propertyName.equals("BEGIN")) { + if (propertyValue.equals("VCARD")) { + throw new VCardNestedException("This vCard has nested vCard data in it."); + } else { + throw new VCardException("Unknown BEGIN type: " + propertyValue); + } + } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { + throw new VCardVersionException("Incompatible version: " + + propertyValue + " != " + getVersion()); + } + start = System.currentTimeMillis(); + handlePropertyValue(propertyName, propertyValue); + mTimeParsePropertyValues += System.currentTimeMillis() - start; + return false; + } + + throw new VCardException("Unknown property name: \"" + + propertyName + "\""); + } + + static private final int STATE_GROUP_OR_PROPNAME = 0; + static private final int STATE_PARAMS = 1; + // vCard 3.1 specification allows double-quoted param-value, while vCard 2.1 does not. + // This is just for safety. + static private final int STATE_PARAMS_IN_DQUOTE = 2; + + protected String[] separateLineAndHandleGroup(String line) throws VCardException { + int length = line.length(); + int state = STATE_GROUP_OR_PROPNAME; + int nameIndex = 0; + + String[] propertyNameAndValue = new String[2]; + + if (length > 0 && line.charAt(0) == '#') { + throw new VCardInvalidCommentLineException(); + } + + for (int i = 0; i < length; i++) { + char ch = line.charAt(i); + switch (state) { + case STATE_GROUP_OR_PROPNAME: + if (ch == ':') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } else if (ch == '.') { + String groupName = line.substring(nameIndex, i); + if (mBuilder != null) { + mBuilder.propertyGroup(groupName); + } + nameIndex = i + 1; + } else if (ch == ';') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; + } + break; + case STATE_PARAMS: + if (ch == '"') { + state = STATE_PARAMS_IN_DQUOTE; + } else if (ch == ';') { + handleParams(line.substring(nameIndex, i)); + nameIndex = i + 1; + } else if (ch == ':') { + handleParams(line.substring(nameIndex, i)); + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } + break; + case STATE_PARAMS_IN_DQUOTE: + if (ch == '"') { + state = STATE_PARAMS; + } + break; + } + } + + throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); + } + + + /** + * params = ";" [ws] paramlist + * paramlist = paramlist [ws] ";" [ws] param + * / param + * param = "TYPE" [ws] "=" [ws] ptypeval + * / "VALUE" [ws] "=" [ws] pvalueval + * / "ENCODING" [ws] "=" [ws] pencodingval + * / "CHARSET" [ws] "=" [ws] charsetval + * / "LANGUAGE" [ws] "=" [ws] langval + * / "X-" word [ws] "=" [ws] word + * / knowntype + */ + protected void handleParams(String params) throws VCardException { + String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + String paramName = strArray[0].trim(); + String paramValue = strArray[1].trim(); + if (paramName.equals("TYPE")) { + handleType(paramValue); + } else if (paramName.equals("VALUE")) { + handleValue(paramValue); + } else if (paramName.equals("ENCODING")) { + handleEncoding(paramValue); + } else if (paramName.equals("CHARSET")) { + handleCharset(paramValue); + } else if (paramName.equals("LANGUAGE")) { + handleLanguage(paramValue); + } else if (paramName.startsWith("X-")) { + handleAnyParam(paramName, paramValue); + } else { + throw new VCardException("Unknown type \"" + paramName + "\""); + } + } else { + handleType(strArray[0]); + } + } + + /** + * ptypeval = knowntype / "X-" word + */ + protected void handleType(final String ptypeval) { + String upperTypeValue = ptypeval; + if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && + !mWarningValueMap.contains(ptypeval)) { + mWarningValueMap.add(ptypeval); + Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval); + } + if (mBuilder != null) { + mBuilder.propertyParamType("TYPE"); + mBuilder.propertyParamValue(upperTypeValue); + } + } + + /** + * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word + */ + protected void handleValue(final String pvalueval) throws VCardException { + if (sKnownValueSet.contains(pvalueval.toUpperCase()) || + pvalueval.startsWith("X-")) { + if (mBuilder != null) { + mBuilder.propertyParamType("VALUE"); + mBuilder.propertyParamValue(pvalueval); + } + } else { + throw new VCardException("Unknown value \"" + pvalueval + "\""); + } + } + + /** + * pencodingval = "7BIT" / "8BIT" / "QUOTED-PRINTABLE" / "BASE64" / "X-" word + */ + protected void handleEncoding(String pencodingval) throws VCardException { + if (isValidEncoding(pencodingval) || + pencodingval.startsWith("X-")) { + if (mBuilder != null) { + mBuilder.propertyParamType("ENCODING"); + mBuilder.propertyParamValue(pencodingval); + } + mEncoding = pencodingval; + } else { + throw new VCardException("Unknown encoding \"" + pencodingval + "\""); + } + } + + /** + * vCard specification only allows us-ascii and iso-8859-xxx (See RFC 1521), + * but some vCard contains other charset, so we allow them. + */ + protected void handleCharset(String charsetval) { + if (mBuilder != null) { + mBuilder.propertyParamType("CHARSET"); + mBuilder.propertyParamValue(charsetval); + } + } + + /** + * See also Section 7.1 of RFC 1521 + */ + protected void handleLanguage(String langval) throws VCardException { + String[] strArray = langval.split("-"); + if (strArray.length != 2) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + String tmp = strArray[0]; + int length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + tmp = strArray[1]; + length = tmp.length(); + for (int i = 0; i < length; i++) { + if (!isLetter(tmp.charAt(i))) { + throw new VCardException("Invalid Language: \"" + langval + "\""); + } + } + if (mBuilder != null) { + mBuilder.propertyParamType("LANGUAGE"); + mBuilder.propertyParamValue(langval); + } + } + + /** + * Mainly for "X-" type. This accepts any kind of type without check. + */ + protected void handleAnyParam(String paramName, String paramValue) { + if (mBuilder != null) { + mBuilder.propertyParamType(paramName); + mBuilder.propertyParamValue(paramValue); + } + } + + protected void handlePropertyValue(String propertyName, String propertyValue) throws + IOException, VCardException { + if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + long start = System.currentTimeMillis(); + String result = getQuotedPrintable(propertyValue); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mBuilder.propertyValues(v); + } + mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; + } else if (mEncoding.equalsIgnoreCase("BASE64") || + mEncoding.equalsIgnoreCase("B")) { + long start = System.currentTimeMillis(); + // It is very rare, but some BASE64 data may be so big that + // OutOfMemoryError occurs. To ignore such cases, use try-catch. + try { + String result = getBase64(propertyValue); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mBuilder.propertyValues(v); + } + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); + if (mBuilder != null) { + mBuilder.propertyValues(null); + } + } + mTimeHandleBase64 += System.currentTimeMillis() - start; + } else { + if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") + || mEncoding.equalsIgnoreCase("8BIT") + || mEncoding.toUpperCase().startsWith("X-"))) { + Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); + } + + long start = System.currentTimeMillis(); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(maybeUnescapeText(propertyValue)); + mBuilder.propertyValues(v); + } + mTimeHandleMiscPropertyValue += System.currentTimeMillis() - start; + } + } + + protected String getQuotedPrintable(String firstString) throws IOException, VCardException { + // Specifically, there may be some padding between = and CRLF. + // See the following: + // + // qp-line := *(qp-segment transport-padding CRLF) + // qp-part transport-padding + // qp-segment := qp-section *(SPACE / TAB) "=" + // ; Maximum length of 76 characters + // + // e.g. (from RFC 2045) + // Now's the time = + // for all folk to come= + // to the aid of their country. + if (firstString.trim().endsWith("=")) { + // remove "transport-padding" + int pos = firstString.length() - 1; + while(firstString.charAt(pos) != '=') { + } + StringBuilder builder = new StringBuilder(); + builder.append(firstString.substring(0, pos + 1)); + builder.append("\r\n"); + String line; + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException( + "File ended during parsing quoted-printable String"); + } + if (line.trim().endsWith("=")) { + // remove "transport-padding" + pos = line.length() - 1; + while(line.charAt(pos) != '=') { + } + builder.append(line.substring(0, pos + 1)); + builder.append("\r\n"); + } else { + builder.append(line); + break; + } + } + return builder.toString(); + } else { + return firstString; + } + } + + protected String getBase64(String firstString) throws IOException, VCardException { + StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + String line = getLine(); + if (line == null) { + throw new VCardException( + "File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * Mainly for "ADR", "ORG", and "N" + * We do not care the number of strnosemi here. + * + * addressparts = 0*6(strnosemi ";") strnosemi + * ; PO Box, Extended Addr, Street, Locality, Region, + * Postal Code, Country Name + * orgparts = *(strnosemi ";") strnosemi + * ; First is Organization Name, + * remainder are Organization Units. + * nameparts = 0*4(strnosemi ";") strnosemi + * ; Family, Given, Middle, Prefix, Suffix. + * ; Example:Public;John;Q.;Reverend Dr.;III, Esq. + * strnosemi = *(*nonsemi ("\;" / "\" CRLF)) *nonsemi + * ; To include a semicolon in this string, it must be escaped + * ; with a "\" character. + * + * We are not sure whether we should add "\" CRLF to each value. + * For now, we exclude them. + */ + protected void handleMultiplePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { + // vCard 2.1 does not allow QUOTED-PRINTABLE here, + // but some softwares/devices emit such data. + if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + propertyValue = getQuotedPrintable(propertyValue); + } + + if (mBuilder != null) { + StringBuilder builder = new StringBuilder(); + ArrayList<String> list = new ArrayList<String>(); + int length = propertyValue.length(); + for (int i = 0; i < length; i++) { + char ch = propertyValue.charAt(i); + if (ch == '\\' && i < length - 1) { + char nextCh = propertyValue.charAt(i + 1); + String unescapedString = maybeUnescapeCharacter(nextCh); + if (unescapedString != null) { + builder.append(unescapedString); + i++; + } else { + builder.append(ch); + } + } else if (ch == ';') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(ch); + } + } + list.add(builder.toString()); + mBuilder.propertyValues(list); + } + } + + /** + * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. + * + * item = ... + * / [groups "."] "AGENT" + * [params] ":" vcard CRLF + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF "END" [ws] ":" [ws] "VCARD" + * + */ + protected void handleAgent(String propertyValue) throws VCardException { + throw new VCardNotSupportedException("AGENT Property is not supported now."); + /* This is insufficient support. Also, AGENT Property is very rare. + Ignore it for now. + + String[] strArray = propertyValue.split(":", 2); + if (!(strArray.length == 2 || + strArray[0].trim().equalsIgnoreCase("BEGIN") && + strArray[1].trim().equalsIgnoreCase("VCARD"))) { + throw new VCardException("BEGIN:VCARD != \"" + propertyValue + "\""); + } + parseItems(); + readEndVCard(); + */ + } + + /** + * For vCard 3.0. + */ + protected String maybeUnescapeText(String text) { + return text; + } + + /** + * Returns unescaped String if the character should be unescaped. Return null otherwise. + * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. + */ + protected String maybeUnescapeCharacter(char ch) { + // Original vCard 2.1 specification does not allow transformation + // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of + // this class allowed them, so keep it as is. + if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { + return String.valueOf(ch); + } else { + return null; + } + } + + @Override + public boolean parse(InputStream is, VCardBuilder builder) + throws IOException, VCardException { + return parse(is, VCardConfig.DEFAULT_CHARSET, builder); + } + + @Override + public boolean parse(InputStream is, String charset, VCardBuilder builder) + throws IOException, VCardException { + final InputStreamReader tmpReader = new InputStreamReader(is, charset); + if (VCardConfig.showPerformanceLog()) { + mReader = new CustomBufferedReader(tmpReader); + } else { + mReader = new BufferedReader(tmpReader); + } + + mBuilder = builder; + + long start = System.currentTimeMillis(); + if (mBuilder != null) { + mBuilder.start(); + } + parseVCardFile(); + if (mBuilder != null) { + mBuilder.end(); + } + mTimeTotal += System.currentTimeMillis() - start; + + if (VCardConfig.showPerformanceLog()) { + showPerformanceInfo(); + } + + return true; + } + + @Override + public void parse(InputStream is, String charset, VCardBuilder builder, boolean canceled) + throws IOException, VCardException { + mCanceled = canceled; + parse(is, charset, builder); + } + + private void showPerformanceInfo() { + Log.d(LOG_TAG, "Total parsing time: " + mTimeTotal + " ms"); + if (mReader instanceof CustomBufferedReader) { + Log.d(LOG_TAG, "Total readLine time: " + + ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); + } + Log.d(LOG_TAG, "Time for handling the beggining of the record: " + + mTimeReadStartRecord + " ms"); + Log.d(LOG_TAG, "Time for handling the end of the record: " + + mTimeReadEndRecord + " ms"); + Log.d(LOG_TAG, "Time for parsing line, and handling group: " + + mTimeParseLineAndHandleGroup + " ms"); + Log.d(LOG_TAG, "Time for parsing ADR, ORG, and N fields:" + mTimeParseAdrOrgN + " ms"); + Log.d(LOG_TAG, "Time for parsing property values: " + mTimeParsePropertyValues + " ms"); + Log.d(LOG_TAG, "Time for handling normal property values: " + + mTimeHandleMiscPropertyValue + " ms"); + Log.d(LOG_TAG, "Time for handling Quoted-Printable: " + + mTimeHandleQuotedPrintable + " ms"); + Log.d(LOG_TAG, "Time for handling Base64: " + mTimeHandleBase64 + " ms"); + } + + private boolean isLetter(char ch) { + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + return true; + } + return false; + } +} + +class CustomBufferedReader extends BufferedReader { + private long mTime; + + public CustomBufferedReader(Reader in) { + super(in); + } + + @Override + public String readLine() throws IOException { + long start = System.currentTimeMillis(); + String ret = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + return ret; + } + + public long getTotalmillisecond() { + return mTime; + } +} diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java new file mode 100644 index 0000000..384649a --- /dev/null +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.pim.vcard.exception.VCardException; +import android.util.Log; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +/** + * This class is used to parse vcard3.0. <br> + * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426) + */ +public class VCardParser_V30 extends VCardParser_V21 { + private static final String LOG_TAG = "vcard.VCardParser_V30"; + + private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( + Arrays.asList( + "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", + "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 + "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", + "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0 + + // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety. + private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>( + Arrays.asList("7BIT", "8BIT", "BASE64", "B")); + + // Although RFC 2426 specifies some property must not have parameters, we allow it, + // since there may be some careers which violates the RFC... + private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); + + private String mPreviousLine; + + private boolean mEmittedAgentWarning = false; + + @Override + protected String getVersion() { + return Constants.VERSION_V30; + } + + @Override + protected boolean isValidPropertyName(String propertyName) { + if (!(sAcceptablePropsWithParam.contains(propertyName) || + acceptablePropsWithoutParam.contains(propertyName) || + propertyName.startsWith("X-")) && + !mWarningValueMap.contains(propertyName)) { + mWarningValueMap.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); + } + return true; + } + + @Override + protected boolean isValidEncoding(String encoding) { + return sAcceptableEncodingV30.contains(encoding.toUpperCase()); + } + + @Override + protected String getLine() throws IOException { + if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } else { + return mReader.readLine(); + } + } + + /** + * vCard 3.0 requires that the line with space at the beginning of the line + * must be combined with previous line. + */ + @Override + protected String getNonEmptyLine() throws IOException, VCardException { + String line; + StringBuilder builder = null; + while (true) { + line = mReader.readLine(); + if (line == null) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + throw new VCardException("Reached end of buffer."); + } else if (line.length() == 0) { + if (builder != null) { + return builder.toString(); + } else if (mPreviousLine != null) { + String ret = mPreviousLine; + mPreviousLine = null; + return ret; + } + } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { + if (builder != null) { + // See Section 5.8.1 of RFC 2425 (MIME-DIR document). + // Following is the excerpts from it. + // + // DESCRIPTION:This is a long description that exists on a long line. + // + // Can be represented as: + // + // DESCRIPTION:This is a long description + // that exists on a long line. + // + // It could also be represented as: + // + // DESCRIPTION:This is a long descrip + // tion that exists o + // n a long line. + builder.append(line.substring(1)); + } else if (mPreviousLine != null) { + builder = new StringBuilder(); + builder.append(mPreviousLine); + mPreviousLine = null; + builder.append(line.substring(1)); + } else { + throw new VCardException("Space exists at the beginning of the line"); + } + } else { + if (mPreviousLine == null) { + mPreviousLine = line; + if (builder != null) { + return builder.toString(); + } + } else { + String ret = mPreviousLine; + mPreviousLine = line; + return ret; + } + } + } + } + + + /** + * vcard = [group "."] "BEGIN" ":" "VCARD" 1*CRLF + * 1*(contentline) + * ;A vCard object MUST include the VERSION, FN and N types. + * [group "."] "END" ":" "VCARD" 1*CRLF + */ + @Override + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + return super.readBeginVCard(allowGarbage); + } + + @Override + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + // TODO: vCard 3.0 supports group. + super.readEndVCard(useCache, allowGarbage); + } + + /** + * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not. + */ + @Override + protected void handleParams(String params) throws VCardException { + try { + super.handleParams(params); + } catch (VCardException e) { + // maybe IANA type + String[] strArray = params.split("=", 2); + if (strArray.length == 2) { + handleAnyParam(strArray[0], strArray[1]); + } else { + // Must not come here in the current implementation. + throw new VCardException( + "Unknown params value: " + params); + } + } + } + + @Override + protected void handleAnyParam(String paramName, String paramValue) { + // vCard 3.0 accept comma-separated multiple values, but + // current PropertyNode does not accept it. + // For now, we do not split the values. + // + // TODO: fix this. + super.handleAnyParam(paramName, paramValue); + } + + /** + * vCard 3.0 defines + * + * param = param-name "=" param-value *("," param-value) + * param-name = iana-token / x-name + * param-value = ptext / quoted-string + * quoted-string = DQUOTE QSAFE-CHAR DQUOTE + */ + @Override + protected void handleType(String ptypevalues) { + String[] ptypeArray = ptypevalues.split(","); + mBuilder.propertyParamType("TYPE"); + for (String value : ptypeArray) { + int length = value.length(); + if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + mBuilder.propertyParamValue(value.substring(1, value.length() - 1)); + } else { + mBuilder.propertyParamValue(value); + } + } + } + + @Override + protected void handleAgent(String propertyValue) { + // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.0. + // + // e.g. + // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n + // TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n + // ET:jfriday@host.com\nEND:VCARD\n + // + // TODO: fix this. + // + // issue: + // vCard 3.0 also allows this as an example. + // + // AGENT;VALUE=uri: + // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com + // + // This is not VCARD. Should we support this? + // throw new VCardException("AGENT in vCard 3.0 is not supported yet."); + if (!mEmittedAgentWarning) { + Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); + mEmittedAgentWarning = true; + } + // Just ignore the line for now, since we cannot know how to handle it... + } + + /** + * vCard 3.0 does not require two CRLF at the last of BASE64 data. + * It only requires that data should be MIME-encoded. + */ + @Override + protected String getBase64(String firstString) throws IOException, VCardException { + StringBuilder builder = new StringBuilder(); + builder.append(firstString); + + while (true) { + String line = getLine(); + if (line == null) { + throw new VCardException( + "File ended during parsing BASE64 binary"); + } + if (line.length() == 0) { + break; + } else if (!line.startsWith(" ") && !line.startsWith("\t")) { + mPreviousLine = line; + break; + } + builder.append(line); + } + + return builder.toString(); + } + + /** + * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") + * ; \\ encodes \, \n or \N encodes newline + * ; \; encodes ;, \, encodes , + * + * Note: Apple escape ':' into '\:' while does not escape '\' + */ + @Override + protected String maybeUnescapeText(String text) { + StringBuilder builder = new StringBuilder(); + int length = text.length(); + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + if (ch == '\\' && i < length - 1) { + char next_ch = text.charAt(++i); + if (next_ch == 'n' || next_ch == 'N') { + builder.append("\n"); + } else { + builder.append(next_ch); + } + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + @Override + protected String maybeUnescapeCharacter(char ch) { + if (ch == 'n' || ch == 'N') { + return "\n"; + } else { + return String.valueOf(ch); + } + } +} diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java new file mode 100644 index 0000000..7e2be2b --- /dev/null +++ b/core/java/android/pim/vcard/VCardSourceDetector.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class which tries to detects the source of the vCard from its properties. + * Currently this implementation is very premature. + * @hide + */ +public class VCardSourceDetector implements VCardBuilder { + // Should only be used in package. + static final int TYPE_UNKNOWN = 0; + static final int TYPE_APPLE = 1; + static final int TYPE_JAPANESE_MOBILE_PHONE = 2; // Used in Japanese mobile phones. + static final int TYPE_FOMA = 3; // Used in some Japanese FOMA mobile phones. + static final int TYPE_WINDOWS_MOBILE_JP = 4; + // TODO: Excel, etc. + + private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( + "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", + "X-ABADR", "X-ABUID")); + + private static Set<String> JAPANESE_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( + "X-GNO", "X-GN", "X-REDUCTION")); + + private static Set<String> WINDOWS_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( + "X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC")); + + // Note: these signes appears before the signs of the other type (e.g. "X-GN"). + // In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES. + private static Set<String> FOMA_SIGNS = new HashSet<String>(Arrays.asList( + "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", + "X-SD-DESCRIPTION")); + private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; + + private int mType = TYPE_UNKNOWN; + // Some mobile phones (like FOMA) tells us the charset of the data. + private boolean mNeedParseSpecifiedCharset; + private String mSpecifiedCharset; + + public void start() { + } + + public void end() { + } + + public void startRecord(String type) { + } + + public void startProperty() { + mNeedParseSpecifiedCharset = false; + } + + public void endProperty() { + } + + public void endRecord() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { + mType = TYPE_FOMA; + mNeedParseSpecifiedCharset = true; + return; + } + if (mType != TYPE_UNKNOWN) { + return; + } + if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { + mType = TYPE_WINDOWS_MOBILE_JP; + } else if (FOMA_SIGNS.contains(name)) { + mType = TYPE_FOMA; + } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { + mType = TYPE_JAPANESE_MOBILE_PHONE; + } else if (APPLE_SIGNS.contains(name)) { + mType = TYPE_APPLE; + } + } + + public void propertyParamType(String type) { + } + + public void propertyParamValue(String value) { + } + + public void propertyValues(List<String> values) { + if (mNeedParseSpecifiedCharset && values.size() > 0) { + mSpecifiedCharset = values.get(0); + } + } + + int getType() { + return mType; + } + + /** + * Return charset String guessed from the source's properties. + * This method must be called after parsing target file(s). + * @return Charset String. Null is returned if guessing the source fails. + */ + public String getEstimatedCharset() { + if (mSpecifiedCharset != null) { + return mSpecifiedCharset; + } + switch (mType) { + case TYPE_WINDOWS_MOBILE_JP: + case TYPE_FOMA: + case TYPE_JAPANESE_MOBILE_PHONE: + return "SHIFT_JIS"; + case TYPE_APPLE: + return "UTF-8"; + default: + return null; + } + } +} diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java new file mode 100644 index 0000000..dd44288 --- /dev/null +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard; + +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Utilities for VCard handling codes. + */ +public class VCardUtils { + /* + * TODO: some of methods in this class should be placed to the more appropriate place... + */ + + // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is + // converted to two attribute Strings. These only contain some minor fields valid in both + // vCard and current (as of 2009-08-07) Contacts structure. + private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; + private static final Set<String> sPhoneTypesSetUnknownToContacts; + + private static final Map<String, Integer> sKnownPhoneTypesMap_StoI; + + static { + sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); + sKnownPhoneTypesMap_StoI = new HashMap<String, Integer>(); + + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.ATTR_TYPE_CAR); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.ATTR_TYPE_PAGER); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.ATTR_TYPE_ISDN); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_ISDN, Phone.TYPE_ISDN); + + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CELL, Phone.TYPE_MOBILE); + + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypesMap_StoI.put( + Constants.ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TELEX, Phone.TYPE_TELEX); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TTY_TDD, Phone.TYPE_TTY_TDD); + sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_ASSISTANT, Phone.TYPE_ASSISTANT); + + sPhoneTypesSetUnknownToContacts = new HashSet<String>(); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MODEM); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MSG); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_BBS); + sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_VIDEO); + } + + public static String getPhoneAttributeString(Integer type) { + return sKnownPhoneTypesMap_ItoS.get(type); + } + + /** + * Returns Interger when the given types can be parsed as known type. Returns String object + * when not, which should be set to label. + */ + public static Object getPhoneTypeFromStrings(Collection<String> types) { + int type = -1; + String label = null; + boolean isFax = false; + boolean hasPref = false; + + if (types != null) { + for (String typeString : types) { + typeString = typeString.toUpperCase(); + if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + hasPref = true; + } else if (typeString.equals(Constants.ATTR_TYPE_FAX)) { + isFax = true; + } else { + if (typeString.startsWith("X-") && type < 0) { + typeString = typeString.substring(2); + } + Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + if (tmp != null) { + type = tmp; + } else if (type < 0) { + type = Phone.TYPE_CUSTOM; + label = typeString; + } + } + } + } + if (type < 0) { + if (hasPref) { + type = Phone.TYPE_MAIN; + } else { + // default to TYPE_HOME + type = Phone.TYPE_HOME; + } + } + if (isFax) { + if (type == Phone.TYPE_HOME) { + type = Phone.TYPE_FAX_HOME; + } else if (type == Phone.TYPE_WORK) { + type = Phone.TYPE_FAX_WORK; + } else if (type == Phone.TYPE_OTHER) { + type = Phone.TYPE_OTHER_FAX; + } + } + if (type == Phone.TYPE_CUSTOM) { + return label; + } else { + return type; + } + } + + public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { + // TODO: check the following. + // - it may violate vCard spec + // - it may contain non-ASCII characters + // + // TODO: use vcardType + return (phoneAttribute.startsWith("X-") || phoneAttribute.startsWith("x-") || + sPhoneTypesSetUnknownToContacts.contains(phoneAttribute)); + } + + public static String[] sortNameElements(int vcardType, + String familyName, String middleName, String givenName) { + String[] list = new String[3]; + switch (VCardConfig.getNameOrderType(vcardType)) { + case VCardConfig.NAME_ORDER_JAPANESE: + // TODO: Should handle Ascii case? + list[0] = familyName; + list[1] = middleName; + list[2] = givenName; + break; + case VCardConfig.NAME_ORDER_EUROPE: + list[0] = middleName; + list[1] = givenName; + list[2] = familyName; + break; + default: + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + break; + } + return list; + } + + public static int getPhoneNumberFormat(final int vcardType) { + if (VCardConfig.isJapaneseDevice(vcardType)) { + return PhoneNumberUtils.FORMAT_JAPAN; + } else { + return PhoneNumberUtils.FORMAT_NANP; + } + } + + /** + * Inserts postal data into the builder object. + * + * Note that the data structure of ContactsContract is different from that defined in vCard. + * So some conversion may be performed in this method. See also + * {{@link #getVCardPostalElements(ContentValues)} + */ + public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, + final ContentProviderOperation.Builder builder, + final ContactStruct.PostalData postalData) { + builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); + + builder.withValue(StructuredPostal.TYPE, postalData.type); + if (postalData.type == StructuredPostal.TYPE_CUSTOM) { + builder.withValue(StructuredPostal.LABEL, postalData.label); + } + + builder.withValue(StructuredPostal.POBOX, postalData.pobox); + // Extended address is dropped since there's no relevant entry in ContactsContract. + builder.withValue(StructuredPostal.STREET, postalData.street); + builder.withValue(StructuredPostal.CITY, postalData.localty); + builder.withValue(StructuredPostal.REGION, postalData.region); + builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); + builder.withValue(StructuredPostal.COUNTRY, postalData.country); + + builder.withValue(StructuredPostal.FORMATTED_ADDRESS, + postalData.getFormattedAddress(vcardType)); + if (postalData.isPrimary) { + builder.withValue(Data.IS_PRIMARY, 1); + } + } + + /** + * Returns String[] containing address information based on vCard spec + * (PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name). + * All String objects are non-null ("" is used when the relevant data is empty). + * + * Note that the data structure of ContactsContract is different from that defined in vCard. + * So some conversion may be performed in this method. See also + * {{@link #insertStructuredPostalDataUsingContactsStruct(int, + * android.content.ContentProviderOperation.Builder, + * android.pim.vcard.ContactStruct.PostalData)} + */ + public static String[] getVCardPostalElements(ContentValues contentValues) { + String[] dataArray = new String[7]; + dataArray[0] = contentValues.getAsString(StructuredPostal.POBOX); + if (dataArray[0] == null) { + dataArray[0] = ""; + } + // Extended addr. There's no relevant data in ContactsContract. + dataArray[1] = ""; + dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); + if (dataArray[2] == null) { + dataArray[2] = ""; + } + // Assume that localty == city + dataArray[3] = contentValues.getAsString(StructuredPostal.CITY); + if (dataArray[3] == null) { + dataArray[3] = ""; + } + String region = contentValues.getAsString(StructuredPostal.REGION); + if (!TextUtils.isEmpty(region)) { + dataArray[4] = region; + } else { + dataArray[4] = ""; + } + dataArray[5] = contentValues.getAsString(StructuredPostal.POSTCODE); + if (dataArray[5] == null) { + dataArray[5] = ""; + } + dataArray[6] = contentValues.getAsString(StructuredPostal.COUNTRY); + if (dataArray[6] == null) { + dataArray[6] = ""; + } + + return dataArray; + } + + public static String constructNameFromElements(int nameOrderType, + String familyName, String middleName, String givenName) { + return constructNameFromElements(nameOrderType, familyName, middleName, givenName, + null, null); + } + + public static String constructNameFromElements(int nameOrderType, + String familyName, String middleName, String givenName, + String prefix, String suffix) { + StringBuilder builder = new StringBuilder(); + String[] nameList = sortNameElements(nameOrderType, + familyName, middleName, givenName); + boolean first = true; + if (!TextUtils.isEmpty(prefix)) { + first = false; + builder.append(prefix); + } + for (String namePart : nameList) { + if (!TextUtils.isEmpty(namePart)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(namePart); + } + } + if (!TextUtils.isEmpty(suffix)) { + if (!first) { + builder.append(' '); + } + builder.append(suffix); + } + return builder.toString(); + } + + public static boolean containsOnlyPrintableAscii(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int length = str.length(); + final int asciiFirst = 0x20; + final int asciiLast = 0x126; + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int c = str.codePointAt(i); + if (c < asciiFirst || asciiLast < c) { + return false; + } + } + return true; + } + + /** + * This is useful when checking the string should be encoded into quoted-printable + * or not, which is required by vCard 2.1. + * See the definition of "7bit" in vCard 2.1 spec for more information. + */ + public static boolean containsOnlyNonCrLfPrintableAscii(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int length = str.length(); + final int asciiFirst = 0x20; + final int asciiLast = 0x126; + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int c = str.codePointAt(i); + if (c < asciiFirst || asciiLast < c || c == '\n' || c == '\r') { + return false; + } + } + return true; + } + + /** + * This is useful since vCard 3.0 often requires the ("X-") properties and groups + * should contain only alphabets, digits, and hyphen. + * + * Note: It is already known some devices (wrongly) outputs properties with characters + * which should not be in the field. One example is "X-GOOGLE TALK". We accept + * such kind of input but must never output it unless the target is very specific + * to the device which is able to parse the malformed input. + */ + public static boolean containsOnlyAlphaDigitHyphen(String str) { + if (TextUtils.isEmpty(str)) { + return true; + } + + final int lowerAlphabetFirst = 0x41; // included ('A') + final int lowerAlphabetLast = 0x5b; // not included ('[') + final int upperAlphabetFirst = 0x61; // included ('a') + final int upperAlphabetLast = 0x7b; // included ('{') + final int digitFirst = 0x30; // included ('0') + final int digitLast = 0x39; // included ('9') + final int hyphen = '-'; + final int length = str.length(); + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int codepoint = str.codePointAt(i); + if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetLast) || + (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetLast) || + (digitFirst <= codepoint && codepoint < digitLast) || + (codepoint == hyphen))) { + return false; + } + } + return true; + } + + // TODO: Replace wth the method in Base64 class. + private static char PAD = '='; + private static final char[] ENCODE64 = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' + }; + + static public String encodeBase64(byte[] data) { + if (data == null) { + return ""; + } + + char[] charBuffer = new char[(data.length + 2) / 3 * 4]; + int position = 0; + int _3byte = 0; + for (int i=0; i<data.length-2; i+=3) { + _3byte = ((data[i] & 0xFF) << 16) + ((data[i+1] & 0xFF) << 8) + (data[i+2] & 0xFF); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; + charBuffer[position++] = ENCODE64[_3byte & 0x3F]; + } + switch(data.length % 3) { + case 1: // [111111][11 0000][0000 00][000000] + _3byte = ((data[data.length-1] & 0xFF) << 16); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = PAD; + charBuffer[position++] = PAD; + break; + case 2: // [111111][11 1111][1111 00][000000] + _3byte = ((data[data.length-2] & 0xFF) << 16) + ((data[data.length-1] & 0xFF) << 8); + charBuffer[position++] = ENCODE64[_3byte >> 18]; + charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; + charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; + charBuffer[position++] = PAD; + break; + } + + return new String(charBuffer); + } + + static public String toHalfWidthString(String orgString) { + if (TextUtils.isEmpty(orgString)) { + return null; + } + StringBuilder builder = new StringBuilder(); + int length = orgString.length(); + for (int i = 0; i < length; i++) { + // All Japanese character is able to be expressed by char. + // Do not need to use String#codepPointAt(). + char ch = orgString.charAt(i); + CharSequence halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); + if (halfWidthText != null) { + builder.append(halfWidthText); + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + private VCardUtils() { + } +} + +/** + * TextUtils especially for Japanese. + * TODO: make this in android.text in the future + */ +class JapaneseUtils { + static private final Map<Character, String> sHalfWidthMap = + new HashMap<Character, String>(); + + static { + // There's no logical mapping rule in Unicode. Sigh. + sHalfWidthMap.put('\u3001', "\uFF64"); + sHalfWidthMap.put('\u3002', "\uFF61"); + sHalfWidthMap.put('\u300C', "\uFF62"); + sHalfWidthMap.put('\u300D', "\uFF63"); + sHalfWidthMap.put('\u301C', "~"); + sHalfWidthMap.put('\u3041', "\uFF67"); + sHalfWidthMap.put('\u3042', "\uFF71"); + sHalfWidthMap.put('\u3043', "\uFF68"); + sHalfWidthMap.put('\u3044', "\uFF72"); + sHalfWidthMap.put('\u3045', "\uFF69"); + sHalfWidthMap.put('\u3046', "\uFF73"); + sHalfWidthMap.put('\u3047', "\uFF6A"); + sHalfWidthMap.put('\u3048', "\uFF74"); + sHalfWidthMap.put('\u3049', "\uFF6B"); + sHalfWidthMap.put('\u304A', "\uFF75"); + sHalfWidthMap.put('\u304B', "\uFF76"); + sHalfWidthMap.put('\u304C', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u304D', "\uFF77"); + sHalfWidthMap.put('\u304E', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u304F', "\uFF78"); + sHalfWidthMap.put('\u3050', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u3051', "\uFF79"); + sHalfWidthMap.put('\u3052', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u3053', "\uFF7A"); + sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u3055', "\uFF7B"); + sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u3057', "\uFF7C"); + sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u3059', "\uFF7D"); + sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u305B', "\uFF7E"); + sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u305D', "\uFF7F"); + sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u305F', "\uFF80"); + sHalfWidthMap.put('\u3060', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u3061', "\uFF81"); + sHalfWidthMap.put('\u3062', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u3063', "\uFF6F"); + sHalfWidthMap.put('\u3064', "\uFF82"); + sHalfWidthMap.put('\u3065', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u3066', "\uFF83"); + sHalfWidthMap.put('\u3067', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u3068', "\uFF84"); + sHalfWidthMap.put('\u3069', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u306A', "\uFF85"); + sHalfWidthMap.put('\u306B', "\uFF86"); + sHalfWidthMap.put('\u306C', "\uFF87"); + sHalfWidthMap.put('\u306D', "\uFF88"); + sHalfWidthMap.put('\u306E', "\uFF89"); + sHalfWidthMap.put('\u306F', "\uFF8A"); + sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u3072', "\uFF8B"); + sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u3075', "\uFF8C"); + sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u3078', "\uFF8D"); + sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u307B', "\uFF8E"); + sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u307E', "\uFF8F"); + sHalfWidthMap.put('\u307F', "\uFF90"); + sHalfWidthMap.put('\u3080', "\uFF91"); + sHalfWidthMap.put('\u3081', "\uFF92"); + sHalfWidthMap.put('\u3082', "\uFF93"); + sHalfWidthMap.put('\u3083', "\uFF6C"); + sHalfWidthMap.put('\u3084', "\uFF94"); + sHalfWidthMap.put('\u3085', "\uFF6D"); + sHalfWidthMap.put('\u3086', "\uFF95"); + sHalfWidthMap.put('\u3087', "\uFF6E"); + sHalfWidthMap.put('\u3088', "\uFF96"); + sHalfWidthMap.put('\u3089', "\uFF97"); + sHalfWidthMap.put('\u308A', "\uFF98"); + sHalfWidthMap.put('\u308B', "\uFF99"); + sHalfWidthMap.put('\u308C', "\uFF9A"); + sHalfWidthMap.put('\u308D', "\uFF9B"); + sHalfWidthMap.put('\u308E', "\uFF9C"); + sHalfWidthMap.put('\u308F', "\uFF9C"); + sHalfWidthMap.put('\u3090', "\uFF72"); + sHalfWidthMap.put('\u3091', "\uFF74"); + sHalfWidthMap.put('\u3092', "\uFF66"); + sHalfWidthMap.put('\u3093', "\uFF9D"); + sHalfWidthMap.put('\u309B', "\uFF9E"); + sHalfWidthMap.put('\u309C', "\uFF9F"); + sHalfWidthMap.put('\u30A1', "\uFF67"); + sHalfWidthMap.put('\u30A2', "\uFF71"); + sHalfWidthMap.put('\u30A3', "\uFF68"); + sHalfWidthMap.put('\u30A4', "\uFF72"); + sHalfWidthMap.put('\u30A5', "\uFF69"); + sHalfWidthMap.put('\u30A6', "\uFF73"); + sHalfWidthMap.put('\u30A7', "\uFF6A"); + sHalfWidthMap.put('\u30A8', "\uFF74"); + sHalfWidthMap.put('\u30A9', "\uFF6B"); + sHalfWidthMap.put('\u30AA', "\uFF75"); + sHalfWidthMap.put('\u30AB', "\uFF76"); + sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u30AD', "\uFF77"); + sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u30AF', "\uFF78"); + sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u30B1', "\uFF79"); + sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u30B3', "\uFF7A"); + sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u30B5', "\uFF7B"); + sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u30B7', "\uFF7C"); + sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u30B9', "\uFF7D"); + sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u30BB', "\uFF7E"); + sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u30BD', "\uFF7F"); + sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u30BF', "\uFF80"); + sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u30C1', "\uFF81"); + sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u30C3', "\uFF6F"); + sHalfWidthMap.put('\u30C4', "\uFF82"); + sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u30C6', "\uFF83"); + sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u30C8', "\uFF84"); + sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u30CA', "\uFF85"); + sHalfWidthMap.put('\u30CB', "\uFF86"); + sHalfWidthMap.put('\u30CC', "\uFF87"); + sHalfWidthMap.put('\u30CD', "\uFF88"); + sHalfWidthMap.put('\u30CE', "\uFF89"); + sHalfWidthMap.put('\u30CF', "\uFF8A"); + sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u30D2', "\uFF8B"); + sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u30D5', "\uFF8C"); + sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u30D8', "\uFF8D"); + sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u30DB', "\uFF8E"); + sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u30DE', "\uFF8F"); + sHalfWidthMap.put('\u30DF', "\uFF90"); + sHalfWidthMap.put('\u30E0', "\uFF91"); + sHalfWidthMap.put('\u30E1', "\uFF92"); + sHalfWidthMap.put('\u30E2', "\uFF93"); + sHalfWidthMap.put('\u30E3', "\uFF6C"); + sHalfWidthMap.put('\u30E4', "\uFF94"); + sHalfWidthMap.put('\u30E5', "\uFF6D"); + sHalfWidthMap.put('\u30E6', "\uFF95"); + sHalfWidthMap.put('\u30E7', "\uFF6E"); + sHalfWidthMap.put('\u30E8', "\uFF96"); + sHalfWidthMap.put('\u30E9', "\uFF97"); + sHalfWidthMap.put('\u30EA', "\uFF98"); + sHalfWidthMap.put('\u30EB', "\uFF99"); + sHalfWidthMap.put('\u30EC', "\uFF9A"); + sHalfWidthMap.put('\u30ED', "\uFF9B"); + sHalfWidthMap.put('\u30EE', "\uFF9C"); + sHalfWidthMap.put('\u30EF', "\uFF9C"); + sHalfWidthMap.put('\u30F0', "\uFF72"); + sHalfWidthMap.put('\u30F1', "\uFF74"); + sHalfWidthMap.put('\u30F2', "\uFF66"); + sHalfWidthMap.put('\u30F3', "\uFF9D"); + sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E"); + sHalfWidthMap.put('\u30F5', "\uFF76"); + sHalfWidthMap.put('\u30F6', "\uFF79"); + sHalfWidthMap.put('\u30FB', "\uFF65"); + sHalfWidthMap.put('\u30FC', "\uFF70"); + sHalfWidthMap.put('\uFF01', "!"); + sHalfWidthMap.put('\uFF02', "\""); + sHalfWidthMap.put('\uFF03', "#"); + sHalfWidthMap.put('\uFF04', "$"); + sHalfWidthMap.put('\uFF05', "%"); + sHalfWidthMap.put('\uFF06', "&"); + sHalfWidthMap.put('\uFF07', "'"); + sHalfWidthMap.put('\uFF08', "("); + sHalfWidthMap.put('\uFF09', ")"); + sHalfWidthMap.put('\uFF0A', "*"); + sHalfWidthMap.put('\uFF0B', "+"); + sHalfWidthMap.put('\uFF0C', ","); + sHalfWidthMap.put('\uFF0D', "-"); + sHalfWidthMap.put('\uFF0E', "."); + sHalfWidthMap.put('\uFF0F', "/"); + sHalfWidthMap.put('\uFF10', "0"); + sHalfWidthMap.put('\uFF11', "1"); + sHalfWidthMap.put('\uFF12', "2"); + sHalfWidthMap.put('\uFF13', "3"); + sHalfWidthMap.put('\uFF14', "4"); + sHalfWidthMap.put('\uFF15', "5"); + sHalfWidthMap.put('\uFF16', "6"); + sHalfWidthMap.put('\uFF17', "7"); + sHalfWidthMap.put('\uFF18', "8"); + sHalfWidthMap.put('\uFF19', "9"); + sHalfWidthMap.put('\uFF1A', ":"); + sHalfWidthMap.put('\uFF1B', ";"); + sHalfWidthMap.put('\uFF1C', "<"); + sHalfWidthMap.put('\uFF1D', "="); + sHalfWidthMap.put('\uFF1E', ">"); + sHalfWidthMap.put('\uFF1F', "?"); + sHalfWidthMap.put('\uFF20', "@"); + sHalfWidthMap.put('\uFF21', "A"); + sHalfWidthMap.put('\uFF22', "B"); + sHalfWidthMap.put('\uFF23', "C"); + sHalfWidthMap.put('\uFF24', "D"); + sHalfWidthMap.put('\uFF25', "E"); + sHalfWidthMap.put('\uFF26', "F"); + sHalfWidthMap.put('\uFF27', "G"); + sHalfWidthMap.put('\uFF28', "H"); + sHalfWidthMap.put('\uFF29', "I"); + sHalfWidthMap.put('\uFF2A', "J"); + sHalfWidthMap.put('\uFF2B', "K"); + sHalfWidthMap.put('\uFF2C', "L"); + sHalfWidthMap.put('\uFF2D', "M"); + sHalfWidthMap.put('\uFF2E', "N"); + sHalfWidthMap.put('\uFF2F', "O"); + sHalfWidthMap.put('\uFF30', "P"); + sHalfWidthMap.put('\uFF31', "Q"); + sHalfWidthMap.put('\uFF32', "R"); + sHalfWidthMap.put('\uFF33', "S"); + sHalfWidthMap.put('\uFF34', "T"); + sHalfWidthMap.put('\uFF35', "U"); + sHalfWidthMap.put('\uFF36', "V"); + sHalfWidthMap.put('\uFF37', "W"); + sHalfWidthMap.put('\uFF38', "X"); + sHalfWidthMap.put('\uFF39', "Y"); + sHalfWidthMap.put('\uFF3A', "Z"); + sHalfWidthMap.put('\uFF3B', "["); + sHalfWidthMap.put('\uFF3C', "\\"); + sHalfWidthMap.put('\uFF3D', "]"); + sHalfWidthMap.put('\uFF3E', "^"); + sHalfWidthMap.put('\uFF3F', "_"); + sHalfWidthMap.put('\uFF41', "a"); + sHalfWidthMap.put('\uFF42', "b"); + sHalfWidthMap.put('\uFF43', "c"); + sHalfWidthMap.put('\uFF44', "d"); + sHalfWidthMap.put('\uFF45', "e"); + sHalfWidthMap.put('\uFF46', "f"); + sHalfWidthMap.put('\uFF47', "g"); + sHalfWidthMap.put('\uFF48', "h"); + sHalfWidthMap.put('\uFF49', "i"); + sHalfWidthMap.put('\uFF4A', "j"); + sHalfWidthMap.put('\uFF4B', "k"); + sHalfWidthMap.put('\uFF4C', "l"); + sHalfWidthMap.put('\uFF4D', "m"); + sHalfWidthMap.put('\uFF4E', "n"); + sHalfWidthMap.put('\uFF4F', "o"); + sHalfWidthMap.put('\uFF50', "p"); + sHalfWidthMap.put('\uFF51', "q"); + sHalfWidthMap.put('\uFF52', "r"); + sHalfWidthMap.put('\uFF53', "s"); + sHalfWidthMap.put('\uFF54', "t"); + sHalfWidthMap.put('\uFF55', "u"); + sHalfWidthMap.put('\uFF56', "v"); + sHalfWidthMap.put('\uFF57', "w"); + sHalfWidthMap.put('\uFF58', "x"); + sHalfWidthMap.put('\uFF59', "y"); + sHalfWidthMap.put('\uFF5A', "z"); + sHalfWidthMap.put('\uFF5B', "{"); + sHalfWidthMap.put('\uFF5C', "|"); + sHalfWidthMap.put('\uFF5D', "}"); + sHalfWidthMap.put('\uFF5E', "~"); + sHalfWidthMap.put('\uFF61', "\uFF61"); + sHalfWidthMap.put('\uFF62', "\uFF62"); + sHalfWidthMap.put('\uFF63', "\uFF63"); + sHalfWidthMap.put('\uFF64', "\uFF64"); + sHalfWidthMap.put('\uFF65', "\uFF65"); + sHalfWidthMap.put('\uFF66', "\uFF66"); + sHalfWidthMap.put('\uFF67', "\uFF67"); + sHalfWidthMap.put('\uFF68', "\uFF68"); + sHalfWidthMap.put('\uFF69', "\uFF69"); + sHalfWidthMap.put('\uFF6A', "\uFF6A"); + sHalfWidthMap.put('\uFF6B', "\uFF6B"); + sHalfWidthMap.put('\uFF6C', "\uFF6C"); + sHalfWidthMap.put('\uFF6D', "\uFF6D"); + sHalfWidthMap.put('\uFF6E', "\uFF6E"); + sHalfWidthMap.put('\uFF6F', "\uFF6F"); + sHalfWidthMap.put('\uFF70', "\uFF70"); + sHalfWidthMap.put('\uFF71', "\uFF71"); + sHalfWidthMap.put('\uFF72', "\uFF72"); + sHalfWidthMap.put('\uFF73', "\uFF73"); + sHalfWidthMap.put('\uFF74', "\uFF74"); + sHalfWidthMap.put('\uFF75', "\uFF75"); + sHalfWidthMap.put('\uFF76', "\uFF76"); + sHalfWidthMap.put('\uFF77', "\uFF77"); + sHalfWidthMap.put('\uFF78', "\uFF78"); + sHalfWidthMap.put('\uFF79', "\uFF79"); + sHalfWidthMap.put('\uFF7A', "\uFF7A"); + sHalfWidthMap.put('\uFF7B', "\uFF7B"); + sHalfWidthMap.put('\uFF7C', "\uFF7C"); + sHalfWidthMap.put('\uFF7D', "\uFF7D"); + sHalfWidthMap.put('\uFF7E', "\uFF7E"); + sHalfWidthMap.put('\uFF7F', "\uFF7F"); + sHalfWidthMap.put('\uFF80', "\uFF80"); + sHalfWidthMap.put('\uFF81', "\uFF81"); + sHalfWidthMap.put('\uFF82', "\uFF82"); + sHalfWidthMap.put('\uFF83', "\uFF83"); + sHalfWidthMap.put('\uFF84', "\uFF84"); + sHalfWidthMap.put('\uFF85', "\uFF85"); + sHalfWidthMap.put('\uFF86', "\uFF86"); + sHalfWidthMap.put('\uFF87', "\uFF87"); + sHalfWidthMap.put('\uFF88', "\uFF88"); + sHalfWidthMap.put('\uFF89', "\uFF89"); + sHalfWidthMap.put('\uFF8A', "\uFF8A"); + sHalfWidthMap.put('\uFF8B', "\uFF8B"); + sHalfWidthMap.put('\uFF8C', "\uFF8C"); + sHalfWidthMap.put('\uFF8D', "\uFF8D"); + sHalfWidthMap.put('\uFF8E', "\uFF8E"); + sHalfWidthMap.put('\uFF8F', "\uFF8F"); + sHalfWidthMap.put('\uFF90', "\uFF90"); + sHalfWidthMap.put('\uFF91', "\uFF91"); + sHalfWidthMap.put('\uFF92', "\uFF92"); + sHalfWidthMap.put('\uFF93', "\uFF93"); + sHalfWidthMap.put('\uFF94', "\uFF94"); + sHalfWidthMap.put('\uFF95', "\uFF95"); + sHalfWidthMap.put('\uFF96', "\uFF96"); + sHalfWidthMap.put('\uFF97', "\uFF97"); + sHalfWidthMap.put('\uFF98', "\uFF98"); + sHalfWidthMap.put('\uFF99', "\uFF99"); + sHalfWidthMap.put('\uFF9A', "\uFF9A"); + sHalfWidthMap.put('\uFF9B', "\uFF9B"); + sHalfWidthMap.put('\uFF9C', "\uFF9C"); + sHalfWidthMap.put('\uFF9D', "\uFF9D"); + sHalfWidthMap.put('\uFF9E', "\uFF9E"); + sHalfWidthMap.put('\uFF9F', "\uFF9F"); + sHalfWidthMap.put('\uFFE5', "\u005C\u005C"); + } + + /** + * Return half-width version of that character if possible. Return null if not possible + * @param ch input character + * @return CharSequence object if the mapping for ch exists. Return null otherwise. + */ + public static CharSequence tryGetHalfWidthText(char ch) { + if (sHalfWidthMap.containsKey(ch)) { + return sHalfWidthMap.get(ch); + } else { + return null; + } + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/exception/VCardException.java b/core/java/android/pim/vcard/exception/VCardException.java new file mode 100644 index 0000000..e557219 --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard.exception; + +public class VCardException extends java.lang.Exception { + /** + * Constructs a VCardException object + */ + public VCardException() { + super(); + } + + /** + * Constructs a VCardException object + * + * @param message the error message + */ + public VCardException(String message) { + super(message); + } + +} diff --git a/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java new file mode 100644 index 0000000..67db62c --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardInvalidCommentLineException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidCommentLineException extends VCardInvalidLineException { + public VCardInvalidCommentLineException() { + super(); + } + + public VCardInvalidCommentLineException(final String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/VCardInvalidLineException.java b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java new file mode 100644 index 0000000..330153e --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardInvalidLineException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard.exception; + +/** + * Thrown when the vCard has some line starting with '#'. In the specification, + * both vCard 2.1 and vCard 3.0 does not allow such line, but some actual exporter emit + * such lines. + */ +public class VCardInvalidLineException extends VCardException { + public VCardInvalidLineException() { + super(); + } + + public VCardInvalidLineException(final String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/VCardNestedException.java b/core/java/android/pim/vcard/exception/VCardNestedException.java new file mode 100644 index 0000000..503c2fb --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardNestedException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard.exception; + +/** + * VCardException thrown when VCard is nested without VCardParser's being notified. + */ +public class VCardNestedException extends VCardNotSupportedException { + public VCardNestedException() { + super(); + } + public VCardNestedException(String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/VCardNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java new file mode 100644 index 0000000..616aa77 --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardNotSupportedException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard.exception; + +/** + * The exception which tells that the input VCard is probably valid from the view of + * specification but not supported in the current framework for now. + * + * This is a kind of a good news from the view of development. + * It may be good to ask users to send a report with the VCard example + * for the future development. + */ +public class VCardNotSupportedException extends VCardException { + public VCardNotSupportedException() { + super(); + } + public VCardNotSupportedException(String message) { + super(message); + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/exception/VCardVersionException.java b/core/java/android/pim/vcard/exception/VCardVersionException.java new file mode 100644 index 0000000..9fe8b7f --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardVersionException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009 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 android.pim.vcard.exception; + +/** + * VCardException used only when the version of the vCard is different. + */ +public class VCardVersionException extends VCardException { + public VCardVersionException() { + super(); + } + public VCardVersionException(String message) { + super(message); + } +} diff --git a/core/java/android/pim/vcard/exception/package.html b/core/java/android/pim/vcard/exception/package.html new file mode 100644 index 0000000..26b8a32 --- /dev/null +++ b/core/java/android/pim/vcard/exception/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +{@hide} +</BODY> +</HTML>
\ No newline at end of file diff --git a/core/java/android/pim/vcard/package.html b/core/java/android/pim/vcard/package.html new file mode 100644 index 0000000..26b8a32 --- /dev/null +++ b/core/java/android/pim/vcard/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +{@hide} +</BODY> +</HTML>
\ No newline at end of file |