• 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.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.content.ContentValues;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.graphics.drawable.Drawable;
28 import android.provider.ContactsContract.CommonDataKinds.Phone;
29 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
30 import android.provider.ContactsContract.Contacts;
31 import android.provider.ContactsContract.RawContacts;
32 import android.view.inputmethod.EditorInfo;
33 import android.widget.EditText;
34 
35 import java.text.Collator;
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.HashMap;
40 import java.util.List;
41 
42 /**
43  * Internal structure that represents constraints and styles for a specific data
44  * source, such as the various data types they support, including details on how
45  * those types should be rendered and edited.
46  * <p>
47  * In the future this may be inflated from XML defined by a data source.
48  */
49 public abstract class AccountType {
50     private static final String TAG = "AccountType";
51 
52     /**
53      * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
54      */
55     public String accountType = null;
56 
57     /**
58      * The {@link RawContacts#DATA_SET} these constraints apply to.
59      */
60     public String dataSet = null;
61 
62     /**
63      * Package that resources should be loaded from.  Will be null for embedded types, in which
64      * case resources are stored in this package itself.
65      *
66      * TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and
67      * {@link #getViewContactNotifyServicePackageName()}.
68      *
69      * There's the following invariants:
70      * - {@link #syncAdapterPackageName} is always set to the actual sync adapter package name.
71      * - {@link #resourcePackageName} too is set to the same value, unless {@link #isEmbedded()},
72      *   in which case it'll be null.
73      * There's an unfortunate exception of {@link FallbackAccountType}.  Even though it
74      * {@link #isEmbedded()}, but we set non-null to {@link #resourcePackageName} for unit tests.
75      */
76     public String resourcePackageName;
77     /**
78      * The package name for the authenticator (for the embedded types, i.e. Google and Exchange)
79      * or the sync adapter (for external type, including extensions).
80      */
81     public String syncAdapterPackageName;
82 
83     public int titleRes;
84     public int iconRes;
85 
86     /**
87      * Set of {@link DataKind} supported by this source.
88      */
89     private ArrayList<DataKind> mKinds = Lists.newArrayList();
90 
91     /**
92      * Lookup map of {@link #mKinds} on {@link DataKind#mimeType}.
93      */
94     private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap();
95 
96     protected boolean mIsInitialized;
97 
98     protected static class DefinitionException extends Exception {
DefinitionException(String message)99         public DefinitionException(String message) {
100             super(message);
101         }
102 
DefinitionException(String message, Exception inner)103         public DefinitionException(String message, Exception inner) {
104             super(message, inner);
105         }
106     }
107 
108     /**
109      * Whether this account type was able to be fully initialized.  This may be false if
110      * (for example) the package name associated with the account type could not be found.
111      */
isInitialized()112     public final boolean isInitialized() {
113         return mIsInitialized;
114     }
115 
116     /**
117      * @return Whether this type is an "embedded" type.  i.e. any of {@link FallbackAccountType},
118      * {@link GoogleAccountType} or {@link ExternalAccountType}.
119      *
120      * If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
121      * {@code false}) it's considered critical, and the application will crash.  On the other
122      * hand if it's not an embedded type, we just skip loading the type.
123      */
isEmbedded()124     public boolean isEmbedded() {
125         return true;
126     }
127 
isExtension()128     public boolean isExtension() {
129         return false;
130     }
131 
132     /**
133      * @return True if contacts can be created and edited using this app. If false,
134      * there could still be an external editor as provided by
135      * {@link #getEditContactActivityClassName()} or {@link #getCreateContactActivityClassName()}
136      */
areContactsWritable()137     public abstract boolean areContactsWritable();
138 
139     /**
140      * Returns an optional custom edit activity.
141      *
142      * Only makes sense for non-embedded account types.
143      * The activity class should reside in the sync adapter package as determined by
144      * {@link #syncAdapterPackageName}.
145      */
getEditContactActivityClassName()146     public String getEditContactActivityClassName() {
147         return null;
148     }
149 
150     /**
151      * Returns an optional custom new contact activity.
152      *
153      * Only makes sense for non-embedded account types.
154      * The activity class should reside in the sync adapter package as determined by
155      * {@link #syncAdapterPackageName}.
156      */
getCreateContactActivityClassName()157     public String getCreateContactActivityClassName() {
158         return null;
159     }
160 
161     /**
162      * Returns an optional custom invite contact activity.
163      *
164      * Only makes sense for non-embedded account types.
165      * The activity class should reside in the sync adapter package as determined by
166      * {@link #syncAdapterPackageName}.
167      */
getInviteContactActivityClassName()168     public String getInviteContactActivityClassName() {
169         return null;
170     }
171 
172     /**
173      * Returns an optional service that can be launched whenever a contact is being looked at.
174      * This allows the sync adapter to provide more up-to-date information.
175      *
176      * The service class should reside in the sync adapter package as determined by
177      * {@link #getViewContactNotifyServicePackageName()}.
178      */
getViewContactNotifyServiceClassName()179     public String getViewContactNotifyServiceClassName() {
180         return null;
181     }
182 
183     /**
184      * TODO This is way too hacky should be removed.
185      *
186      * This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName}
187      * is the authenticator package name but the notification service is in the sync adapter
188      * package.  See {@link #resourcePackageName} -- we should clean up those.
189      */
getViewContactNotifyServicePackageName()190     public String getViewContactNotifyServicePackageName() {
191         return syncAdapterPackageName;
192     }
193 
194     /** Returns an optional Activity string that can be used to view the group. */
getViewGroupActivity()195     public String getViewGroupActivity() {
196         return null;
197     }
198 
199     /** Returns an optional Activity string that can be used to view the stream item. */
getViewStreamItemActivity()200     public String getViewStreamItemActivity() {
201         return null;
202     }
203 
204     /** Returns an optional Activity string that can be used to view the stream item photo. */
getViewStreamItemPhotoActivity()205     public String getViewStreamItemPhotoActivity() {
206         return null;
207     }
208 
getDisplayLabel(Context context)209     public CharSequence getDisplayLabel(Context context) {
210         // Note this resource is defined in the sync adapter package, not resourcePackageName.
211         return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
212     }
213 
214     /**
215      * @return resource ID for the "invite contact" action label, or -1 if not defined.
216      */
getInviteContactActionResId()217     protected int getInviteContactActionResId() {
218         return -1;
219     }
220 
221     /**
222      * @return resource ID for the "view group" label, or -1 if not defined.
223      */
getViewGroupLabelResId()224     protected int getViewGroupLabelResId() {
225         return -1;
226     }
227 
228     /**
229      * Returns {@link AccountTypeWithDataSet} for this type.
230      */
getAccountTypeAndDataSet()231     public AccountTypeWithDataSet getAccountTypeAndDataSet() {
232         return AccountTypeWithDataSet.get(accountType, dataSet);
233     }
234 
235     /**
236      * Returns a list of additional package names that should be inspected as additional
237      * external account types.  This allows for a primary account type to indicate other packages
238      * that may not be sync adapters but which still provide contact data, perhaps under a
239      * separate data set within the account.
240      */
getExtensionPackageNames()241     public List<String> getExtensionPackageNames() {
242         return new ArrayList<String>();
243     }
244 
245     /**
246      * Returns an optional custom label for the "invite contact" action, which will be shown on
247      * the contact card.  (If not defined, returns null.)
248      */
getInviteContactActionLabel(Context context)249     public CharSequence getInviteContactActionLabel(Context context) {
250         // Note this resource is defined in the sync adapter package, not resourcePackageName.
251         return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
252     }
253 
254     /**
255      * Returns a label for the "view group" action. If not defined, this falls back to our
256      * own "View Updates" string
257      */
getViewGroupLabel(Context context)258     public CharSequence getViewGroupLabel(Context context) {
259         // Note this resource is defined in the sync adapter package, not resourcePackageName.
260         final CharSequence customTitle =
261                 getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
262 
263         return customTitle == null
264                 ? context.getText(R.string.view_updates_from_group)
265                 : customTitle;
266     }
267 
268     /**
269      * Return a string resource loaded from the given package (or the current package
270      * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns
271      * {@code defaultValue}.
272      *
273      * (The behavior is undefined if the resource or package doesn't exist.)
274      */
275     @VisibleForTesting
getResourceText(Context context, String packageName, int resId, String defaultValue)276     static CharSequence getResourceText(Context context, String packageName, int resId,
277             String defaultValue) {
278         if (resId != -1 && packageName != null) {
279             final PackageManager pm = context.getPackageManager();
280             return pm.getText(packageName, resId, null);
281         } else if (resId != -1) {
282             return context.getText(resId);
283         } else {
284             return defaultValue;
285         }
286     }
287 
getDisplayIcon(Context context)288     public Drawable getDisplayIcon(Context context) {
289         if (this.titleRes != -1 && this.syncAdapterPackageName != null) {
290             final PackageManager pm = context.getPackageManager();
291             return pm.getDrawable(this.syncAdapterPackageName, this.iconRes, null);
292         } else if (this.titleRes != -1) {
293             return context.getResources().getDrawable(this.iconRes);
294         } else {
295             return null;
296         }
297     }
298 
299     /**
300      * Whether or not groups created under this account type have editable membership lists.
301      */
isGroupMembershipEditable()302     abstract public boolean isGroupMembershipEditable();
303 
304     /**
305      * {@link Comparator} to sort by {@link DataKind#weight}.
306      */
307     private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() {
308         @Override
309         public int compare(DataKind object1, DataKind object2) {
310             return object1.weight - object2.weight;
311         }
312     };
313 
314     /**
315      * Return list of {@link DataKind} supported, sorted by
316      * {@link DataKind#weight}.
317      */
getSortedDataKinds()318     public ArrayList<DataKind> getSortedDataKinds() {
319         // TODO: optimize by marking if already sorted
320         Collections.sort(mKinds, sWeightComparator);
321         return mKinds;
322     }
323 
324     /**
325      * Find the {@link DataKind} for a specific MIME-type, if it's handled by
326      * this data source. If you may need a fallback {@link DataKind}, use
327      * {@link AccountTypeManager#getKindOrFallback(String, String, String)}.
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