Home | History | Annotate | Download | only in model
      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;
     18 
     19 import com.android.contacts.R;
     20 import com.google.android.collect.Lists;
     21 import com.google.android.collect.Maps;
     22 import com.google.common.annotations.VisibleForTesting;
     23 
     24 import android.content.ContentValues;
     25 import android.content.Context;
     26 import android.content.pm.PackageManager;
     27 import android.graphics.drawable.Drawable;
     28 import android.provider.ContactsContract.CommonDataKinds.Phone;
     29 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     30 import android.provider.ContactsContract.Contacts;
     31 import android.provider.ContactsContract.RawContacts;
     32 import android.view.inputmethod.EditorInfo;
     33 import android.widget.EditText;
     34 
     35 import java.text.Collator;
     36 import java.util.ArrayList;
     37 import java.util.Collections;
     38 import java.util.Comparator;
     39 import java.util.HashMap;
     40 import java.util.List;
     41 
     42 /**
     43  * Internal structure that represents constraints and styles for a specific data
     44  * source, such as the various data types they support, including details on how
     45  * those types should be rendered and edited.
     46  * <p>
     47  * In the future this may be inflated from XML defined by a data source.
     48  */
     49 public abstract class AccountType {
     50     private static final String TAG = "AccountType";
     51 
     52     /**
     53      * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
     54      */
     55     public String accountType = null;
     56 
     57     /**
     58      * The {@link RawContacts#DATA_SET} these constraints apply to.
     59      */
     60     public String dataSet = null;
     61 
     62     /**
     63      * Package that resources should be loaded from.  Will be null for embedded types, in which
     64      * case resources are stored in this package itself.
     65      *
     66      * TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and
     67      * {@link #getViewContactNotifyServicePackageName()}.
     68      *
     69      * There's the following invariants:
     70      * - {@link #syncAdapterPackageName} is always set to the actual sync adapter package name.
     71      * - {@link #resourcePackageName} too is set to the same value, unless {@link #isEmbedded()},
     72      *   in which case it'll be null.
     73      * There's an unfortunate exception of {@link FallbackAccountType}.  Even though it
     74      * {@link #isEmbedded()}, but we set non-null to {@link #resourcePackageName} for unit tests.
     75      */
     76     public String resourcePackageName;
     77     /**
     78      * The package name for the authenticator (for the embedded types, i.e. Google and Exchange)
     79      * or the sync adapter (for external type, including extensions).
     80      */
     81     public String syncAdapterPackageName;
     82 
     83     public int titleRes;
     84     public int iconRes;
     85 
     86     /**
     87      * Set of {@link DataKind} supported by this source.
     88      */
     89     private ArrayList<DataKind> mKinds = Lists.newArrayList();
     90 
     91     /**
     92      * Lookup map of {@link #mKinds} on {@link DataKind#mimeType}.
     93      */
     94     private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap();
     95 
     96     protected boolean mIsInitialized;
     97 
     98     protected static class DefinitionException extends Exception {
     99         public DefinitionException(String message) {
    100             super(message);
    101         }
    102 
    103         public DefinitionException(String message, Exception inner) {
    104             super(message, inner);
    105         }
    106     }
    107 
    108     /**
    109      * Whether this account type was able to be fully initialized.  This may be false if
    110      * (for example) the package name associated with the account type could not be found.
    111      */
    112     public final boolean isInitialized() {
    113         return mIsInitialized;
    114     }
    115 
    116     /**
    117      * @return Whether this type is an "embedded" type.  i.e. any of {@link FallbackAccountType},
    118      * {@link GoogleAccountType} or {@link ExternalAccountType}.
    119      *
    120      * If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
    121      * {@code false}) it's considered critical, and the application will crash.  On the other
    122      * hand if it's not an embedded type, we just skip loading the type.
    123      */
    124     public boolean isEmbedded() {
    125         return true;
    126     }
    127 
    128     public boolean isExtension() {
    129         return false;
    130     }
    131 
    132     /**
    133      * @return True if contacts can be created and edited using this app. If false,
    134      * there could still be an external editor as provided by
    135      * {@link #getEditContactActivityClassName()} or {@link #getCreateContactActivityClassName()}
    136      */
    137     public abstract boolean areContactsWritable();
    138 
    139     /**
    140      * Returns an optional custom edit activity.
    141      *
    142      * Only makes sense for non-embedded account types.
    143      * The activity class should reside in the sync adapter package as determined by
    144      * {@link #syncAdapterPackageName}.
    145      */
    146     public String getEditContactActivityClassName() {
    147         return null;
    148     }
    149 
    150     /**
    151      * Returns an optional custom new contact activity.
    152      *
    153      * Only makes sense for non-embedded account types.
    154      * The activity class should reside in the sync adapter package as determined by
    155      * {@link #syncAdapterPackageName}.
    156      */
    157     public String getCreateContactActivityClassName() {
    158         return null;
    159     }
    160 
    161     /**
    162      * Returns an optional custom invite contact activity.
    163      *
    164      * Only makes sense for non-embedded account types.
    165      * The activity class should reside in the sync adapter package as determined by
    166      * {@link #syncAdapterPackageName}.
    167      */
    168     public String getInviteContactActivityClassName() {
    169         return null;
    170     }
    171 
    172     /**
    173      * Returns an optional service that can be launched whenever a contact is being looked at.
    174      * This allows the sync adapter to provide more up-to-date information.
    175      *
    176      * The service class should reside in the sync adapter package as determined by
    177      * {@link #getViewContactNotifyServicePackageName()}.
    178      */
    179     public String getViewContactNotifyServiceClassName() {
    180         return null;
    181     }
    182 
    183     /**
    184      * TODO This is way too hacky should be removed.
    185      *
    186      * This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName}
    187      * is the authenticator package name but the notification service is in the sync adapter
    188      * package.  See {@link #resourcePackageName} -- we should clean up those.
    189      */
    190     public String getViewContactNotifyServicePackageName() {
    191         return syncAdapterPackageName;
    192     }
    193 
    194     /** Returns an optional Activity string that can be used to view the group. */
    195     public String getViewGroupActivity() {
    196         return null;
    197     }
    198 
    199     /** Returns an optional Activity string that can be used to view the stream item. */
    200     public String getViewStreamItemActivity() {
    201         return null;
    202     }
    203 
    204     /** Returns an optional Activity string that can be used to view the stream item photo. */
    205     public String getViewStreamItemPhotoActivity() {
    206         return null;
    207     }
    208 
    209     public CharSequence getDisplayLabel(Context context) {
    210         // Note this resource is defined in the sync adapter package, not resourcePackageName.
    211         return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
    212     }
    213 
    214     /**
    215      * @return resource ID for the "invite contact" action label, or -1 if not defined.
    216      */
    217     protected int getInviteContactActionResId() {
    218         return -1;
    219     }
    220 
    221     /**
    222      * @return resource ID for the "view group" label, or -1 if not defined.
    223      */
    224     protected int getViewGroupLabelResId() {
    225         return -1;
    226     }
    227 
    228     /**
    229      * Returns {@link AccountTypeWithDataSet} for this type.
    230      */
    231     public AccountTypeWithDataSet getAccountTypeAndDataSet() {
    232         return AccountTypeWithDataSet.get(accountType, dataSet);
    233     }
    234 
    235     /**
    236      * Returns a list of additional package names that should be inspected as additional
    237      * external account types.  This allows for a primary account type to indicate other packages
    238      * that may not be sync adapters but which still provide contact data, perhaps under a
    239      * separate data set within the account.
    240      */
    241     public List<String> getExtensionPackageNames() {
    242         return new ArrayList<String>();
    243     }
    244 
    245     /**
    246      * Returns an optional custom label for the "invite contact" action, which will be shown on
    247      * the contact card.  (If not defined, returns null.)
    248      */
    249     public CharSequence getInviteContactActionLabel(Context context) {
    250         // Note this resource is defined in the sync adapter package, not resourcePackageName.
    251         return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
    252     }
    253 
    254     /**
    255      * Returns a label for the "view group" action. If not defined, this falls back to our
    256      * own "View Updates" string
    257      */
    258     public CharSequence getViewGroupLabel(Context context) {
    259         // Note this resource is defined in the sync adapter package, not resourcePackageName.
    260         final CharSequence customTitle =
    261                 getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
    262 
    263         return customTitle == null
    264                 ? context.getText(R.string.view_updates_from_group)
    265                 : customTitle;
    266     }
    267 
    268     /**
    269      * Return a string resource loaded from the given package (or the current package
    270      * if {@code packageName} is null), unless {@code resId} is -1, in which case it returns
    271      * {@code defaultValue}.
    272      *
    273      * (The behavior is undefined if the resource or package doesn't exist.)
    274      */
    275     @VisibleForTesting
    276     static CharSequence getResourceText(Context context, String packageName, int resId,
    277             String defaultValue) {
    278         if (resId != -1 && packageName != null) {
    279             final PackageManager pm = context.getPackageManager();
    280             return pm.getText(packageName, resId, null);
    281         } else if (resId != -1) {
    282             return context.getText(resId);
    283         } else {
    284             return defaultValue;
    285         }
    286     }
    287 
    288     public Drawable getDisplayIcon(Context context) {
    289         if (this.titleRes != -1 && this.syncAdapterPackageName != null) {
    290             final PackageManager pm = context.getPackageManager();
    291             return pm.getDrawable(this.syncAdapterPackageName, this.iconRes, null);
    292         } else if (this.titleRes != -1) {
    293             return context.getResources().getDrawable(this.iconRes);
    294         } else {
    295             return null;
    296         }
    297     }
    298 
    299     /**
    300      * Whether or not groups created under this account type have editable membership lists.
    301      */
    302     abstract public boolean isGroupMembershipEditable();
    303 
    304     /**
    305      * {@link Comparator} to sort by {@link DataKind#weight}.
    306      */
    307     private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() {
    308         @Override
    309         public int compare(DataKind object1, DataKind object2) {
    310             return object1.weight - object2.weight;
    311         }
    312     };
    313 
    314     /**
    315      * Return list of {@link DataKind} supported, sorted by
    316      * {@link DataKind#weight}.
    317      */
    318     public ArrayList<DataKind> getSortedDataKinds() {
    319         // TODO: optimize by marking if already sorted
    320         Collections.sort(mKinds, sWeightComparator);
    321         return mKinds;
    322     }
    323 
    324     /**
    325      * Find the {@link DataKind} for a specific MIME-type, if it's handled by
    326      * this data source. If you may need a fallback {@link DataKind}, use
    327      * {@link AccountTypeManager#getKindOrFallback(String, String, String)}.
    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