blob: 499286a13d5f0d89b74cc98648aeeae2e67055e8 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.common.model.account;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.util.ArrayMap;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.dialer.contacts.resources.R;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Internal structure that represents constraints and styles for a specific data source, such as the
* various data types they support, including details on how those types should be rendered and
* edited.
*
* <p>In the future this may be inflated from XML defined by a data source.
*/
public abstract class AccountType {
private static final String TAG = "AccountType";
/** {@link Comparator} to sort by {@link DataKind#weight}. */
private static Comparator<DataKind> sWeightComparator =
new Comparator<DataKind>() {
@Override
public int compare(DataKind object1, DataKind object2) {
return object1.weight - object2.weight;
}
};
/** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */
public String accountType = null;
/** The {@link RawContacts#DATA_SET} these constraints apply to. */
public String dataSet = null;
/**
* Package that resources should be loaded from. Will be null for embedded types, in which case
* resources are stored in this package itself.
*
* <p>TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link
* #getViewContactNotifyServicePackageName()}.
*
* <p>There's the following invariants: - {@link #syncAdapterPackageName} is always set to the
* actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value,
* unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of
* {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to
* {@link #resourcePackageName} for unit tests.
*/
public String resourcePackageName;
/**
* The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or
* the sync adapter (for external type, including extensions).
*/
public String syncAdapterPackageName;
public int titleRes;
public int iconRes;
protected boolean mIsInitialized;
/** Set of {@link DataKind} supported by this source. */
private ArrayList<DataKind> mKinds = new ArrayList<>();
/** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */
private Map<String, DataKind> mMimeKinds = new ArrayMap<>();
/**
* Return a string resource loaded from the given package (or the current package if {@code
* packageName} is null), unless {@code resId} is -1, in which case it returns {@code
* defaultValue}.
*
* <p>(The behavior is undefined if the resource or package doesn't exist.)
*/
@VisibleForTesting
static CharSequence getResourceText(
Context context, String packageName, int resId, String defaultValue) {
if (resId != -1 && packageName != null) {
final PackageManager pm = context.getPackageManager();
return pm.getText(packageName, resId, null);
} else if (resId != -1) {
return context.getText(resId);
} else {
return defaultValue;
}
}
public static Drawable getDisplayIcon(
Context context, int titleRes, int iconRes, String syncAdapterPackageName) {
if (titleRes != -1 && syncAdapterPackageName != null) {
final PackageManager pm = context.getPackageManager();
return pm.getDrawable(syncAdapterPackageName, iconRes, null);
} else if (titleRes != -1) {
return ContextCompat.getDrawable(context, iconRes);
} else {
return null;
}
}
/**
* Whether this account type was able to be fully initialized. This may be false if (for example)
* the package name associated with the account type could not be found.
*/
public final boolean isInitialized() {
return mIsInitialized;
}
/**
* @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType},
* {@link GoogleAccountType} or {@link ExternalAccountType}.
* <p>If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
* {@code false}) it's considered critical, and the application will crash. On the other hand
* if it's not an embedded type, we just skip loading the type.
*/
public boolean isEmbedded() {
return true;
}
public boolean isExtension() {
return false;
}
/**
* @return True if contacts can be created and edited using this app. If false, there could still
* be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link
* #getCreateContactActivityClassName()}
*/
public abstract boolean areContactsWritable();
/**
* Returns an optional custom edit activity.
*
* <p>Only makes sense for non-embedded account types. The activity class should reside in the
* sync adapter package as determined by {@link #syncAdapterPackageName}.
*/
public String getEditContactActivityClassName() {
return null;
}
/**
* Returns an optional custom new contact activity.
*
* <p>Only makes sense for non-embedded account types. The activity class should reside in the
* sync adapter package as determined by {@link #syncAdapterPackageName}.
*/
public String getCreateContactActivityClassName() {
return null;
}
/**
* Returns an optional custom invite contact activity.
*
* <p>Only makes sense for non-embedded account types. The activity class should reside in the
* sync adapter package as determined by {@link #syncAdapterPackageName}.
*/
public String getInviteContactActivityClassName() {
return null;
}
/**
* Returns an optional service that can be launched whenever a contact is being looked at. This
* allows the sync adapter to provide more up-to-date information.
*
* <p>The service class should reside in the sync adapter package as determined by {@link
* #getViewContactNotifyServicePackageName()}.
*/
public String getViewContactNotifyServiceClassName() {
return null;
}
/**
* TODO This is way too hacky should be removed.
*
* <p>This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is
* the authenticator package name but the notification service is in the sync adapter package. See
* {@link #resourcePackageName} -- we should clean up those.
*/
public String getViewContactNotifyServicePackageName() {
return syncAdapterPackageName;
}
/** Returns an optional Activity string that can be used to view the group. */
public String getViewGroupActivity() {
return null;
}
public CharSequence getDisplayLabel(Context context) {
// Note this resource is defined in the sync adapter package, not resourcePackageName.
return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
}
/** @return resource ID for the "invite contact" action label, or -1 if not defined. */
protected int getInviteContactActionResId() {
return -1;
}
/** @return resource ID for the "view group" label, or -1 if not defined. */
protected int getViewGroupLabelResId() {
return -1;
}
/** Returns {@link AccountTypeWithDataSet} for this type. */
public AccountTypeWithDataSet getAccountTypeAndDataSet() {
return AccountTypeWithDataSet.get(accountType, dataSet);
}
/**
* Returns a list of additional package names that should be inspected as additional external
* account types. This allows for a primary account type to indicate other packages that may not
* be sync adapters but which still provide contact data, perhaps under a separate data set within
* the account.
*/
public List<String> getExtensionPackageNames() {
return new ArrayList<String>();
}
/**
* Returns an optional custom label for the "invite contact" action, which will be shown on the
* contact card. (If not defined, returns null.)
*/
public CharSequence getInviteContactActionLabel(Context context) {
// Note this resource is defined in the sync adapter package, not resourcePackageName.
return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
}
/**
* Returns a label for the "view group" action. If not defined, this falls back to our own "View
* Updates" string
*/
public CharSequence getViewGroupLabel(Context context) {
// Note this resource is defined in the sync adapter package, not resourcePackageName.
final CharSequence customTitle =
getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle;
}
public Drawable getDisplayIcon(Context context) {
return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName);
}
/** Whether or not groups created under this account type have editable membership lists. */
public abstract boolean isGroupMembershipEditable();
/** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */
public ArrayList<DataKind> getSortedDataKinds() {
// TODO: optimize by marking if already sorted
Collections.sort(mKinds, sWeightComparator);
return mKinds;
}
/** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */
public DataKind getKindForMimetype(String mimeType) {
return this.mMimeKinds.get(mimeType);
}
/** Add given {@link DataKind} to list of those provided by this source. */
public DataKind addKind(DataKind kind) throws DefinitionException {
if (kind.mimeType == null) {
throw new DefinitionException("null is not a valid mime type");
}
if (mMimeKinds.get(kind.mimeType) != null) {
throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered");
}
kind.resourcePackageName = this.resourcePackageName;
this.mKinds.add(kind);
this.mMimeKinds.put(kind.mimeType, kind);
return kind;
}
/**
* Generic method of inflating a given {@link ContentValues} into a user-readable {@link
* CharSequence}. For example, an inflater could combine the multiple columns of {@link
* StructuredPostal} together using a string resource before presenting to the user.
*/
public interface StringInflater {
CharSequence inflateUsing(Context context, ContentValues values);
}
protected static class DefinitionException extends Exception {
public DefinitionException(String message) {
super(message);
}
public DefinitionException(String message, Exception inner) {
super(message, inner);
}
}
/**
* Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link
* Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of
* this type, and details on how user-defined labels are stored.
*/
public static class EditType {
public int rawValue;
public int labelRes;
public boolean secondary;
/**
* The number of entries allowed for the type. -1 if not specified.
*
* @see DataKind#typeOverallMax
*/
public int specificMax;
public String customColumn;
public EditType(int rawValue, int labelRes) {
this.rawValue = rawValue;
this.labelRes = labelRes;
this.specificMax = -1;
}
public EditType setSecondary(boolean secondary) {
this.secondary = secondary;
return this;
}
public EditType setSpecificMax(int specificMax) {
this.specificMax = specificMax;
return this;
}
public EditType setCustomColumn(String customColumn) {
this.customColumn = customColumn;
return this;
}
@Override
public boolean equals(Object object) {
if (object instanceof EditType) {
final EditType other = (EditType) object;
return other.rawValue == rawValue;
}
return false;
}
@Override
public int hashCode() {
return rawValue;
}
@Override
public String toString() {
return this.getClass().getSimpleName()
+ " rawValue="
+ rawValue
+ " labelRes="
+ labelRes
+ " secondary="
+ secondary
+ " specificMax="
+ specificMax
+ " customColumn="
+ customColumn;
}
}
public static class EventEditType extends EditType {
private boolean mYearOptional;
public EventEditType(int rawValue, int labelRes) {
super(rawValue, labelRes);
}
public boolean isYearOptional() {
return mYearOptional;
}
public EventEditType setYearOptional(boolean yearOptional) {
mYearOptional = yearOptional;
return this;
}
@Override
public String toString() {
return super.toString() + " mYearOptional=" + mYearOptional;
}
}
/**
* Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}.
* Includes flags to apply to an {@link EditText}, and the column where this field is stored.
*/
public static final class EditField {
public String column;
public int titleRes;
public int inputType;
public int minLines;
public boolean optional;
public boolean shortForm;
public boolean longForm;
public EditField(String column, int titleRes) {
this.column = column;
this.titleRes = titleRes;
}
public EditField(String column, int titleRes, int inputType) {
this(column, titleRes);
this.inputType = inputType;
}
public EditField setOptional(boolean optional) {
this.optional = optional;
return this;
}
public EditField setShortForm(boolean shortForm) {
this.shortForm = shortForm;
return this;
}
public EditField setLongForm(boolean longForm) {
this.longForm = longForm;
return this;
}
public EditField setMinLines(int minLines) {
this.minLines = minLines;
return this;
}
public boolean isMultiLine() {
return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
@Override
public String toString() {
return this.getClass().getSimpleName()
+ ":"
+ " column="
+ column
+ " titleRes="
+ titleRes
+ " inputType="
+ inputType
+ " minLines="
+ minLines
+ " optional="
+ optional
+ " shortForm="
+ shortForm
+ " longForm="
+ longForm;
}
}
/**
* Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current
* locale.
*/
public static class DisplayLabelComparator implements Comparator<AccountType> {
private final Context mContext;
/** {@link Comparator} for the current locale. */
private final Collator mCollator = Collator.getInstance();
public DisplayLabelComparator(Context context) {
mContext = context;
}
private String getDisplayLabel(AccountType type) {
CharSequence label = type.getDisplayLabel(mContext);
return (label == null) ? "" : label.toString();
}
@Override
public int compare(AccountType lhs, AccountType rhs) {
return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
}
}
}