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