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