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