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