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