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.accounts.Account; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.pm.PackageManager; 28 import android.database.Cursor; 29 import android.graphics.drawable.Drawable; 30 import android.provider.ContactsContract.CommonDataKinds.Phone; 31 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 32 import android.provider.ContactsContract.Contacts; 33 import android.provider.ContactsContract.Data; 34 import android.provider.ContactsContract.RawContacts; 35 import android.view.inputmethod.EditorInfo; 36 import android.widget.EditText; 37 38 import java.text.Collator; 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.Comparator; 42 import java.util.HashMap; 43 import 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 */ 52 public 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 public boolean isExtension() { 86 return false; 87 } 88 89 /** 90 * @return True if contacts can be created and edited using this app. If false, 91 * there could still be an external editor as provided by 92 * {@link #getEditContactActivityClassName()} or {@link #getCreateContactActivityClassName()} 93 */ 94 public abstract boolean areContactsWritable(); 95 96 /** 97 * Returns an optional custom edit activity. The activity class should reside 98 * in the sync adapter package as determined by {@link #resPackageName}. 99 */ 100 public String getEditContactActivityClassName() { 101 return null; 102 } 103 104 /** 105 * Returns an optional custom new contact activity. The activity class should reside 106 * in the sync adapter package as determined by {@link #resPackageName}. 107 */ 108 public String getCreateContactActivityClassName() { 109 return null; 110 } 111 112 /** 113 * Returns an optional custom invite contact activity. The activity class should reside 114 * in the sync adapter package as determined by {@link #resPackageName}. 115 */ 116 public String getInviteContactActivityClassName() { 117 return null; 118 } 119 120 /** 121 * Returns an optional service that can be launched whenever a contact is being looked at. 122 * This allows the sync adapter to provide more up-to-date information. 123 * The service class should reside in the sync adapter package as determined by 124 * {@link #resPackageName}. 125 */ 126 public String getViewContactNotifyServiceClassName() { 127 return null; 128 } 129 130 /** Returns an optional Activity string that can be used to view the group. */ 131 public String getViewGroupActivity() { 132 return null; 133 } 134 135 /** Returns an optional Activity string that can be used to view the stream item. */ 136 public String getViewStreamItemActivity() { 137 return null; 138 } 139 140 /** Returns an optional Activity string that can be used to view the stream item photo. */ 141 public String getViewStreamItemPhotoActivity() { 142 return null; 143 } 144 145 public CharSequence getDisplayLabel(Context context) { 146 return getResourceText(context, summaryResPackageName, titleRes, accountType); 147 } 148 149 /** 150 * @return resource ID for the "invite contact" action label, or -1 if not defined. 151 */ 152 protected int getInviteContactActionResId() { 153 return -1; 154 } 155 156 /** 157 * @return resource ID for the "view group" label, or -1 if not defined. 158 */ 159 protected int getViewGroupLabelResId() { 160 return -1; 161 } 162 163 /** 164 * Returns {@link AccountTypeWithDataSet} for this type. 165 */ 166 public AccountTypeWithDataSet getAccountTypeAndDataSet() { 167 return AccountTypeWithDataSet.get(accountType, dataSet); 168 } 169 170 /** 171 * Returns a list of additional package names that should be inspected as additional 172 * external account types. This allows for a primary account type to indicate other packages 173 * that may not be sync adapters but which still provide contact data, perhaps under a 174 * separate data set within the account. 175 */ 176 public List<String> getExtensionPackageNames() { 177 return new ArrayList<String>(); 178 } 179 180 /** 181 * Returns an optional custom label for the "invite contact" action, which will be shown on 182 * the contact card. (If not defined, returns null.) 183 */ 184 public CharSequence getInviteContactActionLabel(Context context) { 185 return getResourceText(context, summaryResPackageName, getInviteContactActionResId(), ""); 186 } 187 188 /** 189 * Returns a label for the "view group" action. If not defined, this falls back to our 190 * own "View Updates" string 191 */ 192 public CharSequence getViewGroupLabel(Context context) { 193 final CharSequence customTitle = 194 getResourceText(context, summaryResPackageName, getViewGroupLabelResId(), null); 195 196 return customTitle == null 197 ? context.getText(R.string.view_updates_from_group) 198 : customTitle; 199 } 200 201 /** 202 * Return a string resource loaded from the given package (or the current package 203 * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns 204 * {@code defaultValue}. 205 * 206 * (The behavior is undefined if the resource or package doesn't exist.) 207 */ 208 @VisibleForTesting 209 static CharSequence getResourceText(Context context, String packageName, int resId, 210 String defaultValue) { 211 if (resId != -1 && packageName != null) { 212 final PackageManager pm = context.getPackageManager(); 213 return pm.getText(packageName, resId, null); 214 } else if (resId != -1) { 215 return context.getText(resId); 216 } else { 217 return defaultValue; 218 } 219 } 220 221 public Drawable getDisplayIcon(Context context) { 222 if (this.titleRes != -1 && this.summaryResPackageName != null) { 223 final PackageManager pm = context.getPackageManager(); 224 return pm.getDrawable(this.summaryResPackageName, this.iconRes, null); 225 } else if (this.titleRes != -1) { 226 return context.getResources().getDrawable(this.iconRes); 227 } else { 228 return null; 229 } 230 } 231 232 /** 233 * Whether or not groups created under this account type have editable membership lists. 234 */ 235 abstract public boolean isGroupMembershipEditable(); 236 237 abstract public int getHeaderColor(Context context); 238 239 abstract public int getSideBarColor(Context context); 240 241 /** 242 * {@link Comparator} to sort by {@link DataKind#weight}. 243 */ 244 private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() { 245 public int compare(DataKind object1, DataKind object2) { 246 return object1.weight - object2.weight; 247 } 248 }; 249 250 /** 251 * Return list of {@link DataKind} supported, sorted by 252 * {@link DataKind#weight}. 253 */ 254 public ArrayList<DataKind> getSortedDataKinds() { 255 // TODO: optimize by marking if already sorted 256 Collections.sort(mKinds, sWeightComparator); 257 return mKinds; 258 } 259 260 /** 261 * Find the {@link DataKind} for a specific MIME-type, if it's handled by 262 * this data source. If you may need a fallback {@link DataKind}, use 263 * {@link AccountTypeManager#getKindOrFallback(String, String, String)}. 264 */ 265 public DataKind getKindForMimetype(String mimeType) { 266 return this.mMimeKinds.get(mimeType); 267 } 268 269 /** 270 * Add given {@link DataKind} to list of those provided by this source. 271 */ 272 public DataKind addKind(DataKind kind) { 273 kind.resPackageName = this.resPackageName; 274 this.mKinds.add(kind); 275 this.mMimeKinds.put(kind.mimeType, kind); 276 return kind; 277 } 278 279 /** 280 * Description of a specific "type" or "label" of a {@link DataKind} row, 281 * such as {@link Phone#TYPE_WORK}. Includes constraints on total number of 282 * rows a {@link Contacts} may have of this type, and details on how 283 * user-defined labels are stored. 284 */ 285 public static class EditType { 286 public int rawValue; 287 public int labelRes; 288 public boolean secondary; 289 /** 290 * The number of entries allowed for the type. -1 if not specified. 291 * @see DataKind#typeOverallMax 292 */ 293 public int specificMax; 294 public String customColumn; 295 296 public EditType(int rawValue, int labelRes) { 297 this.rawValue = rawValue; 298 this.labelRes = labelRes; 299 this.specificMax = -1; 300 } 301 302 public EditType setSecondary(boolean secondary) { 303 this.secondary = secondary; 304 return this; 305 } 306 307 public EditType setSpecificMax(int specificMax) { 308 this.specificMax = specificMax; 309 return this; 310 } 311 312 public EditType setCustomColumn(String customColumn) { 313 this.customColumn = customColumn; 314 return this; 315 } 316 317 @Override 318 public boolean equals(Object object) { 319 if (object instanceof EditType) { 320 final EditType other = (EditType)object; 321 return other.rawValue == rawValue; 322 } 323 return false; 324 } 325 326 @Override 327 public int hashCode() { 328 return rawValue; 329 } 330 } 331 332 public static class EventEditType extends EditType { 333 private boolean mYearOptional; 334 335 public EventEditType(int rawValue, int labelRes) { 336 super(rawValue, labelRes); 337 } 338 339 public boolean isYearOptional() { 340 return mYearOptional; 341 } 342 343 public EventEditType setYearOptional(boolean yearOptional) { 344 mYearOptional = yearOptional; 345 return this; 346 } 347 } 348 349 /** 350 * Description of a user-editable field on a {@link DataKind} row, such as 351 * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and 352 * the column where this field is stored. 353 */ 354 public static class EditField { 355 public String column; 356 public int titleRes; 357 public int inputType; 358 public int minLines; 359 public boolean optional; 360 public boolean shortForm; 361 public boolean longForm; 362 363 public EditField(String column, int titleRes) { 364 this.column = column; 365 this.titleRes = titleRes; 366 } 367 368 public EditField(String column, int titleRes, int inputType) { 369 this(column, titleRes); 370 this.inputType = inputType; 371 } 372 373 public EditField setOptional(boolean optional) { 374 this.optional = optional; 375 return this; 376 } 377 378 public EditField setShortForm(boolean shortForm) { 379 this.shortForm = shortForm; 380 return this; 381 } 382 383 public EditField setLongForm(boolean longForm) { 384 this.longForm = longForm; 385 return this; 386 } 387 388 public EditField setMinLines(int minLines) { 389 this.minLines = minLines; 390 return this; 391 } 392 393 public boolean isMultiLine() { 394 return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0; 395 } 396 } 397 398 /** 399 * Generic method of inflating a given {@link Cursor} into a user-readable 400 * {@link CharSequence}. For example, an inflater could combine the multiple 401 * columns of {@link StructuredPostal} together using a string resource 402 * before presenting to the user. 403 */ 404 public interface StringInflater { 405 public CharSequence inflateUsing(Context context, Cursor cursor); 406 public CharSequence inflateUsing(Context context, ContentValues values); 407 } 408 409 /** 410 * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the 411 * current locale. 412 */ 413 public static class DisplayLabelComparator implements Comparator<AccountType> { 414 private final Context mContext; 415 /** {@link Comparator} for the current locale. */ 416 private final Collator mCollator = Collator.getInstance(); 417 418 public DisplayLabelComparator(Context context) { 419 mContext = context; 420 } 421 422 private String getDisplayLabel(AccountType type) { 423 CharSequence label = type.getDisplayLabel(mContext); 424 return (label == null) ? "" : label.toString(); 425 } 426 427 @Override 428 public int compare(AccountType lhs, AccountType rhs) { 429 return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs)); 430 } 431 } 432 } 433