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