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