EditSchema parser for ExternalAccountType
This also includes:
- Removed getHeaderColor/getSideBarColor from AccountType
- Implemented a test authenticator/sync adapter in the test apk.
This defines a test account type "com.android.contacts.tests.authtest.basic".
We could potentially add more account types to the test apk to test different
edit schema variations, but at this point this is impossible, as
ExternalAccountType doesn't have a way to tell which contacts.xml belongs
to which account type. The current contacts.xml defined here builds
the fallback account type equivalent.
The sync adapter is pretty rudimentary; it doesn't clear the isDirty flag
on modified raw contacts or delete raw contacts with isDeleted set. At
this point this doesn't seem to be necessary to test EditSchema.
Note:
For now it's still not meant to be public API. Right now it's only manually
tested with the edit schema defition in the test apk described above.
Bug 5381810
Change-Id: Ifefdb969b4e08775125924b1366d24effc4db2f2
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 6a438c6..bf11188 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -388,7 +388,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";
+}