ExternalAccountType.java revision 3ef27fb18a2fe075c43131b653cd2e6306e187e2
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.Intent; 26import android.content.pm.PackageInfo; 27import android.content.pm.PackageManager; 28import android.content.pm.PackageManager.NameNotFoundException; 29import android.content.pm.ResolveInfo; 30import android.content.pm.ServiceInfo; 31import android.content.res.Resources; 32import android.content.res.TypedArray; 33import android.content.res.XmlResourceParser; 34import android.text.TextUtils; 35import android.util.AttributeSet; 36import android.util.Log; 37import android.util.Xml; 38 39import java.io.IOException; 40import java.util.ArrayList; 41import java.util.List; 42 43/** 44 * A general contacts account type descriptor. 45 */ 46public class ExternalAccountType extends BaseAccountType { 47 private static final String TAG = "ExternalAccountType"; 48 49 private static final String ACTION_SYNC_ADAPTER = "android.content.SyncAdapter"; 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 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_DATA_SET = "dataSet"; 62 private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames"; 63 64 // The following attributes should only be set in non-sync-adapter account types. They allow 65 // for the account type and resource IDs to be specified without an associated authenticator. 66 private static final String ATTR_ACCOUNT_TYPE = "accountType"; 67 private static final String ATTR_READ_ONLY = "readOnly"; 68 private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel"; 69 private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon"; 70 71 private String mEditContactActivityClassName; 72 private String mCreateContactActivityClassName; 73 private String mInviteContactActivity; 74 private String mInviteActionLabelAttribute; 75 private String mViewContactNotifyService; 76 private List<String> mExtensionPackageNames; 77 private int mInviteActionLabelResId; 78 private String mAccountTypeLabelAttribute; 79 private String mAccountTypeIconAttribute; 80 private boolean mInitSuccessful; 81 82 public ExternalAccountType(Context context, String resPackageName) { 83 this.resPackageName = resPackageName; 84 this.summaryResPackageName = resPackageName; 85 86 // Handle unknown sources by searching their package 87 final PackageManager pm = context.getPackageManager(); 88 try { 89 PackageInfo packageInfo = pm.getPackageInfo(resPackageName, 90 PackageManager.GET_SERVICES|PackageManager.GET_META_DATA); 91 for (ServiceInfo serviceInfo : packageInfo.services) { 92 final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, 93 METADATA_CONTACTS); 94 if (parser == null) continue; 95 inflate(context, parser); 96 } 97 } catch (NameNotFoundException nnfe) { 98 // If the package name is not found, we can't initialize this account type. 99 return; 100 } 101 102 mExtensionPackageNames = new ArrayList<String>(); 103 mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute, 104 summaryResPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL); 105 titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute, 106 this.resPackageName, ATTR_ACCOUNT_LABEL); 107 iconRes = resolveExternalResId(context, mAccountTypeIconAttribute, 108 this.resPackageName, ATTR_ACCOUNT_ICON); 109 110 // Bring in name and photo from fallback source, which are non-optional 111 addDataKindStructuredName(context); 112 addDataKindDisplayName(context); 113 addDataKindPhoneticName(context); 114 addDataKindPhoto(context); 115 116 // If we reach this point, the account type has been successfully initialized. 117 mInitSuccessful = true; 118 } 119 120 @Override 121 public boolean isExternal() { 122 return true; 123 } 124 125 /** 126 * Whether this account type was able to be fully initialized. This may be false if 127 * (for example) the package name associated with the account type could not be found. 128 */ 129 public boolean isInitialized() { 130 return mInitSuccessful; 131 } 132 133 @Override 134 public String getEditContactActivityClassName() { 135 return mEditContactActivityClassName; 136 } 137 138 @Override 139 public String getCreateContactActivityClassName() { 140 return mCreateContactActivityClassName; 141 } 142 143 @Override 144 public String getInviteContactActivityClassName() { 145 return mInviteContactActivity; 146 } 147 148 @Override 149 protected int getInviteContactActionResId(Context context) { 150 return mInviteActionLabelResId; 151 } 152 153 @Override 154 public String getViewContactNotifyServiceClassName() { 155 return mViewContactNotifyService; 156 } 157 158 @Override 159 public List<String> getExtensionPackageNames() { 160 return mExtensionPackageNames; 161 } 162 163 /** 164 * Inflate this {@link AccountType} from the given parser. This may only 165 * load details matching the publicly-defined schema. 166 */ 167 protected void inflate(Context context, XmlPullParser parser) { 168 final AttributeSet attrs = Xml.asAttributeSet(parser); 169 170 try { 171 int type; 172 while ((type = parser.next()) != XmlPullParser.START_TAG 173 && type != XmlPullParser.END_DOCUMENT) { 174 // Drain comments and whitespace 175 } 176 177 if (type != XmlPullParser.START_TAG) { 178 throw new IllegalStateException("No start tag found"); 179 } 180 181 String rootTag = parser.getName(); 182 if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) && 183 !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) { 184 throw new IllegalStateException("Top level element must be " 185 + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag); 186 } 187 188 int attributeCount = parser.getAttributeCount(); 189 for (int i = 0; i < attributeCount; i++) { 190 String attr = parser.getAttributeName(i); 191 String value = parser.getAttributeValue(i); 192 if (Log.isLoggable(TAG, Log.DEBUG)) { 193 Log.d(TAG, attr + "=" + value); 194 } 195 if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) { 196 mEditContactActivityClassName = value; 197 } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) { 198 mCreateContactActivityClassName = value; 199 } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) { 200 mInviteContactActivity = value; 201 } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) { 202 mInviteActionLabelAttribute = value; 203 } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) { 204 mViewContactNotifyService = value; 205 } else if (ATTR_DATA_SET.equals(attr)) { 206 dataSet = value; 207 } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) { 208 mExtensionPackageNames.add(value); 209 } else if (ATTR_ACCOUNT_TYPE.equals(attr)) { 210 accountType = value; 211 } else if (ATTR_READ_ONLY.equals(attr)) { 212 readOnly = !"0".equals(value) && !"false".equals(value); 213 } else if (ATTR_ACCOUNT_LABEL.equals(attr)) { 214 mAccountTypeLabelAttribute = value; 215 } else if (ATTR_ACCOUNT_ICON.equals(attr)) { 216 mAccountTypeIconAttribute = value; 217 } else { 218 Log.e(TAG, "Unsupported attribute " + attr); 219 } 220 } 221 222 // Parse all children kinds 223 final int depth = parser.getDepth(); 224 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 225 && type != XmlPullParser.END_DOCUMENT) { 226 String tag = parser.getName(); 227 if (type == XmlPullParser.END_TAG || !TAG_CONTACTS_DATA_KIND.equals(tag)) { 228 continue; 229 } 230 231 final TypedArray a = context.obtainStyledAttributes(attrs, 232 android.R.styleable.ContactsDataKind); 233 final DataKind kind = new DataKind(); 234 235 kind.mimeType = a 236 .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType); 237 kind.iconRes = a.getResourceId( 238 com.android.internal.R.styleable.ContactsDataKind_icon, -1); 239 240 final String summaryColumn = a 241 .getString(com.android.internal.R.styleable.ContactsDataKind_summaryColumn); 242 if (summaryColumn != null) { 243 // Inflate a specific column as summary when requested 244 kind.actionHeader = new FallbackAccountType.SimpleInflater(summaryColumn); 245 } 246 247 final String detailColumn = a 248 .getString(com.android.internal.R.styleable.ContactsDataKind_detailColumn); 249 final boolean detailSocialSummary = a.getBoolean( 250 com.android.internal.R.styleable.ContactsDataKind_detailSocialSummary, 251 false); 252 253 if (detailSocialSummary) { 254 // Inflate social summary when requested 255 kind.actionBodySocial = true; 256 } 257 258 if (detailColumn != null) { 259 // Inflate specific column as summary 260 kind.actionBody = new FallbackAccountType.SimpleInflater(detailColumn); 261 } 262 263 addKind(kind); 264 a.recycle(); 265 } 266 } catch (XmlPullParserException e) { 267 throw new IllegalStateException("Problem reading XML", e); 268 } catch (IOException e) { 269 throw new IllegalStateException("Problem reading XML", e); 270 } 271 } 272 273 @Override 274 public int getHeaderColor(Context context) { 275 return 0xff6d86b4; 276 } 277 278 @Override 279 public int getSideBarColor(Context context) { 280 return 0xff6d86b4; 281 } 282 283 /** 284 * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in 285 * the resource package. 286 * 287 * If the argument is in the invalid format or isn't a resource name, it returns -1. 288 * 289 * @param context context 290 * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel" 291 * @param packageName name of the package containing the resource. 292 * @param xmlAttributeName attribute name which the resource came from. Used for logging. 293 */ 294 @VisibleForTesting 295 static int resolveExternalResId(Context context, String resourceName, 296 String packageName, String xmlAttributeName) { 297 if (TextUtils.isEmpty(resourceName)) { 298 return -1; // Empty text is okay. 299 } 300 if (resourceName.charAt(0) != '@') { 301 Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'"); 302 return -1; 303 } 304 final String name = resourceName.substring(1); 305 final Resources res; 306 try { 307 res = context.getPackageManager().getResourcesForApplication(packageName); 308 } catch (NameNotFoundException e) { 309 Log.e(TAG, "Unable to load package " + packageName); 310 return -1; 311 } 312 final int resId = res.getIdentifier(name, null, packageName); 313 if (resId == 0) { 314 Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName); 315 return -1; 316 } 317 return resId; 318 } 319} 320