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