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