diff options
Diffstat (limited to 'java/com/android/contacts/common/model/account/ExternalAccountType.java')
-rw-r--r-- | java/com/android/contacts/common/model/account/ExternalAccountType.java | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/java/com/android/contacts/common/model/account/ExternalAccountType.java b/java/com/android/contacts/common/model/account/ExternalAccountType.java new file mode 100644 index 000000000..aca1f70d2 --- /dev/null +++ b/java/com/android/contacts/common/model/account/ExternalAccountType.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** A general contacts account type descriptor. */ +public class ExternalAccountType extends BaseAccountType { + + private static final String TAG = "ExternalAccountType"; + + private static final String SYNC_META_DATA = "android.content.SyncAdapter"; + + /** + * The metadata name for so-called "contacts.xml". + * + * <p>On LMP and later, we also accept the "alternate" name. This is to allow sync adapters to + * have a contacts.xml without making it visible on older platforms. If you modify this also + * update the corresponding list in ContactsProvider/PhotoPriorityResolver + */ + private static final String[] METADATA_CONTACTS_NAMES = + new String[] { + "android.provider.ALTERNATE_CONTACTS_STRUCTURE", "android.provider.CONTACTS_STRUCTURE" + }; + + private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource"; + private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType"; + private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind"; + private static final String TAG_EDIT_SCHEMA = "EditSchema"; + + private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity"; + private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel"; + private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService"; + private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity"; + private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel"; + private static final String ATTR_DATA_SET = "dataSet"; + private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames"; + + // The following attributes should only be set in non-sync-adapter account types. They allow + // for the account type and resource IDs to be specified without an associated authenticator. + private static final String ATTR_ACCOUNT_TYPE = "accountType"; + private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel"; + private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon"; + + private final boolean mIsExtension; + + private String mEditContactActivityClassName; + private String mCreateContactActivityClassName; + private String mInviteContactActivity; + private String mInviteActionLabelAttribute; + private int mInviteActionLabelResId; + private String mViewContactNotifyService; + private String mViewGroupActivity; + private String mViewGroupLabelAttribute; + private int mViewGroupLabelResId; + private List<String> mExtensionPackageNames; + private String mAccountTypeLabelAttribute; + private String mAccountTypeIconAttribute; + private boolean mHasContactsMetadata; + private boolean mHasEditSchema; + + public ExternalAccountType(Context context, String resPackageName, boolean isExtension) { + this(context, resPackageName, isExtension, null); + } + + /** + * Constructor used for testing to initialize with any arbitrary XML. + * + * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by tests. + * If null, the metadata is loaded from the specified package. + */ + ExternalAccountType( + Context context, + String packageName, + boolean isExtension, + XmlResourceParser injectedMetadata) { + this.mIsExtension = isExtension; + this.resourcePackageName = packageName; + this.syncAdapterPackageName = packageName; + + final XmlResourceParser parser; + if (injectedMetadata == null) { + parser = loadContactsXml(context, packageName); + } else { + parser = injectedMetadata; + } + boolean needLineNumberInErrorLog = true; + try { + if (parser != null) { + inflate(context, parser); + } + + // Done parsing; line number no longer needed in error log. + needLineNumberInErrorLog = false; + if (mHasEditSchema) { + checkKindExists(StructuredName.CONTENT_ITEM_TYPE); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME); + checkKindExists(Photo.CONTENT_ITEM_TYPE); + } else { + // Bring in name and photo from fallback source, which are non-optional + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindPhoto(context); + } + } catch (DefinitionException e) { + final StringBuilder error = new StringBuilder(); + error.append("Problem reading XML"); + if (needLineNumberInErrorLog && (parser != null)) { + error.append(" in line "); + error.append(parser.getLineNumber()); + } + error.append(" for external package "); + error.append(packageName); + + Log.e(TAG, error.toString(), e); + return; + } finally { + if (parser != null) { + parser.close(); + } + } + + mExtensionPackageNames = new ArrayList<String>(); + mInviteActionLabelResId = + resolveExternalResId( + context, + mInviteActionLabelAttribute, + syncAdapterPackageName, + ATTR_INVITE_CONTACT_ACTION_LABEL); + mViewGroupLabelResId = + resolveExternalResId( + context, + mViewGroupLabelAttribute, + syncAdapterPackageName, + ATTR_VIEW_GROUP_ACTION_LABEL); + titleRes = + resolveExternalResId( + context, mAccountTypeLabelAttribute, syncAdapterPackageName, ATTR_ACCOUNT_LABEL); + iconRes = + resolveExternalResId( + context, mAccountTypeIconAttribute, syncAdapterPackageName, ATTR_ACCOUNT_ICON); + + // If we reach this point, the account type has been successfully initialized. + mIsInitialized = true; + } + + /** + * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package. + * + * <p>This method looks through all services in the package that handle sync adapter intents for + * the first one that contains CONTACTS_STRUCTURE metadata. We have to look through all sync + * adapters in the package in case there are contacts and other sync adapters (eg, calendar) in + * the same package. + * + * <p>Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case the + * account type *will* be initialized with minimal configuration. + */ + public static XmlResourceParser loadContactsXml(Context context, String resPackageName) { + final PackageManager pm = context.getPackageManager(); + final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName); + final List<ResolveInfo> intentServices = + pm.queryIntentServices(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); + + if (intentServices != null) { + for (final ResolveInfo resolveInfo : intentServices) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo == null) { + continue; + } + for (String metadataName : METADATA_CONTACTS_NAMES) { + final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, metadataName); + if (parser != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + String.format( + "Metadata loaded from: %s, %s, %s", + serviceInfo.packageName, serviceInfo.name, metadataName)); + } + return parser; + } + } + } + } + + // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. + return null; + } + + /** Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata. */ + public static boolean hasContactsXml(Context context, String resPackageName) { + return loadContactsXml(context, resPackageName) != null; + } + + /** + * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in the + * resource package. + * + * <p>If the argument is in the invalid format or isn't a resource name, it returns -1. + * + * @param context context + * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel" + * @param packageName name of the package containing the resource. + * @param xmlAttributeName attribute name which the resource came from. Used for logging. + */ + @VisibleForTesting + static int resolveExternalResId( + Context context, String resourceName, String packageName, String xmlAttributeName) { + if (TextUtils.isEmpty(resourceName)) { + return -1; // Empty text is okay. + } + if (resourceName.charAt(0) != '@') { + Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'"); + return -1; + } + final String name = resourceName.substring(1); + final Resources res; + try { + res = context.getPackageManager().getResourcesForApplication(packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to load package " + packageName); + return -1; + } + final int resId = res.getIdentifier(name, null, packageName); + if (resId == 0) { + Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName); + return -1; + } + return resId; + } + + private void checkKindExists(String mimeType) throws DefinitionException { + if (getKindForMimetype(mimeType) == null) { + throw new DefinitionException(mimeType + " must be supported"); + } + } + + @Override + public boolean isEmbedded() { + return false; + } + + @Override + public boolean isExtension() { + return mIsExtension; + } + + @Override + public boolean areContactsWritable() { + return mHasEditSchema; + } + + /** Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. */ + public boolean hasContactsMetadata() { + return mHasContactsMetadata; + } + + @Override + public String getEditContactActivityClassName() { + return mEditContactActivityClassName; + } + + @Override + public String getCreateContactActivityClassName() { + return mCreateContactActivityClassName; + } + + @Override + public String getInviteContactActivityClassName() { + return mInviteContactActivity; + } + + @Override + protected int getInviteContactActionResId() { + return mInviteActionLabelResId; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return mViewContactNotifyService; + } + + @Override + public String getViewGroupActivity() { + return mViewGroupActivity; + } + + @Override + protected int getViewGroupLabelResId() { + return mViewGroupLabelResId; + } + + @Override + public List<String> getExtensionPackageNames() { + return mExtensionPackageNames; + } + + /** + * 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) throws DefinitionException { + final AttributeSet attrs = Xml.asAttributeSet(parser); + + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Drain comments and whitespace + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("No start tag found"); + } + + String rootTag = parser.getName(); + if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) + && !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) { + throw new IllegalStateException( + "Top level element must be " + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag); + } + + mHasContactsMetadata = true; + + int attributeCount = parser.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + String attr = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, attr + "=" + value); + } + if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) { + mEditContactActivityClassName = value; + } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) { + mCreateContactActivityClassName = value; + } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) { + mInviteContactActivity = value; + } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) { + mInviteActionLabelAttribute = value; + } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) { + mViewContactNotifyService = value; + } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) { + mViewGroupActivity = value; + } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) { + mViewGroupLabelAttribute = value; + } else if (ATTR_DATA_SET.equals(attr)) { + dataSet = value; + } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) { + mExtensionPackageNames.add(value); + } else if (ATTR_ACCOUNT_TYPE.equals(attr)) { + accountType = value; + } else if (ATTR_ACCOUNT_LABEL.equals(attr)) { + mAccountTypeLabelAttribute = value; + } else if (ATTR_ACCOUNT_ICON.equals(attr)) { + mAccountTypeIconAttribute = value; + } else { + Log.e(TAG, "Unsupported attribute " + attr); + } + } + + // Parse all children kinds + final int startDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > startDepth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) { + continue; // Not a direct child tag + } + + String tag = parser.getName(); + if (TAG_EDIT_SCHEMA.equals(tag)) { + mHasEditSchema = true; + parseEditSchema(context, parser, attrs); + } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) { + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactsDataKind); + final DataKind kind = new DataKind(); + + kind.mimeType = a.getString(R.styleable.ContactsDataKind_android_mimeType); + final String summaryColumn = + a.getString(R.styleable.ContactsDataKind_android_summaryColumn); + if (summaryColumn != null) { + // Inflate a specific column as summary when requested + kind.actionHeader = new SimpleInflater(summaryColumn); + } + final String detailColumn = + a.getString(R.styleable.ContactsDataKind_android_detailColumn); + if (detailColumn != null) { + // Inflate specific column as summary + kind.actionBody = new SimpleInflater(detailColumn); + } + + a.recycle(); + + addKind(kind); + } + } + } catch (XmlPullParserException e) { + throw new DefinitionException("Problem reading XML", e); + } catch (IOException e) { + throw new DefinitionException("Problem reading XML", e); + } + } +} |