Home | History | Annotate | Download | only in account
      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.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.R;
     31 import com.android.contacts.model.AccountTypeManager;
     32 import com.android.contacts.model.dataitem.DataKind;
     33 import com.google.common.annotations.VisibleForTesting;
     34 import com.google.common.collect.Lists;
     35 import com.google.common.collect.Maps;
     36 
     37 import java.text.Collator;
     38 import java.util.ArrayList;
     39 import java.util.Collections;
     40 import java.util.Comparator;
     41 import java.util.HashMap;
     42 import 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  */
     51 public 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     /**
    142      * Returns an optional custom edit 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 getEditContactActivityClassName() {
    149         return null;
    150     }
    151 
    152     /**
    153      * Returns an optional custom new contact activity.
    154      *
    155      * Only makes sense for non-embedded account types.
    156      * The activity class should reside in the sync adapter package as determined by
    157      * {@link #syncAdapterPackageName}.
    158      */
    159     public String getCreateContactActivityClassName() {
    160         return null;
    161     }
    162 
    163     /**
    164      * Returns an optional custom invite contact activity.
    165      *
    166      * Only makes sense for non-embedded account types.
    167      * The activity class should reside in the sync adapter package as determined by
    168      * {@link #syncAdapterPackageName}.
    169      */
    170     public String getInviteContactActivityClassName() {
    171         return null;
    172     }
    173 
    174     /**
    175      * Returns an optional service that can be launched whenever a contact is being looked at.
    176      * This allows the sync adapter to provide more up-to-date information.
    177      *
    178      * The service class should reside in the sync adapter package as determined by
    179      * {@link #getViewContactNotifyServicePackageName()}.
    180      */
    181     public String getViewContactNotifyServiceClassName() {
    182         return null;
    183     }
    184 
    185     /**
    186      * TODO This is way too hacky should be removed.
    187      *
    188      * This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName}
    189      * is the authenticator package name but the notification service is in the sync adapter
    190      * package.  See {@link #resourcePackageName} -- we should clean up those.
    191      */
    192     public String getViewContactNotifyServicePackageName() {
    193         return syncAdapterPackageName;
    194     }
    195 
    196     /** Returns an optional Activity string that can be used to view the group. */
    197     public String getViewGroupActivity() {
    198         return null;
    199     }
    200 
    201     /** Returns an optional Activity string that can be used to view the stream item. */
    202     public String getViewStreamItemActivity() {
    203         return null;
    204     }
    205 
    206     /** Returns an optional Activity string that can be used to view the stream item photo. */
    207     public String getViewStreamItemPhotoActivity() {
    208         return null;
    209     }
    210 
    211     public CharSequence getDisplayLabel(Context context) {
    212         // Note this resource is defined in the sync adapter package, not resourcePackageName.
    213         return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
    214     }
    215 
    216     /**
    217      * @return resource ID for the "invite contact" action label, or -1 if not defined.
    218      */
    219     protected int getInviteContactActionResId() {
    220         return -1;
    221     }
    222 
    223     /**
    224      * @return resource ID for the "view group" label, or -1 if not defined.
    225      */
    226     protected int getViewGroupLabelResId() {
    227         return -1;
    228     }
    229 
    230     /**
    231      * Returns {@link AccountTypeWithDataSet} for this type.
    232      */
    233     public AccountTypeWithDataSet getAccountTypeAndDataSet() {
    234         return AccountTypeWithDataSet.get(accountType, dataSet);
    235     }
    236 
    237     /**
    238      * Returns a list of additional package names that should be inspected as additional
    239      * external account types.  This allows for a primary account type to indicate other packages
    240      * that may not be sync adapters but which still provide contact data, perhaps under a
    241      * separate data set within the account.
    242      */
    243     public List<String> getExtensionPackageNames() {
    244         return new ArrayList<String>();
    245     }
    246 
    247     /**
    248      * Returns an optional custom label for the "invite contact" action, which will be shown on
    249      * the contact card.  (If not defined, returns null.)
    250      */
    251     public CharSequence getInviteContactActionLabel(Context context) {
    252         // Note this resource is defined in the sync adapter package, not resourcePackageName.
    253         return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
    254     }
    255 
    256     /**
    257      * Returns a label for the "view group" action. If not defined, this falls back to our
    258      * own "View Updates" string
    259      */
    260     public CharSequence getViewGroupLabel(Context context) {
    261         // Note this resource is defined in the sync adapter package, not resourcePackageName.
    262         final CharSequence customTitle =
    263                 getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
    264 
    265         return customTitle == null
    266                 ? context.getText(R.string.view_updates_from_group)
    267                 : customTitle;
    268     }
    269 
    270     /**
    271      * Return a string resource loaded from the given package (or the current package
    272      * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns
    273      * {@code defaultValue}.
    274      *
    275      * (The behavior is undefined if the resource or package doesn't exist.)
    276      */
    277     @VisibleForTesting
    278     static CharSequence getResourceText(Context context, String packageName, int resId,
    279             String defaultValue) {
    280         if (resId != -1 && packageName != null) {
    281             final PackageManager pm = context.getPackageManager();
    282             return pm.getText(packageName, resId, null);
    283         } else if (resId != -1) {
    284             return context.getText(resId);
    285         } else {
    286             return defaultValue;
    287         }
    288     }
    289 
    290     public Drawable getDisplayIcon(Context context) {
    291         if (this.titleRes != -1 && this.syncAdapterPackageName != null) {
    292             final PackageManager pm = context.getPackageManager();
    293             return pm.getDrawable(this.syncAdapterPackageName, this.iconRes, null);
    294         } else if (this.titleRes != -1) {
    295             return context.getResources().getDrawable(this.iconRes);
    296         } else {
    297             return null;
    298         }
    299     }
    300 
    301     /**
    302      * Whether or not groups created under this account type have editable membership lists.
    303      */
    304     abstract public boolean isGroupMembershipEditable();
    305 
    306     /**
    307      * {@link Comparator} to sort by {@link DataKind#weight}.
    308      */
    309     private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() {
    310         @Override
    311         public int compare(DataKind object1, DataKind object2) {
    312             return object1.weight - object2.weight;
    313         }
    314     };
    315 
    316     /**
    317      * Return list of {@link DataKind} supported, sorted by
    318      * {@link DataKind#weight}.
    319      */
    320     public ArrayList<DataKind> getSortedDataKinds() {
    321         // TODO: optimize by marking if already sorted
    322         Collections.sort(mKinds, sWeightComparator);
    323         return mKinds;
    324     }
    325 
    326     /**
    327      * Find the {@link DataKind} for a specific MIME-type, if it's handled by
    328      * this data source. If you may need a fallback {@link DataKind}, use
    329      * {@link AccountTypeManager#getKindOrFallback(String, String, String)}.
    330      */
    331     public DataKind getKindForMimetype(String mimeType) {
    332         return this.mMimeKinds.get(mimeType);
    333     }
    334 
    335     /**
    336      * Add given {@link DataKind} to list of those provided by this source.
    337      */
    338     public DataKind addKind(DataKind kind) throws DefinitionException {
    339         if (kind.mimeType == null) {
    340             throw new DefinitionException("null is not a valid mime type");
    341         }
    342         if (mMimeKinds.get(kind.mimeType) != null) {
    343             throw new DefinitionException(
    344                     "mime type '" + kind.mimeType + "' is already registered");
    345         }
    346 
    347         kind.resourcePackageName = this.resourcePackageName;
    348         this.mKinds.add(kind);
    349         this.mMimeKinds.put(kind.mimeType, kind);
    350         return kind;
    351     }
    352 
    353     /**
    354      * Description of a specific "type" or "label" of a {@link DataKind} row,
    355      * such as {@link Phone#TYPE_WORK}. Includes constraints on total number of
    356      * rows a {@link Contacts} may have of this type, and details on how
    357      * user-defined labels are stored.
    358      */
    359     public static class EditType {
    360         public int rawValue;
    361         public int labelRes;
    362         public boolean secondary;
    363         /**
    364          * The number of entries allowed for the type. -1 if not specified.
    365          * @see DataKind#typeOverallMax
    366          */
    367         public int specificMax;
    368         public String customColumn;
    369 
    370         public EditType(int rawValue, int labelRes) {
    371             this.rawValue = rawValue;
    372             this.labelRes = labelRes;
    373             this.specificMax = -1;
    374         }
    375 
    376         public EditType setSecondary(boolean secondary) {
    377             this.secondary = secondary;
    378             return this;
    379         }
    380 
    381         public EditType setSpecificMax(int specificMax) {
    382             this.specificMax = specificMax;
    383             return this;
    384         }
    385 
    386         public EditType setCustomColumn(String customColumn) {
    387             this.customColumn = customColumn;
    388             return this;
    389         }
    390 
    391         @Override
    392         public boolean equals(Object object) {
    393             if (object instanceof EditType) {
    394                 final EditType other = (EditType)object;
    395                 return other.rawValue == rawValue;
    396             }
    397             return false;
    398         }
    399 
    400         @Override
    401         public int hashCode() {
    402             return rawValue;
    403         }
    404 
    405         @Override
    406         public String toString() {
    407             return this.getClass().getSimpleName()
    408                     + " rawValue=" + rawValue
    409                     + " labelRes=" + labelRes
    410                     + " secondary=" + secondary
    411                     + " specificMax=" + specificMax
    412                     + " customColumn=" + customColumn;
    413         }
    414     }
    415 
    416     public static class EventEditType extends EditType {
    417         private boolean mYearOptional;
    418 
    419         public EventEditType(int rawValue, int labelRes) {
    420             super(rawValue, labelRes);
    421         }
    422 
    423         public boolean isYearOptional() {
    424             return mYearOptional;
    425         }
    426 
    427         public EventEditType setYearOptional(boolean yearOptional) {
    428             mYearOptional = yearOptional;
    429             return this;
    430         }
    431 
    432         @Override
    433         public String toString() {
    434             return super.toString() + " mYearOptional=" + mYearOptional;
    435         }
    436     }
    437 
    438     /**
    439      * Description of a user-editable field on a {@link DataKind} row, such as
    440      * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and
    441      * the column where this field is stored.
    442      */
    443     public static final class EditField {
    444         public String column;
    445         public int titleRes;
    446         public int inputType;
    447         public int minLines;
    448         public boolean optional;
    449         public boolean shortForm;
    450         public boolean longForm;
    451 
    452         public EditField(String column, int titleRes) {
    453             this.column = column;
    454             this.titleRes = titleRes;
    455         }
    456 
    457         public EditField(String column, int titleRes, int inputType) {
    458             this(column, titleRes);
    459             this.inputType = inputType;
    460         }
    461 
    462         public EditField setOptional(boolean optional) {
    463             this.optional = optional;
    464             return this;
    465         }
    466 
    467         public EditField setShortForm(boolean shortForm) {
    468             this.shortForm = shortForm;
    469             return this;
    470         }
    471 
    472         public EditField setLongForm(boolean longForm) {
    473             this.longForm = longForm;
    474             return this;
    475         }
    476 
    477         public EditField setMinLines(int minLines) {
    478             this.minLines = minLines;
    479             return this;
    480         }
    481 
    482         public boolean isMultiLine() {
    483             return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
    484         }
    485 
    486 
    487         @Override
    488         public String toString() {
    489             return this.getClass().getSimpleName() + ":"
    490                     + " column=" + column
    491                     + " titleRes=" + titleRes
    492                     + " inputType=" + inputType
    493                     + " minLines=" + minLines
    494                     + " optional=" + optional
    495                     + " shortForm=" + shortForm
    496                     + " longForm=" + longForm;
    497         }
    498     }
    499 
    500     /**
    501      * Generic method of inflating a given {@link ContentValues} into a user-readable
    502      * {@link CharSequence}. For example, an inflater could combine the multiple
    503      * columns of {@link StructuredPostal} together using a string resource
    504      * before presenting to the user.
    505      */
    506     public interface StringInflater {
    507         public CharSequence inflateUsing(Context context, ContentValues values);
    508     }
    509 
    510     /**
    511      * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the
    512      * current locale.
    513      */
    514     public static class DisplayLabelComparator implements Comparator<AccountType> {
    515         private final Context mContext;
    516         /** {@link Comparator} for the current locale. */
    517         private final Collator mCollator = Collator.getInstance();
    518 
    519         public DisplayLabelComparator(Context context) {
    520             mContext = context;
    521         }
    522 
    523         private String getDisplayLabel(AccountType type) {
    524             CharSequence label = type.getDisplayLabel(mContext);
    525             return (label == null) ? "" : label.toString();
    526         }
    527 
    528         @Override
    529         public int compare(AccountType lhs, AccountType rhs) {
    530             return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
    531         }
    532     }
    533 }
    534