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