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