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