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.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.GroupMembership; 29 import android.provider.ContactsContract.CommonDataKinds.Photo; 30 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 31 import android.text.TextUtils; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.util.Xml; 35 36 import com.android.contacts.R; 37 import com.android.contacts.model.dataitem.DataKind; 38 import com.android.contactsbind.FeedbackHelper; 39 40 import com.google.common.annotations.VisibleForTesting; 41 42 import org.xmlpull.v1.XmlPullParser; 43 import org.xmlpull.v1.XmlPullParserException; 44 45 import java.io.IOException; 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * A general contacts account type descriptor. 51 */ 52 public class ExternalAccountType extends BaseAccountType { 53 private static final String TAG = "ExternalAccountType"; 54 55 private static final String SYNC_META_DATA = "android.content.SyncAdapter"; 56 57 /** 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 62 * platforms. If you modify this also update the corresponding list in 63 * ContactsProvider/PhotoPriorityResolver 64 */ 65 private static final String[] METADATA_CONTACTS_NAMES = new String[] { 66 "android.provider.ALTERNATE_CONTACTS_STRUCTURE", 67 "android.provider.CONTACTS_STRUCTURE" 68 }; 69 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 75 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"; 80 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 91 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; 98 private List<String> mExtensionPackageNames; 99 private String mAccountTypeLabelAttribute; 100 private String mAccountTypeIconAttribute; 101 private boolean mHasContactsMetadata; 102 private boolean mHasEditSchema; 103 private boolean mGroupMembershipEditable; 104 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 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_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 addDataKindName(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 // 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 } 162 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 179 final DataKind dataKind = getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE); 180 mGroupMembershipEditable = dataKind != null && dataKind.editable; 181 182 // 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 * 189 * 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. 193 * 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. 196 */ 197 public static XmlResourceParser loadContactsXml(Context context, String resPackageName) { 198 final PackageManager pm = context.getPackageManager(); 199 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; 219 } 220 } 221 } 222 } 223 224 // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. 225 return null; 226 } 227 228 /** 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 235 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 264 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 289 public List<String> getExtensionPackageNames() { 290 return mExtensionPackageNames; 291 } 292 293 @Override 294 public boolean isGroupMembershipEditable() { 295 return mGroupMembershipEditable; 296 } 297 298 /** 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 } 332 if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) { 333 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; 342 } 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; 352 } else if (Log.isLoggable(TAG, Log.WARN)) { 353 Log.w(TAG, "Unsupported attribute " + attr); 354 } 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, 373 R.styleable.ContactsDataKind); 374 final DataKind kind = new DataKind(); 375 376 kind.mimeType = a 377 .getString(R.styleable.ContactsDataKind_android_mimeType); 378 final String summaryColumn = a.getString( 379 R.styleable.ContactsDataKind_android_summaryColumn); 380 if (summaryColumn != null) { 381 // Inflate a specific column as summary when requested 382 kind.actionHeader = new SimpleInflater(summaryColumn); 383 } 384 final String detailColumn = a.getString( 385 R.styleable.ContactsDataKind_android_detailColumn); 386 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) != '@') { 421 if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) { 422 Log.w(TAG, xmlAttributeName + " must be a resource name beginnig with '@'"); 423 } 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 if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) { 432 Log.w(TAG, "Unable to load package " + packageName); 433 } 434 return -1; 435 } 436 final int resId = res.getIdentifier(name, null, packageName); 437 if (resId == 0) { 438 if (Log.isLoggable(TAG, Log.WARN) && !isFromTestApp(packageName)) { 439 Log.w(TAG, "Unable to load " + resourceName + " from package " + packageName); 440 } 441 return -1; 442 } 443 return resId; 444 } 445 446 @VisibleForTesting 447 static boolean isFromTestApp(String packageName) { 448 return TextUtils.equals(packageName, "com.google.android.contacts.tests"); 449 } 450 } 451