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