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