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