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