AccountType.java revision 273399bf829133a8385332ad43add3c34c889102
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
17package com.android.contacts.common.model.account;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.pm.PackageManager;
22import android.graphics.drawable.Drawable;
23import android.provider.ContactsContract.CommonDataKinds.Phone;
24import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
25import android.provider.ContactsContract.Contacts;
26import android.provider.ContactsContract.RawContacts;
27import android.view.inputmethod.EditorInfo;
28import android.widget.EditText;
29
30import com.android.contacts.common.R;
31import com.android.contacts.common.model.dataitem.DataKind;
32import com.google.common.annotations.VisibleForTesting;
33import com.google.common.collect.Lists;
34import com.google.common.collect.Maps;
35
36import java.text.Collator;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.Comparator;
40import java.util.HashMap;
41import 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 */
50public 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 {
100        public DefinitionException(String message) {
101            super(message);
102        }
103
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     */
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     */
125    public boolean isEmbedded() {
126        return true;
127    }
128
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     */
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     */
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     */
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     */
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     */
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     */
191    public String getViewContactNotifyServicePackageName() {
192        return syncAdapterPackageName;
193    }
194
195    /** Returns an optional Activity string that can be used to view the group. */
196    public String getViewGroupActivity() {
197        return null;
198    }
199
200    /** Returns an optional Activity string that can be used to view the stream item. */
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. */
206    public String getViewStreamItemPhotoActivity() {
207        return null;
208    }
209
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     */
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     */
225    protected int getViewGroupLabelResId() {
226        return -1;
227    }
228
229    /**
230     * Returns {@link AccountTypeWithDataSet} for this type.
231     */
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     */
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     */
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     */
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
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
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     */
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     */
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     */
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     */
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
368        public EditType(int rawValue, int labelRes) {
369            this.rawValue = rawValue;
370            this.labelRes = labelRes;
371            this.specificMax = -1;
372        }
373
374        public EditType setSecondary(boolean secondary) {
375            this.secondary = secondary;
376            return this;
377        }
378
379        public EditType setSpecificMax(int specificMax) {
380            this.specificMax = specificMax;
381            return this;
382        }
383
384        public EditType setCustomColumn(String customColumn) {
385            this.customColumn = customColumn;
386            return this;
387        }
388
389        @Override
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
399        public int hashCode() {
400            return rawValue;
401        }
402
403        @Override
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
417        public EventEditType(int rawValue, int labelRes) {
418            super(rawValue, labelRes);
419        }
420
421        public boolean isYearOptional() {
422            return mYearOptional;
423        }
424
425        public EventEditType setYearOptional(boolean yearOptional) {
426            mYearOptional = yearOptional;
427            return this;
428        }
429
430        @Override
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
450        public EditField(String column, int titleRes) {
451            this.column = column;
452            this.titleRes = titleRes;
453        }
454
455        public EditField(String column, int titleRes, int inputType) {
456            this(column, titleRes);
457            this.inputType = inputType;
458        }
459
460        public EditField setOptional(boolean optional) {
461            this.optional = optional;
462            return this;
463        }
464
465        public EditField setShortForm(boolean shortForm) {
466            this.shortForm = shortForm;
467            return this;
468        }
469
470        public EditField setLongForm(boolean longForm) {
471            this.longForm = longForm;
472            return this;
473        }
474
475        public EditField setMinLines(int minLines) {
476            this.minLines = minLines;
477            return this;
478        }
479
480        public boolean isMultiLine() {
481            return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
482        }
483
484
485        @Override
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 {
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
517        public DisplayLabelComparator(Context context) {
518            mContext = context;
519        }
520
521        private String getDisplayLabel(AccountType type) {
522            CharSequence label = type.getDisplayLabel(mContext);
523            return (label == null) ? "" : label.toString();
524        }
525
526        @Override
527        public int compare(AccountType lhs, AccountType rhs) {
528            return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
529        }
530    }
531}
532