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