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 
ExternalAccountType(Context context, String resPackageName, boolean isExtension)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      */
ExternalAccountType(Context context, String packageName, boolean isExtension, XmlResourceParser injectedMetadata)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      */
loadContactsXml(Context context, String resPackageName)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      */
hasContactsXml(Context context, String resPackageName)224     public static boolean hasContactsXml(Context context, String resPackageName) {
225         return loadContactsXml(context, resPackageName) != null;
226     }
227 
checkKindExists(String mimeType)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
isEmbedded()235     public boolean isEmbedded() {
236         return false;
237     }
238 
239     @Override
isExtension()240     public boolean isExtension() {
241         return mIsExtension;
242     }
243 
244     @Override
areContactsWritable()245     public boolean areContactsWritable() {
246         return mHasEditSchema;
247     }
248 
249     /**
250      * Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml.
251      */
hasContactsMetadata()252     public boolean hasContactsMetadata() {
253         return mHasContactsMetadata;
254     }
255 
256     @Override
getEditContactActivityClassName()257     public String getEditContactActivityClassName() {
258         return mEditContactActivityClassName;
259     }
260 
261     @Override
getCreateContactActivityClassName()262     public String getCreateContactActivityClassName() {
263         return mCreateContactActivityClassName;
264     }
265 
266     @Override
getInviteContactActivityClassName()267     public String getInviteContactActivityClassName() {
268         return mInviteContactActivity;
269     }
270 
271     @Override
getInviteContactActionResId()272     protected int getInviteContactActionResId() {
273         return mInviteActionLabelResId;
274     }
275 
276     @Override
getViewContactNotifyServiceClassName()277     public String getViewContactNotifyServiceClassName() {
278         return mViewContactNotifyService;
279     }
280 
281     @Override
getViewGroupActivity()282     public String getViewGroupActivity() {
283         return mViewGroupActivity;
284     }
285 
286     @Override
getViewGroupLabelResId()287     protected int getViewGroupLabelResId() {
288         return mViewGroupLabelResId;
289     }
290 
291     @Override
getExtensionPackageNames()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      */
inflate(Context context, XmlPullParser parser)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
resolveExternalResId(Context context, String resourceName, String packageName, String xmlAttributeName)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