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