Merge "EditSchema parser for ExternalAccountType"
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/AccountType.java
index 7871dbb..654adeb 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/AccountType.java
@@ -32,6 +32,7 @@
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
@@ -82,6 +83,14 @@
*/
private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap();
+ /**
+ * Whether this account type was able to be fully initialized. This may be false if
+ * (for example) the package name associated with the account type could not be found.
+ */
+ public boolean isInitialized() {
+ return true;
+ }
+
public boolean isExtension() {
return false;
}
@@ -234,10 +243,6 @@
*/
abstract public boolean isGroupMembershipEditable();
- abstract public int getHeaderColor(Context context);
-
- abstract public int getSideBarColor(Context context);
-
/**
* {@link Comparator} to sort by {@link DataKind#weight}.
*/
@@ -270,6 +275,11 @@
* Add given {@link DataKind} to list of those provided by this source.
*/
public DataKind addKind(DataKind kind) {
+ if (mMimeKinds.get(kind.mimeType) != null) {
+ // TODO Make it exception.
+ Log.w(TAG, "mime type '" + kind.mimeType + "' is already registered");
+ }
+
kind.resPackageName = this.resPackageName;
this.mKinds.add(kind);
this.mMimeKinds.put(kind.mimeType, kind);
@@ -351,7 +361,7 @@
* {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and
* the column where this field is stored.
*/
- public static class EditField {
+ public static final class EditField {
public String column;
public int titleRes;
public int inputType;
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index cb4e9f5..92323fa 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -411,7 +411,7 @@
Log.d(TAG, "Registering external account type=" + type
+ ", packageName=" + auth.packageName);
accountType = new ExternalAccountType(mContext, auth.packageName, false);
- if (!((ExternalAccountType) accountType).isInitialized()) {
+ if (!accountType.isInitialized()) {
// Skip external account types that couldn't be initialized.
continue;
}
diff --git a/src/com/android/contacts/model/BaseAccountType.java b/src/com/android/contacts/model/BaseAccountType.java
index a685dee..34beca2 100644
--- a/src/com/android/contacts/model/BaseAccountType.java
+++ b/src/com/android/contacts/model/BaseAccountType.java
@@ -17,7 +17,12 @@
package com.android.contacts.model;
import com.android.contacts.R;
+import com.android.contacts.util.DateUtils;
import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
import android.content.ContentValues;
import android.content.Context;
@@ -38,9 +43,18 @@
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.AttributeSet;
+import android.util.Log;
import android.view.inputmethod.EditorInfo;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
public abstract class BaseAccountType extends AccountType {
+ private static final String TAG = "BaseAccountType";
+
protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
protected static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
| EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
@@ -61,6 +75,47 @@
protected static final int FLAGS_SIP_ADDRESS = EditorInfo.TYPE_CLASS_TEXT
| EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same
// basic format as email addresses
+ protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+
+ private interface Tag {
+ static final String DATA_KIND = "DataKind";
+ static final String TYPE = "Type";
+ }
+
+ private interface Attr {
+ static final String MAX_OCCURRENCE = "maxOccurs";
+ static final String DATE_WITH_TIME = "dateWithTime";
+ static final String YEAR_OPTIONAL = "yearOptional";
+ static final String KIND = "kind";
+ static final String TYPE = "type";
+ }
+
+ private interface Weight {
+ static final int NONE = -1;
+ static final int ORGANIZATION = 5;
+ static final int PHONE = 10;
+ static final int EMAIL = 15;
+ static final int IM = 20;
+ static final int STRUCTURED_POSTAL = 25;
+ static final int NOTE = 110;
+ static final int NICKNAME = 115;
+ static final int WEBSITE = 120;
+ static final int SIP_ADDRESS = 130;
+ static final int EVENT = 150;
+ static final int RELATIONSHIP = 160;
+ static final int GROUP_MEMBERSHIP = 999;
+ }
+
+ protected static class DefinitionException extends Exception {
+ public DefinitionException(String message) {
+ super(message);
+ }
+
+ public DefinitionException(String message, Exception inner) {
+ super(message, inner);
+ }
+ }
public BaseAccountType() {
this.accountType = null;
@@ -69,27 +124,27 @@
this.iconRes = R.mipmap.ic_launcher_contacts;
}
- protected EditType buildPhoneType(int type) {
+ protected static EditType buildPhoneType(int type) {
return new EditType(type, Phone.getTypeLabelResource(type));
}
- protected EditType buildEmailType(int type) {
+ protected static EditType buildEmailType(int type) {
return new EditType(type, Email.getTypeLabelResource(type));
}
- protected EditType buildPostalType(int type) {
+ protected static EditType buildPostalType(int type) {
return new EditType(type, StructuredPostal.getTypeLabelResource(type));
}
- protected EditType buildImType(int type) {
+ protected static EditType buildImType(int type) {
return new EditType(type, Im.getProtocolLabelResource(type));
}
- protected EditType buildEventType(int type, boolean yearOptional) {
+ protected static EditType buildEventType(int type, boolean yearOptional) {
return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
}
- protected EditType buildRelationType(int type) {
+ protected static EditType buildRelationType(int type) {
return new EditType(type, Relation.getTypeLabelResource(type));
}
@@ -360,14 +415,6 @@
}
protected DataKind addDataKindSipAddress(Context context) {
- // The icon specified here is the one that gets displayed for
- // "Internet call" items, in the "view contact" UI within the
- // Contacts app.
- //
- // This is independent of the "SIP call" icon that gets
- // displayed in the Quick Contacts widget, which comes from
- // the android:icon attribute of the SIP-related
- // intent-filters in the Phone app's manifest.
DataKind kind = addKind(new DataKind(SipAddress.CONTENT_ITEM_TYPE,
R.string.label_sip_address, 130, true, R.layout.text_fields_editor_view));
@@ -382,8 +429,7 @@
}
protected DataKind addDataKindGroupMembership(Context context) {
- DataKind kind = getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
- kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
+ DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
R.string.groupsLabel, 999, true, -1));
kind.typeOverallMax = 1;
@@ -632,17 +678,857 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0xff7f93bc;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0xffbdc7b8;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return false;
}
+
+ /**
+ * Parses the content of the EditSchema tag in contacts.xml.
+ */
+ protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException, DefinitionException {
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT
+ || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+
+ if (Tag.DATA_KIND.equals(tag)) {
+ for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
+ addKind(kind);
+ }
+ } else {
+ Log.w(TAG, "Skipping unknown tag " + tag);
+ }
+ }
+ }
+
+ // Utility methods to keep code shorter.
+ private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
+ return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
+ }
+
+ private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
+ return attrs.getAttributeIntValue(null, attribute, defaultValue);
+ }
+
+ private static String getAttr(AttributeSet attrs, String attribute) {
+ return attrs.getAttributeValue(null, attribute);
+ }
+
+ // TODO Extract it to its own class, and move all KindBuilders to it as well.
+ private static class KindParser {
+ public static final KindParser INSTANCE = new KindParser();
+
+ private final Map<String, KindBuilder> mBuilders = Maps.newHashMap();
+
+ private KindParser() {
+ addBuilder(new NameKindBuilder());
+ addBuilder(new NicknameKindBuilder());
+ addBuilder(new PhoneKindBuilder());
+ addBuilder(new EmailKindBuilder());
+ addBuilder(new StructuredPostalKindBuilder());
+ addBuilder(new ImKindBuilder());
+ addBuilder(new OrganizationKindBuilder());
+ addBuilder(new PhotoKindBuilder());
+ addBuilder(new NoteKindBuilder());
+ addBuilder(new WebsiteKindBuilder());
+ addBuilder(new SipAddressKindBuilder());
+ addBuilder(new GroupMembershipKindBuilder());
+ addBuilder(new EventKindBuilder());
+ addBuilder(new RelationshipKindBuilder());
+ }
+
+ private void addBuilder(KindBuilder builder) {
+ mBuilders.put(builder.getTagName(), builder);
+ }
+
+ /**
+ * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns
+ * {@link DataKind}s. (Usually just one, but there are three for the "name" kind.)
+ *
+ * This method returns a list, because we need to add 3 kinds for the name data kind.
+ * (structured, display and phonetic)
+ */
+ public List<DataKind> parseDataKindTag(Context context, XmlPullParser parser,
+ AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final String kind = getAttr(attrs, Attr.KIND);
+ final KindBuilder builder = mBuilders.get(kind);
+ if (builder != null) {
+ return builder.parseDataKind(context, parser, attrs);
+ } else {
+ throw new DefinitionException("Undefined data kind '" + kind + "'");
+ }
+ }
+ }
+
+ private static abstract class KindBuilder {
+
+ public abstract String getTagName();
+
+ /**
+ * DataKind tag parser specific to each kind. Subclasses must implement it.
+ */
+ public abstract List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException, IOException;
+
+ /**
+ * Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind
+ * tag.
+ */
+ protected final DataKind newDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs, boolean isPseudo, String mimeType, String typeColumn,
+ int titleRes, int weight, int editorLayoutResourceId,
+ StringInflater actionHeader, StringInflater actionBody)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Adding DataKind: " + mimeType);
+ }
+
+ final DataKind kind = new DataKind(mimeType, titleRes, weight, true,
+ editorLayoutResourceId);
+ kind.typeColumn = typeColumn;
+ kind.actionHeader = actionHeader;
+ kind.actionBody = actionBody;
+ kind.fieldList = Lists.newArrayList();
+
+ kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ // Handle "types".
+ // If a kind has the type column, contacts.xml must have at least one type definition.
+ // Otherwise, it mustn't have a type definition.
+ //
+ // If it's a pseudo data kind (== data kind that doesn't have the corresponding
+ // DataKind tag in the XML), we just skip this process.
+ if (!isPseudo) {
+ if (kind.typeColumn != null) {
+ // Parse and add types.
+ kind.typeList = Lists.newArrayList();
+ parseTypes(context, parser, attrs, kind, true);
+ if (kind.typeList.size() == 0) {
+ throw new DefinitionException(
+ "Kind " + kind.mimeType + " must have at least one type");
+ }
+ } else {
+ // Make sure it has no types.
+ parseTypes(context, parser, attrs, kind, false /* can't have types */);
+ }
+ }
+
+ return kind;
+ }
+
+ /**
+ * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds
+ * them to the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type,
+ * so throws {@link DefinitionException}.
+ */
+ private void parseTypes(Context context, XmlPullParser parser, AttributeSet attrs,
+ DataKind kind, boolean canHaveTypes)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT
+ || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+ if (Tag.TYPE.equals(tag)) {
+ if (canHaveTypes) {
+ kind.typeList.add(parseTypeTag(parser, attrs, kind));
+ } else {
+ throw new DefinitionException(
+ "Kind " + kind.mimeType + " can't have types");
+ }
+ } else {
+ throw new DefinitionException("Unknown tag: " + tag);
+ }
+ }
+ }
+
+ /**
+ * Parses a single Type element and returns an {@link EditType} built from it. Uses
+ * {@link #buildEditTypeForTypeTag} defined in subclasses to actually build an
+ * {@link EditType}.
+ */
+ private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
+ throws DefinitionException {
+
+ final String typeName = getAttr(attrs, Attr.TYPE);
+
+ final EditType et = buildEditTypeForTypeTag(attrs, typeName);
+ if (et == null) {
+ throw new DefinitionException(
+ "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
+ }
+ et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ return et;
+ }
+
+ /**
+ * Returns an {@link EditType} for the given "type". Subclasses may optionally use
+ * the attributes in the tag to set optional values.
+ * (e.g. "yearOptional" for the event kind)
+ */
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ return null;
+ }
+
+ protected final void throwIfList(DataKind kind) throws DefinitionException {
+ if (kind.typeOverallMax != 1) {
+ throw new DefinitionException(
+ "Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
+ }
+ }
+ }
+
+ /**
+ * DataKind parser for Name. (structured, display, phonetic)
+ */
+ private static class NameKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "name";
+ }
+
+ private static void checkAttributeTrue(boolean value, String attrName)
+ throws DefinitionException {
+ if (!value) {
+ throw new DefinitionException(attrName + "must be true");
+ }
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+
+ // Build 3 data kinds:
+ // - StructuredName.CONTENT_ITEM_TYPE
+ // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
+ // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
+
+ final boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", true);
+ final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", true);
+ final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", true);
+ final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", true);
+ final boolean supportsPhoneticFamilyName =
+ getAttr(attrs, "supportsPhoneticFamilyName", true);
+ final boolean supportsPhoneticMiddleName =
+ getAttr(attrs, "supportsPhoneticMiddleName", true);
+ final boolean supportsPhoneticGivenName =
+ getAttr(attrs, "supportsPhoneticGivenName", true);
+
+ // For now, every things must be supported.
+ checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
+ checkAttributeTrue(supportsPrefix, "supportsPrefix");
+ checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
+ checkAttributeTrue(supportsSuffix, "supportsSuffix");
+ checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
+ checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
+ checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
+
+ final List<DataKind> kinds = Lists.newArrayList();
+
+ // Structured name
+ final DataKind ks = newDataKind(context, parser, attrs, false,
+ StructuredName.CONTENT_ITEM_TYPE, null, R.string.nameLabelsGroup, Weight.NONE,
+ R.layout.structured_name_editor_view,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ throwIfList(ks);
+ kinds.add(ks);
+
+
+ // Note about setLongForm/setShortForm below.
+ // We need to set this only when the type supports display name. (=supportsDisplayName)
+ // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
+ // "optional".
+
+ ks.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, R.string.full_name,
+ FLAGS_PERSON_NAME));
+ ks.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ ks.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ ks.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ ks.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ ks.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ ks.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+ R.string.name_phonetic_family, FLAGS_PHONETIC));
+ ks.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+ R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ ks.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+ R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ // Display name
+ final DataKind kd = newDataKind(context, parser, attrs, true,
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, null,
+ R.string.nameLabelsGroup, Weight.NONE, R.layout.text_fields_editor_view,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kinds.add(kd);
+
+ kd.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
+ R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
+
+ if (!displayOrderPrimary) {
+ kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ } else {
+ kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+ FLAGS_PERSON_NAME).setLongForm(true));
+ }
+
+ // Phonetic name
+ final DataKind kp = newDataKind(context, parser, attrs, true,
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, null,
+ R.string.name_phonetic, Weight.NONE, R.layout.phonetic_name_editor_view,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kinds.add(kp);
+
+ kp.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+ R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
+ if (!displayOrderPrimary) {
+ kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+ R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
+ kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+ R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
+ kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+ R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
+ } else {
+ kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+ R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
+ kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+ R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
+ kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+ R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
+ }
+ return kinds;
+ }
+ }
+
+ private static class NicknameKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "nickname";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Nickname.CONTENT_ITEM_TYPE, null, R.string.nicknameLabelsGroup, Weight.NICKNAME,
+ R.layout.text_fields_editor_view,
+ new SimpleInflater(R.string.nicknameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+ FLAGS_PERSON_NAME));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ throwIfList(kind);
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ private static class PhoneKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "phone";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Phone.CONTENT_ITEM_TYPE, Phone.TYPE, R.string.phoneLabelsGroup, Weight.PHONE,
+ R.layout.text_fields_editor_view,
+ new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER));
+
+ kind.iconAltRes = R.drawable.ic_text_holo_light;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionAltHeader = new PhoneActionAltInflater();
+
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return Lists.newArrayList(kind);
+ }
+
+ /** Just to avoid line-wrapping... */
+ protected static EditType build(int type, boolean secondary) {
+ return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("home".equals(type)) return build(Phone.TYPE_HOME, false);
+ if ("mobile".equals(type)) return build(Phone.TYPE_MOBILE, false);
+ if ("work".equals(type)) return build(Phone.TYPE_WORK, false);
+ if ("fax_work".equals(type)) return build(Phone.TYPE_FAX_WORK, true);
+ if ("fax_home".equals(type)) return build(Phone.TYPE_FAX_HOME, true);
+ if ("pager".equals(type)) return build(Phone.TYPE_PAGER, true);
+ if ("other".equals(type)) return build(Phone.TYPE_OTHER, false);
+ if ("callback".equals(type)) return build(Phone.TYPE_CALLBACK, true);
+ if ("car".equals(type)) return build(Phone.TYPE_CAR, true);
+ if ("company_main".equals(type)) return build(Phone.TYPE_COMPANY_MAIN, true);
+ if ("isdn".equals(type)) return build(Phone.TYPE_ISDN, true);
+ if ("main".equals(type)) return build(Phone.TYPE_MAIN, true);
+ if ("other_fax".equals(type)) return build(Phone.TYPE_OTHER_FAX, true);
+ if ("radio".equals(type)) return build(Phone.TYPE_RADIO, true);
+ if ("telex".equals(type)) return build(Phone.TYPE_TELEX, true);
+ if ("tty_tdd".equals(type)) return build(Phone.TYPE_TTY_TDD, true);
+ if ("work_mobile".equals(type)) return build(Phone.TYPE_WORK_MOBILE, true);
+ if ("work_pager".equals(type)) return build(Phone.TYPE_WORK_PAGER, true);
+
+ // Note "assistant" used to be a custom column for the fallback type, but not anymore.
+ if ("assistant".equals(type)) return build(Phone.TYPE_ASSISTANT, true);
+ if ("mms".equals(type)) return build(Phone.TYPE_MMS, true);
+ if ("custom".equals(type)) {
+ return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class EmailKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "email";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Email.CONTENT_ITEM_TYPE, Email.TYPE, R.string.emailLabelsGroup, Weight.EMAIL,
+ R.layout.text_fields_editor_view,
+ new EmailActionInflater(), new SimpleInflater(Email.DATA));
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return Lists.newArrayList(kind);
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) return buildEmailType(Email.TYPE_HOME);
+ if ("work".equals(type)) return buildEmailType(Email.TYPE_WORK);
+ if ("other".equals(type)) return buildEmailType(Email.TYPE_OTHER);
+ if ("mobile".equals(type)) return buildEmailType(Email.TYPE_MOBILE);
+ if ("custom".equals(type)) {
+ return buildEmailType(Email.TYPE_CUSTOM)
+ .setSecondary(true).setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class StructuredPostalKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "postal";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE,
+ R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL,
+ R.layout.text_fields_editor_view, new PostalActionInflater(),
+ new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
+
+ if (getAttr(attrs, "needsStructured", false)) {
+ if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
+ // Japanese order
+ kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+ R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+ kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+ R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.REGION,
+ R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY,
+ R.string.postal_city,FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.STREET,
+ R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ // Generic order
+ kind.fieldList.add(new EditField(StructuredPostal.STREET,
+ R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY,
+ R.string.postal_city,FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.REGION,
+ R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+ R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+ R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+ }
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
+ FLAGS_POSTAL));
+ }
+
+ return Lists.newArrayList(kind);
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) return buildPostalType(StructuredPostal.TYPE_HOME);
+ if ("work".equals(type)) return buildPostalType(StructuredPostal.TYPE_WORK);
+ if ("other".equals(type)) return buildPostalType(StructuredPostal.TYPE_OTHER);
+ if ("custom".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true).setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class ImKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "im";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+
+ // IM is special:
+ // - It uses "protocol" as the custom label field
+ // - Its TYPE is fixed to TYPE_OTHER
+
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Im.CONTENT_ITEM_TYPE, Im.PROTOCOL, R.string.imLabelsGroup, Weight.IM,
+ R.layout.text_fields_editor_view,
+ new ImActionInflater(), new SimpleInflater(Im.DATA) // header / action
+ );
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ return Lists.newArrayList(kind);
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("aim".equals(type)) return buildImType(Im.PROTOCOL_AIM);
+ if ("msn".equals(type)) return buildImType(Im.PROTOCOL_MSN);
+ if ("yahoo".equals(type)) return buildImType(Im.PROTOCOL_YAHOO);
+ if ("skype".equals(type)) return buildImType(Im.PROTOCOL_SKYPE);
+ if ("qq".equals(type)) return buildImType(Im.PROTOCOL_QQ);
+ if ("google_talk".equals(type)) return buildImType(Im.PROTOCOL_GOOGLE_TALK);
+ if ("icq".equals(type)) return buildImType(Im.PROTOCOL_ICQ);
+ if ("jabber".equals(type)) return buildImType(Im.PROTOCOL_JABBER);
+ if ("custom".equals(type)) {
+ return buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true)
+ .setCustomColumn(Im.CUSTOM_PROTOCOL);
+ }
+ return null;
+ }
+ }
+
+ private static class OrganizationKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "organization";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Organization.CONTENT_ITEM_TYPE, null, R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION, R.layout.text_fields_editor_view ,
+ new SimpleInflater(Organization.COMPANY),
+ new SimpleInflater(Organization.TITLE));
+
+ kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
+ FLAGS_GENERIC_NAME));
+ kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
+ FLAGS_GENERIC_NAME));
+
+ throwIfList(kind);
+
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ private static class PhotoKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "photo";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Photo.CONTENT_ITEM_TYPE, null /* no type */, -1, Weight.NONE, -1,
+ null, null // no header, no body
+ );
+
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ throwIfList(kind);
+
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ private static class NoteKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "note";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Note.CONTENT_ITEM_TYPE, null, R.string.label_notes, Weight.NOTE,
+ R.layout.text_fields_editor_view,
+ new SimpleInflater(R.string.label_notes), new SimpleInflater(Note.NOTE));
+
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ throwIfList(kind);
+
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ private static class WebsiteKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "website";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Website.CONTENT_ITEM_TYPE, null, R.string.websiteLabelsGroup, Weight.WEBSITE,
+ R.layout.text_fields_editor_view,
+ new SimpleInflater(R.string.websiteLabelsGroup),
+ new SimpleInflater(Website.URL));
+
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup,
+ FLAGS_WEBSITE));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ private static class SipAddressKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "sip_address";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ SipAddress.CONTENT_ITEM_TYPE, null, R.string.label_sip_address,
+ Weight.SIP_ADDRESS, R.layout.text_fields_editor_view,
+ new SimpleInflater(R.string.label_sip_address),
+ new SimpleInflater(SipAddress.SIP_ADDRESS));
+
+ kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
+ R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+ throwIfList(kind);
+
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ private static class GroupMembershipKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "group_membership";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ GroupMembership.CONTENT_ITEM_TYPE, null,
+ R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, -1, null, null);
+
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+
+ throwIfList(kind);
+
+ return Lists.newArrayList(kind);
+ }
+ }
+
+ /**
+ * Event DataKind parser.
+ *
+ * Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
+ */
+ private static class EventKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "event";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Event.CONTENT_ITEM_TYPE, Event.TYPE, R.string.eventLabelsGroup, Weight.EVENT,
+ R.layout.event_field_editor_view,
+ new EventActionInflater(), new SimpleInflater(Event.START_DATE));
+
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
+ kind.dateFormatWithoutYear = DateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
+ kind.dateFormatWithYear = DateUtils.DATE_AND_TIME_FORMAT;
+ } else {
+ kind.dateFormatWithoutYear = DateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = DateUtils.FULL_DATE_FORMAT;
+ }
+
+ return Lists.newArrayList(kind);
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
+
+ if ("birthday".equals(type)) {
+ return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
+ }
+ if ("anniversary".equals(type)) return buildEventType(Event.TYPE_ANNIVERSARY, yo);
+ if ("other".equals(type)) return buildEventType(Event.TYPE_OTHER, yo);
+ if ("custom".equals(type)) {
+ return buildEventType(Event.TYPE_CUSTOM, yo)
+ .setSecondary(true).setCustomColumn(Event.LABEL);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Relationship DataKind parser.
+ *
+ * Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
+ * now.
+ */
+ private static class RelationshipKindBuilder extends KindBuilder {
+ @Override
+ public String getTagName() {
+ return "relationship";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+ AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+ IOException {
+ final DataKind kind = newDataKind(context, parser, attrs, false,
+ Relation.CONTENT_ITEM_TYPE, Relation.TYPE,
+ R.string.relationLabelsGroup, Weight.RELATIONSHIP,
+ R.layout.text_fields_editor_view,
+ new RelationActionInflater(), new SimpleInflater(Relation.NAME));
+
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup,
+ FLAGS_RELATION));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ return Lists.newArrayList(kind);
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("assistant".equals(type)) return buildRelationType(Relation.TYPE_ASSISTANT);
+ if ("brother".equals(type)) return buildRelationType(Relation.TYPE_BROTHER);
+ if ("child".equals(type)) return buildRelationType(Relation.TYPE_CHILD);
+ if ("domestic_partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
+ }
+ if ("father".equals(type)) return buildRelationType(Relation.TYPE_FATHER);
+ if ("friend".equals(type)) return buildRelationType(Relation.TYPE_FRIEND);
+ if ("manager".equals(type)) return buildRelationType(Relation.TYPE_MANAGER);
+ if ("mother".equals(type)) return buildRelationType(Relation.TYPE_MOTHER);
+ if ("parent".equals(type)) return buildRelationType(Relation.TYPE_PARENT);
+ if ("partner".equals(type)) return buildRelationType(Relation.TYPE_PARTNER);
+ if ("referred_by".equals(type)) return buildRelationType(Relation.TYPE_REFERRED_BY);
+ if ("relative".equals(type)) return buildRelationType(Relation.TYPE_RELATIVE);
+ if ("sister".equals(type)) return buildRelationType(Relation.TYPE_SISTER);
+ if ("spouse".equals(type)) return buildRelationType(Relation.TYPE_SPOUSE);
+ if ("custom".equals(type)) {
+ return buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true)
+ .setCustomColumn(Relation.LABEL);
+ }
+ return null;
+ }
+ }
}
diff --git a/src/com/android/contacts/model/DataKind.java b/src/com/android/contacts/model/DataKind.java
index 70f43ab..aaf7bb5 100644
--- a/src/com/android/contacts/model/DataKind.java
+++ b/src/com/android/contacts/model/DataKind.java
@@ -33,7 +33,7 @@
* {@link Data} rows of this kind, including the possible {@link EditType}
* labels and editable {@link EditField}.
*/
-public class DataKind {
+public final class DataKind {
public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName";
public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName";
diff --git a/src/com/android/contacts/model/ExchangeAccountType.java b/src/com/android/contacts/model/ExchangeAccountType.java
index c109fea..bb11cf6 100644
--- a/src/com/android/contacts/model/ExchangeAccountType.java
+++ b/src/com/android/contacts/model/ExchangeAccountType.java
@@ -323,16 +323,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0xffd5ba96;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0xffb58e59;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return true;
}
diff --git a/src/com/android/contacts/model/ExternalAccountType.java b/src/com/android/contacts/model/ExternalAccountType.java
index 73aa773..968993a 100644
--- a/src/com/android/contacts/model/ExternalAccountType.java
+++ b/src/com/android/contacts/model/ExternalAccountType.java
@@ -16,6 +16,8 @@
package com.android.contacts.model;
+import com.android.contacts.R;
+import com.android.contacts.model.BaseAccountType.DefinitionException;
import com.google.common.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParser;
@@ -97,11 +99,12 @@
// Handle unknown sources by searching their package
final PackageManager pm = context.getPackageManager();
+ XmlResourceParser parser = null;
try {
PackageInfo packageInfo = pm.getPackageInfo(resPackageName,
PackageManager.GET_SERVICES|PackageManager.GET_META_DATA);
for (ServiceInfo serviceInfo : packageInfo.services) {
- final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm,
+ parser = serviceInfo.loadXmlMetaData(pm,
METADATA_CONTACTS);
if (parser == null) continue;
inflate(context, parser);
@@ -109,6 +112,17 @@
} catch (NameNotFoundException nnfe) {
// If the package name is not found, we can't initialize this account type.
return;
+ } catch (DefinitionException e) {
+ String message = "Problem reading XML";
+ if (parser != null) {
+ message = message + " in line " + parser.getLineNumber();
+ }
+ Log.e(TAG, message, e);
+ return;
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
}
mExtensionPackageNames = new ArrayList<String>();
@@ -138,10 +152,7 @@
return mIsExtension;
}
- /**
- * Whether this account type was able to be fully initialized. This may be false if
- * (for example) the package name associated with the account type could not be found.
- */
+ @Override
public boolean isInitialized() {
return mInitSuccessful;
}
@@ -212,7 +223,7 @@
* Inflate this {@link AccountType} from the given parser. This may only
* load details matching the publicly-defined schema.
*/
- protected void inflate(Context context, XmlPullParser parser) {
+ protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
@@ -279,9 +290,11 @@
final int depth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
+
String tag = parser.getName();
if (TAG_EDIT_SCHEMA.equals(tag)) {
- parseEditSchema(context, parser);
+ mHasEditSchema = true;
+ parseEditSchema(context, parser, attrs);
} else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
final TypedArray a = context.obtainStyledAttributes(attrs,
android.R.styleable.ContactsDataKind);
@@ -319,53 +332,13 @@
}
}
} catch (XmlPullParserException e) {
- throw new IllegalStateException("Problem reading XML", e);
+ throw new DefinitionException("Problem reading XML", e);
} catch (IOException e) {
- throw new IllegalStateException("Problem reading XML", e);
+ throw new DefinitionException("Problem reading XML", e);
}
}
/**
- * Has to be started while the parser is on the EditSchema tag. Will finish on the end tag
- */
- private void parseEditSchema(Context context, XmlPullParser parser)
- throws XmlPullParserException, IOException {
- // Loop until we left this tag
- final int startingDepth = parser.getDepth();
- int type;
- do {
- type = parser.next();
- } while (!(parser.getDepth() == startingDepth && type == XmlPullParser.END_TAG));
-
- // Just add all defaults for now
- addDataKindStructuredName(context);
- addDataKindDisplayName(context);
- addDataKindPhoneticName(context);
- addDataKindNickname(context);
- addDataKindPhone(context);
- addDataKindEmail(context);
- addDataKindStructuredPostal(context);
- addDataKindIm(context);
- addDataKindOrganization(context);
- addDataKindPhoto(context);
- addDataKindNote(context);
- addDataKindWebsite(context);
- addDataKindSipAddress(context);
-
- mHasEditSchema = true;
- }
-
- @Override
- public int getHeaderColor(Context context) {
- return 0xff6d86b4;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0xff6d86b4;
- }
-
- /**
* Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
* the resource package.
*
diff --git a/src/com/android/contacts/model/FallbackAccountType.java b/src/com/android/contacts/model/FallbackAccountType.java
index 3b56b04..216d6d0 100644
--- a/src/com/android/contacts/model/FallbackAccountType.java
+++ b/src/com/android/contacts/model/FallbackAccountType.java
@@ -47,16 +47,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0xff7f93bc;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0xffbdc7b8;
- }
-
- @Override
public boolean areContactsWritable() {
return true;
}
diff --git a/src/com/android/contacts/model/GoogleAccountType.java b/src/com/android/contacts/model/GoogleAccountType.java
index 094312b..c101602 100644
--- a/src/com/android/contacts/model/GoogleAccountType.java
+++ b/src/com/android/contacts/model/GoogleAccountType.java
@@ -26,14 +26,11 @@
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.view.inputmethod.EditorInfo;
import java.util.List;
public class GoogleAccountType extends BaseAccountType {
public static final String ACCOUNT_TYPE = "com.google";
- protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
private static final List<String> mExtensionPackages =
Lists.newArrayList("com.google.android.apps.plus");
@@ -168,16 +165,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0xff89c2c2;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0xff5bb4b4;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return true;
}
diff --git a/src/com/android/contacts/util/DateUtils.java b/src/com/android/contacts/util/DateUtils.java
index ed9eb94..1ea84a1 100644
--- a/src/com/android/contacts/util/DateUtils.java
+++ b/src/com/android/contacts/util/DateUtils.java
@@ -38,6 +38,8 @@
new SimpleDateFormat("yyyy-MM-dd", Locale.US);
public static final SimpleDateFormat DATE_AND_TIME_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
+ new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
// Variations of ISO 8601 date format. Do not change the order - it does affect the
// result in ambiguous cases.
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 739f5f0..6ea42d6 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -21,6 +21,13 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+ <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+
<application>
<uses-library android:name="android.test.runner" />
<meta-data android:name="com.android.contacts.iconset" android:resource="@xml/iconset" />
@@ -83,6 +90,35 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+
+ <!--
+ Test authenticators/sync adapters.
+
+ The idea is to have multiple account types with various edit schemas. We use subclasses
+ so we could easily add multiple pairs of authenticators and sync adapters.
+ Unfortunately there's an issue with the contacts app which prevents a single apk from
+ having multiple contacts.xml files, so for now we only declare one account type here.
+ -->
+ <service android:name=".testauth.TestAuthenticationService$Basic" android:exported="true">
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/test_basic_authenticator" />
+ </service>
+
+ <service android:name=".testauth.TestSyncService$Basic" android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data
+ android:name="android.content.SyncAdapter"
+ android:resource="@xml/test_basic_syncadapter" />
+ <meta-data
+ android:name="android.provider.CONTACTS_STRUCTURE"
+ android:resource="@xml/test_basic_contacts" />
+ </service>
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 27b9176..19ebde3 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -113,4 +113,6 @@
<string name="attribution_google_talk">Google Talk</string>
<string name="attribution_flicker">Flicker</string>
<string name="attribution_twitter">Twitter</string>
+
+ <string name="authenticator_basic_label">Test adapter</string>
</resources>
diff --git a/tests/res/xml/test_basic_authenticator.xml b/tests/res/xml/test_basic_authenticator.xml
new file mode 100644
index 0000000..ecd100a
--- /dev/null
+++ b/tests/res/xml/test_basic_authenticator.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, 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.
+ */
+-->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="com.android.contacts.tests.authtest.basic"
+ android:icon="@drawable/ic_contact_picture"
+ android:smallIcon="@drawable/ic_contact_picture"
+ android:label="@string/authenticator_basic_label"
+/>
diff --git a/tests/res/xml/test_basic_contacts.xml b/tests/res/xml/test_basic_contacts.xml
new file mode 100644
index 0000000..ad82706
--- /dev/null
+++ b/tests/res/xml/test_basic_contacts.xml
@@ -0,0 +1,296 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, 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.
+ */
+-->
+
+<!--
+ contacts.xml to build "fallback account type" equivalent.
+-->
+
+<ContactsAccountType
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ >
+ <EditSchema
+ >
+ <!--
+ Name:
+ - maxOccurs must be 1
+ - No types.
+
+ - Currently all the supportsXxx attributes must be true, but here's the plan for the
+ future:
+ (There's some hardcoded assumptions in the contact editor, which is one reason
+ for the above restriction)
+
+ - "Family name" and "Given name" must be supported.
+ - All sync adapters must support structured name. "display name only" is not
+ supported.
+ -> Supporting this would require relatively large changes to
+ the contact editor.
+
+ - Fields are decided from the attributes:
+ StructuredName.DISPLAY_NAME if supportsDisplayName == true
+ StructuredName.PREFIX if supportsPrefix == true
+ StructuredName.FAMILY_NAME (always)
+ StructuredName.MIDDLE_NAME if supportsPrefix == true
+ StructuredName.GIVEN_NAME (always)
+ StructuredName.SUFFIX if supportsSuffix == true
+ StructuredName.PHONETIC_FAMILY_NAME if supportsPhoneticFamilyName == true
+ StructuredName.PHONETIC_MIDDLE_NAME if supportsPhoneticMiddleName == true
+ StructuredName.PHONETIC_GIVEN_NAME if supportsPhoneticGivenName == true
+
+ - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME is always added.
+ - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME is added
+ if any of supportsPhoneticXxx == true
+ -->
+ <!-- Fallback/Google definition. Supports all. -->
+ <DataKind kind="name"
+ maxOccurs="1"
+ supportsDisplayName="true"
+ supportsPrefix="true"
+ supportsMiddleName="true"
+ supportsSuffix="true"
+ supportsPhoneticFamilyName="true"
+ supportsPhoneticMiddleName="true"
+ supportsPhoneticGivenName="true"
+ >
+ </DataKind>
+
+ <!-- Exchange definition. No display-name, no phonetic-middle.
+ <DataKind kind="name"
+ supportsDisplayName="false"
+ supportsPrefix="true"
+ supportsMiddleName="true"
+ supportsSuffix="true"
+ supportsPhoneticFamilyName="true"
+ supportsPhoneticMiddleName="false"
+ supportsPhoneticGivenName ="true"
+ >
+ </DataKind>
+ -->
+
+ <!--
+ Photo:
+ - maxOccurs must be 1
+ - No types.
+ -->
+ <DataKind kind="photo" maxOccurs="1" />
+
+ <!--
+ Phone definition.
+ - "is secondary?" is inferred from type.
+ -->
+ <!-- Fallback, Google definition. -->
+ <DataKind kind="phone" >
+ <!-- Note: Google type doesn't have obsolete ones -->
+ <Type type="mobile" />
+ <Type type="work" />
+ <Type type="home" />
+ <Type type="fax_work" />
+ <Type type="fax_home" />
+ <Type type="pager" />
+ <Type type="other" />
+ <Type type="custom"/>
+ <Type type="callback" />
+ <Type type="car" />
+ <Type type="company_main" />
+ <Type type="isdn" />
+ <Type type="main" />
+ <Type type="other_fax" />
+ <Type type="radio" />
+ <Type type="telex" />
+ <Type type="tty_tdd" />
+ <Type type="work_mobile"/>
+ <Type type="work_pager" />
+ <Type type="assistant" />
+ <Type type="mms" />
+ </DataKind>
+
+ <!-- Exchange definition.
+ <DataKind kind="phone" >
+ <Type type="home" maxOccurs="2" />
+ <Type type="mobile" maxOccurs="1" />
+ <Type type="work" maxOccurs="2" />
+ <Type type="fax_work" maxOccurs="1" />
+ <Type type="fax_home" maxOccurs="1" />
+ <Type type="pager" maxOccurs="1" />
+ <Type type="car" maxOccurs="1" />
+ <Type type="company_main" maxOccurs="1" />
+ <Type type="mms" maxOccurs="1" />
+ <Type type="radio" maxOccurs="1" />
+ <Type type="assistant" maxOccurs="1" />
+ </DataKind>
+ -->
+
+ <!--
+ Email
+ -->
+ <!-- Fallback/ Google definition. -->
+ <DataKind kind="email" >
+ <!-- Note: Google type doesn't support some of these. -->
+ <Type type="home" />
+ <Type type="work" />
+ <Type type="other" />
+ <Type type="mobile" />
+ <Type type="custom" />
+ </DataKind>
+
+ <!--
+ Exchange definition.
+ - Same definition as "fallback" except for maxOccurs=3
+ <DataKind kind="email" maxOccurs="3" >
+ <Type type="home" />
+ <Type type="work" />
+ <Type type="other" />
+ <Type type="mobile" />
+ <Type type="custom" />
+ </DataKind>
+ -->
+
+ <!--
+ Nickname
+ - maxOccurs must be 1
+ - No types.
+ -->
+ <DataKind kind="nickname" maxOccurs="1" />
+
+ <!--
+ Im:
+ - The TYPE column always stores Im.TYPE_OTHER (defaultValues is always set)
+ - The user-selected type is stored in Im.PROTOCOL
+ -->
+ <!-- Fallback, Google definition. -->
+ <DataKind kind="im" >
+ <Type type="aim" />
+ <Type type="msn" />
+ <Type type="yahoo" />
+ <Type type="skype" />
+ <Type type="qq" />
+ <Type type="google_talk" />
+ <Type type="icq" />
+ <Type type="jabber" />
+ <Type type="custom" />
+ </DataKind>
+
+ <!-- Exchange definition.
+ <DataKind kind="im" maxOccurs="3" >
+ <Type type="aim" />
+ <Type type="msn" />
+ <Type type="yahoo" />
+ <Type type="skype" />
+ <Type type="qq" />
+ <Type type="google_talk" />
+ <Type type="icq" />
+ <Type type="jabber" />
+ <Type type="custom" />
+ </DataKind>
+ -->
+
+ <!--
+ Postal address.
+ -->
+ <!-- Fallback/Google definition. Not structured. -->
+ <DataKind kind="postal" needsStructured="false" >
+ <Type type="home" />
+ <Type type="work" />
+ <Type type="other" />
+ <Type type="custom" />
+ </DataKind>
+
+ <!-- Exchange definition. Structured.
+ <DataKind kind="postal" needsStructured="true" >
+ <Type type="work" />
+ <Type type="home" />
+ <Type type="other" />
+ </DataKind>
+ -->
+
+ <!--
+ Organization:
+ - Fields are fixed: COMPANY, TITLE
+ - maxOccurs must be 1
+ - No types.
+ -->
+ <DataKind kind="organization" maxOccurs="1" />
+
+ <!--
+ Website:
+ - No types.
+ -->
+ <DataKind kind="website" />
+
+ <!--
+ Below kinds have nothing configurable.
+ - No types are supported.
+ - maxOccurs must be 1
+ -->
+ <DataKind kind="sip_address" maxOccurs="1" />
+ <DataKind kind="note" maxOccurs="1" />
+
+ <!--
+ Google/Exchange supports it, but fallback doesn't.
+ <DataKind kind="group_membership" maxOccurs="1" />
+ -->
+
+ <!--
+ Event
+
+ The parser should be able to handle it, but not tested.
+ -->
+ <!-- Google definition.
+ <DataKind kind="event" dateWithTime="false">
+ <Type type="birthday" maxOccurs="1" yearOptional="true" />
+ <Type type="anniversary" />
+ <Type type="other" />
+ <Type type="custom" />
+ </DataKind>
+ -->
+
+ <!--
+ Exchange definition. dateWithTime is needed only for Exchange.
+ <DataKind kind="event" dateWithTime="true">
+ <Type type="birthday" maxOccurs="1" />
+ </DataKind>
+ -->
+
+ <!--
+ Relationship.
+
+ The parser should be able to handle it, but not tested.
+
+ <DataKind kind="relation" >
+ <Type type="assistant" />
+ <Type type="brother" />
+ <Type type="child" />
+ <Type type="domestic_partner" />
+ <Type type="father" />
+ <Type type="friend" />
+ <Type type="manager" />
+ <Type type="mother" />
+ <Type type="parent" />
+ <Type type="partner" />
+ <Type type="referred_by" />
+ <Type type="relative" />
+ <Type type="sister" />
+ <Type type="spouse" />
+ <Type type="custom" />
+ </DataKind>
+ -->
+
+ </EditSchema>
+
+</ContactsAccountType>
diff --git a/tests/res/xml/test_basic_syncadapter.xml b/tests/res/xml/test_basic_syncadapter.xml
new file mode 100644
index 0000000..fecc0eb
--- /dev/null
+++ b/tests/res/xml/test_basic_syncadapter.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, 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.
+ */
+-->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.contacts"
+ android:accountType="com.android.contacts.tests.authtest.basic"
+ android:supportsUploading="true"
+ android:userVisible="true"
+/>
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index d6f99ce..4db73b3 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -140,16 +140,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0xffffff;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return false;
}
diff --git a/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
index b2cb39c..8007aee 100644
--- a/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
+++ b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
@@ -313,16 +313,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return true;
}
diff --git a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
index 09902a3..6f5bbf2 100644
--- a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
@@ -183,16 +183,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return false;
}
diff --git a/tests/src/com/android/contacts/model/AccountTypeTest.java b/tests/src/com/android/contacts/model/AccountTypeTest.java
index 42fe200..8bc7429 100644
--- a/tests/src/com/android/contacts/model/AccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeTest.java
@@ -73,14 +73,6 @@
return externalResID;
}
- @Override public int getHeaderColor(Context context) {
- return 0;
- }
-
- @Override public int getSideBarColor(Context context) {
- return 0;
- }
-
@Override public boolean isGroupMembershipEditable() {
return false;
}
@@ -128,16 +120,6 @@
}
@Override
- public int getHeaderColor(Context context) {
- return 0;
- }
-
- @Override
- public int getSideBarColor(Context context) {
- return 0;
- }
-
- @Override
public boolean isGroupMembershipEditable() {
return false;
}
diff --git a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
index eb8c059..15a1320 100644
--- a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
@@ -19,6 +19,16 @@
import com.android.contacts.tests.R;
import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
@@ -52,4 +62,26 @@
assertEquals(R.string.test_string, ExternalAccountType.resolveExternalResId(c,
"@string/test_string", packageName, ""));
}
+
+ public void testEditSchema() {
+ final ExternalAccountType type = new ExternalAccountType(getContext(),
+ getTestContext().getPackageName(), false);
+
+ assertTrue(type.isInitialized());
+
+ assertNotNull(type.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME));
+ assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME));
+ assertNotNull(type.getKindForMimetype(Email.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(Im.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(Organization.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(Note.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(Website.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(SipAddress.CONTENT_ITEM_TYPE));
+ assertNotNull(type.getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE));
+
+ // TODO Write more extensive check -- compare to FallbackAccountType?
+ }
}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestAuthenticationService.java b/tests/src/com/android/contacts/tests/testauth/TestAuthenticationService.java
new file mode 100644
index 0000000..84f3f0f
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestAuthenticationService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.tests.testauth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+public abstract class TestAuthenticationService extends Service {
+
+ private TestAuthenticator mAuthenticator;
+
+ @Override
+ public void onCreate() {
+ Log.v(TestauthConstants.LOG_TAG, this + " Service started.");
+ mAuthenticator = new TestAuthenticator(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.v(TestauthConstants.LOG_TAG, this + " Service stopped.");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.v(TestauthConstants.LOG_TAG, this + " getBinder() intent=" + intent);
+ return mAuthenticator.getIBinder();
+ }
+
+ public static class Basic extends TestAuthenticationService {
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestAuthenticator.java b/tests/src/com/android/contacts/tests/testauth/TestAuthenticator.java
new file mode 100644
index 0000000..97e2e4d
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestAuthenticator.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.tests.testauth;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * Simple authenticator. It has no "login" dialogs/activities. When you add a new account, it'll
+ * just create a new account with a unique name.
+ */
+class TestAuthenticator extends AbstractAccountAuthenticator {
+ private static final String PASSWORD = "xxx"; // any string will do.
+
+ // To remember the last user-ID.
+ private static final String PREF_KEY_LAST_USER_ID = "TestAuthenticator.PREF_KEY_LAST_USER_ID";
+
+ private final Context mContext;
+
+ public TestAuthenticator(Context context) {
+ super(context);
+ mContext = context.getApplicationContext();
+ }
+
+ /**
+ * @return a new, unique username.
+ */
+ private String newUniqueUserName() {
+ final SharedPreferences prefs =
+ PreferenceManager.getDefaultSharedPreferences(mContext);
+ final int nextId = prefs.getInt(PREF_KEY_LAST_USER_ID, 0) + 1;
+ prefs.edit().putInt(PREF_KEY_LAST_USER_ID, nextId).apply();
+
+ return "User-" + nextId;
+ }
+
+ /**
+ * Create a new account with the name generated by {@link #newUniqueUserName()}.
+ */
+ @Override
+ public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+ String authTokenType, String[] requiredFeatures, Bundle options) {
+ Log.v(TestauthConstants.LOG_TAG, "addAccount() type=" + accountType);
+ final Bundle bundle = new Bundle();
+
+ final Account account = new Account(newUniqueUserName(), accountType);
+
+ // Create an account.
+ AccountManager.get(mContext).addAccountExplicitly(account, PASSWORD, null);
+
+ // And return it.
+ bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+ bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+ return bundle;
+ }
+
+ /**
+ * Just return the user name as the authtoken.
+ */
+ @Override
+ public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle loginOptions) {
+ Log.v(TestauthConstants.LOG_TAG, "getAuthToken() account=" + account);
+ final Bundle bundle = new Bundle();
+ bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+ bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+ bundle.putString(AccountManager.KEY_AUTHTOKEN, account.name);
+
+ return bundle;
+ }
+
+ @Override
+ public Bundle confirmCredentials(
+ AccountAuthenticatorResponse response, Account account, Bundle options) {
+ Log.v(TestauthConstants.LOG_TAG, "confirmCredentials()");
+ return null;
+ }
+
+ @Override
+ public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+ Log.v(TestauthConstants.LOG_TAG, "editProperties()");
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getAuthTokenLabel(String authTokenType) {
+ // null means we don't support multiple authToken types
+ Log.v(TestauthConstants.LOG_TAG, "getAuthTokenLabel()");
+ return null;
+ }
+
+ @Override
+ public Bundle hasFeatures(
+ AccountAuthenticatorResponse response, Account account, String[] features) {
+ // This call is used to query whether the Authenticator supports
+ // specific features. We don't expect to get called, so we always
+ // return false (no) for any queries.
+ Log.v(TestauthConstants.LOG_TAG, "hasFeatures()");
+ final Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+ return result;
+ }
+
+ @Override
+ public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
+ String authTokenType, Bundle loginOptions) {
+ Log.v(TestauthConstants.LOG_TAG, "updateCredentials()");
+ return null;
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestSyncAdapter.java b/tests/src/com/android/contacts/tests/testauth/TestSyncAdapter.java
new file mode 100644
index 0000000..9e6fbf8
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestSyncAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.contacts.tests.testauth;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+/**
+ * Simple (minimal) sync adapter.
+ *
+ */
+public class TestSyncAdapter extends AbstractThreadedSyncAdapter {
+ private final AccountManager mAccountManager;
+
+ private final Context mContext;
+
+ public TestSyncAdapter(Context context, boolean autoInitialize) {
+ super(context, autoInitialize);
+ mContext = context.getApplicationContext();
+ mAccountManager = AccountManager.get(mContext);
+ }
+
+ /**
+ * Doesn't actually sync, but sweep up all existing local-only contacts.
+ */
+ @Override
+ public void onPerformSync(Account account, Bundle extras, String authority,
+ ContentProviderClient provider, SyncResult syncResult) {
+ Log.v(TestauthConstants.LOG_TAG, "TestSyncAdapter.onPerformSync() account=" + account);
+
+ // First, claim all local-only contacts, if any.
+ ContentResolver cr = mContext.getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, account.name);
+ values.put(RawContacts.ACCOUNT_TYPE, account.type);
+ final int count = cr.update(RawContacts.CONTENT_URI, values,
+ RawContacts.ACCOUNT_NAME + " IS NULL AND " + RawContacts.ACCOUNT_TYPE + " IS NULL",
+ null);
+ if (count > 0) {
+ Log.v(TestauthConstants.LOG_TAG, "Claimed " + count + " local raw contacts");
+ }
+
+ // TODO: Clear isDirty flag
+ // TODO: Remove isDeleted raw contacts
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestSyncService.java b/tests/src/com/android/contacts/tests/testauth/TestSyncService.java
new file mode 100644
index 0000000..9928777
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestSyncService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.contacts.tests.testauth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public abstract class TestSyncService extends Service {
+
+ private static TestSyncAdapter sSyncAdapter;
+
+ @Override
+ public void onCreate() {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new TestSyncAdapter(getApplicationContext(), true);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+
+ public static class Basic extends TestSyncService {
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestauthConstants.java b/tests/src/com/android/contacts/tests/testauth/TestauthConstants.java
new file mode 100644
index 0000000..717ed35
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestauthConstants.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.tests.testauth;
+
+class TestauthConstants {
+ public static final String LOG_TAG = "Testauth";
+}