Home | History | Annotate | Download | only in account
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.common.model.account;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageInfo;
     21 import android.content.pm.PackageManager;
     22 import android.content.pm.PackageManager.NameNotFoundException;
     23 import android.content.pm.ServiceInfo;
     24 import android.content.res.Resources;
     25 import android.content.res.TypedArray;
     26 import android.content.res.XmlResourceParser;
     27 import android.provider.ContactsContract.CommonDataKinds.Photo;
     28 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     29 import android.text.TextUtils;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.util.Xml;
     33 
     34 import com.android.contacts.common.R;
     35 import com.android.contacts.common.model.dataitem.DataKind;
     36 import com.google.common.annotations.VisibleForTesting;
     37 
     38 import org.xmlpull.v1.XmlPullParser;
     39 import org.xmlpull.v1.XmlPullParserException;
     40 
     41 import java.io.IOException;
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 /**
     46  * A general contacts account type descriptor.
     47  */
     48 public class ExternalAccountType extends BaseAccountType {
     49     private static final String TAG = "ExternalAccountType";
     50 
     51     /**
     52      * The metadata name for so-called "contacts.xml".
     53      *
     54      * On LMP and later, we also accept the "alternate" name.
     55      * This is to allow sync adapters to have a contacts.xml without making it visible on older
     56      * platforms.
     57      */
     58     private static final String[] METADATA_CONTACTS_NAMES = new String[] {
     59             "android.provider.ALTERNATE_CONTACTS_STRUCTURE",
     60             "android.provider.CONTACTS_STRUCTURE"
     61     };
     62 
     63     private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
     64     private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
     65     private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
     66     private static final String TAG_EDIT_SCHEMA = "EditSchema";
     67 
     68     private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
     69     private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
     70     private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
     71     private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
     72     private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
     73     private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
     74     private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
     75     private static final String ATTR_DATA_SET = "dataSet";
     76     private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
     77 
     78     // The following attributes should only be set in non-sync-adapter account types.  They allow
     79     // for the account type and resource IDs to be specified without an associated authenticator.
     80     private static final String ATTR_ACCOUNT_TYPE = "accountType";
     81     private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
     82     private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
     83 
     84     private final boolean mIsExtension;
     85 
     86     private String mEditContactActivityClassName;
     87     private String mCreateContactActivityClassName;
     88     private String mInviteContactActivity;
     89     private String mInviteActionLabelAttribute;
     90     private int mInviteActionLabelResId;
     91     private String mViewContactNotifyService;
     92     private String mViewGroupActivity;
     93     private String mViewGroupLabelAttribute;
     94     private int mViewGroupLabelResId;
     95     private List<String> mExtensionPackageNames;
     96     private String mAccountTypeLabelAttribute;
     97     private String mAccountTypeIconAttribute;
     98     private boolean mHasContactsMetadata;
     99     private boolean mHasEditSchema;
    100 
    101     public ExternalAccountType(Context context, String resPackageName, boolean isExtension) {
    102         this(context, resPackageName, isExtension, null);
    103     }
    104 
    105     /**
    106      * Constructor used for testing to initialize with any arbitrary XML.
    107      *
    108      * @param injectedMetadata If non-null, it'll be used to initialize the type.  Only set by
    109      *     tests.  If null, the metadata is loaded from the specified package.
    110      */
    111     ExternalAccountType(Context context, String packageName, boolean isExtension,
    112             XmlResourceParser injectedMetadata) {
    113         this.mIsExtension = isExtension;
    114         this.resourcePackageName = packageName;
    115         this.syncAdapterPackageName = packageName;
    116 
    117         final PackageManager pm = context.getPackageManager();
    118         final XmlResourceParser parser;
    119         if (injectedMetadata == null) {
    120             try {
    121                 parser = loadContactsXml(context, packageName);
    122             } catch (NameNotFoundException e1) {
    123                 // If the package name is not found, we can't initialize this account type.
    124                 return;
    125             }
    126         } else {
    127             parser = injectedMetadata;
    128         }
    129         boolean needLineNumberInErrorLog = true;
    130         try {
    131             if (parser != null) {
    132                 inflate(context, parser);
    133             }
    134 
    135             // Done parsing; line number no longer needed in error log.
    136             needLineNumberInErrorLog = false;
    137             if (mHasEditSchema) {
    138                 checkKindExists(StructuredName.CONTENT_ITEM_TYPE);
    139                 checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME);
    140                 checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
    141                 checkKindExists(Photo.CONTENT_ITEM_TYPE);
    142             } else {
    143                 // Bring in name and photo from fallback source, which are non-optional
    144                 addDataKindStructuredName(context);
    145                 addDataKindDisplayName(context);
    146                 addDataKindPhoneticName(context);
    147                 addDataKindPhoto(context);
    148             }
    149         } catch (DefinitionException e) {
    150             final StringBuilder error = new StringBuilder();
    151             error.append("Problem reading XML");
    152             if (needLineNumberInErrorLog && (parser != null)) {
    153                 error.append(" in line ");
    154                 error.append(parser.getLineNumber());
    155             }
    156             error.append(" for external package ");
    157             error.append(packageName);
    158 
    159             Log.e(TAG, error.toString(), e);
    160             return;
    161         } finally {
    162             if (parser != null) {
    163                 parser.close();
    164             }
    165         }
    166 
    167         mExtensionPackageNames = new ArrayList<String>();
    168         mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute,
    169                 syncAdapterPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL);
    170         mViewGroupLabelResId = resolveExternalResId(context, mViewGroupLabelAttribute,
    171                 syncAdapterPackageName, ATTR_VIEW_GROUP_ACTION_LABEL);
    172         titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute,
    173                 syncAdapterPackageName, ATTR_ACCOUNT_LABEL);
    174         iconRes = resolveExternalResId(context, mAccountTypeIconAttribute,
    175                 syncAdapterPackageName, ATTR_ACCOUNT_ICON);
    176 
    177         // If we reach this point, the account type has been successfully initialized.
    178         mIsInitialized = true;
    179     }
    180 
    181     /**
    182      * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
    183      *
    184      * Unfortunately, there's no public way to determine which service defines a sync service for
    185      * which account type, so this method looks through all services in the package, and just
    186      * returns the first CONTACTS_STRUCTURE metadata defined in any of them.
    187      *
    188      * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata.  In this case
    189      * the account type *will* be initialized with minimal configuration.
    190      *
    191      * On the other hand, if the package is not found, it throws a {@link NameNotFoundException},
    192      * in which case the account type will *not* be initialized.
    193      */
    194     private XmlResourceParser loadContactsXml(Context context, String resPackageName)
    195             throws NameNotFoundException {
    196         final PackageManager pm = context.getPackageManager();
    197         PackageInfo packageInfo = pm.getPackageInfo(resPackageName,
    198                 PackageManager.GET_SERVICES|PackageManager.GET_META_DATA);
    199         for (ServiceInfo serviceInfo : packageInfo.services) {
    200             for (String metadataName : METADATA_CONTACTS_NAMES) {
    201                 final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm,
    202                         metadataName);
    203                 if (parser != null) {
    204                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    205                         Log.d(TAG, String.format("Metadata loaded from: %s, %s, %s",
    206                                 serviceInfo.packageName, serviceInfo.name,
    207                                 metadataName));
    208                     }
    209                     return parser;
    210                 }
    211             }
    212         }
    213         // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
    214         return null;
    215     }
    216 
    217     private void checkKindExists(String mimeType) throws DefinitionException {
    218         if (getKindForMimetype(mimeType) == null) {
    219             throw new DefinitionException(mimeType + " must be supported");
    220         }
    221     }
    222 
    223     @Override
    224     public boolean isEmbedded() {
    225         return false;
    226     }
    227 
    228     @Override
    229     public boolean isExtension() {
    230         return mIsExtension;
    231     }
    232 
    233     @Override
    234     public boolean areContactsWritable() {
    235         return mHasEditSchema;
    236     }
    237 
    238     /**
    239      * Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml.
    240      */
    241     public boolean hasContactsMetadata() {
    242         return mHasContactsMetadata;
    243     }
    244 
    245     @Override
    246     public String getEditContactActivityClassName() {
    247         return mEditContactActivityClassName;
    248     }
    249 
    250     @Override
    251     public String getCreateContactActivityClassName() {
    252         return mCreateContactActivityClassName;
    253     }
    254 
    255     @Override
    256     public String getInviteContactActivityClassName() {
    257         return mInviteContactActivity;
    258     }
    259 
    260     @Override
    261     protected int getInviteContactActionResId() {
    262         return mInviteActionLabelResId;
    263     }
    264 
    265     @Override
    266     public String getViewContactNotifyServiceClassName() {
    267         return mViewContactNotifyService;
    268     }
    269 
    270     @Override
    271     public String getViewGroupActivity() {
    272         return mViewGroupActivity;
    273     }
    274 
    275     @Override
    276     protected int getViewGroupLabelResId() {
    277         return mViewGroupLabelResId;
    278     }
    279 
    280     @Override
    281     public List<String> getExtensionPackageNames() {
    282         return mExtensionPackageNames;
    283     }
    284 
    285     /**
    286      * Inflate this {@link AccountType} from the given parser. This may only
    287      * load details matching the publicly-defined schema.
    288      */
    289     protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
    290         final AttributeSet attrs = Xml.asAttributeSet(parser);
    291 
    292         try {
    293             int type;
    294             while ((type = parser.next()) != XmlPullParser.START_TAG
    295                     && type != XmlPullParser.END_DOCUMENT) {
    296                 // Drain comments and whitespace
    297             }
    298 
    299             if (type != XmlPullParser.START_TAG) {
    300                 throw new IllegalStateException("No start tag found");
    301             }
    302 
    303             String rootTag = parser.getName();
    304             if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) &&
    305                     !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
    306                 throw new IllegalStateException("Top level element must be "
    307                         + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
    308             }
    309 
    310             mHasContactsMetadata = true;
    311 
    312             int attributeCount = parser.getAttributeCount();
    313             for (int i = 0; i < attributeCount; i++) {
    314                 String attr = parser.getAttributeName(i);
    315                 String value = parser.getAttributeValue(i);
    316                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    317                     Log.d(TAG, attr + "=" + value);
    318                 }
    319                 if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
    320                     mEditContactActivityClassName = value;
    321                 } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
    322                     mCreateContactActivityClassName = value;
    323                 } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
    324                     mInviteContactActivity = value;
    325                 } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
    326                     mInviteActionLabelAttribute = value;
    327                 } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
    328                     mViewContactNotifyService = value;
    329                 } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
    330                     mViewGroupActivity = value;
    331                 } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
    332                     mViewGroupLabelAttribute = value;
    333                 } else if (ATTR_DATA_SET.equals(attr)) {
    334                     dataSet = value;
    335                 } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
    336                     mExtensionPackageNames.add(value);
    337                 } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
    338                     accountType = value;
    339                 } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
    340                     mAccountTypeLabelAttribute = value;
    341                 } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
    342                     mAccountTypeIconAttribute = value;
    343                 } else {
    344                     Log.e(TAG, "Unsupported attribute " + attr);
    345                 }
    346             }
    347 
    348             // Parse all children kinds
    349             final int startDepth = parser.getDepth();
    350             while (((type = parser.next()) != XmlPullParser.END_TAG
    351                         || parser.getDepth() > startDepth)
    352                     && type != XmlPullParser.END_DOCUMENT) {
    353 
    354                 if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) {
    355                     continue; // Not a direct child tag
    356                 }
    357 
    358                 String tag = parser.getName();
    359                 if (TAG_EDIT_SCHEMA.equals(tag)) {
    360                     mHasEditSchema = true;
    361                     parseEditSchema(context, parser, attrs);
    362                 } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
    363                     final TypedArray a = context.obtainStyledAttributes(attrs,
    364                             R.styleable.ContactsDataKind);
    365                     final DataKind kind = new DataKind();
    366 
    367                     kind.mimeType = a
    368                             .getString(R.styleable.ContactsDataKind_android_mimeType);
    369                     final String summaryColumn = a.getString(
    370                             R.styleable.ContactsDataKind_android_summaryColumn);
    371                     if (summaryColumn != null) {
    372                         // Inflate a specific column as summary when requested
    373                         kind.actionHeader = new SimpleInflater(summaryColumn);
    374                     }
    375                     final String detailColumn = a.getString(
    376                             R.styleable.ContactsDataKind_android_detailColumn);
    377                     if (detailColumn != null) {
    378                         // Inflate specific column as summary
    379                         kind.actionBody = new SimpleInflater(detailColumn);
    380                     }
    381 
    382                     a.recycle();
    383 
    384                     addKind(kind);
    385                 }
    386             }
    387         } catch (XmlPullParserException e) {
    388             throw new DefinitionException("Problem reading XML", e);
    389         } catch (IOException e) {
    390             throw new DefinitionException("Problem reading XML", e);
    391         }
    392     }
    393 
    394     /**
    395      * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
    396      * the resource package.
    397      *
    398      * If the argument is in the invalid format or isn't a resource name, it returns -1.
    399      *
    400      * @param context context
    401      * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
    402      * @param packageName name of the package containing the resource.
    403      * @param xmlAttributeName attribute name which the resource came from.  Used for logging.
    404      */
    405     @VisibleForTesting
    406     static int resolveExternalResId(Context context, String resourceName,
    407             String packageName, String xmlAttributeName) {
    408         if (TextUtils.isEmpty(resourceName)) {
    409             return -1; // Empty text is okay.
    410         }
    411         if (resourceName.charAt(0) != '@') {
    412             Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
    413             return -1;
    414         }
    415         final String name = resourceName.substring(1);
    416         final Resources res;
    417         try {
    418              res = context.getPackageManager().getResourcesForApplication(packageName);
    419         } catch (NameNotFoundException e) {
    420             Log.e(TAG, "Unable to load package " + packageName);
    421             return -1;
    422         }
    423         final int resId = res.getIdentifier(name, null, packageName);
    424         if (resId == 0) {
    425             Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
    426             return -1;
    427         }
    428         return resId;
    429     }
    430 }
    431