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