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.account; 18 19 import android.accounts.AuthenticatorDescription; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.graphics.drawable.Drawable; 24 import android.provider.ContactsContract.CommonDataKinds.Phone; 25 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 26 import android.provider.ContactsContract.Contacts; 27 import android.provider.ContactsContract.RawContacts; 28 import android.view.inputmethod.EditorInfo; 29 import android.widget.EditText; 30 31 import com.android.contacts.R; 32 import com.android.contacts.model.dataitem.DataKind; 33 34 import com.google.common.base.Preconditions; 35 import com.google.common.annotations.VisibleForTesting; 36 import com.google.common.base.Objects; 37 import com.google.common.collect.Lists; 38 import com.google.common.collect.Maps; 39 40 import java.text.Collator; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.HashMap; 45 import java.util.List; 46 47 /** 48 * Internal structure that represents constraints and styles for a specific data 49 * source, such as the various data types they support, including details on how 50 * those types should be rendered and edited. 51 * <p> 52 * In the future this may be inflated from XML defined by a data source. 53 */ 54 public abstract class AccountType { 55 private static final String TAG = "AccountType"; 56 57 /** 58 * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. 59 */ 60 public String accountType = null; 61 62 /** 63 * The {@link RawContacts#DATA_SET} these constraints apply to. 64 */ 65 public String dataSet = null; 66 67 /** 68 * Package that resources should be loaded from. Will be null for embedded types, in which 69 * case resources are stored in this package itself. 70 * 71 * TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and 72 * {@link #getViewContactNotifyServicePackageName()}. 73 * 74 * There's the following invariants: 75 * - {@link #syncAdapterPackageName} is always set to the actual sync adapter package name. 76 * - {@link #resourcePackageName} too is set to the same value, unless {@link #isEmbedded()}, 77 * in which case it'll be null. 78 * There's an unfortunate exception of {@link FallbackAccountType}. Even though it 79 * {@link #isEmbedded()}, but we set non-null to {@link #resourcePackageName} for unit tests. 80 */ 81 public String resourcePackageName; 82 /** 83 * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) 84 * or the sync adapter (for external type, including extensions). 85 */ 86 public String syncAdapterPackageName; 87 88 public int titleRes; 89 public int iconRes; 90 91 /** 92 * Set of {@link DataKind} supported by this source. 93 */ 94 private ArrayList<DataKind> mKinds = Lists.newArrayList(); 95 96 /** 97 * Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. 98 */ 99 private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap(); 100 101 protected boolean mIsInitialized; 102 103 protected static class DefinitionException extends Exception { 104 public DefinitionException(String message) { 105 super(message); 106 } 107 108 public DefinitionException(String message, Exception inner) { 109 super(message, inner); 110 } 111 } 112 113 /** 114 * Whether this account type was able to be fully initialized. This may be false if 115 * (for example) the package name associated with the account type could not be found. 116 */ 117 public final boolean isInitialized() { 118 return mIsInitialized; 119 } 120 121 /** 122 * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType}, 123 * {@link GoogleAccountType} or {@link ExternalAccountType}. 124 * 125 * If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns 126 * {@code false}) it's considered critical, and the application will crash. On the other 127 * hand if it's not an embedded type, we just skip loading the type. 128 */ 129 public boolean isEmbedded() { 130 return true; 131 } 132 133 public boolean isExtension() { 134 return false; 135 } 136 137 /** 138 * @return True if contacts can be created and edited using this app. If false, 139 * there could still be an external editor as provided by 140 * {@link #getEditContactActivityClassName()} or {@link #getCreateContactActivityClassName()} 141 */ 142 public abstract boolean areContactsWritable(); 143 144 /** 145 * Returns an optional custom invite contact activity. 146 * 147 * Only makes sense for non-embedded account types. 148 * The activity class should reside in the sync adapter package as determined by 149 * {@link #syncAdapterPackageName}. 150 */ 151 public String getInviteContactActivityClassName() { 152 return null; 153 } 154 155 /** 156 * Returns an optional service that can be launched whenever a contact is being looked at. 157 * This allows the sync adapter to provide more up-to-date information. 158 * 159 * The service class should reside in the sync adapter package as determined by 160 * {@link #getViewContactNotifyServicePackageName()}. 161 */ 162 public String getViewContactNotifyServiceClassName() { 163 return null; 164 } 165 166 /** 167 * TODO This is way too hacky should be removed. 168 * 169 * This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} 170 * is the authenticator package name but the notification service is in the sync adapter 171 * package. See {@link #resourcePackageName} -- we should clean up those. 172 */ 173 public String getViewContactNotifyServicePackageName() { 174 return syncAdapterPackageName; 175 } 176 177 /** Returns an optional Activity string that can be used to view the group. */ 178 public String getViewGroupActivity() { 179 return null; 180 } 181 182 public CharSequence getDisplayLabel(Context context) { 183 // Note this resource is defined in the sync adapter package, not resourcePackageName. 184 return getResourceText(context, syncAdapterPackageName, titleRes, accountType); 185 } 186 187 /** 188 * Creates an {@link AccountInfo} for the specified account with the same type 189 * 190 * <p>The {@link AccountWithDataSet#type} must match {@link #accountType} of this instance</p> 191 */ 192 public AccountInfo wrapAccount(Context context, AccountWithDataSet account) { 193 Preconditions.checkArgument(Objects.equal(account.type, accountType), 194 "Account types must match: account.type=%s but accountType=%s", 195 account.type, accountType); 196 197 return new AccountInfo( 198 new AccountDisplayInfo(account, account.name, 199 getDisplayLabel(context), getDisplayIcon(context), false), this); 200 } 201 202 /** 203 * @return resource ID for the "invite contact" action label, or -1 if not defined. 204 */ 205 protected int getInviteContactActionResId() { 206 return -1; 207 } 208 209 /** 210 * @return resource ID for the "view group" label, or -1 if not defined. 211 */ 212 protected int getViewGroupLabelResId() { 213 return -1; 214 } 215 216 /** 217 * Returns {@link AccountTypeWithDataSet} for this type. 218 */ 219 public AccountTypeWithDataSet getAccountTypeAndDataSet() { 220 return AccountTypeWithDataSet.get(accountType, dataSet); 221 } 222 223 /** 224 * Returns a list of additional package names that should be inspected as additional 225 * external account types. This allows for a primary account type to indicate other packages 226 * that may not be sync adapters but which still provide contact data, perhaps under a 227 * separate data set within the account. 228 */ 229 public List<String> getExtensionPackageNames() { 230 return new ArrayList<String>(); 231 } 232 233 /** 234 * Returns an optional custom label for the "invite contact" action, which will be shown on 235 * the contact card. (If not defined, returns null.) 236 */ 237 public CharSequence getInviteContactActionLabel(Context context) { 238 // Note this resource is defined in the sync adapter package, not resourcePackageName. 239 return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), ""); 240 } 241 242 /** 243 * Returns a label for the "view group" action. If not defined, this falls back to our 244 * own "View Updates" string 245 */ 246 public CharSequence getViewGroupLabel(Context context) { 247 // Note this resource is defined in the sync adapter package, not resourcePackageName. 248 final CharSequence customTitle = 249 getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null); 250 251 return customTitle == null 252 ? context.getText(R.string.view_updates_from_group) 253 : customTitle; 254 } 255 256 /** 257 * Return a string resource loaded from the given package (or the current package 258 * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns 259 * {@code defaultValue}. 260 * 261 * (The behavior is undefined if the resource or package doesn't exist.) 262 */ 263 @VisibleForTesting 264 static CharSequence getResourceText(Context context, String packageName, int resId, 265 String defaultValue) { 266 if (resId != -1 && packageName != null) { 267 final PackageManager pm = context.getPackageManager(); 268 return pm.getText(packageName, resId, null); 269 } else if (resId != -1) { 270 return context.getText(resId); 271 } else { 272 return defaultValue; 273 } 274 } 275 276 public Drawable getDisplayIcon(Context context) { 277 return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName); 278 } 279 280 public static Drawable getDisplayIcon(Context context, int titleRes, int iconRes, 281 String syncAdapterPackageName) { 282 if (titleRes != -1 && syncAdapterPackageName != null) { 283 final PackageManager pm = context.getPackageManager(); 284 return pm.getDrawable(syncAdapterPackageName, iconRes, null); 285 } else if (titleRes != -1) { 286 return context.getResources().getDrawable(iconRes); 287 } else { 288 return null; 289 } 290 } 291 292 /** 293 * Whether or not groups created under this account type have editable membership lists. 294 */ 295 abstract public boolean isGroupMembershipEditable(); 296 297 /** 298 * {@link Comparator} to sort by {@link DataKind#weight}. 299 */ 300 private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() { 301 @Override 302 public int compare(DataKind object1, DataKind object2) { 303 return object1.weight - object2.weight; 304 } 305 }; 306 307 /** 308 * Return list of {@link DataKind} supported, sorted by 309 * {@link DataKind#weight}. 310 */ 311 public ArrayList<DataKind> getSortedDataKinds() { 312 // TODO: optimize by marking if already sorted 313 Collections.sort(mKinds, sWeightComparator); 314 return mKinds; 315 } 316 317 /** 318 * Find the {@link DataKind} for a specific MIME-type, if it's handled by 319 * this data source. 320 */ 321 public DataKind getKindForMimetype(String mimeType) { 322 return this.mMimeKinds.get(mimeType); 323 } 324 325 public void initializeFieldsFromAuthenticator(AuthenticatorDescription authenticator) { 326 accountType = authenticator.type; 327 titleRes = authenticator.labelId; 328 iconRes = authenticator.iconId; 329 } 330 331 /** 332 * Add given {@link DataKind} to list of those provided by this source. 333 */ 334 public DataKind addKind(DataKind kind) throws DefinitionException { 335 if (kind.mimeType == null) { 336 throw new DefinitionException("null is not a valid mime type"); 337 } 338 if (mMimeKinds.get(kind.mimeType) != null) { 339 throw new DefinitionException( 340 "mime type '" + kind.mimeType + "' is already registered"); 341 } 342 343 kind.resourcePackageName = this.resourcePackageName; 344 this.mKinds.add(kind); 345 this.mMimeKinds.put(kind.mimeType, kind); 346 return kind; 347 } 348 349 /** 350 * Description of a specific "type" or "label" of a {@link DataKind} row, 351 * such as {@link Phone#TYPE_WORK}. Includes constraints on total number of 352 * rows a {@link Contacts} may have of this type, and details on how 353 * user-defined labels are stored. 354 */ 355 public static class EditType { 356 public int rawValue; 357 public int labelRes; 358 public boolean secondary; 359 /** 360 * The number of entries allowed for the type. -1 if not specified. 361 * @see DataKind#typeOverallMax 362 */ 363 public int specificMax; 364 public String customColumn; 365 366 public EditType(int rawValue, int labelRes) { 367 this.rawValue = rawValue; 368 this.labelRes = labelRes; 369 this.specificMax = -1; 370 } 371 372 public EditType setSecondary(boolean secondary) { 373 this.secondary = secondary; 374 return this; 375 } 376 377 public EditType setSpecificMax(int specificMax) { 378 this.specificMax = specificMax; 379 return this; 380 } 381 382 public EditType setCustomColumn(String customColumn) { 383 this.customColumn = customColumn; 384 return this; 385 } 386 387 @Override 388 public boolean equals(Object object) { 389 if (object instanceof EditType) { 390 final EditType other = (EditType)object; 391 return other.rawValue == rawValue; 392 } 393 return false; 394 } 395 396 @Override 397 public int hashCode() { 398 return rawValue; 399 } 400 401 @Override 402 public String toString() { 403 return this.getClass().getSimpleName() 404 + " rawValue=" + rawValue 405 + " labelRes=" + labelRes 406 + " secondary=" + secondary 407 + " specificMax=" + specificMax 408 + " customColumn=" + customColumn; 409 } 410 } 411 412 public static class EventEditType extends EditType { 413 private boolean mYearOptional; 414 415 public EventEditType(int rawValue, int labelRes) { 416 super(rawValue, labelRes); 417 } 418 419 public boolean isYearOptional() { 420 return mYearOptional; 421 } 422 423 public EventEditType setYearOptional(boolean yearOptional) { 424 mYearOptional = yearOptional; 425 return this; 426 } 427 428 @Override 429 public String toString() { 430 return super.toString() + " mYearOptional=" + mYearOptional; 431 } 432 } 433 434 /** 435 * Description of a user-editable field on a {@link DataKind} row, such as 436 * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and 437 * the column where this field is stored. 438 */ 439 public static final class EditField { 440 public String column; 441 public int titleRes; 442 public int inputType; 443 public int minLines; 444 public boolean optional; 445 public boolean shortForm; 446 public boolean longForm; 447 448 public EditField(String column, int titleRes) { 449 this.column = column; 450 this.titleRes = titleRes; 451 } 452 453 public EditField(String column, int titleRes, int inputType) { 454 this(column, titleRes); 455 this.inputType = inputType; 456 } 457 458 public EditField setOptional(boolean optional) { 459 this.optional = optional; 460 return this; 461 } 462 463 public EditField setShortForm(boolean shortForm) { 464 this.shortForm = shortForm; 465 return this; 466 } 467 468 public EditField setLongForm(boolean longForm) { 469 this.longForm = longForm; 470 return this; 471 } 472 473 public EditField setMinLines(int minLines) { 474 this.minLines = minLines; 475 return this; 476 } 477 478 public boolean isMultiLine() { 479 return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0; 480 } 481 482 483 @Override 484 public String toString() { 485 return this.getClass().getSimpleName() + ":" 486 + " column=" + column 487 + " titleRes=" + titleRes 488 + " inputType=" + inputType 489 + " minLines=" + minLines 490 + " optional=" + optional 491 + " shortForm=" + shortForm 492 + " longForm=" + longForm; 493 } 494 } 495 496 /** 497 * Generic method of inflating a given {@link ContentValues} into a user-readable 498 * {@link CharSequence}. For example, an inflater could combine the multiple 499 * columns of {@link StructuredPostal} together using a string resource 500 * before presenting to the user. 501 */ 502 public interface StringInflater { 503 public CharSequence inflateUsing(Context context, ContentValues values); 504 } 505 506 /** 507 * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the 508 * current locale. 509 */ 510 public static class DisplayLabelComparator implements Comparator<AccountType> { 511 private final Context mContext; 512 /** {@link Comparator} for the current locale. */ 513 private final Collator mCollator = Collator.getInstance(); 514 515 public DisplayLabelComparator(Context context) { 516 mContext = context; 517 } 518 519 private String getDisplayLabel(AccountType type) { 520 CharSequence label = type.getDisplayLabel(mContext); 521 return (label == null) ? "" : label.toString(); 522 } 523 524 @Override 525 public int compare(AccountType lhs, AccountType rhs) { 526 return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs)); 527 } 528 } 529 } 530