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