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