| /* |
| * 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.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| 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.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.Xml; |
| |
| import com.android.contacts.common.model.dataitem.DataKind; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A general contacts account type descriptor. |
| */ |
| public class ExternalAccountType extends BaseAccountType { |
| private static final String TAG = "ExternalAccountType"; |
| |
| private static final String METADATA_CONTACTS = "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_VIEW_STREAM_ITEM_ACTIVITY = "viewStreamItemActivity"; |
| private static final String ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY = |
| "viewStreamItemPhotoActivity"; |
| 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 String mViewStreamItemActivity; |
| private String mViewStreamItemPhotoActivity; |
| 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 PackageManager pm = context.getPackageManager(); |
| final XmlResourceParser parser; |
| if (injectedMetadata == null) { |
| try { |
| parser = loadContactsXml(context, packageName); |
| } catch (NameNotFoundException e1) { |
| // If the package name is not found, we can't initialize this account type. |
| return; |
| } |
| } 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. |
| * |
| * Unfortunately, there's no public way to determine which service defines a sync service for |
| * which account type, so this method looks through all services in the package, and just |
| * returns the first CONTACTS_STRUCTURE metadata defined in any of them. |
| * |
| * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case |
| * the account type *will* be initialized with minimal configuration. |
| * |
| * On the other hand, if the package is not found, it throws a {@link NameNotFoundException}, |
| * in which case the account type will *not* be initialized. |
| */ |
| private XmlResourceParser loadContactsXml(Context context, String resPackageName) |
| throws NameNotFoundException { |
| final PackageManager pm = context.getPackageManager(); |
| PackageInfo packageInfo = pm.getPackageInfo(resPackageName, |
| PackageManager.GET_SERVICES|PackageManager.GET_META_DATA); |
| for (ServiceInfo serviceInfo : packageInfo.services) { |
| final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, |
| METADATA_CONTACTS); |
| if (parser != null) { |
| return parser; |
| } |
| } |
| // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. |
| return null; |
| } |
| |
| 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 String getViewStreamItemActivity() { |
| return mViewStreamItemActivity; |
| } |
| |
| @Override |
| public String getViewStreamItemPhotoActivity() { |
| return mViewStreamItemPhotoActivity; |
| } |
| |
| @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_VIEW_STREAM_ITEM_ACTIVITY.equals(attr)) { |
| mViewStreamItemActivity = value; |
| } else if (ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY.equals(attr)) { |
| mViewStreamItemPhotoActivity = 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, |
| android.R.styleable.ContactsDataKind); |
| final DataKind kind = new DataKind(); |
| |
| kind.mimeType = a |
| .getString(android.R.styleable.ContactsDataKind_mimeType); |
| final String summaryColumn = a.getString( |
| android.R.styleable.ContactsDataKind_summaryColumn); |
| if (summaryColumn != null) { |
| // Inflate a specific column as summary when requested |
| kind.actionHeader = new SimpleInflater(summaryColumn); |
| } |
| |
| final String detailColumn = a.getString( |
| android.R.styleable.ContactsDataKind_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); |
| } |
| } |
| |
| /** |
| * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in |
| * the resource package. |
| * |
| * 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; |
| } |
| } |