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