blob: c914c749448777e04f3f0b23a3f22cde101ad789 [file] [log] [blame]
Chiao Chenge88fcd32012-11-13 18:38:05 -08001/*
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
Gary Mai69c182a2016-12-05 13:07:03 -080017package com.android.contacts.model.account;
Chiao Chenge88fcd32012-11-13 18:38:05 -080018
19import android.content.Context;
Jay Shraunere5ef2f72014-12-03 15:46:26 -080020import android.content.Intent;
Chiao Chenge88fcd32012-11-13 18:38:05 -080021import android.content.pm.PackageManager;
22import android.content.pm.PackageManager.NameNotFoundException;
Jay Shraunere5ef2f72014-12-03 15:46:26 -080023import android.content.pm.ResolveInfo;
Chiao Chenge88fcd32012-11-13 18:38:05 -080024import android.content.pm.ServiceInfo;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.content.res.XmlResourceParser;
Walter Jang0fed9b62016-09-07 15:23:22 -070028import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
Chiao Chenge88fcd32012-11-13 18:38:05 -080029import android.provider.ContactsContract.CommonDataKinds.Photo;
30import android.provider.ContactsContract.CommonDataKinds.StructuredName;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.util.Xml;
35
Arthur Wang3f6a2442016-12-05 14:51:59 -080036import com.android.contacts.R;
Gary Mai69c182a2016-12-05 13:07:03 -080037import com.android.contacts.model.dataitem.DataKind;
Walter Jang3a0b4832016-10-12 11:02:54 -070038import com.android.contactsbind.FeedbackHelper;
39
Chiao Chenge88fcd32012-11-13 18:38:05 -080040import com.google.common.annotations.VisibleForTesting;
41
42import org.xmlpull.v1.XmlPullParser;
43import org.xmlpull.v1.XmlPullParserException;
44
45import java.io.IOException;
46import java.util.ArrayList;
47import java.util.List;
48
49/**
50 * A general contacts account type descriptor.
51 */
52public class ExternalAccountType extends BaseAccountType {
53 private static final String TAG = "ExternalAccountType";
54
Jay Shraunere5ef2f72014-12-03 15:46:26 -080055 private static final String SYNC_META_DATA = "android.content.SyncAdapter";
56
Makoto Onuki05c73e62014-05-12 15:19:44 -070057 /**
58 * The metadata name for so-called "contacts.xml".
59 *
60 * On LMP and later, we also accept the "alternate" name.
61 * This is to allow sync adapters to have a contacts.xml without making it visible on older
Jay Shraunere5ef2f72014-12-03 15:46:26 -080062 * platforms. If you modify this also update the corresponding list in
63 * ContactsProvider/PhotoPriorityResolver
Makoto Onuki05c73e62014-05-12 15:19:44 -070064 */
65 private static final String[] METADATA_CONTACTS_NAMES = new String[] {
66 "android.provider.ALTERNATE_CONTACTS_STRUCTURE",
67 "android.provider.CONTACTS_STRUCTURE"
68 };
Chiao Chenge88fcd32012-11-13 18:38:05 -080069
70 private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
71 private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
72 private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
73 private static final String TAG_EDIT_SCHEMA = "EditSchema";
74
Chiao Chenge88fcd32012-11-13 18:38:05 -080075 private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
76 private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
77 private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
78 private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
79 private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
Chiao Chenge88fcd32012-11-13 18:38:05 -080080 private static final String ATTR_DATA_SET = "dataSet";
81 private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
82
83 // The following attributes should only be set in non-sync-adapter account types. They allow
84 // for the account type and resource IDs to be specified without an associated authenticator.
85 private static final String ATTR_ACCOUNT_TYPE = "accountType";
86 private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
87 private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
88
89 private final boolean mIsExtension;
90
Chiao Chenge88fcd32012-11-13 18:38:05 -080091 private String mInviteContactActivity;
92 private String mInviteActionLabelAttribute;
93 private int mInviteActionLabelResId;
94 private String mViewContactNotifyService;
95 private String mViewGroupActivity;
96 private String mViewGroupLabelAttribute;
97 private int mViewGroupLabelResId;
Chiao Chenge88fcd32012-11-13 18:38:05 -080098 private List<String> mExtensionPackageNames;
99 private String mAccountTypeLabelAttribute;
100 private String mAccountTypeIconAttribute;
101 private boolean mHasContactsMetadata;
102 private boolean mHasEditSchema;
Walter Jang0fed9b62016-09-07 15:23:22 -0700103 private boolean mGroupMembershipEditable;
Chiao Chenge88fcd32012-11-13 18:38:05 -0800104
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 */
115 ExternalAccountType(Context context, String packageName, boolean isExtension,
116 XmlResourceParser injectedMetadata) {
117 this.mIsExtension = isExtension;
118 this.resourcePackageName = packageName;
119 this.syncAdapterPackageName = packageName;
120
Chiao Chenge88fcd32012-11-13 18:38:05 -0800121 final XmlResourceParser parser;
122 if (injectedMetadata == null) {
Jay Shraunere5ef2f72014-12-03 15:46:26 -0800123 parser = loadContactsXml(context, packageName);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800124 } 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);
Gary Mai7a6daea2016-10-10 15:41:48 -0700137 checkKindExists(DataKind.PSEUDO_MIME_TYPE_NAME);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800138 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);
Gary Mai7a6daea2016-10-10 15:41:48 -0700143 addDataKindName(context);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800144 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);
Gary Maie83e6212016-11-16 10:52:19 -0800156 // Only send feedback if not from tests. There are tests that expect failures so no need
157 // to report those.
158 if (injectedMetadata == null) {
159 FeedbackHelper.sendFeedback(context, TAG, "Failed to build external account type",
160 e);
161 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800162 return;
163 } finally {
164 if (parser != null) {
165 parser.close();
166 }
167 }
168
169 mExtensionPackageNames = new ArrayList<String>();
170 mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute,
171 syncAdapterPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL);
172 mViewGroupLabelResId = resolveExternalResId(context, mViewGroupLabelAttribute,
173 syncAdapterPackageName, ATTR_VIEW_GROUP_ACTION_LABEL);
174 titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute,
175 syncAdapterPackageName, ATTR_ACCOUNT_LABEL);
176 iconRes = resolveExternalResId(context, mAccountTypeIconAttribute,
177 syncAdapterPackageName, ATTR_ACCOUNT_ICON);
178
Walter Jang0fed9b62016-09-07 15:23:22 -0700179 final DataKind dataKind = getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
180 mGroupMembershipEditable = dataKind != null && dataKind.editable;
181
Chiao Chenge88fcd32012-11-13 18:38:05 -0800182 // If we reach this point, the account type has been successfully initialized.
183 mIsInitialized = true;
184 }
185
186 /**
187 * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
188 *
Jay Shraunere5ef2f72014-12-03 15:46:26 -0800189 * This method looks through all services in the package that handle sync adapter
190 * intents for the first one that contains CONTACTS_STRUCTURE metadata. We have to look
191 * through all sync adapters in the package in case there are contacts and other sync
192 * adapters (eg, calendar) in the same package.
Chiao Chenge88fcd32012-11-13 18:38:05 -0800193 *
194 * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case
195 * the account type *will* be initialized with minimal configuration.
Chiao Chenge88fcd32012-11-13 18:38:05 -0800196 */
Jay Shraunere5ef2f72014-12-03 15:46:26 -0800197 public static XmlResourceParser loadContactsXml(Context context, String resPackageName) {
Chiao Chenge88fcd32012-11-13 18:38:05 -0800198 final PackageManager pm = context.getPackageManager();
Jay Shraunere5ef2f72014-12-03 15:46:26 -0800199 final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName);
200 final List<ResolveInfo> intentServices = pm.queryIntentServices(intent,
201 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
202
203 if (intentServices != null) {
204 for (final ResolveInfo resolveInfo : intentServices) {
205 final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
206 if (serviceInfo == null) {
207 continue;
208 }
209 for (String metadataName : METADATA_CONTACTS_NAMES) {
210 final XmlResourceParser parser = serviceInfo.loadXmlMetaData(
211 pm, metadataName);
212 if (parser != null) {
213 if (Log.isLoggable(TAG, Log.DEBUG)) {
214 Log.d(TAG, String.format("Metadata loaded from: %s, %s, %s",
215 serviceInfo.packageName, serviceInfo.name,
216 metadataName));
217 }
218 return parser;
Makoto Onuki05c73e62014-05-12 15:19:44 -0700219 }
Makoto Onuki05c73e62014-05-12 15:19:44 -0700220 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800221 }
222 }
Jay Shraunere5ef2f72014-12-03 15:46:26 -0800223
Chiao Chenge88fcd32012-11-13 18:38:05 -0800224 // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
225 return null;
226 }
227
Brian Attwell684318f2015-02-25 12:39:28 -0800228 /**
229 * Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata.
230 */
231 public static boolean hasContactsXml(Context context, String resPackageName) {
232 return loadContactsXml(context, resPackageName) != null;
233 }
234
Chiao Chenge88fcd32012-11-13 18:38:05 -0800235 private void checkKindExists(String mimeType) throws DefinitionException {
236 if (getKindForMimetype(mimeType) == null) {
237 throw new DefinitionException(mimeType + " must be supported");
238 }
239 }
240
241 @Override
242 public boolean isEmbedded() {
243 return false;
244 }
245
246 @Override
247 public boolean isExtension() {
248 return mIsExtension;
249 }
250
251 @Override
252 public boolean areContactsWritable() {
253 return mHasEditSchema;
254 }
255
256 /**
257 * Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml.
258 */
259 public boolean hasContactsMetadata() {
260 return mHasContactsMetadata;
261 }
262
263 @Override
Chiao Chenge88fcd32012-11-13 18:38:05 -0800264 public String getInviteContactActivityClassName() {
265 return mInviteContactActivity;
266 }
267
268 @Override
269 protected int getInviteContactActionResId() {
270 return mInviteActionLabelResId;
271 }
272
273 @Override
274 public String getViewContactNotifyServiceClassName() {
275 return mViewContactNotifyService;
276 }
277
278 @Override
279 public String getViewGroupActivity() {
280 return mViewGroupActivity;
281 }
282
283 @Override
284 protected int getViewGroupLabelResId() {
285 return mViewGroupLabelResId;
286 }
287
288 @Override
Chiao Chenge88fcd32012-11-13 18:38:05 -0800289 public List<String> getExtensionPackageNames() {
290 return mExtensionPackageNames;
291 }
292
Walter Jang0fed9b62016-09-07 15:23:22 -0700293 @Override
294 public boolean isGroupMembershipEditable() {
295 return mGroupMembershipEditable;
296 }
297
Chiao Chenge88fcd32012-11-13 18:38:05 -0800298 /**
299 * Inflate this {@link AccountType} from the given parser. This may only
300 * load details matching the publicly-defined schema.
301 */
302 protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
303 final AttributeSet attrs = Xml.asAttributeSet(parser);
304
305 try {
306 int type;
307 while ((type = parser.next()) != XmlPullParser.START_TAG
308 && type != XmlPullParser.END_DOCUMENT) {
309 // Drain comments and whitespace
310 }
311
312 if (type != XmlPullParser.START_TAG) {
313 throw new IllegalStateException("No start tag found");
314 }
315
316 String rootTag = parser.getName();
317 if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) &&
318 !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
319 throw new IllegalStateException("Top level element must be "
320 + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
321 }
322
323 mHasContactsMetadata = true;
324
325 int attributeCount = parser.getAttributeCount();
326 for (int i = 0; i < attributeCount; i++) {
327 String attr = parser.getAttributeName(i);
328 String value = parser.getAttributeValue(i);
329 if (Log.isLoggable(TAG, Log.DEBUG)) {
330 Log.d(TAG, attr + "=" + value);
331 }
Gary Maiaebf3202016-09-22 18:11:15 -0700332 if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
Chiao Chenge88fcd32012-11-13 18:38:05 -0800333 mInviteContactActivity = value;
334 } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
335 mInviteActionLabelAttribute = value;
336 } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
337 mViewContactNotifyService = value;
338 } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
339 mViewGroupActivity = value;
340 } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
341 mViewGroupLabelAttribute = value;
Chiao Chenge88fcd32012-11-13 18:38:05 -0800342 } else if (ATTR_DATA_SET.equals(attr)) {
343 dataSet = value;
344 } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
345 mExtensionPackageNames.add(value);
346 } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
347 accountType = value;
348 } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
349 mAccountTypeLabelAttribute = value;
350 } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
351 mAccountTypeIconAttribute = value;
Walter Jang3a0b4832016-10-12 11:02:54 -0700352 } else if (Log.isLoggable(TAG, Log.WARN)) {
353 Log.w(TAG, "Unsupported attribute " + attr);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800354 }
355 }
356
357 // Parse all children kinds
358 final int startDepth = parser.getDepth();
359 while (((type = parser.next()) != XmlPullParser.END_TAG
360 || parser.getDepth() > startDepth)
361 && type != XmlPullParser.END_DOCUMENT) {
362
363 if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) {
364 continue; // Not a direct child tag
365 }
366
367 String tag = parser.getName();
368 if (TAG_EDIT_SCHEMA.equals(tag)) {
369 mHasEditSchema = true;
370 parseEditSchema(context, parser, attrs);
371 } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
372 final TypedArray a = context.obtainStyledAttributes(attrs,
Yorke Lee8bb62472013-11-25 17:18:33 -0800373 R.styleable.ContactsDataKind);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800374 final DataKind kind = new DataKind();
375
376 kind.mimeType = a
Yorke Lee8bb62472013-11-25 17:18:33 -0800377 .getString(R.styleable.ContactsDataKind_android_mimeType);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800378 final String summaryColumn = a.getString(
Yorke Lee8bb62472013-11-25 17:18:33 -0800379 R.styleable.ContactsDataKind_android_summaryColumn);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800380 if (summaryColumn != null) {
381 // Inflate a specific column as summary when requested
382 kind.actionHeader = new SimpleInflater(summaryColumn);
383 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800384 final String detailColumn = a.getString(
Yorke Lee8bb62472013-11-25 17:18:33 -0800385 R.styleable.ContactsDataKind_android_detailColumn);
Chiao Chenge88fcd32012-11-13 18:38:05 -0800386 if (detailColumn != null) {
387 // Inflate specific column as summary
388 kind.actionBody = new SimpleInflater(detailColumn);
389 }
390
391 a.recycle();
392
393 addKind(kind);
394 }
395 }
396 } catch (XmlPullParserException e) {
397 throw new DefinitionException("Problem reading XML", e);
398 } catch (IOException e) {
399 throw new DefinitionException("Problem reading XML", e);
400 }
401 }
402
403 /**
404 * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
405 * the resource package.
406 *
407 * If the argument is in the invalid format or isn't a resource name, it returns -1.
408 *
409 * @param context context
410 * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
411 * @param packageName name of the package containing the resource.
412 * @param xmlAttributeName attribute name which the resource came from. Used for logging.
413 */
414 @VisibleForTesting
415 static int resolveExternalResId(Context context, String resourceName,
416 String packageName, String xmlAttributeName) {
417 if (TextUtils.isEmpty(resourceName)) {
418 return -1; // Empty text is okay.
419 }
420 if (resourceName.charAt(0) != '@') {
Wenyi Wangdaa6fb82016-11-21 15:37:36 -0800421 if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700422 Log.w(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
423 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800424 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) {
Wenyi Wangdaa6fb82016-11-21 15:37:36 -0800431 if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700432 Log.w(TAG, "Unable to load package " + packageName);
433 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800434 return -1;
435 }
436 final int resId = res.getIdentifier(name, null, packageName);
437 if (resId == 0) {
Wenyi Wangdaa6fb82016-11-21 15:37:36 -0800438 if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700439 Log.w(TAG, "Unable to load " + resourceName + " from package " + packageName);
440 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800441 return -1;
442 }
443 return resId;
444 }
Wenyi Wangdaa6fb82016-11-21 15:37:36 -0800445
446 @VisibleForTesting
447 static boolean isFromTestApp(String packageName) {
448 return TextUtils.equals(packageName, "com.google.android.contacts.tests");
449 }
Chiao Chenge88fcd32012-11-13 18:38:05 -0800450}