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