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