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