blob: 59b2170832bf0cba6c36a26576037441909968d7 [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.providers.contacts;
import com.android.common.content.SyncStateContentProviderHelper;
import com.android.providers.contacts.ContactAggregator.AggregationSuggestionParameter;
import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Joins;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.ContactsDatabaseHelper.Views;
import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder;
import com.android.providers.contacts.util.DbQueryUtils;
import com.android.vcard.VCardComposer;
import com.android.vcard.VCardConfig;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;
import com.google.common.annotations.VisibleForTesting;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.IContentService;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncAdapterType;
import android.content.UriMatcher;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.database.AbstractCursor;
import android.database.CrossProcessCursor;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.CursorWrapper;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Authorization;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.ContactCounts;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.AggregationSuggestions;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.DataUsageFeedback;
import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.DisplayPhoto;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.PhotoFiles;
import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.ProviderStatus;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContactsEntity;
import android.provider.ContactsContract.SearchSnippetColumns;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.StreamItemPhotos;
import android.provider.ContactsContract.StreamItems;
import android.provider.OpenableColumns;
import android.provider.SyncStateContract;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* Contacts content provider. The contract between this provider and applications
* is defined in {@link ContactsContract}.
*/
public class ContactsProvider2 extends AbstractContactsProvider
implements OnAccountsUpdateListener {
private static final String TAG = "ContactsProvider";
private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
private static final int BACKGROUND_TASK_INITIALIZE = 0;
private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1;
private static final int BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS = 2;
private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3;
private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4;
private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5;
private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6;
private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7;
private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8;
private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10;
/** Default for the maximum number of returned aggregation suggestions. */
private static final int DEFAULT_MAX_SUGGESTIONS = 5;
/** Limit for the maximum number of social stream items to store under a raw contact. */
private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5;
/** Rate limit (in ms) for photo cleanup. Do it at most once per day. */
private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000;
/**
* Default expiration duration for pre-authorized URIs. May be overridden from a secure
* setting.
*/
private static final int DEFAULT_PREAUTHORIZED_URI_EXPIRATION = 5 * 60 * 1000;
/**
* Random URI parameter that will be appended to preauthorized URIs for uniqueness.
*/
private static final String PREAUTHORIZED_URI_TOKEN = "perm_token";
/**
* Property key for the legacy contact import version. The need for a version
* as opposed to a boolean flag is that if we discover bugs in the contact import process,
* we can trigger re-import by incrementing the import version.
*/
private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1";
private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1;
private static final String PREF_LOCALE = "locale";
private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2";
private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2;
private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
private static final ProfileAwareUriMatcher sUriMatcher =
new ProfileAwareUriMatcher(UriMatcher.NO_MATCH);
/**
* Used to insert a column into strequent results, which enables SQL to sort the list using
* the total times contacted. See also {@link #sStrequentFrequentProjectionMap}.
*/
private static final String TIMES_USED_SORT_COLUMN = "times_used_sort";
private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC,"
+ Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
/* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
"UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
" CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
" (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
/* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
"UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
" CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
" (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
/* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
// Regex for splitting query strings - we split on any group of non-alphanumeric characters,
// excluding the @ symbol.
/* package */ static final String QUERY_TOKENIZER_REGEX = "[^\\w@]+";
private static final int CONTACTS = 1000;
private static final int CONTACTS_ID = 1001;
private static final int CONTACTS_LOOKUP = 1002;
private static final int CONTACTS_LOOKUP_ID = 1003;
private static final int CONTACTS_ID_DATA = 1004;
private static final int CONTACTS_FILTER = 1005;
private static final int CONTACTS_STREQUENT = 1006;
private static final int CONTACTS_STREQUENT_FILTER = 1007;
private static final int CONTACTS_GROUP = 1008;
private static final int CONTACTS_ID_PHOTO = 1009;
private static final int CONTACTS_LOOKUP_PHOTO = 1010;
private static final int CONTACTS_LOOKUP_ID_PHOTO = 1011;
private static final int CONTACTS_ID_DISPLAY_PHOTO = 1012;
private static final int CONTACTS_LOOKUP_DISPLAY_PHOTO = 1013;
private static final int CONTACTS_LOOKUP_ID_DISPLAY_PHOTO = 1014;
private static final int CONTACTS_AS_VCARD = 1015;
private static final int CONTACTS_AS_MULTI_VCARD = 1016;
private static final int CONTACTS_LOOKUP_DATA = 1017;
private static final int CONTACTS_LOOKUP_ID_DATA = 1018;
private static final int CONTACTS_ID_ENTITIES = 1019;
private static final int CONTACTS_LOOKUP_ENTITIES = 1020;
private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1021;
private static final int CONTACTS_ID_STREAM_ITEMS = 1022;
private static final int CONTACTS_LOOKUP_STREAM_ITEMS = 1023;
private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1024;
private static final int CONTACTS_FREQUENT = 1025;
private static final int RAW_CONTACTS = 2002;
private static final int RAW_CONTACTS_ID = 2003;
private static final int RAW_CONTACTS_DATA = 2004;
private static final int RAW_CONTACT_ENTITY_ID = 2005;
private static final int RAW_CONTACTS_ID_DISPLAY_PHOTO = 2006;
private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2007;
private static final int RAW_CONTACTS_ID_STREAM_ITEMS_ID = 2008;
private static final int DATA = 3000;
private static final int DATA_ID = 3001;
private static final int PHONES = 3002;
private static final int PHONES_ID = 3003;
private static final int PHONES_FILTER = 3004;
private static final int EMAILS = 3005;
private static final int EMAILS_ID = 3006;
private static final int EMAILS_LOOKUP = 3007;
private static final int EMAILS_FILTER = 3008;
private static final int POSTALS = 3009;
private static final int POSTALS_ID = 3010;
private static final int PHONE_LOOKUP = 4000;
private static final int AGGREGATION_EXCEPTIONS = 6000;
private static final int AGGREGATION_EXCEPTION_ID = 6001;
private static final int STATUS_UPDATES = 7000;
private static final int STATUS_UPDATES_ID = 7001;
private static final int AGGREGATION_SUGGESTIONS = 8000;
private static final int SETTINGS = 9000;
private static final int GROUPS = 10000;
private static final int GROUPS_ID = 10001;
private static final int GROUPS_SUMMARY = 10003;
private static final int SYNCSTATE = 11000;
private static final int SYNCSTATE_ID = 11001;
private static final int PROFILE_SYNCSTATE = 11002;
private static final int PROFILE_SYNCSTATE_ID = 11003;
private static final int SEARCH_SUGGESTIONS = 12001;
private static final int SEARCH_SHORTCUT = 12002;
private static final int RAW_CONTACT_ENTITIES = 15001;
private static final int PROVIDER_STATUS = 16001;
private static final int DIRECTORIES = 17001;
private static final int DIRECTORIES_ID = 17002;
private static final int COMPLETE_NAME = 18000;
private static final int PROFILE = 19000;
private static final int PROFILE_ENTITIES = 19001;
private static final int PROFILE_DATA = 19002;
private static final int PROFILE_DATA_ID = 19003;
private static final int PROFILE_AS_VCARD = 19004;
private static final int PROFILE_RAW_CONTACTS = 19005;
private static final int PROFILE_RAW_CONTACTS_ID = 19006;
private static final int PROFILE_RAW_CONTACTS_ID_DATA = 19007;
private static final int PROFILE_RAW_CONTACTS_ID_ENTITIES = 19008;
private static final int PROFILE_STATUS_UPDATES = 19009;
private static final int PROFILE_RAW_CONTACT_ENTITIES = 19010;
private static final int PROFILE_PHOTO = 19011;
private static final int PROFILE_DISPLAY_PHOTO = 19012;
private static final int DATA_USAGE_FEEDBACK_ID = 20001;
private static final int STREAM_ITEMS = 21000;
private static final int STREAM_ITEMS_PHOTOS = 21001;
private static final int STREAM_ITEMS_ID = 21002;
private static final int STREAM_ITEMS_ID_PHOTOS = 21003;
private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004;
private static final int STREAM_ITEMS_LIMIT = 21005;
private static final int DISPLAY_PHOTO = 22000;
private static final int PHOTO_DIMENSIONS = 22001;
// Inserts into URIs in this map will direct to the profile database if the parent record's
// value (looked up from the ContentValues object with the key specified by the value in this
// map) is in the profile ID-space (see {@link ProfileDatabaseHelper#PROFILE_ID_SPACE}).
private static final Map<Integer, String> INSERT_URI_ID_VALUE_MAP = Maps.newHashMap();
static {
INSERT_URI_ID_VALUE_MAP.put(DATA, Data.RAW_CONTACT_ID);
INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_DATA, Data.RAW_CONTACT_ID);
INSERT_URI_ID_VALUE_MAP.put(STATUS_UPDATES, StatusUpdates.DATA_ID);
INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID);
INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_ID_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID);
}
// Any interactions that involve these URIs will also require the calling package to have either
// android.permission.READ_SOCIAL_STREAM permission or android.permission.WRITE_SOCIAL_STREAM
// permission, depending on the type of operation being performed.
private static final List<Integer> SOCIAL_STREAM_URIS = Lists.newArrayList(
CONTACTS_ID_STREAM_ITEMS,
CONTACTS_LOOKUP_STREAM_ITEMS,
CONTACTS_LOOKUP_ID_STREAM_ITEMS,
RAW_CONTACTS_ID_STREAM_ITEMS,
RAW_CONTACTS_ID_STREAM_ITEMS_ID,
STREAM_ITEMS,
STREAM_ITEMS_PHOTOS,
STREAM_ITEMS_ID,
STREAM_ITEMS_ID_PHOTOS,
STREAM_ITEMS_ID_PHOTOS_ID
);
private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
RawContactsColumns.CONCRETE_ID + "=? AND "
+ GroupsColumns.CONCRETE_ACCOUNT_NAME
+ "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
+ GroupsColumns.CONCRETE_ACCOUNT_TYPE
+ "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND ("
+ GroupsColumns.CONCRETE_DATA_SET
+ "=" + RawContactsColumns.CONCRETE_DATA_SET + " OR "
+ GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
+ RawContactsColumns.CONCRETE_DATA_SET + " IS NULL)"
+ " AND " + Groups.FAVORITES + " != 0";
private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
RawContactsColumns.CONCRETE_ID + "=? AND "
+ GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
+ RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
+ GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
+ RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND ("
+ GroupsColumns.CONCRETE_DATA_SET + "="
+ RawContactsColumns.CONCRETE_DATA_SET + " OR "
+ GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
+ RawContactsColumns.CONCRETE_DATA_SET + " IS NULL)"
+ " AND " + Groups.AUTO_ADD + " != 0";
private static final String[] PROJECTION_GROUP_ID
= new String[]{Tables.GROUPS + "." + Groups._ID};
private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
+ "AND " + GroupMembership.GROUP_ROW_ID + "=? "
+ "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
"SELECT " + RawContacts.STARRED
+ " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
private interface DataContactsQuery {
public static final String TABLE = "data "
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
public static final String[] PROJECTION = new String[] {
RawContactsColumns.CONCRETE_ID,
RawContactsColumns.CONCRETE_ACCOUNT_TYPE,
RawContactsColumns.CONCRETE_ACCOUNT_NAME,
RawContactsColumns.CONCRETE_DATA_SET,
DataColumns.CONCRETE_ID,
ContactsColumns.CONCRETE_ID
};
public static final int RAW_CONTACT_ID = 0;
public static final int ACCOUNT_TYPE = 1;
public static final int ACCOUNT_NAME = 2;
public static final int DATA_SET = 3;
public static final int DATA_ID = 4;
public static final int CONTACT_ID = 5;
}
interface RawContactsQuery {
String TABLE = Tables.RAW_CONTACTS;
String[] COLUMNS = new String[] {
RawContacts.DELETED,
RawContacts.ACCOUNT_TYPE,
RawContacts.ACCOUNT_NAME,
RawContacts.DATA_SET,
};
int DELETED = 0;
int ACCOUNT_TYPE = 1;
int ACCOUNT_NAME = 2;
int DATA_SET = 3;
}
public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
/** Sql where statement for filtering on groups. */
private static final String CONTACTS_IN_GROUP_SELECT =
Contacts._ID + " IN "
+ "(SELECT " + RawContacts.CONTACT_ID
+ " FROM " + Tables.RAW_CONTACTS
+ " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
+ "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
+ " FROM " + Tables.DATA_JOIN_MIMETYPES
+ " WHERE " + DataColumns.MIMETYPE_ID + "=?"
+ " AND " + GroupMembership.GROUP_ROW_ID + "="
+ "(SELECT " + Tables.GROUPS + "." + Groups._ID
+ " FROM " + Tables.GROUPS
+ " WHERE " + Groups.TITLE + "=?)))";
/** Sql for updating DIRTY flag on multiple raw contacts */
private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
"UPDATE " + Tables.RAW_CONTACTS +
" SET " + RawContacts.DIRTY + "=1" +
" WHERE " + RawContacts._ID + " IN (";
/** Sql for updating VERSION on multiple raw contacts */
private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
"UPDATE " + Tables.RAW_CONTACTS +
" SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
" WHERE " + RawContacts._ID + " IN (";
// Current contacts - those contacted within the last 3 days (in seconds)
private static final long EMAIL_FILTER_CURRENT = 3 * 24 * 60 * 60;
// Recent contacts - those contacted within the last 30 days (in seconds)
private static final long EMAIL_FILTER_RECENT = 30 * 24 * 60 * 60;
private static final String TIME_SINCE_LAST_USED =
"(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)";
/*
* Sorting order for email address suggestions: first starred, then the rest.
* second in_visible_group, then the rest.
* Within the four (starred/unstarred, in_visible_group/not-in_visible_group) groups
* - three buckets: very recently contacted, then fairly
* recently contacted, then the rest. Within each of the bucket - descending count
* of times contacted (both for data row and for contact row). If all else fails, alphabetical.
* (Super)primary email address is returned before other addresses for the same contact.
*/
private static final String EMAIL_FILTER_SORT_ORDER =
Contacts.STARRED + " DESC, "
+ Contacts.IN_VISIBLE_GROUP + " DESC, "
+ "(CASE WHEN " + TIME_SINCE_LAST_USED + " < " + EMAIL_FILTER_CURRENT
+ " THEN 0 "
+ " WHEN " + TIME_SINCE_LAST_USED + " < " + EMAIL_FILTER_RECENT
+ " THEN 1 "
+ " ELSE 2 END), "
+ DataUsageStatColumns.TIMES_USED + " DESC, "
+ Contacts.DISPLAY_NAME + ", "
+ Data.CONTACT_ID + ", "
+ Data.IS_SUPER_PRIMARY + " DESC, "
+ Data.IS_PRIMARY + " DESC";
/** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */
private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER;
/** Name lookup types used for contact filtering */
private static final String CONTACT_LOOKUP_NAME_TYPES =
NameLookupType.NAME_COLLATION_KEY + "," +
NameLookupType.EMAIL_BASED_NICKNAME + "," +
NameLookupType.NICKNAME;
/**
* If any of these columns are used in a Data projection, there is no point in
* using the DISTINCT keyword, which can negatively affect performance.
*/
private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = {
Data._ID,
Data.RAW_CONTACT_ID,
Data.NAME_RAW_CONTACT_ID,
RawContacts.ACCOUNT_NAME,
RawContacts.ACCOUNT_TYPE,
RawContacts.DATA_SET,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.DIRTY,
RawContacts.NAME_VERIFIED,
RawContacts.SOURCE_ID,
RawContacts.VERSION,
};
private static final ProjectionMap sContactsColumns = ProjectionMap.builder()
.add(Contacts.CUSTOM_RINGTONE)
.add(Contacts.DISPLAY_NAME)
.add(Contacts.DISPLAY_NAME_ALTERNATIVE)
.add(Contacts.DISPLAY_NAME_SOURCE)
.add(Contacts.IN_VISIBLE_GROUP)
.add(Contacts.LAST_TIME_CONTACTED)
.add(Contacts.LOOKUP_KEY)
.add(Contacts.PHONETIC_NAME)
.add(Contacts.PHONETIC_NAME_STYLE)
.add(Contacts.PHOTO_ID)
.add(Contacts.PHOTO_FILE_ID)
.add(Contacts.PHOTO_URI)
.add(Contacts.PHOTO_THUMBNAIL_URI)
.add(Contacts.SEND_TO_VOICEMAIL)
.add(Contacts.SORT_KEY_ALTERNATIVE)
.add(Contacts.SORT_KEY_PRIMARY)
.add(Contacts.STARRED)
.add(Contacts.TIMES_CONTACTED)
.add(Contacts.HAS_PHONE_NUMBER)
.build();
private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder()
.add(Contacts.CONTACT_PRESENCE,
Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)
.add(Contacts.CONTACT_CHAT_CAPABILITY,
Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
.add(Contacts.CONTACT_STATUS,
ContactsStatusUpdatesColumns.CONCRETE_STATUS)
.add(Contacts.CONTACT_STATUS_TIMESTAMP,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
.add(Contacts.CONTACT_STATUS_RES_PACKAGE,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
.add(Contacts.CONTACT_STATUS_LABEL,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
.add(Contacts.CONTACT_STATUS_ICON,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
.build();
private static final ProjectionMap sSnippetColumns = ProjectionMap.builder()
.add(SearchSnippetColumns.SNIPPET)
.build();
private static final ProjectionMap sRawContactColumns = ProjectionMap.builder()
.add(RawContacts.ACCOUNT_NAME)
.add(RawContacts.ACCOUNT_TYPE)
.add(RawContacts.DATA_SET)
.add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET)
.add(RawContacts.DIRTY)
.add(RawContacts.NAME_VERIFIED)
.add(RawContacts.SOURCE_ID)
.add(RawContacts.VERSION)
.build();
private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder()
.add(RawContacts.SYNC1)
.add(RawContacts.SYNC2)
.add(RawContacts.SYNC3)
.add(RawContacts.SYNC4)
.build();
private static final ProjectionMap sDataColumns = ProjectionMap.builder()
.add(Data.DATA1)
.add(Data.DATA2)
.add(Data.DATA3)
.add(Data.DATA4)
.add(Data.DATA5)
.add(Data.DATA6)
.add(Data.DATA7)
.add(Data.DATA8)
.add(Data.DATA9)
.add(Data.DATA10)
.add(Data.DATA11)
.add(Data.DATA12)
.add(Data.DATA13)
.add(Data.DATA14)
.add(Data.DATA15)
.add(Data.DATA_VERSION)
.add(Data.IS_PRIMARY)
.add(Data.IS_SUPER_PRIMARY)
.add(Data.MIMETYPE)
.add(Data.RES_PACKAGE)
.add(Data.SYNC1)
.add(Data.SYNC2)
.add(Data.SYNC3)
.add(Data.SYNC4)
.add(GroupMembership.GROUP_SOURCE_ID)
.build();
private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder()
.add(Contacts.CONTACT_PRESENCE,
Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE)
.add(Contacts.CONTACT_CHAT_CAPABILITY,
Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY)
.add(Contacts.CONTACT_STATUS,
ContactsStatusUpdatesColumns.CONCRETE_STATUS)
.add(Contacts.CONTACT_STATUS_TIMESTAMP,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
.add(Contacts.CONTACT_STATUS_RES_PACKAGE,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
.add(Contacts.CONTACT_STATUS_LABEL,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
.add(Contacts.CONTACT_STATUS_ICON,
ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
.build();
private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder()
.add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)
.add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
.add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)
.add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
.add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
.add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)
.add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)
.build();
/** Contains just BaseColumns._COUNT */
private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder()
.add(BaseColumns._COUNT, "COUNT(*)")
.build();
/** Contains just the contacts columns */
private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder()
.add(Contacts._ID)
.add(Contacts.HAS_PHONE_NUMBER)
.add(Contacts.NAME_RAW_CONTACT_ID)
.add(Contacts.IS_USER_PROFILE)
.addAll(sContactsColumns)
.addAll(sContactsPresenceColumns)
.build();
/** Contains just the contacts columns */
private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder()
.addAll(sContactsProjectionMap)
.addAll(sSnippetColumns)
.build();
/** Used for pushing starred contacts to the top of a times contacted list **/
private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
.addAll(sContactsProjectionMap)
.add(TIMES_USED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE))
.build();
private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
.addAll(sContactsProjectionMap)
.add(TIMES_USED_SORT_COLUMN, "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")")
.build();
/**
* Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows
* users to obtain part of Data columns. Right now Starred part just returns NULL for
* those data columns (frequent part should return real ones in data table).
**/
private static final ProjectionMap sStrequentPhoneOnlyStarredProjectionMap
= ProjectionMap.builder()
.addAll(sContactsProjectionMap)
.add(TIMES_USED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE))
.add(Phone.NUMBER, "NULL")
.add(Phone.TYPE, "NULL")
.add(Phone.LABEL, "NULL")
.build();
/**
* Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows
* users to obtain part of Data columns. We hard-code {@link Contacts#IS_USER_PROFILE} to NULL,
* because sContactsProjectionMap specifies a field that doesn't exist in the view behind the
* query that uses this projection map.
**/
private static final ProjectionMap sStrequentPhoneOnlyFrequentProjectionMap
= ProjectionMap.builder()
.addAll(sContactsProjectionMap)
.add(TIMES_USED_SORT_COLUMN, DataUsageStatColumns.CONCRETE_TIMES_USED)
.add(Phone.NUMBER)
.add(Phone.TYPE)
.add(Phone.LABEL)
.add(Contacts.IS_USER_PROFILE, "NULL")
.build();
/** Contains just the contacts vCard columns */
private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder()
.add(Contacts._ID)
.add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'")
.add(OpenableColumns.SIZE, "NULL")
.build();
/** Contains just the raw contacts columns */
private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder()
.add(RawContacts._ID)
.add(RawContacts.CONTACT_ID)
.add(RawContacts.DELETED)
.add(RawContacts.DISPLAY_NAME_PRIMARY)
.add(RawContacts.DISPLAY_NAME_ALTERNATIVE)
.add(RawContacts.DISPLAY_NAME_SOURCE)
.add(RawContacts.PHONETIC_NAME)
.add(RawContacts.PHONETIC_NAME_STYLE)
.add(RawContacts.SORT_KEY_PRIMARY)
.add(RawContacts.SORT_KEY_ALTERNATIVE)
.add(RawContacts.TIMES_CONTACTED)
.add(RawContacts.LAST_TIME_CONTACTED)
.add(RawContacts.CUSTOM_RINGTONE)
.add(RawContacts.SEND_TO_VOICEMAIL)
.add(RawContacts.STARRED)
.add(RawContacts.AGGREGATION_MODE)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
.addAll(sRawContactColumns)
.addAll(sRawContactSyncColumns)
.build();
/** Contains the columns from the raw entity view*/
private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder()
.add(RawContacts._ID)
.add(RawContacts.CONTACT_ID)
.add(RawContacts.Entity.DATA_ID)
.add(RawContacts.DELETED)
.add(RawContacts.STARRED)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
.addAll(sRawContactColumns)
.addAll(sRawContactSyncColumns)
.addAll(sDataColumns)
.build();
/** Contains the columns from the contact entity view*/
private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder()
.add(Contacts.Entity._ID)
.add(Contacts.Entity.CONTACT_ID)
.add(Contacts.Entity.RAW_CONTACT_ID)
.add(Contacts.Entity.DATA_ID)
.add(Contacts.Entity.NAME_RAW_CONTACT_ID)
.add(Contacts.Entity.DELETED)
.add(Contacts.IS_USER_PROFILE)
.addAll(sContactsColumns)
.addAll(sContactPresenceColumns)
.addAll(sRawContactColumns)
.addAll(sRawContactSyncColumns)
.addAll(sDataColumns)
.addAll(sDataPresenceColumns)
.build();
/** Contains columns in PhoneLookup which are not contained in the data view. */
private static final ProjectionMap sSipLookupColumns = ProjectionMap.builder()
.add(PhoneLookup.NUMBER, SipAddress.SIP_ADDRESS)
.add(PhoneLookup.TYPE, "0")
.add(PhoneLookup.LABEL, "NULL")
.add(PhoneLookup.NORMALIZED_NUMBER, "NULL")
.build();
/** Contains columns from the data view */
private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
.add(Data._ID)
.add(Data.RAW_CONTACT_ID)
.add(Data.CONTACT_ID)
.add(Data.NAME_RAW_CONTACT_ID)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
.addAll(sDataColumns)
.addAll(sDataPresenceColumns)
.addAll(sRawContactColumns)
.addAll(sContactsColumns)
.addAll(sContactPresenceColumns)
.build();
/** Contains columns from the data view used for SIP address lookup. */
private static final ProjectionMap sDataSipLookupProjectionMap = ProjectionMap.builder()
.addAll(sDataProjectionMap)
.addAll(sSipLookupColumns)
.build();
/** Contains columns from the data view */
private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder()
.add(Data._ID, "MIN(" + Data._ID + ")")
.add(RawContacts.CONTACT_ID)
.add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
.addAll(sDataColumns)
.addAll(sDataPresenceColumns)
.addAll(sContactsColumns)
.addAll(sContactPresenceColumns)
.build();
/** Contains columns from the data view used for SIP address lookup. */
private static final ProjectionMap sDistinctDataSipLookupProjectionMap = ProjectionMap.builder()
.addAll(sDistinctDataProjectionMap)
.addAll(sSipLookupColumns)
.build();
/** Contains the data and contacts columns, for joined tables */
private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder()
.add(PhoneLookup._ID, "contacts_view." + Contacts._ID)
.add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY)
.add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME)
.add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
.add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
.add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
.add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
.add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID)
.add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI)
.add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI)
.add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE)
.add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER)
.add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL)
.add(PhoneLookup.NUMBER, Phone.NUMBER)
.add(PhoneLookup.TYPE, Phone.TYPE)
.add(PhoneLookup.LABEL, Phone.LABEL)
.add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER)
.build();
/** Contains the just the {@link Groups} columns */
private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder()
.add(Groups._ID)
.add(Groups.ACCOUNT_NAME)
.add(Groups.ACCOUNT_TYPE)
.add(Groups.DATA_SET)
.add(Groups.ACCOUNT_TYPE_AND_DATA_SET)
.add(Groups.SOURCE_ID)
.add(Groups.DIRTY)
.add(Groups.VERSION)
.add(Groups.RES_PACKAGE)
.add(Groups.TITLE)
.add(Groups.TITLE_RES)
.add(Groups.GROUP_VISIBLE)
.add(Groups.SYSTEM_ID)
.add(Groups.DELETED)
.add(Groups.NOTES)
.add(Groups.SHOULD_SYNC)
.add(Groups.FAVORITES)
.add(Groups.AUTO_ADD)
.add(Groups.GROUP_IS_READ_ONLY)
.add(Groups.SYNC1)
.add(Groups.SYNC2)
.add(Groups.SYNC3)
.add(Groups.SYNC4)
.build();
/**
* Contains {@link Groups} columns along with summary details.
*
* Note {@link Groups#SUMMARY_COUNT} doesn't exist in groups/view_groups.
* When we detect this column being requested, we join {@link Joins#GROUP_MEMBER_COUNT} to
* generate it.
*/
private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder()
.addAll(sGroupsProjectionMap)
.add(Groups.SUMMARY_COUNT, "ifnull(group_member_count, 0)")
.add(Groups.SUMMARY_WITH_PHONES,
"(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM "
+ Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP
+ " WHERE " + Contacts.HAS_PHONE_NUMBER + ")")
.build();
// This is only exposed as hidden API for the contacts app, so we can be very specific in
// the filtering
private static final ProjectionMap sGroupsSummaryProjectionMapWithGroupCountPerAccount =
ProjectionMap.builder()
.addAll(sGroupsSummaryProjectionMap)
.add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
"(SELECT COUNT(*) FROM " + Views.GROUPS + " WHERE "
+ "(" + Groups.ACCOUNT_NAME + "="
+ GroupsColumns.CONCRETE_ACCOUNT_NAME
+ " AND "
+ Groups.ACCOUNT_TYPE + "=" + GroupsColumns.CONCRETE_ACCOUNT_TYPE
+ " AND "
+ Groups.DELETED + "=0 AND "
+ Groups.FAVORITES + "=0 AND "
+ Groups.AUTO_ADD + "=0"
+ ")"
+ " GROUP BY "
+ Groups.ACCOUNT_NAME + ", " + Groups.ACCOUNT_TYPE
+ ")")
.build();
/** Contains the agg_exceptions columns */
private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder()
.add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id")
.add(AggregationExceptions.TYPE)
.add(AggregationExceptions.RAW_CONTACT_ID1)
.add(AggregationExceptions.RAW_CONTACT_ID2)
.build();
/** Contains the agg_exceptions columns */
private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder()
.add(Settings.ACCOUNT_NAME)
.add(Settings.ACCOUNT_TYPE)
.add(Settings.DATA_SET)
.add(Settings.UNGROUPED_VISIBLE)
.add(Settings.SHOULD_SYNC)
.add(Settings.ANY_UNSYNCED,
"(CASE WHEN MIN(" + Settings.SHOULD_SYNC
+ ",(SELECT "
+ "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL"
+ " THEN 1"
+ " ELSE MIN(" + Groups.SHOULD_SYNC + ")"
+ " END)"
+ " FROM " + Tables.GROUPS
+ " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
+ SettingsColumns.CONCRETE_ACCOUNT_NAME
+ " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
+ SettingsColumns.CONCRETE_ACCOUNT_TYPE
+ " AND ((" + GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
+ SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
+ GroupsColumns.CONCRETE_DATA_SET + "="
+ SettingsColumns.CONCRETE_DATA_SET + "))))=0"
+ " THEN 1"
+ " ELSE 0"
+ " END)")
.add(Settings.UNGROUPED_COUNT,
"(SELECT COUNT(*)"
+ " FROM (SELECT 1"
+ " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
+ " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
+ " HAVING " + Clauses.HAVING_NO_GROUPS
+ "))")
.add(Settings.UNGROUPED_WITH_PHONES,
"(SELECT COUNT(*)"
+ " FROM (SELECT 1"
+ " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
+ " WHERE " + Contacts.HAS_PHONE_NUMBER
+ " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
+ " HAVING " + Clauses.HAVING_NO_GROUPS
+ "))")
.build();
/** Contains StatusUpdates columns */
private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder()
.add(PresenceColumns.RAW_CONTACT_ID)
.add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID)
.add(StatusUpdates.IM_ACCOUNT)
.add(StatusUpdates.IM_HANDLE)
.add(StatusUpdates.PROTOCOL)
// We cannot allow a null in the custom protocol field, because SQLite3 does not
// properly enforce uniqueness of null values
.add(StatusUpdates.CUSTOM_PROTOCOL,
"(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''"
+ " THEN NULL"
+ " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)")
.add(StatusUpdates.PRESENCE)
.add(StatusUpdates.CHAT_CAPABILITY)
.add(StatusUpdates.STATUS)
.add(StatusUpdates.STATUS_TIMESTAMP)
.add(StatusUpdates.STATUS_RES_PACKAGE)
.add(StatusUpdates.STATUS_ICON)
.add(StatusUpdates.STATUS_LABEL)
.build();
/** Contains StreamItems columns */
private static final ProjectionMap sStreamItemsProjectionMap = ProjectionMap.builder()
.add(StreamItems._ID)
.add(StreamItems.CONTACT_ID)
.add(StreamItems.CONTACT_LOOKUP_KEY)
.add(StreamItems.ACCOUNT_NAME)
.add(StreamItems.ACCOUNT_TYPE)
.add(StreamItems.DATA_SET)
.add(StreamItems.RAW_CONTACT_ID)
.add(StreamItems.RAW_CONTACT_SOURCE_ID)
.add(StreamItems.RES_PACKAGE)
.add(StreamItems.RES_ICON)
.add(StreamItems.RES_LABEL)
.add(StreamItems.TEXT)
.add(StreamItems.TIMESTAMP)
.add(StreamItems.COMMENTS)
.add(StreamItems.SYNC1)
.add(StreamItems.SYNC2)
.add(StreamItems.SYNC3)
.add(StreamItems.SYNC4)
.build();
private static final ProjectionMap sStreamItemPhotosProjectionMap = ProjectionMap.builder()
.add(StreamItemPhotos._ID, StreamItemPhotosColumns.CONCRETE_ID)
.add(StreamItems.RAW_CONTACT_ID)
.add(StreamItems.RAW_CONTACT_SOURCE_ID, RawContactsColumns.CONCRETE_SOURCE_ID)
.add(StreamItemPhotos.STREAM_ITEM_ID)
.add(StreamItemPhotos.SORT_INDEX)
.add(StreamItemPhotos.PHOTO_FILE_ID)
.add(StreamItemPhotos.PHOTO_URI,
"'" + DisplayPhoto.CONTENT_URI + "'||'/'||" + StreamItemPhotos.PHOTO_FILE_ID)
.add(PhotoFiles.HEIGHT)
.add(PhotoFiles.WIDTH)
.add(PhotoFiles.FILESIZE)
.add(StreamItemPhotos.SYNC1)
.add(StreamItemPhotos.SYNC2)
.add(StreamItemPhotos.SYNC3)
.add(StreamItemPhotos.SYNC4)
.build();
/** Contains {@link Directory} columns */
private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder()
.add(Directory._ID)
.add(Directory.PACKAGE_NAME)
.add(Directory.TYPE_RESOURCE_ID)
.add(Directory.DISPLAY_NAME)
.add(Directory.DIRECTORY_AUTHORITY)
.add(Directory.ACCOUNT_TYPE)
.add(Directory.ACCOUNT_NAME)
.add(Directory.EXPORT_SUPPORT)
.add(Directory.SHORTCUT_SUPPORT)
.add(Directory.PHOTO_SUPPORT)
.build();
// where clause to update the status_updates table
private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
" FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
" ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
private static final String[] EMPTY_STRING_ARRAY = new String[0];
/**
* Notification ID for failure to import contacts.
*/
private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
private static final String DEFAULT_SNIPPET_ARG_START_MATCH = "[";
private static final String DEFAULT_SNIPPET_ARG_END_MATCH = "]";
private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "...";
private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = -10;
private boolean sIsPhoneInitialized;
private boolean sIsPhone;
private StringBuilder mSb = new StringBuilder();
private String[] mSelectionArgs1 = new String[1];
private String[] mSelectionArgs2 = new String[2];
private ArrayList<String> mSelectionArgs = Lists.newArrayList();
private Account mAccount;
/**
* Stores mapping from type Strings exposed via {@link DataUsageFeedback} to
* type integers in {@link DataUsageStatColumns}.
*/
private static final Map<String, Integer> sDataUsageTypeMap;
static {
// Contacts URI matching table
final UriMatcher matcher = sUriMatcher;
matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo",
CONTACTS_ID_DISPLAY_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items",
CONTACTS_ID_STREAM_ITEMS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/photo",
CONTACTS_LOOKUP_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
CONTACTS_LOOKUP_ID_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/photo",
CONTACTS_LOOKUP_ID_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/display_photo",
CONTACTS_LOOKUP_DISPLAY_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/display_photo",
CONTACTS_LOOKUP_ID_DISPLAY_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities",
CONTACTS_LOOKUP_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities",
CONTACTS_LOOKUP_ID_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/stream_items",
CONTACTS_LOOKUP_STREAM_ITEMS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/stream_items",
CONTACTS_LOOKUP_ID_STREAM_ITEMS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
CONTACTS_AS_MULTI_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
CONTACTS_STREQUENT_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/display_photo",
RAW_CONTACTS_ID_DISPLAY_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items",
RAW_CONTACTS_ID_STREAM_ITEMS);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items/#",
RAW_CONTACTS_ID_STREAM_ITEMS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
/** "*" is in CSV form with data ids ("123,456,789") */
matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID);
matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
SYNCSTATE_ID);
matcher.addURI(ContactsContract.AUTHORITY, "profile/" + SyncStateContentProviderHelper.PATH,
PROFILE_SYNCSTATE);
matcher.addURI(ContactsContract.AUTHORITY,
"profile/" + SyncStateContentProviderHelper.PATH + "/#",
PROFILE_SYNCSTATE_ID);
matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
AGGREGATION_EXCEPTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
AGGREGATION_EXCEPTION_ID);
matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
SEARCH_SHORTCUT);
matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID);
matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME);
matcher.addURI(ContactsContract.AUTHORITY, "profile", PROFILE);
matcher.addURI(ContactsContract.AUTHORITY, "profile/entities", PROFILE_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "profile/data", PROFILE_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "profile/data/#", PROFILE_DATA_ID);
matcher.addURI(ContactsContract.AUTHORITY, "profile/photo", PROFILE_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "profile/display_photo", PROFILE_DISPLAY_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "profile/as_vcard", PROFILE_AS_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts", PROFILE_RAW_CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#",
PROFILE_RAW_CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/data",
PROFILE_RAW_CONTACTS_ID_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity",
PROFILE_RAW_CONTACTS_ID_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "profile/status_updates",
PROFILE_STATUS_UPDATES);
matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contact_entities",
PROFILE_RAW_CONTACT_ENTITIES);
matcher.addURI(ContactsContract.AUTHORITY, "stream_items", STREAM_ITEMS);
matcher.addURI(ContactsContract.AUTHORITY, "stream_items/photo", STREAM_ITEMS_PHOTOS);
matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#", STREAM_ITEMS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo", STREAM_ITEMS_ID_PHOTOS);
matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo/#",
STREAM_ITEMS_ID_PHOTOS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT);
matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS);
HashMap<String, Integer> tmpTypeMap = new HashMap<String, Integer>();
tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_CALL, DataUsageStatColumns.USAGE_TYPE_INT_CALL);
tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_LONG_TEXT,
DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT);
tmpTypeMap.put(DataUsageFeedback.USAGE_TYPE_SHORT_TEXT,
DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT);
sDataUsageTypeMap = Collections.unmodifiableMap(tmpTypeMap);
}
private static class DirectoryInfo {
String authority;
String accountName;
String accountType;
}
/**
* Cached information about contact directories.
*/
private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>();
private boolean mDirectoryCacheValid = false;
/**
* An entry in group id cache. It maps the combination of (account type, account name, data set,
* and source id) to group row id.
*/
public static class GroupIdCacheEntry {
String accountType;
String accountName;
String dataSet;
String sourceId;
long groupId;
}
// We don't need a soft cache for groups - the assumption is that there will only
// be a small number of contact groups. The cache is keyed off source id. The value
// is a list of groups with this group id.
private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
/**
* Maximum dimension (height or width) of display photos. Larger images will be scaled
* to fit.
*/
private int mMaxDisplayPhotoDim;
/**
* Maximum dimension (height or width) of photo thumbnails.
*/
private int mMaxThumbnailPhotoDim;
/**
* Sub-provider for handling profile requests against the profile database.
*/
private ProfileProvider mProfileProvider;
private NameSplitter mNameSplitter;
private NameLookupBuilder mNameLookupBuilder;
private PostalSplitter mPostalSplitter;
private ContactDirectoryManager mContactDirectoryManager;
// The database tag to use for representing the contacts DB in contacts transactions.
/* package */ static final String CONTACTS_DB_TAG = "contacts";
// The database tag to use for representing the profile DB in contacts transactions.
/* package */ static final String PROFILE_DB_TAG = "profile";
/**
* The active (thread-local) database. This will be switched between a contacts-specific
* database and a profile-specific database, depending on what the current operation is
* targeted to.
*/
private final ThreadLocal<SQLiteDatabase> mActiveDb = new ThreadLocal<SQLiteDatabase>();
/**
* The thread-local holder of the active transaction. Shared between this and the profile
* provider, to keep transactions on both databases synchronized.
*/
private final ThreadLocal<ContactsTransaction> mTransactionHolder =
new ThreadLocal<ContactsTransaction>();
// This variable keeps track of whether the current operation is intended for the profile DB.
private final ThreadLocal<Boolean> mInProfileMode = new ThreadLocal<Boolean>();
// Separate data row handler instances for contact data and profile data.
private HashMap<String, DataRowHandler> mDataRowHandlers;
private HashMap<String, DataRowHandler> mProfileDataRowHandlers;
// Depending on whether the action being performed is for the profile, we will use one of two
// database helper instances.
private final ThreadLocal<ContactsDatabaseHelper> mDbHelper =
new ThreadLocal<ContactsDatabaseHelper>();
private ContactsDatabaseHelper mContactsHelper;
private ProfileDatabaseHelper mProfileHelper;
// Depending on whether the action being performed is for the profile or not, we will use one of
// two aggregator instances.
private final ThreadLocal<ContactAggregator> mAggregator = new ThreadLocal<ContactAggregator>();
private ContactAggregator mContactAggregator;
private ContactAggregator mProfileAggregator;
// Depending on whether the action being performed is for the profile or not, we will use one of
// two photo store instances (with their files stored in separate subdirectories).
private final ThreadLocal<PhotoStore> mPhotoStore = new ThreadLocal<PhotoStore>();
private PhotoStore mContactsPhotoStore;
private PhotoStore mProfilePhotoStore;
// The active transaction context will switch depending on the operation being performed.
// Both transaction contexts will be cleared out when a batch transaction is started, and
// each will be processed separately when a batch transaction completes.
private TransactionContext mContactTransactionContext = new TransactionContext(false);
private TransactionContext mProfileTransactionContext = new TransactionContext(true);
private final ThreadLocal<TransactionContext> mTransactionContext =
new ThreadLocal<TransactionContext>();
// Duration in milliseconds that pre-authorized URIs will remain valid.
private long mPreAuthorizedUriDuration;
// Map of single-use pre-authorized URIs to expiration times.
private Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap();
// Random number generator.
private SecureRandom mRandom = new SecureRandom();
private LegacyApiSupport mLegacyApiSupport;
private GlobalSearchSupport mGlobalSearchSupport;
private CommonNicknameCache mCommonNicknameCache;
private SearchIndexManager mSearchIndexManager;
private ContentValues mValues = new ContentValues();
private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
private boolean mProviderStatusUpdateNeeded;
private long mEstimatedStorageRequirement = 0;
private volatile CountDownLatch mReadAccessLatch;
private volatile CountDownLatch mWriteAccessLatch;
private boolean mAccountUpdateListenerRegistered;
private boolean mOkToOpenAccess = true;
private boolean mVisibleTouched = false;
private boolean mSyncToNetwork;
private Locale mCurrentLocale;
private int mContactsAccountCount;
private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler;
private long mLastPhotoCleanup = 0;
@Override
public boolean onCreate() {
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start");
}
super.onCreate();
try {
return initialize();
} catch (RuntimeException e) {
Log.e(TAG, "Cannot start provider", e);
return false;
} finally {
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate finish");
}
}
}
private boolean initialize() {
StrictMode.setThreadPolicy(
new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
Resources resources = getContext().getResources();
mMaxDisplayPhotoDim = resources.getInteger(
R.integer.config_max_display_photo_dim);
mMaxThumbnailPhotoDim = resources.getInteger(
R.integer.config_max_thumbnail_photo_dim);
mContactsHelper = getDatabaseHelper(getContext());
mDbHelper.set(mContactsHelper);
// Set up the DB helper for keeping transactions serialized.
setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG);
mContactDirectoryManager = new ContactDirectoryManager(this);
mGlobalSearchSupport = new GlobalSearchSupport(this);
// The provider is closed for business until fully initialized
mReadAccessLatch = new CountDownLatch(1);
mWriteAccessLatch = new CountDownLatch(1);
mBackgroundThread = new HandlerThread("ContactsProviderWorker",
Process.THREAD_PRIORITY_BACKGROUND);
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
performBackgroundTask(msg.what, msg.obj);
}
};
// Set up the sub-provider for handling profiles.
mProfileProvider = getProfileProvider();
mProfileProvider.setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG);
ProviderInfo profileInfo = new ProviderInfo();
profileInfo.readPermission = "android.permission.READ_PROFILE";
profileInfo.writePermission = "android.permission.WRITE_PROFILE";
mProfileProvider.attachInfo(getContext(), profileInfo);
mProfileHelper = mProfileProvider.getDatabaseHelper(getContext());
// Initialize the pre-authorized URI duration.
mPreAuthorizedUriDuration = android.provider.Settings.Secure.getLong(
getContext().getContentResolver(),
android.provider.Settings.Secure.CONTACTS_PREAUTH_URI_EXPIRATION,
DEFAULT_PREAUTHORIZED_URI_EXPIRATION);
scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS);
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE);
scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM);
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX);
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_PROVIDER_STATUS);
scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS);
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
return true;
}
/**
* (Re)allocates all locale-sensitive structures.
*/
private void initForDefaultLocale() {
Context context = getContext();
mLegacyApiSupport = new LegacyApiSupport(context, mContactsHelper, this,
mGlobalSearchSupport);
mCurrentLocale = getLocale();
mNameSplitter = mContactsHelper.createNameSplitter();
mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
mPostalSplitter = new PostalSplitter(mCurrentLocale);
mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase());
ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
mContactAggregator = new ContactAggregator(this, mContactsHelper,
createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
mProfileAggregator = new ProfileAggregator(this, mProfileHelper,
createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
mProfileAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
mSearchIndexManager = new SearchIndexManager(this);
mContactsPhotoStore = new PhotoStore(getContext().getFilesDir(), mContactsHelper);
mProfilePhotoStore = new PhotoStore(new File(getContext().getFilesDir(), "profile"),
mProfileHelper);
mDataRowHandlers = new HashMap<String, DataRowHandler>();
initDataRowHandlers(mDataRowHandlers, mContactsHelper, mContactAggregator,
mContactsPhotoStore);
mProfileDataRowHandlers = new HashMap<String, DataRowHandler>();
initDataRowHandlers(mProfileDataRowHandlers, mProfileHelper, mProfileAggregator,
mProfilePhotoStore);
// Set initial thread-local state variables for the Contacts DB.
switchToContactMode();
}
private void initDataRowHandlers(Map<String, DataRowHandler> handlerMap,
ContactsDatabaseHelper dbHelper, ContactAggregator contactAggregator,
PhotoStore photoStore) {
Context context = getContext();
handlerMap.put(Email.CONTENT_ITEM_TYPE,
new DataRowHandlerForEmail(context, dbHelper, contactAggregator));
handlerMap.put(Im.CONTENT_ITEM_TYPE,
new DataRowHandlerForIm(context, dbHelper, contactAggregator));
handlerMap.put(Organization.CONTENT_ITEM_TYPE,
new DataRowHandlerForOrganization(context, dbHelper, contactAggregator));
handlerMap.put(Phone.CONTENT_ITEM_TYPE,
new DataRowHandlerForPhoneNumber(context, dbHelper, contactAggregator));
handlerMap.put(Nickname.CONTENT_ITEM_TYPE,
new DataRowHandlerForNickname(context, dbHelper, contactAggregator));
handlerMap.put(StructuredName.CONTENT_ITEM_TYPE,
new DataRowHandlerForStructuredName(context, dbHelper, contactAggregator,
mNameSplitter, mNameLookupBuilder));
handlerMap.put(StructuredPostal.CONTENT_ITEM_TYPE,
new DataRowHandlerForStructuredPostal(context, dbHelper, contactAggregator,
mPostalSplitter));
handlerMap.put(GroupMembership.CONTENT_ITEM_TYPE,
new DataRowHandlerForGroupMembership(context, dbHelper, contactAggregator,
mGroupIdCache));
handlerMap.put(Photo.CONTENT_ITEM_TYPE,
new DataRowHandlerForPhoto(context, dbHelper, contactAggregator, photoStore));
handlerMap.put(Note.CONTENT_ITEM_TYPE,
new DataRowHandlerForNote(context, dbHelper, contactAggregator));
}
/**
* Visible for testing.
*/
/* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
return new PhotoPriorityResolver(context);
}
protected void scheduleBackgroundTask(int task) {
mBackgroundHandler.sendEmptyMessage(task);
}
protected void scheduleBackgroundTask(int task, Object arg) {
mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg));
}
protected void performBackgroundTask(int task, Object arg) {
switch (task) {
case BACKGROUND_TASK_INITIALIZE: {
initForDefaultLocale();
mReadAccessLatch.countDown();
mReadAccessLatch = null;
break;
}
case BACKGROUND_TASK_OPEN_WRITE_ACCESS: {
if (mOkToOpenAccess) {
mWriteAccessLatch.countDown();
mWriteAccessLatch = null;
}
break;
}
case BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS: {
if (isLegacyContactImportNeeded()) {
importLegacyContactsInBackground();
}
break;
}
case BACKGROUND_TASK_UPDATE_ACCOUNTS: {
Context context = getContext();
if (!mAccountUpdateListenerRegistered) {
AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false);
mAccountUpdateListenerRegistered = true;
}
// Update the accounts for both the contacts and profile DBs.
Account[] accounts = AccountManager.get(context).getAccounts();
switchToContactMode();
boolean accountsChanged = updateAccountsInBackground(accounts);
switchToProfileMode();
accountsChanged |= updateAccountsInBackground(accounts);
updateContactsAccountCount(accounts);
updateDirectoriesInBackground(accountsChanged);
break;
}
case BACKGROUND_TASK_UPDATE_LOCALE: {
updateLocaleInBackground();
break;
}
case BACKGROUND_TASK_CHANGE_LOCALE: {
changeLocaleInBackground();
break;
}
case BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM: {
if (isAggregationUpgradeNeeded()) {
upgradeAggregationAlgorithmInBackground();
}
break;
}
case BACKGROUND_TASK_UPDATE_SEARCH_INDEX: {
updateSearchIndexInBackground();
break;
}
case BACKGROUND_TASK_UPDATE_PROVIDER_STATUS: {
updateProviderStatus();
break;
}
case BACKGROUND_TASK_UPDATE_DIRECTORIES: {
if (arg != null) {
mContactDirectoryManager.onPackageChanged((String) arg);
}
break;
}
case BACKGROUND_TASK_CLEANUP_PHOTOS: {
// Check rate limit.
long now = System.currentTimeMillis();
if (now - mLastPhotoCleanup > PHOTO_CLEANUP_RATE_LIMIT) {
mLastPhotoCleanup = now;
// Clean up photo stores for both contacts and profiles.
switchToContactMode();
cleanupPhotoStore();
switchToProfileMode();
cleanupPhotoStore();
break;
}
}
}
}
public void onLocaleChanged() {
if (mProviderStatus != ProviderStatus.STATUS_NORMAL
&& mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
return;
}
scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE);
}
/**
* Verifies that the contacts database is properly configured for the current locale.
* If not, changes the database locale to the current locale using an asynchronous task.
* This needs to be done asynchronously because the process involves rebuilding
* large data structures (name lookup, sort keys), which can take minutes on
* a large set of contacts.
*/
protected void updateLocaleInBackground() {
// The process is already running - postpone the change
if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
return;
}
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
final String providerLocale = prefs.getString(PREF_LOCALE, null);
final Locale currentLocale = mCurrentLocale;
if (currentLocale.toString().equals(providerLocale)) {
return;
}
int providerStatus = mProviderStatus;
setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
mContactsHelper.setLocale(this, currentLocale);
mProfileHelper.setLocale(this, currentLocale);
prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
setProviderStatus(providerStatus);
}
/**
* Reinitializes the provider for a new locale.
*/
private void changeLocaleInBackground() {
// Re-initializing the provider without stopping it.
// Locking the database will prevent inserts/updates/deletes from
// running at the same time, but queries may still be running
// on other threads. Those queries may return inconsistent results.
SQLiteDatabase db = mContactsHelper.getWritableDatabase();
SQLiteDatabase profileDb = mProfileHelper.getWritableDatabase();
db.beginTransaction();
profileDb.beginTransaction();
try {
initForDefaultLocale();
db.setTransactionSuccessful();
profileDb.setTransactionSuccessful();
} finally {
db.endTransaction();
profileDb.endTransaction();
}
updateLocaleInBackground();
}
protected void updateSearchIndexInBackground() {
mSearchIndexManager.updateIndex();
}
protected void updateDirectoriesInBackground(boolean rescan) {
mContactDirectoryManager.scanAllPackages(rescan);
}
private void updateProviderStatus() {
if (mProviderStatus != ProviderStatus.STATUS_NORMAL
&& mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
return;
}
// No accounts/no contacts status is true if there are no account and
// there are no contacts or one profile contact
if (mContactsAccountCount == 0) {
long contactsNum = DatabaseUtils.queryNumEntries(mContactsHelper.getReadableDatabase(),
Tables.CONTACTS, null);
long profileNum = DatabaseUtils.queryNumEntries(mProfileHelper.getReadableDatabase(),
Tables.CONTACTS, null);
// TODO: Different status if there is a profile but no contacts?
if (contactsNum == 0 && profileNum <= 1) {
setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS);
} else {
setProviderStatus(ProviderStatus.STATUS_NORMAL);
}
} else {
setProviderStatus(ProviderStatus.STATUS_NORMAL);
}
}
/* Visible for testing */
protected void cleanupPhotoStore() {
SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
mActiveDb.set(db);
// Assemble the set of photo store file IDs that are in use, and send those to the photo
// store. Any photos that aren't in that set will be deleted, and any photos that no
// longer exist in the photo store will be returned for us to clear out in the DB.
long photoMimeTypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
Cursor c = db.query(Views.DATA, new String[]{Data._ID, Photo.PHOTO_FILE_ID},
DataColumns.MIMETYPE_ID + "=" + photoMimeTypeId + " AND "
+ Photo.PHOTO_FILE_ID + " IS NOT NULL", null, null, null, null);
Set<Long> usedPhotoFileIds = Sets.newHashSet();
Map<Long, Long> photoFileIdToDataId = Maps.newHashMap();
try {
while (c.moveToNext()) {
long dataId = c.getLong(0);
long photoFileId = c.getLong(1);
usedPhotoFileIds.add(photoFileId);
photoFileIdToDataId.put(photoFileId, dataId);
}
} finally {
c.close();
}
// Also query for all social stream item photos.
c = db.query(Tables.STREAM_ITEM_PHOTOS + " JOIN " + Tables.STREAM_ITEMS
+ " ON " + StreamItemPhotos.STREAM_ITEM_ID + "=" + StreamItemsColumns.CONCRETE_ID
+ " JOIN " + Tables.RAW_CONTACTS
+ " ON " + StreamItems.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID,
new String[]{
StreamItemPhotosColumns.CONCRETE_ID,
StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID,
StreamItemPhotos.PHOTO_FILE_ID,
RawContacts.ACCOUNT_TYPE,
RawContacts.ACCOUNT_NAME
},
null, null, null, null, null);
Map<Long, Long> photoFileIdToStreamItemPhotoId = Maps.newHashMap();
Map<Long, Long> streamItemPhotoIdToStreamItemId = Maps.newHashMap();
Map<Long, Account> streamItemPhotoIdToAccount = Maps.newHashMap();
try {
while (c.moveToNext()) {
long streamItemPhotoId = c.getLong(0);
long streamItemId = c.getLong(1);
long photoFileId = c.getLong(2);
String accountType = c.getString(3);
String accountName = c.getString(4);
usedPhotoFileIds.add(photoFileId);
photoFileIdToStreamItemPhotoId.put(photoFileId, streamItemPhotoId);
streamItemPhotoIdToStreamItemId.put(streamItemPhotoId, streamItemId);
Account account = new Account(accountName, accountType);
streamItemPhotoIdToAccount.put(photoFileId, account);
}
} finally {
c.close();
}
// Run the photo store cleanup.
Set<Long> missingPhotoIds = mPhotoStore.get().cleanup(usedPhotoFileIds);
// If any of the keys we're using no longer exist, clean them up. We need to do these
// using internal APIs or direct DB access to avoid permission errors.
if (!missingPhotoIds.isEmpty()) {
try {
db.beginTransactionWithListener(this);
for (long missingPhotoId : missingPhotoIds) {
if (photoFileIdToDataId.containsKey(missingPhotoId)) {
long dataId = photoFileIdToDataId.get(missingPhotoId);
ContentValues updateValues = new ContentValues();
updateValues.putNull(Photo.PHOTO_FILE_ID);
updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
updateValues, null, null, false);
}
if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) {
// For missing photos that were in stream item photos, just delete the
// stream item photo.
long streamItemPhotoId = photoFileIdToStreamItemPhotoId.get(missingPhotoId);
db.delete(Tables.STREAM_ITEM_PHOTOS, StreamItemPhotos._ID + "=?",
new String[]{String.valueOf(streamItemPhotoId)});
}
}
db.setTransactionSuccessful();
} catch (Exception e) {
// Cleanup failure is not a fatal problem. We'll try again later.
Log.e(TAG, "Failed to clean up outdated photo references", e);
} finally {
db.endTransaction();
}
}
}
@Override
protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
return ContactsDatabaseHelper.getInstance(context);
}
@Override
protected ThreadLocal<ContactsTransaction> getTransactionHolder() {
return mTransactionHolder;
}
public ProfileProvider getProfileProvider() {
return new ProfileProvider(this);
}
@VisibleForTesting
/* package */ PhotoStore getPhotoStore() {
return mContactsPhotoStore;
}
@VisibleForTesting
/* package */ PhotoStore getProfilePhotoStore() {
return mProfilePhotoStore;
}
/* package */ int getMaxDisplayPhotoDim() {
return mMaxDisplayPhotoDim;
}
/* package */ int getMaxThumbnailPhotoDim() {
return mMaxThumbnailPhotoDim;
}
/* package */ NameSplitter getNameSplitter() {
return mNameSplitter;
}
/* package */ NameLookupBuilder getNameLookupBuilder() {
return mNameLookupBuilder;
}
/* Visible for testing */
public ContactDirectoryManager getContactDirectoryManagerForTest() {
return mContactDirectoryManager;
}
/* Visible for testing */
protected Locale getLocale() {
return Locale.getDefault();
}
private boolean inProfileMode() {
Boolean profileMode = mInProfileMode.get();
return profileMode != null && profileMode;
}
protected boolean isLegacyContactImportNeeded() {
int version = Integer.parseInt(
mContactsHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
return version < PROPERTY_CONTACTS_IMPORT_VERSION;
}
protected LegacyContactImporter getLegacyContactImporter() {
return new LegacyContactImporter(getContext(), this);
}
/**
* Imports legacy contacts as a background task.
*/
private void importLegacyContactsInBackground() {
Log.v(TAG, "Importing legacy contacts");
setProviderStatus(ProviderStatus.STATUS_UPGRADING);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
mContactsHelper.setLocale(this, mCurrentLocale);
prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
LegacyContactImporter importer = getLegacyContactImporter();
if (importLegacyContacts(importer)) {
onLegacyContactImportSuccess();
} else {
onLegacyContactImportFailure();
}
}
/**
* Unlocks the provider and declares that the import process is complete.
*/
private void onLegacyContactImportSuccess() {
NotificationManager nm =
(NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
// Store a property in the database indicating that the conversion process succeeded
mContactsHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
setProviderStatus(ProviderStatus.STATUS_NORMAL);
Log.v(TAG, "Completed import of legacy contacts");
}
/**
* Announces the provider status and keeps the provider locked.
*/
private void onLegacyContactImportFailure() {
Context context = getContext();
NotificationManager nm =
(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
// Show a notification
Notification n = new Notification(android.R.drawable.stat_notify_error,
context.getString(R.string.upgrade_out_of_memory_notification_ticker),
System.currentTimeMillis());
n.setLatestEventInfo(context,
context.getString(R.string.upgrade_out_of_memory_notification_title),
context.getString(R.string.upgrade_out_of_memory_notification_text),
PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
Log.v(TAG, "Failed to import legacy contacts");
// Do not let any database changes until this issue is resolved.
mOkToOpenAccess = false;
}
/* Visible for testing */
/* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
boolean aggregatorEnabled = mContactAggregator.isEnabled();
mContactAggregator.setEnabled(false);
try {
if (importer.importContacts()) {
// TODO aggregate all newly added raw contacts
mContactAggregator.setEnabled(aggregatorEnabled);
return true;
}
} catch (Throwable e) {
Log.e(TAG, "Legacy contact import failed", e);
}
mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
return false;
}
/**
* Wipes all data from the contacts database.
*/
/* package */ void wipeData() {
mContactsHelper.wipeData();
mProfileHelper.wipeData();
mContactsPhotoStore.clear();
mProfilePhotoStore.clear();
mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS;
}
/**
* During intialization, this content provider will
* block all attempts to change contacts data. In particular, it will hold
* up all contact syncs. As soon as the import process is complete, all
* processes waiting to write to the provider are unblocked and can proceed
* to compete for the database transaction monitor.
*/
private void waitForAccess(CountDownLatch latch) {
if (latch == null) {
return;
}
while (true) {
try {
latch.await();
return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* Determines whether the given URI should be directed to the profile
* database rather than the contacts database. This is true under either
* of three conditions:
* 1. The URI itself is specifically for the profile.
* 2. The URI contains ID references that are in the profile ID-space.
* 3. The URI contains lookup key references that match the special profile lookup key.
* @param uri The URI to examine.
* @return Whether to direct the DB operation to the profile database.
*/
private boolean mapsToProfileDb(Uri uri) {
return sUriMatcher.mapsToProfile(uri);
}
/**
* Determines whether the given URI with the given values being inserted
* should be directed to the profile database rather than the contacts
* database. This is true if the URI already maps to the profile DB from
* a call to {@link #mapsToProfileDb} or if the URI matches a URI that
* specifies parent IDs via the ContentValues, and the given ContentValues
* contains an ID in the profile ID-space.
* @param uri The URI to examine.
* @param values The values being inserted.
* @return Whether to direct the DB insert to the profile database.
*/
private boolean mapsToProfileDbWithInsertedValues(Uri uri, ContentValues values) {
if (mapsToProfileDb(uri)) {
return true;
}
int match = sUriMatcher.match(uri);
if (INSERT_URI_ID_VALUE_MAP.containsKey(match)) {
String idField = INSERT_URI_ID_VALUE_MAP.get(match);
if (values.containsKey(idField)) {
long id = values.getAsLong(idField);
if (ContactsContract.isProfileId(id)) {
return true;
}
}
}
return false;
}
/**
* Switches the provider's thread-local context variables to prepare for performing
* a profile operation.
*/
protected void switchToProfileMode() {
mDbHelper.set(mProfileHelper);
mTransactionContext.set(mProfileTransactionContext);
mAggregator.set(mProfileAggregator);
mPhotoStore.set(mProfilePhotoStore);
mInProfileMode.set(true);
}
/**
* Switches the provider's thread-local context variables to prepare for performing
* a contacts operation.
*/
protected void switchToContactMode() {
mDbHelper.set(mContactsHelper);
mTransactionContext.set(mContactTransactionContext);
mAggregator.set(mContactAggregator);
mPhotoStore.set(mContactsPhotoStore);
mInProfileMode.set(false);
// Clear out the active database; modification operations will set this to the contacts DB.
mActiveDb.set(null);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
waitForAccess(mWriteAccessLatch);
// Enforce stream items access check if applicable.
enforceSocialStreamWritePermission(uri);
if (mapsToProfileDbWithInsertedValues(uri, values)) {
switchToProfileMode();
return mProfileProvider.insert(uri, values);
} else {
switchToContactMode();
return super.insert(uri, values);
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (mWriteAccessLatch != null) {
// We are stuck trying to upgrade contacts db. The only update request
// allowed in this case is an update of provider status, which will trigger
// an attempt to upgrade contacts again.
int match = sUriMatcher.match(uri);
if (match == PROVIDER_STATUS) {
Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS);
return 1;
} else {
return 0;
}
}
}
waitForAccess(mWriteAccessLatch);
// Enforce stream items access check if applicable.
enforceSocialStreamWritePermission(uri);
if (mapsToProfileDb(uri)) {
switchToProfileMode();
return mProfileProvider.update(uri, values, selection, selectionArgs);
} else {
switchToContactMode();
return super.update(uri, values, selection, selectionArgs);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
waitForAccess(mWriteAccessLatch);
// Enforce stream items access check if applicable.
enforceSocialStreamWritePermission(uri);
if (mapsToProfileDb(uri)) {
switchToProfileMode();
return mProfileProvider.delete(uri, selection, selectionArgs);
} else {
switchToContactMode();
return super.delete(uri, selection, selectionArgs);
}
}
/**
* Replaces the current (thread-local) database to use for the operation with the given one.
* @param db The database to use.
*/
/* package */ void substituteDb(SQLiteDatabase db) {
mActiveDb.set(db);
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
waitForAccess(mReadAccessLatch);
if (method.equals(Authorization.AUTHORIZATION_METHOD)) {
Uri uri = (Uri) extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE);
// Check permissions on the caller. The URI can only be pre-authorized if the caller
// already has the necessary permissions.
enforceSocialStreamReadPermission(uri);
if (mapsToProfileDb(uri)) {
mProfileProvider.enforceReadPermission(uri);
}
// If there hasn't been a security violation yet, we're clear to pre-authorize the URI.
Uri authUri = preAuthorizeUri(uri);
Bundle response = new Bundle();
response.putParcelable(Authorization.KEY_AUTHORIZED_URI, authUri);
return response;
}
return null;
}
/**
* Pre-authorizes the given URI, adding an expiring permission token to it and placing that
* in our map of pre-authorized URIs.
* @param uri The URI to pre-authorize.
* @return A pre-authorized URI that will not require special permissions to use.
*/
private Uri preAuthorizeUri(Uri uri) {
String token = String.valueOf(mRandom.nextLong());
Uri authUri = uri.buildUpon()
.appendQueryParameter(PREAUTHORIZED_URI_TOKEN, token)
.build();
long expiration = SystemClock.elapsedRealtime() + mPreAuthorizedUriDuration;
mPreAuthorizedUris.put(authUri, expiration);
return authUri;
}
/**
* Checks whether the given URI has an unexpired permission token that would grant access to
* query the content. If it does, the regular permission check should be skipped.
* @param uri The URI being accessed.
* @return Whether the URI is a pre-authorized URI that is still valid.
*/
public boolean isValidPreAuthorizedUri(Uri uri) {
// Only proceed if the URI has a permission token parameter.
if (uri.getQueryParameter(PREAUTHORIZED_URI_TOKEN) != null) {
// First expire any pre-authorization URIs that are no longer valid.
long now = SystemClock.elapsedRealtime();
Set<Uri> expiredUris = Sets.newHashSet();
for (Uri preAuthUri : mPreAuthorizedUris.keySet()) {
if (mPreAuthorizedUris.get(preAuthUri) < now) {
expiredUris.add(preAuthUri);
}
}
for (Uri expiredUri : expiredUris) {
mPreAuthorizedUris.remove(expiredUri);
}
// Now check to see if the pre-authorized URI map contains the URI.
if (mPreAuthorizedUris.containsKey(uri)) {
// Unexpired token - skip the permission check.
return true;
}
}
return false;
}
@Override
protected boolean yield(ContactsTransaction transaction) {
// If there's a profile transaction in progress, and we're yielding, we need to
// end it. Unlike the Contacts DB yield (which re-starts a transaction at its
// conclusion), we can just go back into a state in which we have no active
// profile transaction, and let it be re-created as needed. We can't hold onto
// the transaction without risking a deadlock.
SQLiteDatabase profileDb = transaction.removeDbForTag(PROFILE_DB_TAG);
if (profileDb != null) {
profileDb.setTransactionSuccessful();
profileDb.endTransaction();
}
// Now proceed with the Contacts DB yield.
SQLiteDatabase contactsDb = transaction.getDbForTag(CONTACTS_DB_TAG);
return contactsDb != null && contactsDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
waitForAccess(mWriteAccessLatch);
return super.applyBatch(operations);
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
waitForAccess(mWriteAccessLatch);
return super.bulkInsert(uri, values);
}
@Override
public void onBegin() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "onBeginTransaction");
}
if (inProfileMode()) {
mProfileAggregator.clearPendingAggregations();
mProfileTransactionContext.clear();
} else {
mContactAggregator.clearPendingAggregations();
mContactTransactionContext.clear();
}
}
@Override
public void onCommit() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "beforeTransactionCommit");
}
flushTransactionalChanges();
mAggregator.get().aggregateInTransaction(mTransactionContext.get(), mActiveDb.get());
if (mVisibleTouched) {
mVisibleTouched = false;
mDbHelper.get().updateAllVisible();
}
updateSearchIndexInTransaction();
if (mProviderStatusUpdateNeeded) {
updateProviderStatus();
mProviderStatusUpdateNeeded = false;
}
}
@Override
public void onRollback() {
// Not used.
}
private void updateSearchIndexInTransaction() {
Set<Long> staleContacts = mTransactionContext.get().getStaleSearchIndexContactIds();
Set<Long> staleRawContacts = mTransactionContext.get().getStaleSearchIndexRawContactIds();
if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) {
mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts);
mTransactionContext.get().clearSearchIndexUpdates();
}
}
private void flushTransactionalChanges() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "flushTransactionChanges");
}
for (long rawContactId : mTransactionContext.get().getInsertedRawContactIds()) {
mDbHelper.get().updateRawContactDisplayName(mActiveDb.get(), rawContactId);
mAggregator.get().onRawContactInsert(mTransactionContext.get(), mActiveDb.get(),
rawContactId);
}
Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
if (!dirtyRawContacts.isEmpty()) {
mSb.setLength(0);
mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
appendIds(mSb, dirtyRawContacts);
mSb.append(")");
mActiveDb.get().execSQL(mSb.toString());
}
Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
if (!updatedRawContacts.isEmpty()) {
mSb.setLength(0);
mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
appendIds(mSb, updatedRawContacts);
mSb.append(")");
mActiveDb.get().execSQL(mSb.toString());
}
// Update sync states.
for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) {
long id = entry.getKey();
if (mDbHelper.get().getSyncState().update(mActiveDb.get(), id, entry.getValue()) <= 0) {
throw new IllegalStateException(
"unable to update sync state, does it still exist?");
}
}
mTransactionContext.get().clear();
}
/**
* Appends comma separated ids.
* @param ids Should not be empty
*/
private void appendIds(StringBuilder sb, Set<Long> ids) {
for (long id : ids) {
sb.append(id).append(',');
}
sb.setLength(sb.length() - 1); // Yank the last comma
}
@Override
protected void notifyChange() {
notifyChange(mSyncToNetwork);
mSyncToNetwork = false;
}
protected void notifyChange(boolean syncToNetwork) {
getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
syncToNetwork);
}
protected void setProviderStatus(int status) {
if (mProviderStatus != status) {
mProviderStatus = status;
getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false);
}
}
public DataRowHandler getDataRowHandler(final String mimeType) {
if (inProfileMode()) {
return getDataRowHandlerForProfile(mimeType);
}
DataRowHandler handler = mDataRowHandlers.get(mimeType);
if (handler == null) {
handler = new DataRowHandlerForCustomMimetype(
getContext(), mContactsHelper, mContactAggregator, mimeType);
mDataRowHandlers.put(mimeType, handler);
}
return handler;
}
public DataRowHandler getDataRowHandlerForProfile(final String mimeType) {
DataRowHandler handler = mProfileDataRowHandlers.get(mimeType);
if (handler == null) {
handler = new DataRowHandlerForCustomMimetype(
getContext(), mProfileHelper, mProfileAggregator, mimeType);
mProfileDataRowHandlers.put(mimeType, handler);
}
return handler;
}
@Override
protected Uri insertInTransaction(Uri uri, ContentValues values) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "insertInTransaction: " + uri + " " + values);
}
// Default active DB to the contacts DB if none has been set.
if (mActiveDb.get() == null) {
mActiveDb.set(mContactsHelper.getWritableDatabase());
}
final boolean callerIsSyncAdapter =
readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
final int match = sUriMatcher.match(uri);
long id = 0;
switch (match) {
case SYNCSTATE:
case PROFILE_SYNCSTATE:
id = mDbHelper.get().getSyncState().insert(mActiveDb.get(), values);
break;
case CONTACTS: {
insertContact(values);
break;
}
case PROFILE: {
throw new UnsupportedOperationException(
"The profile contact is created automatically");
}
case RAW_CONTACTS:
case PROFILE_RAW_CONTACTS: {
id = insertRawContact(uri, values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case RAW_CONTACTS_DATA:
case PROFILE_RAW_CONTACTS_ID_DATA: {
int segment = match == RAW_CONTACTS_DATA ? 1 : 2;
values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(segment));
id = insertData(values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case RAW_CONTACTS_ID_STREAM_ITEMS: {
values.put(StreamItems.RAW_CONTACT_ID, uri.getPathSegments().get(1));
id = insertStreamItem(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case DATA:
case PROFILE_DATA: {
id = insertData(values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case GROUPS: {
id = insertGroup(uri, values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case SETTINGS: {
id = insertSettings(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case STATUS_UPDATES:
case PROFILE_STATUS_UPDATES: {
id = insertStatusUpdate(values);
break;
}
case STREAM_ITEMS: {
id = insertStreamItem(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case STREAM_ITEMS_PHOTOS: {
id = insertStreamItemPhoto(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case STREAM_ITEMS_ID_PHOTOS: {
values.put(StreamItemPhotos.STREAM_ITEM_ID, uri.getPathSegments().get(1));
id = insertStreamItemPhoto(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
default:
mSyncToNetwork = true;
return mLegacyApiSupport.insert(uri, values);
}
if (id < 0) {
return null;
}
return ContentUris.withAppendedId(uri, id);
}
/**
* If account is non-null then store it in the values. If the account is
* already specified in the values then it must be consistent with the
* account, if it is non-null.
*
* @param uri Current {@link Uri} being operated on.
* @param values {@link ContentValues} to read and possibly update.
* @throws IllegalArgumentException when only one of
* {@link RawContacts#ACCOUNT_NAME} or
* {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
* other undefined.
* @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
* and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
* the given {@link Uri} and {@link ContentValues}.
*/
private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
final boolean partialValues = TextUtils.isEmpty(valueAccountName)
^ TextUtils.isEmpty(valueAccountType);
if (partialUri || partialValues) {
// Throw when either account is incomplete
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
}
// Accounts are valid by only checking one parameter, since we've
// already ruled out partial accounts.
final boolean validUri = !TextUtils.isEmpty(accountName);
final boolean validValues = !TextUtils.isEmpty(valueAccountName);
if (validValues && validUri) {
// Check that accounts match when both present
final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
&& TextUtils.equals(accountType, valueAccountType);
if (!accountMatch) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
}
} else if (validUri) {
// Fill values from Uri when not present
values.put(RawContacts.ACCOUNT_NAME, accountName);
values.put(RawContacts.ACCOUNT_TYPE, accountType);
} else if (validValues) {
accountName = valueAccountName;
accountType = valueAccountType;
} else {
return null;
}
// Use cached Account object when matches, otherwise create
if (mAccount == null
|| !mAccount.name.equals(accountName)
|| !mAccount.type.equals(accountType)) {
mAccount = new Account(accountName, accountType);
}
return mAccount;
}
/**
* Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified
* in the URI or values (if any).
* @param uri Current {@link Uri} being operated on.
* @param values {@link ContentValues} to read and possibly update.
*/
private AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values) {
final Account account = resolveAccount(uri, values);
AccountWithDataSet accountWithDataSet = null;
if (account != null) {
String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
if (dataSet == null) {
dataSet = values.getAsString(RawContacts.DATA_SET);
} else {
values.put(RawContacts.DATA_SET, dataSet);
}
accountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
}
return accountWithDataSet;
}
/**
* Inserts an item in the contacts table
*
* @param values the values for the new row
* @return the row ID of the newly created row
*/
private long insertContact(ContentValues values) {
throw new UnsupportedOperationException("Aggregate contacts are created automatically");
}
/**
* Inserts an item in the raw contacts table
*
* @param uri the values for the new row
* @param values the account this contact should be associated with. may be null.
* @param callerIsSyncAdapter
* @return the row ID of the newly created row
*/
private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
mValues.clear();
mValues.putAll(values);
mValues.putNull(RawContacts.CONTACT_ID);
AccountWithDataSet accountWithDataSet = resolveAccountWithDataSet(uri, mValues);
if (values.containsKey(RawContacts.DELETED)
&& values.getAsInteger(RawContacts.DELETED) != 0) {
mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
}
long rawContactId = mActiveDb.get().insert(Tables.RAW_CONTACTS,
RawContacts.CONTACT_ID, mValues);
int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
}
mAggregator.get().markNewForAggregation(rawContactId, aggregationMode);
// Trigger creation of a Contact based on this RawContact at the end of transaction
mTransactionContext.get().rawContactInserted(rawContactId, accountWithDataSet);
if (!callerIsSyncAdapter) {
addAutoAddMembership(rawContactId);
final Long starred = values.getAsLong(RawContacts.STARRED);
if (starred != null && starred != 0) {
updateFavoritesMembership(rawContactId, starred != 0);
}
}
mProviderStatusUpdateNeeded = true;
return rawContactId;
}
private void addAutoAddMembership(long rawContactId) {
final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
rawContactId);
if (groupId != null) {
insertDataGroupMembership(rawContactId, groupId);
}
}
private Long findGroupByRawContactId(String selection, long rawContactId) {
Cursor c = mActiveDb.get().query(Tables.GROUPS + "," + Tables.RAW_CONTACTS,
PROJECTION_GROUP_ID, selection,
new String[]{Long.toString(rawContactId)},
null /* groupBy */, null /* having */, null /* orderBy */);
try {
while (c.moveToNext()) {
return c.getLong(0);
}
return null;
} finally {
c.close();
}
}
private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
rawContactId);
if (groupId != null) {
if (isStarred) {
insertDataGroupMembership(rawContactId, groupId);
} else {
deleteDataGroupMembership(rawContactId, groupId);
}
}
}
private void insertDataGroupMembership(long rawContactId, long groupId) {
ContentValues groupMembershipValues = new ContentValues();
groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
groupMembershipValues.put(DataColumns.MIMETYPE_ID,
mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
mActiveDb.get().insert(Tables.DATA, null, groupMembershipValues);
}
private void deleteDataGroupMembership(long rawContactId, long groupId) {
final String[] selectionArgs = {
Long.toString(mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
Long.toString(groupId),
Long.toString(rawContactId)};
mActiveDb.get().delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
}
/**
* Inserts an item in the data table
*
* @param values the values for the new row
* @return the row ID of the newly created row
*/
private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
long id = 0;
mValues.clear();
mValues.putAll(values);
long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
// Replace package with internal mapping
final String packageName = mValues.getAsString(Data.RES_PACKAGE);
if (packageName != null) {
mValues.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
}
mValues.remove(Data.RES_PACKAGE);
// Replace mimetype with internal mapping
final String mimeType = mValues.getAsString(Data.MIMETYPE);
if (TextUtils.isEmpty(mimeType)) {
throw new IllegalArgumentException(Data.MIMETYPE + " is required");
}
mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.get().getMimeTypeId(mimeType));
mValues.remove(Data.MIMETYPE);
DataRowHandler rowHandler = getDataRowHandler(mimeType);
id = rowHandler.insert(mActiveDb.get(), mTransactionContext.get(), rawContactId, mValues);
if (!callerIsSyncAdapter) {
mTransactionContext.get().markRawContactDirty(rawContactId);
}
mTransactionContext.get().rawContactUpdated(rawContactId);
return id;
}
/**
* Inserts an item in the stream_items table. The account is checked against the
* account in the raw contact for which the stream item is being inserted. If the
* new stream item results in more stream items under this raw contact than the limit,
* the oldest one will be deleted (note that if the stream item inserted was the
* oldest, it will be immediately deleted, and this will return 0).
*
* @param uri the insertion URI
* @param values the values for the new row
* @return the stream item _ID of the newly created row, or 0 if it was not created
*/
private long insertStreamItem(Uri uri, ContentValues values) {
long id = 0;
mValues.clear();
mValues.putAll(values);
long rawContactId = mValues.getAsLong(StreamItems.RAW_CONTACT_ID);
// Ensure that the raw contact exists and belongs to the caller's account.
Account account = resolveAccount(uri, mValues);
enforceModifyingAccount(account, rawContactId);
// Don't attempt to insert accounts params - they don't exist in the stream items table.
mValues.remove(RawContacts.ACCOUNT_NAME);
mValues.remove(RawContacts.ACCOUNT_TYPE);
// Insert the new stream item.
id = mActiveDb.get().insert(Tables.STREAM_ITEMS, null, mValues);
if (id == -1) {
// Insertion failed.
return 0;
}
// Check to see if we're over the limit for stream items under this raw contact.
// It's possible that the inserted stream item is older than the the existing
// ones, in which case it may be deleted immediately (resetting the ID to 0).
id = cleanUpOldStreamItems(rawContactId, id);
return id;
}
/**
* Inserts an item in the stream_item_photos table. The account is checked against
* the account in the raw contact that owns the stream item being modified.
*
* @param uri the insertion URI
* @param values the values for the new row
* @return the stream item photo _ID of the newly created row, or 0 if there was an issue
* with processing the photo or creating the row
*/
private long insertStreamItemPhoto(Uri uri, ContentValues values) {
long id = 0;
mValues.clear();
mValues.putAll(values);
long streamItemId = mValues.getAsLong(StreamItemPhotos.STREAM_ITEM_ID);
if (streamItemId != 0) {
long rawContactId = lookupRawContactIdForStreamId(streamItemId);
// Ensure that the raw contact exists and belongs to the caller's account.
Account account = resolveAccount(uri, mValues);
enforceModifyingAccount(account, rawContactId);
// Don't attempt to insert accounts params - they don't exist in the stream item
// photos table.
mValues.remove(RawContacts.ACCOUNT_NAME);
mValues.remove(RawContacts.ACCOUNT_TYPE);
// Process the photo and store it.
if (processStreamItemPhoto(mValues, false)) {
// Insert the stream item photo.
id = mActiveDb.get().insert(Tables.STREAM_ITEM_PHOTOS, null, mValues);
}
}
return id;
}
/**
* Processes the photo contained in the {@link ContactsContract.StreamItemPhotos#PHOTO}
* field of the given values, attempting to store it in the photo store. If successful,
* the resulting photo file ID will be added to the values for insert/update in the table.
* <p>
* If updating, it is valid for the picture to be empty or unspecified (the function will
* still return true). If inserting, a valid picture must be specified.
* @param values The content values provided by the caller.
* @param forUpdate Whether this photo is being processed for update (vs. insert).
* @return Whether the insert or update should proceed.
*/
private boolean processStreamItemPhoto(ContentValues values, boolean forUpdate) {
if (!values.containsKey(StreamItemPhotos.PHOTO)) {
return forUpdate;
}
byte[] photoBytes = values.getAsByteArray(StreamItemPhotos.PHOTO);
if (photoBytes == null) {
return forUpdate;
}
// Process the photo and store it.
try {
long photoFileId = mPhotoStore.get().insert(new PhotoProcessor(photoBytes,
mMaxDisplayPhotoDim, mMaxThumbnailPhotoDim, true), true);
if (photoFileId != 0) {
values.put(StreamItemPhotos.PHOTO_FILE_ID, photoFileId);
values.remove(StreamItemPhotos.PHOTO);
return true;
} else {
// Couldn't store the photo, return 0.
Log.e(TAG, "Could not process stream item photo for insert");
return false;
}
} catch (IOException ioe) {
Log.e(TAG, "Could not process stream item photo for insert", ioe);
return false;
}
}
/**
* Looks up the raw contact ID that owns the specified stream item.
* @param streamItemId The ID of the stream item.
* @return The associated raw contact ID, or -1 if no such stream item exists.
*/
private long lookupRawContactIdForStreamId(long streamItemId) {
long rawContactId = -1;
Cursor c = mActiveDb.get().query(Tables.STREAM_ITEMS,
new String[]{StreamItems.RAW_CONTACT_ID},
StreamItems._ID + "=?", new String[]{String.valueOf(streamItemId)},
null, null, null);
try {
if (c.moveToFirst()) {
rawContactId = c.getLong(0);
}
} finally {
c.close();
}
return rawContactId;
}
/**
* If the given URI is reading stream items or stream photos, this will run a permission check
* for the android.permission.READ_SOCIAL_STREAM permission - otherwise it will do nothing.
* @param uri The URI to check.
*/
private void enforceSocialStreamReadPermission(Uri uri) {
if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))
&& !isValidPreAuthorizedUri(uri)) {
getContext().enforceCallingOrSelfPermission(
"android.permission.READ_SOCIAL_STREAM", null);
}
}
/**
* If the given URI is modifying stream items or stream photos, this will run a permission check
* for the android.permission.WRITE_SOCIAL_STREAM permission - otherwise it will do nothing.
* @param uri The URI to check.
*/
private void enforceSocialStreamWritePermission(Uri uri) {
if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))) {
getContext().enforceCallingOrSelfPermission(
"android.permission.WRITE_SOCIAL_STREAM", null);
}
}
/**
* Checks whether the given raw contact ID is owned by the given account.
* If the resolved account is null, this will return true iff the raw contact
* is also associated with the "null" account.
*
* If the resolved account does not match, this will throw a security exception.
* @param account The resolved account (may be null).
* @param rawContactId The raw contact ID to check for.
*/
private void enforceModifyingAccount(Account account, long rawContactId) {
String accountSelection = RawContactsColumns.CONCRETE_ID + "=? AND "
+ RawContactsColumns.CONCRETE_ACCOUNT_NAME + "=? AND "
+ RawContactsColumns.CONCRETE_ACCOUNT_TYPE + "=?";
String noAccountSelection = RawContactsColumns.CONCRETE_ID + "=? AND "
+ RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL AND "
+ RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL";
Cursor c;
if (account != null) {
c = mActiveDb.get().query(Tables.RAW_CONTACTS,
new String[]{RawContactsColumns.CONCRETE_ID}, accountSelection,
new String[]{String.valueOf(rawContactId), mAccount.name, mAccount.type},
null, null, null);
} else {
c = mActiveDb.get().query(Tables.RAW_CONTACTS,
new String[]{RawContactsColumns.CONCRETE_ID}, noAccountSelection,
new String[]{String.valueOf(rawContactId)},
null, null, null);
}
try {
if(c.getCount() == 0) {
throw new SecurityException("Caller account does not match raw contact ID "
+ rawContactId);
}
} finally {
c.close();
}
}
/**
* Checks whether the given selection of stream items matches up with the given
* account. If any of the raw contacts fail the account check, this will throw a
* security exception.
* @param account The resolved account (may be null).
* @param selection The selection.
* @param selectionArgs The selection arguments.
* @return The list of stream item IDs that would be included in this selection.
*/
private List<Long> enforceModifyingAccountForStreamItems(Account account, String selection,
String[] selectionArgs) {
List<Long> streamItemIds = Lists.newArrayList();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForStreamItems(qb);
Cursor c = qb.query(mActiveDb.get(),
new String[]{StreamItems._ID, StreamItems.RAW_CONTACT_ID},
selection, selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
streamItemIds.add(c.getLong(0));
// Throw a security exception if the account doesn't match the raw contact's.
enforceModifyingAccount(account, c.getLong(1));
}
} finally {
c.close();
}
return streamItemIds;
}
/**
* Checks whether the given selection of stream item photos matches up with the given
* account. If any of the raw contacts fail the account check, this will throw a
* security exception.
* @param account The resolved account (may be null).
* @param selection The selection.
* @param selectionArgs The selection arguments.
* @return The list of stream item photo IDs that would be included in this selection.
*/
private List<Long> enforceModifyingAccountForStreamItemPhotos(Account account, String selection,
String[] selectionArgs) {
List<Long> streamItemPhotoIds = Lists.newArrayList();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForStreamItemPhotos(qb);
Cursor c = qb.query(mActiveDb.get(),
new String[]{StreamItemPhotos._ID, StreamItems.RAW_CONTACT_ID},
selection, selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
streamItemPhotoIds.add(c.getLong(0));
// Throw a security exception if the account doesn't match the raw contact's.
enforceModifyingAccount(account, c.getLong(1));
}
} finally {
c.close();
}
return streamItemPhotoIds;
}
/**
* Queries the database for stream items under the given raw contact. If there are
* more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT},
* the oldest entries (as determined by timestamp) will be deleted.
* @param rawContactId The raw contact ID to examine for stream items.
* @param insertedStreamItemId The ID of the stream item that was just inserted,
* prompting this cleanup. Callers may pass 0 if no insertion prompted the
* cleanup.
* @return The ID of the inserted stream item if it still exists after cleanup;
* 0 otherwise.
*/
private long cleanUpOldStreamItems(long rawContactId, long insertedStreamItemId) {
long postCleanupInsertedStreamId = insertedStreamItemId;
Cursor c = mActiveDb.get().query(Tables.STREAM_ITEMS, new String[]{StreamItems._ID},
StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)},
null, null, StreamItems.TIMESTAMP + " DESC, " + StreamItems._ID + " DESC");
try {
int streamItemCount = c.getCount();
if (streamItemCount <= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
// Still under the limit - nothing to clean up!
return insertedStreamItemId;
} else {
c.moveToLast();
while (c.getPosition() >= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
long streamItemId = c.getLong(0);
if (insertedStreamItemId == streamItemId) {
// The stream item just inserted is being deleted.
postCleanupInsertedStreamId = 0;
}
deleteStreamItem(c.getLong(0));
c.moveToPrevious();
}
}
} finally {
c.close();
}
return postCleanupInsertedStreamId;
}
/**
* Delete data row by row so that fixing of primaries etc work correctly.
*/
private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
int count = 0;
// Note that the query will return data according to the access restrictions,
// so we don't need to worry about deleting data we don't have permission to read.
Uri dataUri = inProfileMode()
? Uri.withAppendedPath(Profile.CONTENT_URI, RawContacts.Data.CONTENT_DIRECTORY)
: Data.CONTENT_URI;
Cursor c = query(dataUri, DataRowHandler.DataDeleteQuery.COLUMNS,
selection, selectionArgs, null);
try {
while(c.moveToNext()) {
long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID);
String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
DataRowHandler rowHandler = getDataRowHandler(mimeType);
count += rowHandler.delete(mActiveDb.get(), mTransactionContext.get(), c);
if (!callerIsSyncAdapter) {
mTransactionContext.get().markRawContactDirty(rawContactId);
}
}
} finally {
c.close();
}
return count;
}
/**
* Delete a data row provided that it is one of the allowed mime types.
*/
public int deleteData(long dataId, String[] allowedMimeTypes) {
// Note that the query will return data according to the access restrictions,
// so we don't need to worry about deleting data we don't have permission to read.
mSelectionArgs1[0] = String.valueOf(dataId);
Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?",
mSelectionArgs1, null);
try {
if (!c.moveToFirst()) {
return 0;
}
String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
boolean valid = false;
for (int i = 0; i < allowedMimeTypes.length; i++) {
if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
valid = true;
break;
}
}
if (!valid) {
throw new IllegalArgumentException("Data type mismatch: expected "
+ Lists.newArrayList(allowedMimeTypes));
}
DataRowHandler rowHandler = getDataRowHandler(mimeType);
return rowHandler.delete(mActiveDb.get(), mTransactionContext.get(), c);
} finally {
c.close();
}
}
/**
* Inserts an item in the groups table
*/
private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
mValues.clear();
mValues.putAll(values);
final AccountWithDataSet accountWithDataSet = resolveAccountWithDataSet(uri, mValues);
// Replace package with internal mapping
final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
if (packageName != null) {
mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
}
mValues.remove(Groups.RES_PACKAGE);
final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
? mValues.getAsLong(Groups.FAVORITES) != 0
: false;
if (!callerIsSyncAdapter) {
mValues.put(Groups.DIRTY, 1);
}
long result = mActiveDb.get().insert(Tables.GROUPS, Groups.TITLE, mValues);
if (!callerIsSyncAdapter && isFavoritesGroup) {
// add all starred raw contacts to this group
String selection;
String[] selectionArgs;
if (accountWithDataSet == null) {
selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
+ RawContacts.ACCOUNT_TYPE + " IS NULL AND "
+ RawContacts.DATA_SET + " IS NULL";
selectionArgs = null;
} else if (accountWithDataSet.getDataSet() == null) {
selection = RawContacts.ACCOUNT_NAME + "=? AND "
+ RawContacts.ACCOUNT_TYPE + "=? AND "
+ RawContacts.DATA_SET + " IS NULL";
selectionArgs = new String[] {
accountWithDataSet.getAccountName(),
accountWithDataSet.getAccountType()
};
} else {
selection = RawContacts.ACCOUNT_NAME + "=? AND "
+ RawContacts.ACCOUNT_TYPE + "=? AND "
+ RawContacts.DATA_SET + "=?";
selectionArgs = new String[] {
accountWithDataSet.getAccountName(),
accountWithDataSet.getAccountType(),
accountWithDataSet.getDataSet()
};
}
Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS,
new String[]{RawContacts._ID, RawContacts.STARRED},
selection, selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
if (c.getLong(1) != 0) {
final long rawContactId = c.getLong(0);
insertDataGroupMembership(rawContactId, result);
mTransactionContext.get().markRawContactDirty(rawContactId);
}
}
} finally {
c.close();
}
}
if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
mVisibleTouched = true;
}
return result;
}
private long insertSettings(Uri uri, ContentValues values) {
// Before inserting, ensure that no settings record already exists for the
// values being inserted (this used to be enforced by a primary key, but that no
// longer works with the nullable data_set field added).
String accountName = values.getAsString(Settings.ACCOUNT_NAME);
String accountType = values.getAsString(Settings.ACCOUNT_TYPE);
String dataSet = values.getAsString(Settings.DATA_SET);
Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon();
if (accountName != null) {
settingsUri.appendQueryParameter(Settings.ACCOUNT_NAME, accountName);
}
if (accountType != null) {
settingsUri.appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
}
if (dataSet != null) {
settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
}
Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0);
try {
if (c.getCount() > 0) {
// If a record was found, replace it with the new values.
String selection = null;
String[] selectionArgs = null;
if (accountName != null && accountType != null) {
selection = Settings.ACCOUNT_NAME + "=? AND " + Settings.ACCOUNT_TYPE + "=?";
if (dataSet == null) {
selection += " AND " + Settings.DATA_SET + " IS NULL";
selectionArgs = new String[] {accountName, accountType};
} else {
selection += " AND " + Settings.DATA_SET + "=?";
selectionArgs = new String[] {accountName, accountType, dataSet};
}
}
return updateSettings(uri, values, selection, selectionArgs);
}
} finally {
c.close();
}
// If we didn't find a duplicate, we're fine to insert.
final long id = mActiveDb.get().insert(Tables.SETTINGS, null, values);
if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
mVisibleTouched = true;
}
return id;
}
/**
* Inserts a status update.
*/
public long insertStatusUpdate(ContentValues values) {
final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
String customProtocol = null;
if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
if (TextUtils.isEmpty(customProtocol)) {
throw new IllegalArgumentException(
"CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
}
}
long rawContactId = -1;
long contactId = -1;
Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
String accountType = null;
String accountName = null;
mSb.setLength(0);
mSelectionArgs.clear();
if (dataId != null) {
// Lookup the contact info for the given data row.
mSb.append(Tables.DATA + "." + Data._ID + "=?");
mSelectionArgs.add(String.valueOf(dataId));
} else {
// Lookup the data row to attach this presence update to
if (TextUtils.isEmpty(handle) || protocol == null) {
throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
}
// TODO: generalize to allow other providers to match against email
boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
String mimeTypeIdIm = String.valueOf(mDbHelper.get().getMimeTypeIdForIm());
if (matchEmail) {
String mimeTypeIdEmail = String.valueOf(mDbHelper.get().getMimeTypeIdForEmail());
// The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
// the "OR" conjunction confuses it and it switches to a full scan of
// the raw_contacts table.
// This code relies on the fact that Im.DATA and Email.DATA are in fact the same
// column - Data.DATA1
mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
" AND " + Data.DATA1 + "=?" +
" AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
mSelectionArgs.add(mimeTypeIdEmail);
mSelectionArgs.add(mimeTypeIdIm);
mSelectionArgs.add(handle);
mSelectionArgs.add(mimeTypeIdIm);
mSelectionArgs.add(String.valueOf(protocol));
if (customProtocol != null) {
mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
mSelectionArgs.add(customProtocol);
}
mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
mSelectionArgs.add(mimeTypeIdEmail);
} else {
mSb.append(DataColumns.MIMETYPE_ID + "=?" +
" AND " + Im.PROTOCOL + "=?" +
" AND " + Im.DATA + "=?");
mSelectionArgs.add(mimeTypeIdIm);
mSelectionArgs.add(String.valueOf(protocol));
mSelectionArgs.add(handle);
if (customProtocol != null) {
mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
mSelectionArgs.add(customProtocol);
}
}
if (values.containsKey(StatusUpdates.DATA_ID)) {
mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
}
}
Cursor cursor = null;
try {
cursor = mActiveDb.get().query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
if (cursor.moveToFirst()) {
dataId = cursor.getLong(DataContactsQuery.DATA_ID);
rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
accountType = cursor.getString(DataContactsQuery.ACCOUNT_TYPE);
accountName = cursor.getString(DataContactsQuery.ACCOUNT_NAME);
contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
} else {
// No contact found, return a null URI
return -1;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (values.containsKey(StatusUpdates.PRESENCE)) {
if (customProtocol == null) {
// We cannot allow a null in the custom protocol field, because SQLite3 does not
// properly enforce uniqueness of null values
customProtocol = "";
}
mValues.clear();
mValues.put(StatusUpdates.DATA_ID, dataId);
mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
mValues.put(PresenceColumns.CONTACT_ID, contactId);
mValues.put(StatusUpdates.PROTOCOL, protocol);
mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
mValues.put(StatusUpdates.IM_HANDLE, handle);
if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
}
mValues.put(StatusUpdates.PRESENCE,
values.getAsString(StatusUpdates.PRESENCE));
mValues.put(StatusUpdates.CHAT_CAPABILITY,
values.getAsString(StatusUpdates.CHAT_CAPABILITY));
// Insert the presence update
mActiveDb.get().replace(Tables.PRESENCE, null, mValues);
}
if (values.containsKey(StatusUpdates.STATUS)) {
String status = values.getAsString(StatusUpdates.STATUS);
String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
Resources resources = getContext().getResources();
if (!TextUtils.isEmpty(resPackage)) {
PackageManager pm = getContext().getPackageManager();
try {
resources = pm.getResourcesForApplication(resPackage);
} catch (NameNotFoundException e) {
Log.w(TAG, "Contact status update resource package not found: "
+ resPackage);
}
}
Integer labelResourceId = values.getAsInteger(StatusUpdates.STATUS_LABEL);
if ((labelResourceId == null || labelResourceId == 0) && protocol != null) {
labelResourceId = Im.getProtocolLabelResource(protocol);
}
String labelResource = getResourceName(resources, "string", labelResourceId);
Integer iconResourceId = values.getAsInteger(StatusUpdates.STATUS_ICON);
// TODO compute the default icon based on the protocol
String iconResource = getResourceName(resources, "drawable", iconResourceId);
if (TextUtils.isEmpty(status)) {
mDbHelper.get().deleteStatusUpdate(dataId);
} else {
Long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
if (timestamp != null) {
mDbHelper.get().replaceStatusUpdate(dataId, timestamp, status, resPackage,
iconResourceId, labelResourceId);
} else {
mDbHelper.get().insertStatusUpdate(dataId, status, resPackage, iconResourceId,
labelResourceId);
}
// For forward compatibility with the new stream item API, insert this status update
// there as well. If we already have a stream item from this source, update that
// one instead of inserting a new one (since the semantics of the old status update
// API is to only have a single record).
if (rawContactId != -1 && !TextUtils.isEmpty(status)) {
ContentValues streamItemValues = new ContentValues();
streamItemValues.put(StreamItems.RAW_CONTACT_ID, rawContactId);
// Status updates are text only but stream items are HTML.
streamItemValues.put(StreamItems.TEXT, statusUpdateToHtml(status));
streamItemValues.put(StreamItems.COMMENTS, "");
streamItemValues.put(StreamItems.RES_PACKAGE, resPackage);
streamItemValues.put(StreamItems.RES_ICON, iconResource);
streamItemValues.put(StreamItems.RES_LABEL, labelResource);
streamItemValues.put(StreamItems.TIMESTAMP,
timestamp == null ? System.currentTimeMillis() : timestamp);
// Note: The following is basically a workaround for the fact that status
// updates didn't do any sort of account enforcement, while social stream item
// updates do. We can't expect callers of the old API to start passing account
// information along, so we just populate the account params appropriately for
// the raw contact. Data set is not relevant here, as we only check account
// name and type.
if (accountName != null && accountType != null) {
streamItemValues.put(RawContacts.ACCOUNT_NAME, accountName);
streamItemValues.put(RawContacts.ACCOUNT_TYPE, accountType);
}
// Check for an existing stream item from this source, and insert or update.
Uri streamUri = StreamItems.CONTENT_URI;
Cursor c = queryLocal(streamUri, new String[]{StreamItems._ID},
StreamItems.RAW_CONTACT_ID + "=?",
new String[]{String.valueOf(rawContactId)},
null, -1 /* directory ID */);
try {
if (c.getCount() > 0) {
c.moveToFirst();
updateInTransaction(ContentUris.withAppendedId(streamUri, c.getLong(0)),
streamItemValues, null, null);
} else {
insertInTransaction(streamUri, streamItemValues);
}
} finally {
c.close();
}
}
}
}
if (contactId != -1) {
mAggregator.get().updateLastStatusUpdateId(contactId);
}
return dataId;
}
/** Converts a status update to HTML. */
private String statusUpdateToHtml(String status) {
return TextUtils.htmlEncode(status);
}
private String getResourceName(Resources resources, String expectedType, Integer resourceId) {
try {
if (resourceId == null || resourceId == 0) return null;
// Resource has an invalid type (e.g. a string as icon)? ignore
final String resourceEntryName = resources.getResourceEntryName(resourceId);
final String resourceTypeName = resources.getResourceTypeName(resourceId);
if (!expectedType.equals(resourceTypeName)) {
Log.w(TAG, "Resource " + resourceId + " (" + resourceEntryName + ") is of type " +
resourceTypeName + " but " + expectedType + " is required.");
return null;
}
return resourceEntryName;
} catch (NotFoundException e) {
return null;
}
}
@Override
protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "deleteInTransaction: " + uri);
}
// Default active DB to the contacts DB if none has been set.
if (mActiveDb.get() == null) {
mActiveDb.set(mContactsHelper.getWritableDatabase());
}
flushTransactionalChanges();
final boolean callerIsSyncAdapter =
readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
final int match = sUriMatcher.match(uri);
switch (match) {
case SYNCSTATE:
case PROFILE_SYNCSTATE:
return mDbHelper.get().getSyncState().delete(mActiveDb.get(), selection,
selectionArgs);
case SYNCSTATE_ID: {
String selectionWithId =
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+ (selection == null ? "" : " AND (" + selection + ")");
return mDbHelper.get().getSyncState().delete(mActiveDb.get(), selectionWithId,
selectionArgs);
}
case PROFILE_SYNCSTATE_ID: {
String selectionWithId =
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+ (selection == null ? "" : " AND (" + selection + ")");
return mProfileHelper.getSyncState().delete(mActiveDb.get(), selectionWithId,
selectionArgs);
}
case CONTACTS: {
// TODO
return 0;
}
case CONTACTS_ID: {
long contactId = ContentUris.parseId(uri);
return deleteContact(contactId, callerIsSyncAdapter);
}
case CONTACTS_LOOKUP: {
final List<String> pathSegments = uri.getPathSegments();
final int segmentCount = pathSegments.size();
if (segmentCount < 3) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
final String lookupKey = pathSegments.get(2);
final long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
return deleteContact(contactId, callerIsSyncAdapter);
}
case CONTACTS_LOOKUP_ID: {
// lookup contact by id and lookup key to see if they still match the actual record
final List<String> pathSegments = uri.getPathSegments();
final String lookupKey = pathSegments.get(2);
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForContacts(lookupQb, uri, null);
long contactId = ContentUris.parseId(uri);
String[] args;
if (selectionArgs == null) {
args = new String[2];
} else {
args = new String[selectionArgs.length + 2];
System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
}
args[0] = String.valueOf(contactId);
args[1] = Uri.encode(lookupKey);
lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
Cursor c = query(mActiveDb.get(), lookupQb, null, selection, args, null, null,
null);
try {
if (c.getCount() == 1) {
// contact was unmodified so go ahead and delete it
return deleteContact(contactId, callerIsSyncAdapter);
} else {
// row was changed (e.g. the merging might have changed), we got multiple
// rows or the supplied selection filtered the record out
return 0;
}
} finally {
c.close();
}
}
case RAW_CONTACTS:
case PROFILE_RAW_CONTACTS: {
int numDeletes = 0;
Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS,
new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
final long rawContactId = c.getLong(0);
long contactId = c.getLong(1);
numDeletes += deleteRawContact(rawContactId, contactId,
callerIsSyncAdapter);
}
} finally {
c.close();
}
return numDeletes;
}
case RAW_CONTACTS_ID:
case PROFILE_RAW_CONTACTS_ID: {
final long rawContactId = ContentUris.parseId(uri);
return deleteRawContact(rawContactId, mDbHelper.get().getContactId(rawContactId),
callerIsSyncAdapter);
}
case DATA:
case PROFILE_DATA: {
mSyncToNetwork |= !callerIsSyncAdapter;
return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
callerIsSyncAdapter);
}
case DATA_ID:
case PHONES_ID:
case EMAILS_ID:
case POSTALS_ID:
case PROFILE_DATA_ID: {
long dataId = ContentUris.parseId(uri);
mSyncToNetwork |= !callerIsSyncAdapter;
mSelectionArgs1[0] = String.valueOf(dataId);
return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
}
case GROUPS_ID: {
mSyncToNetwork |= !callerIsSyncAdapter;
return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
}
case GROUPS: {
int numDeletes = 0;
Cursor c = mActiveDb.get().query(Tables.GROUPS, new String[]{Groups._ID},
appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
}
} finally {
c.close();
}
if (numDeletes > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
return numDeletes;
}
case SETTINGS: {
mSyncToNetwork |= !callerIsSyncAdapter;
return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
}
case STATUS_UPDATES:
case PROFILE_STATUS_UPDATES: {
return deleteStatusUpdates(selection, selectionArgs);
}
case STREAM_ITEMS: {
mSyncToNetwork |= !callerIsSyncAdapter;
return deleteStreamItems(uri, new ContentValues(), selection, selectionArgs);
}
case STREAM_ITEMS_ID: {
mSyncToNetwork |= !callerIsSyncAdapter;
return deleteStreamItems(uri, new ContentValues(),
StreamItems._ID + "=?",
new String[]{uri.getLastPathSegment()});
}
case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
mSyncToNetwork |= !callerIsSyncAdapter;
String rawContactId = uri.getPathSegments().get(1);
String streamItemId = uri.getLastPathSegment();
return deleteStreamItems(uri, new ContentValues(),
StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?",
new String[]{rawContactId, streamItemId});
}
case STREAM_ITEMS_ID_PHOTOS: {
mSyncToNetwork |= !callerIsSyncAdapter;
String streamItemId = uri.getPathSegments().get(1);
String selectionWithId =
(StreamItemPhotos.STREAM_ITEM_ID + "=" + streamItemId + " ")
+ (selection == null ? "" : " AND (" + selection + ")");
return deleteStreamItemPhotos(uri, new ContentValues(),
selectionWithId, selectionArgs);
}
case STREAM_ITEMS_ID_PHOTOS_ID: {
mSyncToNetwork |= !callerIsSyncAdapter;
String streamItemId = uri.getPathSegments().get(1);
String streamItemPhotoId = uri.getPathSegments().get(3);
return deleteStreamItemPhotos(uri, new ContentValues(),
StreamItemPhotosColumns.CONCRETE_ID + "=? AND "
+ StreamItemPhotos.STREAM_ITEM_ID + "=?",
new String[]{streamItemPhotoId, streamItemId});
}
default: {
mSyncToNetwork = true;
return mLegacyApiSupport.delete(uri, selection, selectionArgs);
}
}
}
public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
mGroupIdCache.clear();
final long groupMembershipMimetypeId = mDbHelper.get()
.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
mActiveDb.get().delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
+ groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
+ groupId, null);
try {
if (callerIsSyncAdapter) {
return mActiveDb.get().delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
} else {
mValues.clear();
mValues.put(Groups.DELETED, 1);
mValues.put(Groups.DIRTY, 1);
return mActiveDb.get().update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId,
null);
}
} finally {
mVisibleTouched = true;
}
}
private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
final int count = mActiveDb.get().delete(Tables.SETTINGS, selection, selectionArgs);
mVisibleTouched = true;
return count;
}
private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
mSelectionArgs1[0] = Long.toString(contactId);
Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
null, null, null);
try {
while (c.moveToNext()) {
long rawContactId = c.getLong(0);
markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
}
} finally {
c.close();
}
mProviderStatusUpdateNeeded = true;
return mActiveDb.get().delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
}
public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
mAggregator.get().invalidateAggregationExceptionCache();
mProviderStatusUpdateNeeded = true;
// Find and delete stream items associated with the raw contact.
Cursor c = mActiveDb.get().query(Tables.STREAM_ITEMS,
new String[]{StreamItems._ID},
StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)},
null, null, null);
try {
while (c.moveToNext()) {
deleteStreamItem(c.getLong(0));
}
} finally {
c.close();
}
if (callerIsSyncAdapter || rawContactIsLocal(rawContactId)) {
mActiveDb.get().delete(Tables.PRESENCE,
PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
int count = mActiveDb.get().delete(Tables.RAW_CONTACTS,
RawContacts._ID + "=" + rawContactId, null);
mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
return count;
} else {
mDbHelper.get().removeContactIfSingleton(rawContactId);
return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
}
}
/**
* Returns whether the given raw contact ID is local (i.e. has no account associated with it).
*/
private boolean rawContactIsLocal(long rawContactId) {
Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS,
new String[] {
RawContacts.ACCOUNT_NAME,
RawContacts.ACCOUNT_TYPE,
RawContacts.DATA_SET
},
RawContacts._ID + "=?",
new String[] {String.valueOf(rawContactId)}, null, null, null);
try {
return c.moveToFirst() && c.isNull(0) && c.isNull(1) && c.isNull(2);
} finally {
c.close();
}
}
private int deleteStatusUpdates(String selection, String[] selectionArgs) {
// delete from both tables: presence and status_updates
// TODO should account type/name be appended to the where clause?
if (VERBOSE_LOGGING) {
Log.v(TAG, "deleting data from status_updates for " + selection);
}
mActiveDb.get().delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
selectionArgs);
return mActiveDb.get().delete(Tables.PRESENCE, selection, selectionArgs);
}
private int deleteStreamItems(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// First query for the stream items to be deleted, and check that they belong
// to the account.
Account account = resolveAccount(uri, values);
List<Long> streamItemIds = enforceModifyingAccountForStreamItems(
account, selection, selectionArgs);
// If no security exception has been thrown, we're fine to delete.
for (long streamItemId : streamItemIds) {
deleteStreamItem(streamItemId);
}
mVisibleTouched = true;
return streamItemIds.size();
}
private int deleteStreamItem(long streamItemId) {
// Note that this does not enforce the modifying account.
deleteStreamItemPhotos(streamItemId);
return mActiveDb.get().delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?",
new String[]{String.valueOf(streamItemId)});
}
private int deleteStreamItemPhotos(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// First query for the stream item photos to be deleted, and check that they
// belong to the account.
Account account = resolveAccount(uri, values);
enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs);
// If no security exception has been thrown, we're fine to delete.
return mActiveDb.get().delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs);
}
private int deleteStreamItemPhotos(long streamItemId) {
// Note that this does not enforce the modifying account.
return mActiveDb.get().delete(Tables.STREAM_ITEM_PHOTOS,
StreamItemPhotos.STREAM_ITEM_ID + "=?",
new String[]{String.valueOf(streamItemId)});
}
private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
mSyncToNetwork = true;
mValues.clear();
mValues.put(RawContacts.DELETED, 1);
mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
mValues.putNull(RawContacts.CONTACT_ID);
mValues.put(RawContacts.DIRTY, 1);
return updateRawContact(rawContactId, mValues, callerIsSyncAdapter);
}
@Override
protected int updateInTransaction(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "updateInTransaction: " + uri);
}
// Default active DB to the contacts DB if none has been set.
if (mActiveDb.get() == null) {
mActiveDb.set(mContactsHelper.getWritableDatabase());
}
int count = 0;
final int match = sUriMatcher.match(uri);
if (match == SYNCSTATE_ID && selection == null) {
long rowId = ContentUris.parseId(uri);
Object data = values.get(ContactsContract.SyncState.DATA);
mTransactionContext.get().syncStateUpdated(rowId, data);
return 1;
}
flushTransactionalChanges();
final boolean callerIsSyncAdapter =
readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
switch(match) {
case SYNCSTATE:
case PROFILE_SYNCSTATE:
return mDbHelper.get().getSyncState().update(mActiveDb.get(), values,
appendAccountToSelection(uri, selection), selectionArgs);
case SYNCSTATE_ID: {
selection = appendAccountToSelection(uri, selection);
String selectionWithId =
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+ (selection == null ? "" : " AND (" + selection + ")");
return mDbHelper.get().getSyncState().update(mActiveDb.get(), values,
selectionWithId, selectionArgs);
}
case PROFILE_SYNCSTATE_ID: {
selection = appendAccountToSelection(uri, selection);
String selectionWithId =
(SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+ (selection == null ? "" : " AND (" + selection + ")");
return mProfileHelper.getSyncState().update(mActiveDb.get(), values,
selectionWithId, selectionArgs);
}
case CONTACTS:
case PROFILE: {
count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
break;
}
case CONTACTS_ID: {
count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
break;
}
case CONTACTS_LOOKUP:
case CONTACTS_LOOKUP_ID: {
final List<String> pathSegments = uri.getPathSegments();
final int segmentCount = pathSegments.size();
if (segmentCount < 3) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
final String lookupKey = pathSegments.get(2);
final long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
count = updateContactOptions(contactId, values, callerIsSyncAdapter);
break;
}
case RAW_CONTACTS_DATA:
case PROFILE_RAW_CONTACTS_ID_DATA: {
int segment = match == RAW_CONTACTS_DATA ? 1 : 2;
final String rawContactId = uri.getPathSegments().get(segment);
String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
+ (selection == null ? "" : " AND " + selection);
count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
break;
}
case DATA:
case PROFILE_DATA: {
count = updateData(uri, values, appendAccountToSelection(uri, selection),
selectionArgs, callerIsSyncAdapter);
if (count > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
break;
}
case DATA_ID:
case PHONES_ID:
case EMAILS_ID:
case POSTALS_ID: {
count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
if (count > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
break;
}
case RAW_CONTACTS:
case PROFILE_RAW_CONTACTS: {
selection = appendAccountToSelection(uri, selection);
count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
break;
}
case RAW_CONTACTS_ID: {
long rawContactId = ContentUris.parseId(uri);
if (selection != null) {
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
count = updateRawContacts(values, RawContacts._ID + "=?"
+ " AND(" + selection + ")", selectionArgs,
callerIsSyncAdapter);
} else {
mSelectionArgs1[0] = String.valueOf(rawContactId);
count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
callerIsSyncAdapter);
}
break;
}
case GROUPS: {
count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
selectionArgs, callerIsSyncAdapter);
if (count > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
break;
}
case GROUPS_ID: {
long groupId = ContentUris.parseId(uri);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
String selectionWithId = Groups._ID + "=? "
+ (selection == null ? "" : " AND " + selection);
count = updateGroups(uri, values, selectionWithId, selectionArgs,
callerIsSyncAdapter);
if (count > 0) {
mSyncToNetwork |= !callerIsSyncAdapter;
}
break;
}
case AGGREGATION_EXCEPTIONS: {
count = updateAggregationException(mActiveDb.get(), values);
break;
}
case SETTINGS: {
count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
selectionArgs);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
case STATUS_UPDATES:
case PROFILE_STATUS_UPDATES: {
count = updateStatusUpdate(uri, values, selection, selectionArgs);
break;
}
case STREAM_ITEMS: {
count = updateStreamItems(uri, values, selection, selectionArgs);
break;
}
case STREAM_ITEMS_ID: {
count = updateStreamItems(uri, values, StreamItems._ID + "=?",
new String[]{uri.getLastPathSegment()});
break;
}
case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
String rawContactId = uri.getPathSegments().get(1);
String streamItemId = uri.getLastPathSegment();
count = updateStreamItems(uri, values,
StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?",
new String[]{rawContactId, streamItemId});
break;
}
case STREAM_ITEMS_PHOTOS: {
count = updateStreamItemPhotos(uri, values, selection, selectionArgs);
break;
}
case STREAM_ITEMS_ID_PHOTOS: {
String streamItemId = uri.getPathSegments().get(1);
count = updateStreamItemPhotos(uri, values,
StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[]{streamItemId});
break;
}
case STREAM_ITEMS_ID_PHOTOS_ID: {
String streamItemId = uri.getPathSegments().get(1);
String streamItemPhotoId = uri.getPathSegments().get(3);
count = updateStreamItemPhotos(uri, values,
StreamItemPhotosColumns.CONCRETE_ID + "=? AND " +
StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?",
new String[]{streamItemPhotoId, streamItemId});
break;
}
case DIRECTORIES: {
mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid());
count = 1;
break;
}
case DATA_USAGE_FEEDBACK_ID: {
if (handleDataUsageFeedback(uri)) {
count = 1;
} else {
count = 0;
}
break;
}
default: {
mSyncToNetwork = true;
return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
}
}
return count;
}
private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// update status_updates table, if status is provided
// TODO should account type/name be appended to the where clause?
int updateCount = 0;
ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
if (settableValues.size() > 0) {
updateCount = mActiveDb.get().update(Tables.STATUS_UPDATES,
settableValues,
getWhereClauseForStatusUpdatesTable(selection),
selectionArgs);
}
// now update the Presence table
settableValues = getSettableColumnsForPresenceTable(values);
if (settableValues.size() > 0) {
updateCount = mActiveDb.get().update(Tables.PRESENCE, settableValues,
selection, selectionArgs);
}
// TODO updateCount is not entirely a valid count of updated rows because 2 tables could
// potentially get updated in this method.
return updateCount;
}
private int updateStreamItems(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Stream items can't be moved to a new raw contact.
values.remove(StreamItems.RAW_CONTACT_ID);
// Check that the stream items being updated belong to the account.
Account account = resolveAccount(uri, values);
enforceModifyingAccountForStreamItems(account, selection, selectionArgs);
// Don't attempt to update accounts params - they don't exist in the stream items table.
values.remove(RawContacts.ACCOUNT_NAME);
values.remove(RawContacts.ACCOUNT_TYPE);
// If there's been no exception, the update should be fine.
return mActiveDb.get().update(Tables.STREAM_ITEMS, values, selection, selectionArgs);
}
private int updateStreamItemPhotos(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// Stream item photos can't be moved to a new stream item.
values.remove(StreamItemPhotos.STREAM_ITEM_ID);
// Check that the stream item photos being updated belong to the account.
Account account = resolveAccount(uri, values);
enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs);
// Don't attempt to update accounts params - they don't exist in the stream item
// photos table.
values.remove(RawContacts.ACCOUNT_NAME);
values.remove(RawContacts.ACCOUNT_TYPE);
// Process the photo (since we're updating, it's valid for the photo to not be present).
if (processStreamItemPhoto(values, true)) {
// If there's been no exception, the update should be fine.
return mActiveDb.get().update(Tables.STREAM_ITEM_PHOTOS, values, selection,
selectionArgs);
}
return 0;
}
/**
* Build a where clause to select the rows to be updated in status_updates table.
*/
private String getWhereClauseForStatusUpdatesTable(String selection) {
mSb.setLength(0);
mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
mSb.append(selection);
mSb.append(")");
return mSb.toString();
}
private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
mValues.clear();
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
StatusUpdates.STATUS);
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
StatusUpdates.STATUS_TIMESTAMP);
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
StatusUpdates.STATUS_RES_PACKAGE);
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
StatusUpdates.STATUS_LABEL);
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
StatusUpdates.STATUS_ICON);
return mValues;
}
private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
mValues.clear();
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
StatusUpdates.PRESENCE);
ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values,
StatusUpdates.CHAT_CAPABILITY);
return mValues;
}
private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
String[] selectionArgs, boolean callerIsSyncAdapter) {
mGroupIdCache.clear();
ContentValues updatedValues;
if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
updatedValues = mValues;
updatedValues.clear();
updatedValues.putAll(values);
updatedValues.put(Groups.DIRTY, 1);
} else {
updatedValues = values;
}
int count = mActiveDb.get().update(Tables.GROUPS, updatedValues, selectionWithId,
selectionArgs);
if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
mVisibleTouched = true;
}
// TODO: This will not work for groups that have a data set specified, since the content
// resolver will not be able to request a sync for the right source (unless it is updated
// to key off account with data set).
if (updatedValues.containsKey(Groups.SHOULD_SYNC)
&& updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
Cursor c = mActiveDb.get().query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
null, null);
String accountName;
String accountType;
try {
while (c.moveToNext()) {
accountName = c.getString(0);
accountType = c.getString(1);
if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
Account account = new Account(accountName, accountType);
ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
new Bundle());
break;
}
}
} finally {
c.close();
}
}
return count;
}
private int updateSettings(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
final int count = mActiveDb.get().update(Tables.SETTINGS, values, selection, selectionArgs);
if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
mVisibleTouched = true;
}
return count;
}
private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
boolean callerIsSyncAdapter) {
if (values.containsKey(RawContacts.CONTACT_ID)) {
throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
"in content values. Contact IDs are assigned automatically");
}
if (!callerIsSyncAdapter) {
selection = DatabaseUtils.concatenateWhere(selection,
RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0");
}
int count = 0;
Cursor cursor = mActiveDb.get().query(Views.RAW_CONTACTS,
new String[] { RawContacts._ID }, selection,
selectionArgs, null, null, null);
try {
while (cursor.moveToNext()) {
long rawContactId = cursor.getLong(0);
updateRawContact(rawContactId, values, callerIsSyncAdapter);
count++;
}
} finally {
cursor.close();
}
return count;
}
private int updateRawContact(long rawContactId, ContentValues values,
boolean callerIsSyncAdapter) {
final String selection = RawContacts._ID + " = ?";
mSelectionArgs1[0] = Long.toString(rawContactId);
final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
&& values.getAsInteger(RawContacts.DELETED) == 0);
int previousDeleted = 0;
String accountType = null;
String accountName = null;
String dataSet = null;
if (requestUndoDelete) {
Cursor cursor = mActiveDb.get().query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
selection, mSelectionArgs1, null, null, null);
try {
if (cursor.moveToFirst()) {
previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
dataSet = cursor.getString(RawContactsQuery.DATA_SET);
}
} finally {
cursor.close();
}
values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
}
int count = mActiveDb.get().update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
if (count != 0) {
if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
// As per ContactsContract documentation, changing aggregation mode
// to DEFAULT should not trigger aggregation
if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
mAggregator.get().markForAggregation(rawContactId, aggregationMode, false);
}
}
if (values.containsKey(RawContacts.STARRED)) {
if (!callerIsSyncAdapter) {
updateFavoritesMembership(rawContactId,
values.getAsLong(RawContacts.STARRED) != 0);
}
mAggregator.get().updateStarred(rawContactId);
} else {
// if this raw contact is being associated with an account, then update the
// favorites group membership based on whether or not this contact is starred.
// If it is starred, add a group membership, if one doesn't already exist
// otherwise delete any matching group memberships.
if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
boolean starred = 0 != DatabaseUtils.longForQuery(mActiveDb.get(),
SELECTION_STARRED_FROM_RAW_CONTACTS,
new String[]{Long.toString(rawContactId)});
updateFavoritesMembership(rawContactId, starred);
}
}
// if this raw contact is being associated with an account, then add a
// group membership to the group marked as AutoAdd, if any.
if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
addAutoAddMembership(rawContactId);
}
if (values.containsKey(RawContacts.SOURCE_ID)) {
mAggregator.get().updateLookupKeyForRawContact(mActiveDb.get(), rawContactId);
}
if (values.containsKey(RawContacts.NAME_VERIFIED)) {
// If setting NAME_VERIFIED for this raw contact, reset it for all
// other raw contacts in the same aggregate
if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
mDbHelper.get().resetNameVerifiedForOtherRawContacts(rawContactId);
}
mAggregator.get().updateDisplayNameForRawContact(mActiveDb.get(), rawContactId);
}
if (requestUndoDelete && previousDeleted == 1) {
mTransactionContext.get().rawContactInserted(rawContactId,
new AccountWithDataSet(accountName, accountType, dataSet));
}
}
return count;
}
private int updateData(Uri uri, ContentValues values, String selection,
String[] selectionArgs, boolean callerIsSyncAdapter) {
mValues.clear();
mValues.putAll(values);
mValues.remove(Data._ID);
mValues.remove(Data.RAW_CONTACT_ID);
mValues.remove(Data.MIMETYPE);
String packageName = values.getAsString(Data.RES_PACKAGE);
if (packageName != null) {
mValues.remove(Data.RES_PACKAGE);
mValues.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
}
if (!callerIsSyncAdapter) {
selection = DatabaseUtils.concatenateWhere(selection,
Data.IS_READ_ONLY + "=0");
}
int count = 0;
// Note that the query will return data according to the access restrictions,
// so we don't need to worry about updating data we don't have permission to read.
Cursor c = queryLocal(uri,
DataRowHandler.DataUpdateQuery.COLUMNS,
selection, selectionArgs, null, -1 /* directory ID */);
try {
while(c.moveToNext()) {
count += updateData(mValues, c, callerIsSyncAdapter);
}
} finally {
c.close();
}
return count;
}
private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
if (values.size() == 0) {
return 0;
}
final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE);
DataRowHandler rowHandler = getDataRowHandler(mimeType);
boolean updated =
rowHandler.update(mActiveDb.get(), mTransactionContext.get(), values, c,
callerIsSyncAdapter);
if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
}
return updated ? 1 : 0;
}
private int updateContactOptions(ContentValues values, String selection,
String[] selectionArgs, boolean callerIsSyncAdapter) {
int count = 0;
Cursor cursor = mActiveDb.get().query(Views.CONTACTS,
new String[] { Contacts._ID }, selection, selectionArgs, null, null, null);
try {
while (cursor.moveToNext()) {
long contactId = cursor.getLong(0);
updateContactOptions(contactId, values, callerIsSyncAdapter);
count++;
}
} finally {
cursor.close();
}
return count;
}
private int updateContactOptions(long contactId, ContentValues values,
boolean callerIsSyncAdapter) {
mValues.clear();
ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
values, Contacts.CUSTOM_RINGTONE);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
values, Contacts.SEND_TO_VOICEMAIL);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
values, Contacts.LAST_TIME_CONTACTED);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
values, Contacts.TIMES_CONTACTED);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
values, Contacts.STARRED);
// Nothing to update - just return
if (mValues.size() == 0) {
return 0;
}
if (mValues.containsKey(RawContacts.STARRED)) {
// Mark dirty when changing starred to trigger sync
mValues.put(RawContacts.DIRTY, 1);
}
mSelectionArgs1[0] = String.valueOf(contactId);
mActiveDb.get().update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?"
+ " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1);
if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
Cursor cursor = mActiveDb.get().query(Views.RAW_CONTACTS,
new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
mSelectionArgs1, null, null, null);
try {
while (cursor.moveToNext()) {
long rawContactId = cursor.getLong(0);
updateFavoritesMembership(rawContactId,
mValues.getAsLong(RawContacts.STARRED) != 0);
}
} finally {
cursor.close();
}
}
// Copy changeable values to prevent automatically managed fields from
// being explicitly updated by clients.
mValues.clear();
ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
values, Contacts.CUSTOM_RINGTONE);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
values, Contacts.SEND_TO_VOICEMAIL);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
values, Contacts.LAST_TIME_CONTACTED);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
values, Contacts.TIMES_CONTACTED);
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
values, Contacts.STARRED);
int rslt = mActiveDb.get().update(Tables.CONTACTS, mValues, Contacts._ID + "=?",
mSelectionArgs1);
if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
!values.containsKey(Contacts.TIMES_CONTACTED)) {
mActiveDb.get().execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
mActiveDb.get().execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
}
return rslt;
}
private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
long rawContactId1;
long rawContactId2;
if (rcId1 < rcId2) {
rawContactId1 = rcId1;
rawContactId2 = rcId2;
} else {
rawContactId2 = rcId1;
rawContactId1 = rcId2;
}
if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
mSelectionArgs2[0] = String.valueOf(rawContactId1);
mSelectionArgs2[1] = String.valueOf(rawContactId2);
db.delete(Tables.AGGREGATION_EXCEPTIONS,
AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
+ AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
} else {
ContentValues exceptionValues = new ContentValues(3);
exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
exceptionValues);
}
mAggregator.get().invalidateAggregationExceptionCache();
mAggregator.get().markForAggregation(rawContactId1,
RawContacts.AGGREGATION_MODE_DEFAULT, true);
mAggregator.get().markForAggregation(rawContactId2,
RawContacts.AGGREGATION_MODE_DEFAULT, true);
mAggregator.get().aggregateContact(mTransactionContext.get(), db, rawContactId1);
mAggregator.get().aggregateContact(mTransactionContext.get(), db, rawContactId2);
// The return value is fake - we just confirm that we made a change, not count actual
// rows changed.
return 1;
}
public void onAccountsUpdated(Account[] accounts) {
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
}
protected boolean updateAccountsInBackground(Account[] accounts) {
// TODO : Check the unit test.
boolean accountsChanged = false;
SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
mActiveDb.set(db);
db.beginTransaction();
// WARNING: This method can be run in either contacts mode or profile mode. It is
// absolutely imperative that no calls be made inside the following try block that can
// interact with the contacts DB. Otherwise it is quite possible for a deadlock to occur.
try {
Set<AccountWithDataSet> existingAccountsWithDataSets =
findValidAccountsWithDataSets(Tables.ACCOUNTS);
// Add a row to the ACCOUNTS table (with no data set) for each new account.
for (Account account : accounts) {
AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
account.name, account.type, null);
if (!existingAccountsWithDataSets.contains(accountWithDataSet)) {
accountsChanged = true;
// Add an account entry with an empty data set to match the account.
db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
+ ", " + RawContacts.ACCOUNT_TYPE + ", " + RawContacts.DATA_SET
+ ") VALUES (?, ?, ?)",
new String[] {
accountWithDataSet.getAccountName(),
accountWithDataSet.getAccountType(),
accountWithDataSet.getDataSet()
});
}
}
// Check each of the existing sub-accounts against the account list. If the owning
// account no longer exists, the sub-account and all its data should be deleted.
List<AccountWithDataSet> accountsWithDataSetsToDelete =
new ArrayList<AccountWithDataSet>();
List<Account> accountList = Arrays.asList(accounts);
for (AccountWithDataSet accountWithDataSet : existingAccountsWithDataSets) {
Account owningAccount = new Account(
accountWithDataSet.getAccountName(), accountWithDataSet.getAccountType());
if (!accountList.contains(owningAccount)) {
accountsWithDataSetsToDelete.add(accountWithDataSet);
}
}
if (!accountsWithDataSetsToDelete.isEmpty()) {
accountsChanged = true;
for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) {
Log.d(TAG, "removing data for removed account " + accountWithDataSet);
String[] accountParams = new String[] {
accountWithDataSet.getAccountName(),
accountWithDataSet.getAccountType()
};
String[] accountWithDataSetParams = accountWithDataSet.getDataSet() == null
? accountParams
: new String[] {
accountWithDataSet.getAccountName(),
accountWithDataSet.getAccountType(),
accountWithDataSet.getDataSet()
};
String groupsDataSetClause = " AND " + Groups.DATA_SET
+ (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?");
String rawContactsDataSetClause = " AND " + RawContacts.DATA_SET
+ (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?");
String settingsDataSetClause = " AND " + Settings.DATA_SET
+ (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?");
db.execSQL(
"DELETE FROM " + Tables.GROUPS +
" WHERE " + Groups.ACCOUNT_NAME + " = ?" +
" AND " + Groups.ACCOUNT_TYPE + " = ?" +
groupsDataSetClause, accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.PRESENCE +
" WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
"SELECT " + RawContacts._ID +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
" AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
rawContactsDataSetClause + ")", accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
" WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
"SELECT " + StreamItems._ID +
" FROM " + Tables.STREAM_ITEMS +
" WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
"SELECT " + RawContacts._ID +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
" AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
rawContactsDataSetClause + "))",
accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.STREAM_ITEMS +
" WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
"SELECT " + RawContacts._ID +
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
" AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
rawContactsDataSetClause + ")",
accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
" AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
rawContactsDataSetClause, accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.SETTINGS +
" WHERE " + Settings.ACCOUNT_NAME + " = ?" +
" AND " + Settings.ACCOUNT_TYPE + " = ?" +
settingsDataSetClause, accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.ACCOUNTS +
" WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
" AND " + RawContacts.ACCOUNT_TYPE + "=?" +
rawContactsDataSetClause, accountWithDataSetParams);
db.execSQL(
"DELETE FROM " + Tables.DIRECTORIES +
" WHERE " + Directory.ACCOUNT_NAME + "=?" +
" AND " + Directory.ACCOUNT_TYPE + "=?", accountParams);
resetDirectoryCache();
}
// Find all aggregated contacts that used to contain the raw contacts
// we have just deleted and see if they are still referencing the deleted
// names or photos. If so, fix up those contacts.
HashSet<Long> orphanContactIds = Sets.newHashSet();
Cursor cursor = db.rawQuery("SELECT " + Contacts._ID +
" FROM " + Tables.CONTACTS +
" WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
"(SELECT " + RawContacts._ID +
" FROM " + Tables.RAW_CONTACTS + "))" +
" OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
Contacts.PHOTO_ID + " NOT IN " +
"(SELECT " + Data._ID +
" FROM " + Tables.DATA + "))", null);
try {
while (cursor.moveToNext()) {
orphanContactIds.add(cursor.getLong(0));
}
} finally {
cursor.close();
}
for (Long contactId : orphanContactIds) {
mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
}
mDbHelper.get().updateAllVisible();
// Don't bother updating the search index if we're in profile mode - there is no
// search index for the profile DB, and updating it for the contacts DB in this case
// makes no sense and risks a deadlock.
if (!inProfileMode()) {
updateSearchIndexInTransaction();
}
}
// Now that we've done the account-based additions and subtractions from the Accounts
// table, check for raw contacts that have been added with a data set and add Accounts
// entries for those if necessary.
existingAccountsWithDataSets = findValidAccountsWithDataSets(Tables.ACCOUNTS);
Set<AccountWithDataSet> rawContactAccountsWithDataSets =
findValidAccountsWithDataSets(Tables.RAW_CONTACTS);
rawContactAccountsWithDataSets.removeAll(existingAccountsWithDataSets);
// Any remaining raw contact sub-accounts need to be added to the Accounts table.
for (AccountWithDataSet accountWithDataSet : rawContactAccountsWithDataSets) {
accountsChanged = true;
// Add an account entry to match the raw contact.
db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
+ ", " + RawContacts.ACCOUNT_TYPE + ", " + RawContacts.DATA_SET
+ ") VALUES (?, ?, ?)",
new String[] {
accountWithDataSet.getAccountName(),
accountWithDataSet.getAccountType(),
accountWithDataSet.getDataSet()
});
}
if (accountsChanged) {
// TODO: Should sync state take data set into consideration?
mDbHelper.get().getSyncState().onAccountsChanged(db, accounts);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
mAccountWritability.clear();
if (accountsChanged) {
updateContactsAccountCount(accounts);
updateProviderStatus();
}
return accountsChanged;
}
private void updateContactsAccountCount(Account[] accounts) {
int count = 0;
for (Account account : accounts) {
if (isContactsAccount(account)) {
count++;
}
}
mContactsAccountCount = count;
}
protected boolean isContactsAccount(Account account) {
final IContentService cs = ContentResolver.getContentService();
try {
return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
} catch (RemoteException e) {
Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
return false;
}
}
public void onPackageChanged(String packageName) {
scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName);
}
/**
* Finds all distinct account types and data sets present in the specified table.
*/
private Set<AccountWithDataSet> findValidAccountsWithDataSets(String table) {
Set<AccountWithDataSet> accountsWithDataSets = new HashSet<AccountWithDataSet>();
Cursor c = mActiveDb.get().rawQuery(
"SELECT DISTINCT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
"," + RawContacts.DATA_SET +
" FROM " + table, null);
try {
while (c.moveToNext()) {
if (!c.isNull(0) && !c.isNull(1)) {
accountsWithDataSets.add(
new AccountWithDataSet(c.getString(0), c.getString(1), c.getString(2)));
}
}
} finally {
c.close();
}
return accountsWithDataSets;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
waitForAccess(mReadAccessLatch);
// Enforce stream items access check if applicable.
enforceSocialStreamReadPermission(uri);
// Query the profile DB if appropriate.
if (mapsToProfileDb(uri)) {
switchToProfileMode();
return mProfileProvider.query(uri, projection, selection, selectionArgs, sortOrder);
}
// Otherwise proceed with a normal query against the contacts DB.
switchToContactMode();
mActiveDb.set(mContactsHelper.getReadableDatabase());
String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
if (directory == null) {
return addSnippetExtrasToCursor(uri,
queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1));
} else if (directory.equals("0")) {
return addSnippetExtrasToCursor(uri,
queryLocal(uri, projection, selection, selectionArgs, sortOrder,
Directory.DEFAULT));
} else if (directory.equals("1")) {
return addSnippetExtrasToCursor(uri,
queryLocal(uri, projection, selection, selectionArgs, sortOrder,
Directory.LOCAL_INVISIBLE));
}
DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
if (directoryInfo == null) {
Log.e(TAG, "Invalid directory ID: " + uri);
return null;
}
Builder builder = new Uri.Builder();
builder.scheme(ContentResolver.SCHEME_CONTENT);
builder.authority(directoryInfo.authority);
builder.encodedPath(uri.getEncodedPath());
if (directoryInfo.accountName != null) {
builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName);
}
if (directoryInfo.accountType != null) {
builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
}
String limit = getLimit(uri);
if (limit != null) {
builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit);
}
Uri directoryUri = builder.build();
if (projection == null) {
projection = getDefaultProjection(uri);
}
Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection,
selectionArgs, sortOrder);
if (cursor == null) {
return null;
}
CrossProcessCursor crossProcessCursor = getCrossProcessCursor(cursor);
if (crossProcessCursor != null) {
return addSnippetExtrasToCursor(uri, cursor);
} else {
return matrixCursorFromCursor(addSnippetExtrasToCursor(uri, cursor));
}
}
private Cursor addSnippetExtrasToCursor(Uri uri, Cursor cursor) {
// If the cursor doesn't contain a snippet column, don't bother wrapping it.
if (cursor.getColumnIndex(SearchSnippetColumns.SNIPPET) < 0) {
return cursor;
}
// Parse out snippet arguments for use when snippets are retrieved from the cursor.
String[] args = null;
String snippetArgs =
getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
if (snippetArgs != null) {
args = snippetArgs.split(",");
}
String query = uri.getLastPathSegment();
String startMatch = args != null && args.length > 0 ? args[0]
: DEFAULT_SNIPPET_ARG_START_MATCH;
String endMatch = args != null && args.length > 1 ? args[1]
: DEFAULT_SNIPPET_ARG_END_MATCH;
String ellipsis = args != null && args.length > 2 ? args[2]
: DEFAULT_SNIPPET_ARG_ELLIPSIS;
int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
: DEFAULT_SNIPPET_ARG_MAX_TOKENS;
// Snippet data is needed for the snippeting on the client side, so store it in the cursor
if (cursor instanceof AbstractCursor && deferredSnippetingRequested(uri)){
Bundle oldExtras = cursor.getExtras();
Bundle extras = new Bundle();
if (oldExtras != null) {
extras.putAll(oldExtras);
}
extras.putString(ContactsContract.DEFERRED_SNIPPETING_QUERY, query);
((AbstractCursor) cursor).setExtras(extras);
}
return cursor;
}
private Cursor addDeferredSnippetingExtra(Cursor cursor) {
if (cursor instanceof AbstractCursor){
Bundle oldExtras = cursor.getExtras();
Bundle extras = new Bundle();
if (oldExtras != null) {
extras.putAll(oldExtras);
}
extras.putBoolean(ContactsContract.DEFERRED_SNIPPETING, true);
((AbstractCursor) cursor).setExtras(extras);
}
return cursor;
}
private CrossProcessCursor getCrossProcessCursor(Cursor cursor) {
Cursor c = cursor;
if (c instanceof CrossProcessCursor) {
return (CrossProcessCursor) c;
} else if (c instanceof CursorWindow) {
return getCrossProcessCursor(((CursorWrapper) c).getWrappedCursor());
} else {
return null;
}
}
public MatrixCursor matrixCursorFromCursor(Cursor cursor) {
MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
int numColumns = cursor.getColumnCount();
String data[] = new String[numColumns];
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
for (int i = 0; i < numColumns; i++) {
data[i] = cursor.getString(i);
}
newCursor.addRow(data);
}
return newCursor;
}
private static final class DirectoryQuery {
public static final String[] COLUMNS = new String[] {
Directory._ID,
Directory.DIRECTORY_AUTHORITY,
Directory.ACCOUNT_NAME,
Directory.ACCOUNT_TYPE
};
public static final int DIRECTORY_ID = 0;
public static final int AUTHORITY = 1;
public static final int ACCOUNT_NAME = 2;
public static final int ACCOUNT_TYPE = 3;
}
/**
* Reads and caches directory information for the database.
*/
private DirectoryInfo getDirectoryAuthority(String directoryId) {
synchronized (mDirectoryCache) {
if (!mDirectoryCacheValid) {
mDirectoryCache.clear();
SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
Cursor cursor = db.query(Tables.DIRECTORIES,
DirectoryQuery.COLUMNS,
null, null, null, null, null);
try {
while (cursor.moveToNext()) {
DirectoryInfo info = new DirectoryInfo();
String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
mDirectoryCache.put(id, info);
}
} finally {
cursor.close();
}
mDirectoryCacheValid = true;
}
return mDirectoryCache.get(directoryId);
}
}
public void resetDirectoryCache() {
synchronized(mDirectoryCache) {
mDirectoryCacheValid = false;
}
}
private boolean hasColumn(String[] projection, String column) {
if (projection == null) {
return true; // Null projection means "all columns".
}
for (int i = 0; i < projection.length; i++) {
if (column.equalsIgnoreCase(projection[i])) return true;
}
return false;
}
protected Cursor queryLocal(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, long directoryId) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "query: " + uri);
}
// Default active DB to the contacts DB if none has been set.
if (mActiveDb.get() == null) {
mActiveDb.set(mContactsHelper.getReadableDatabase());
}
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String groupBy = null;
String limit = getLimit(uri);
boolean snippetDeferred = false;
// The expression used in bundleLetterCountExtras() to get count.
String addressBookIndexerCountExpression = null;
final int match = sUriMatcher.match(uri);
switch (match) {
case SYNCSTATE:
case PROFILE_SYNCSTATE:
return mDbHelper.get().getSyncState().query(mActiveDb.get(), projection, selection,
selectionArgs, sortOrder);
case CONTACTS: {
setTablesAndProjectionMapForContacts(qb, uri, projection);
appendLocalDirectorySelectionIfNeeded(qb, directoryId);
break;
}
case CONTACTS_ID: {
long contactId = ContentUris.parseId(uri);
setTablesAndProjectionMapForContacts(qb, uri, projection);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
qb.appendWhere(Contacts._ID + "=?");
break;
}
case CONTACTS_LOOKUP:
case CONTACTS_LOOKUP_ID: {
List<String> pathSegments = uri.getPathSegments();
int segmentCount = pathSegments.size();
if (segmentCount < 3) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
String lookupKey = pathSegments.get(2);
if (segmentCount == 4) {
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
projection, selection, selectionArgs, sortOrder, groupBy, limit,
Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
if (c != null) {
return c;
}
}
setTablesAndProjectionMapForContacts(qb, uri, projection);
selectionArgs = insertSelectionArg(selectionArgs,
String.valueOf(lookupContactIdByLookupKey(mActiveDb.get(), lookupKey)));
qb.appendWhere(Contacts._ID + "=?");
break;
}
case CONTACTS_LOOKUP_DATA:
case CONTACTS_LOOKUP_ID_DATA:
case CONTACTS_LOOKUP_PHOTO:
case CONTACTS_LOOKUP_ID_PHOTO: {
List<String> pathSegments = uri.getPathSegments();
int segmentCount = pathSegments.size();
if (segmentCount < 4) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
String lookupKey = pathSegments.get(2);
if (segmentCount == 5) {
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
}
lookupQb.appendWhere(" AND ");
Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
projection, selection, selectionArgs, sortOrder, groupBy, limit,
Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey);
if (c != null) {
return c;
}
// TODO see if the contact exists but has no data rows (rare)
}
setTablesAndProjectionMapForData(qb, uri, projection, false);
long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
selectionArgs = insertSelectionArg(selectionArgs,
String.valueOf(contactId));
if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
}
qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
break;
}
case CONTACTS_ID_STREAM_ITEMS: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForStreamItems(qb);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
qb.appendWhere(StreamItems.CONTACT_ID + "=?");
break;
}
case CONTACTS_LOOKUP_STREAM_ITEMS:
case CONTACTS_LOOKUP_ID_STREAM_ITEMS: {
List<String> pathSegments = uri.getPathSegments();
int segmentCount = pathSegments.size();
if (segmentCount < 4) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
String lookupKey = pathSegments.get(2);
if (segmentCount == 5) {
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForStreamItems(lookupQb);
Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
projection, selection, selectionArgs, sortOrder, groupBy, limit,
StreamItems.CONTACT_ID, contactId,
StreamItems.CONTACT_LOOKUP_KEY, lookupKey);
if (c != null) {
return c;
}
}
setTablesAndProjectionMapForStreamItems(qb);
long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
qb.appendWhere(RawContacts.CONTACT_ID + "=?");
break;
}
case CONTACTS_AS_VCARD: {
final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
qb.setTables(Views.CONTACTS);
qb.setProjectionMap(sContactsVCardProjectionMap);
selectionArgs = insertSelectionArg(selectionArgs,
String.valueOf(contactId));
qb.appendWhere(Contacts._ID + "=?");
break;
}
case CONTACTS_AS_MULTI_VCARD: {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
String currentDateString = dateFormat.format(new Date()).toString();
return mActiveDb.get().rawQuery(
"SELECT" +
" 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
" NULL AS " + OpenableColumns.SIZE,
new String[] { currentDateString });
}
case CONTACTS_FILTER: {
String filterParam = "";
boolean deferredSnipRequested = deferredSnippetingRequested(uri);
if (uri.getPathSegments().size() > 2) {
filterParam = uri.getLastPathSegment();
}
setTablesAndProjectionMapForContactsWithSnippet(
qb, uri, projection, filterParam, directoryId,
deferredSnipRequested);
snippetDeferred = isSingleWordQuery(filterParam) &&
deferredSnipRequested && snippetNeeded(projection);
break;
}
case CONTACTS_STREQUENT_FILTER:
case CONTACTS_STREQUENT: {
// Basically the resultant SQL should look like this:
// (SQL for listing starred items)
// UNION ALL
// (SQL for listing frequently contacted items)
// ORDER BY ...
final boolean phoneOnly = readBooleanQueryParameter(
uri, ContactsContract.STREQUENT_PHONE_ONLY, false);
if (match == CONTACTS_STREQUENT_FILTER && uri.getPathSegments().size() > 3) {
String filterParam = uri.getLastPathSegment();
StringBuilder sb = new StringBuilder();
sb.append(Contacts._ID + " IN ");
appendContactFilterAsNestedQuery(sb, filterParam);
selection = DbQueryUtils.concatenateClauses(selection, sb.toString());
}
String[] subProjection = null;
if (projection != null) {
subProjection = appendProjectionArg(projection, TIMES_USED_SORT_COLUMN);
}
// Build the first query for starred
setTablesAndProjectionMapForContacts(qb, uri, projection, false);
qb.setProjectionMap(phoneOnly ?
sStrequentPhoneOnlyStarredProjectionMap
: sStrequentStarredProjectionMap);
if (phoneOnly) {
qb.appendWhere(DbQueryUtils.concatenateClauses(
selection, Contacts.HAS_PHONE_NUMBER + "=1"));
}
qb.setStrict(true);
final String starredInnerQuery = qb.buildQuery(subProjection,
Contacts.STARRED + "=1", Contacts._ID, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC", null);
// Reset the builder.
qb = new SQLiteQueryBuilder();
qb.setStrict(true);
// Build the second query for frequent part. These JOINS can be very slow
// if assembled in the wrong order. Be sure to test changes against huge databases.
final String frequentInnerQuery;
if (phoneOnly) {
final StringBuilder tableBuilder = new StringBuilder();
// In phone only mode, we need to look at view_data instead of
// contacts/raw_contacts to obtain actual phone numbers. One problem is that
// view_data is much larger than view_contacts, so our query might become much
// slower.
//
// To avoid the possible slow down, we start from data usage table and join
// view_data to the table, assuming data usage table is quite smaller than
// data rows (almost always it should be), and we don't want any phone
// numbers not used by the user. This way sqlite is able to drop a number of
// rows in view_data in the early stage of data lookup.
tableBuilder.append(Tables.DATA_USAGE_STAT
+ " INNER JOIN " + Views.DATA + " " + Tables.DATA
+ " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
+ DataColumns.CONCRETE_ID + " AND "
+ DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="
+ DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")");
appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID);
appendContactStatusUpdateJoin(tableBuilder, projection,
ContactsColumns.LAST_STATUS_UPDATE_ID);
qb.setTables(tableBuilder.toString());
qb.setProjectionMap(sStrequentPhoneOnlyFrequentProjectionMap);
final long phoneMimeTypeId =
mDbHelper.get().getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
final long sipMimeTypeId =
mDbHelper.get().getMimeTypeId(SipAddress.CONTENT_ITEM_TYPE);
qb.appendWhere(DbQueryUtils.concatenateClauses(
selection,
Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL",
DataColumns.MIMETYPE_ID + " IN (" +
phoneMimeTypeId + ", " + sipMimeTypeId + ")"));
frequentInnerQuery =
qb.buildQuery(subProjection, null, null, null,
TIMES_USED_SORT_COLUMN + " DESC", "25");
} else {
setTablesAndProjectionMapForContacts(qb, uri, projection, true);
qb.setProjectionMap(sStrequentFrequentProjectionMap);
qb.appendWhere(DbQueryUtils.concatenateClauses(
selection,
"(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)"));
frequentInnerQuery = qb.buildQuery(subProjection,
null, Contacts._ID, null, null, "25");
}
// We need to wrap the inner queries in an extra select, because they contain
// their own SORT and LIMIT
final String frequentQuery = "SELECT * FROM (" + frequentInnerQuery + ")";
final String starredQuery = "SELECT * FROM (" + starredInnerQuery + ")";
// Put them together
final String unionQuery =
qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, null, null);
// Here, we need to use selection / selectionArgs (supplied from users) "twice",
// as we want them both for starred items and for frequently contacted items.
//
// e.g. if the user specify selection = "starred =?" and selectionArgs = "0",
// the resultant SQL should be like:
// SELECT ... WHERE starred =? AND ...
// UNION ALL
// SELECT ... WHERE starred =? AND ...
String[] doubledSelectionArgs = null;
if (selectionArgs != null) {
final int length = selectionArgs.length;
doubledSelectionArgs = new String[length * 2];
System.arraycopy(selectionArgs, 0, doubledSelectionArgs, 0, length);
System.arraycopy(selectionArgs, 0, doubledSelectionArgs, length, length);
}
Cursor cursor = mActiveDb.get().rawQuery(unionQuery, doubledSelectionArgs);
if (cursor != null) {
cursor.setNotificationUri(getContext().getContentResolver(),
ContactsContract.AUTHORITY_URI);
}
return cursor;
}
case CONTACTS_FREQUENT: {
setTablesAndProjectionMapForContacts(qb, uri, projection, true);
qb.setProjectionMap(sStrequentFrequentProjectionMap);
groupBy = Contacts._ID;
if (!TextUtils.isEmpty(sortOrder)) {
sortOrder = FREQUENT_ORDER_BY + ", " + sortOrder;
} else {
sortOrder = FREQUENT_ORDER_BY;
}
break;
}
case CONTACTS_GROUP: {
setTablesAndProjectionMapForContacts(qb, uri, projection);
if (uri.getPathSegments().size() > 2) {
qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
String groupMimeTypeId = String.valueOf(
mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
selectionArgs = insertSelectionArg(selectionArgs, groupMimeTypeId);
}
break;
}
case PROFILE: {
setTablesAndProjectionMapForContacts(qb, uri, projection);
break;
}
case PROFILE_ENTITIES: {
setTablesAndProjectionMapForEntities(qb, uri, projection);
break;
}
case PROFILE_AS_VCARD: {
qb.setTables(Views.CONTACTS);
qb.setProjectionMap(sContactsVCardProjectionMap);
break;
}
case CONTACTS_ID_DATA: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
break;
}
case CONTACTS_ID_PHOTO: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
break;
}
case CONTACTS_ID_ENTITIES: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForEntities(qb, uri, projection);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
break;
}
case CONTACTS_LOOKUP_ENTITIES:
case CONTACTS_LOOKUP_ID_ENTITIES: {
List<String> pathSegments = uri.getPathSegments();
int segmentCount = pathSegments.size();
if (segmentCount < 4) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
String lookupKey = pathSegments.get(2);
if (segmentCount == 5) {
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForEntities(lookupQb, uri, projection);
lookupQb.appendWhere(" AND ");
Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
projection, selection, selectionArgs, sortOrder, groupBy, limit,
Contacts.Entity.CONTACT_ID, contactId,
Contacts.Entity.LOOKUP_KEY, lookupKey);
if (c != null) {
return c;
}
}
setTablesAndProjectionMapForEntities(qb, uri, projection);
selectionArgs = insertSelectionArg(selectionArgs,
String.valueOf(lookupContactIdByLookupKey(mActiveDb.get(), lookupKey)));
qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?");
break;
}
case STREAM_ITEMS: {
setTablesAndProjectionMapForStreamItems(qb);
break;
}
case STREAM_ITEMS_ID: {
setTablesAndProjectionMapForStreamItems(qb);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(StreamItems._ID + "=?");
break;
}
case STREAM_ITEMS_LIMIT: {
MatrixCursor cursor = new MatrixCursor(new String[]{StreamItems.MAX_ITEMS}, 1);
cursor.addRow(new Object[]{MAX_STREAM_ITEMS_PER_RAW_CONTACT});
return cursor;
}
case STREAM_ITEMS_PHOTOS: {
setTablesAndProjectionMapForStreamItemPhotos(qb);
break;
}
case STREAM_ITEMS_ID_PHOTOS: {
setTablesAndProjectionMapForStreamItemPhotos(qb);
String streamItemId = uri.getPathSegments().get(1);
selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?");
break;
}
case STREAM_ITEMS_ID_PHOTOS_ID: {
setTablesAndProjectionMapForStreamItemPhotos(qb);
String streamItemId = uri.getPathSegments().get(1);
String streamItemPhotoId = uri.getPathSegments().get(3);
selectionArgs = insertSelectionArg(selectionArgs, streamItemPhotoId);
selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=? AND " +
StreamItemPhotosColumns.CONCRETE_ID + "=?");
break;
}
case PHOTO_DIMENSIONS: {
MatrixCursor cursor = new MatrixCursor(
new String[]{DisplayPhoto.DISPLAY_MAX_DIM, DisplayPhoto.THUMBNAIL_MAX_DIM},
1);
cursor.addRow(new Object[]{mMaxDisplayPhotoDim, mMaxThumbnailPhotoDim});
return cursor;
}
case PHONES: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + "=" +
mDbHelper.get().getMimeTypeIdForPhone());
final boolean removeDuplicates = readBooleanQueryParameter(
uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
if (removeDuplicates) {
groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
// In this case, because we dedupe phone numbers, the address book indexer needs
// to take it into account too. (Otherwise headers will appear in wrong
// positions.)
// So use count(distinct pair(CONTACT_ID, PHONE NUMBER)) instead of count(*).
// But because there's no such thing as pair() on sqlite, we use
// CONTACT_ID || ',' || PHONE NUMBER instead.
// This only slows down the query by 14% with 10,000 contacts.
addressBookIndexerCountExpression = "DISTINCT "
+ RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
}
break;
}
case PHONES_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForPhone());
qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
case PHONES_FILTER: {
String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
Integer typeInt = sDataUsageTypeMap.get(typeParam);
if (typeInt == null) {
typeInt = DataUsageStatColumns.USAGE_TYPE_INT_CALL;
}
setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForPhone());
if (uri.getPathSegments().size() > 2) {
String filterParam = uri.getLastPathSegment();
StringBuilder sb = new StringBuilder();
sb.append(" AND (");
boolean hasCondition = false;
boolean orNeeded = false;
final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
if (ftsMatchQuery.length() > 0) {
sb.append(Data.RAW_CONTACT_ID + " IN " +
"(SELECT " + RawContactsColumns.CONCRETE_ID +
" FROM " + Tables.SEARCH_INDEX +
" JOIN " + Tables.RAW_CONTACTS +
" ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
+ "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
" WHERE " + SearchIndexColumns.NAME + " MATCH '");
sb.append(ftsMatchQuery);
sb.append("')");
orNeeded = true;
hasCondition = true;
}
String number = PhoneNumberUtils.normalizeNumber(filterParam);
if (!TextUtils.isEmpty(number)) {
if (orNeeded) {
sb.append(" OR ");
}
sb.append(Data._ID +
" IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID
+ " FROM " + Tables.PHONE_LOOKUP
+ " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
sb.append(number);
sb.append("%')");
hasCondition = true;
}
if (!hasCondition) {
// If it is neither a phone number nor a name, the query should return
// an empty cursor. Let's ensure that.
sb.append("0");
}
sb.append(")");
qb.appendWhere(sb);
}
groupBy = "(CASE WHEN " + PhoneColumns.NORMALIZED_NUMBER
+ " IS NOT NULL THEN " + PhoneColumns.NORMALIZED_NUMBER
+ " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
if (sortOrder == null) {
final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
sortOrder = accountPromotionSortOrder + ", " + PHONE_FILTER_SORT_ORDER;
} else {
sortOrder = PHONE_FILTER_SORT_ORDER;
}
}
break;
}
case EMAILS: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForEmail());
final boolean removeDuplicates = readBooleanQueryParameter(
uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
if (removeDuplicates) {
groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
// See PHONES for more detail.
addressBookIndexerCountExpression = "DISTINCT "
+ RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
}
break;
}
case EMAILS_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForEmail()
+ " AND " + Data._ID + "=?");
break;
}
case EMAILS_LOOKUP: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForEmail());
if (uri.getPathSegments().size() > 2) {
String email = uri.getLastPathSegment();
String address = mDbHelper.get().extractAddressFromEmailAddress(email);
selectionArgs = insertSelectionArg(selectionArgs, address);
qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
}
// unless told otherwise, we'll return visible before invisible contacts
if (sortOrder == null) {
sortOrder = "(" + RawContacts.CONTACT_ID + " IN " +
Tables.DEFAULT_DIRECTORY + ") DESC";
}
break;
}
case EMAILS_FILTER: {
String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
Integer typeInt = sDataUsageTypeMap.get(typeParam);
if (typeInt == null) {
typeInt = DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT;
}
setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
String filterParam = null;
if (uri.getPathSegments().size() > 3) {
filterParam = uri.getLastPathSegment();
if (TextUtils.isEmpty(filterParam)) {
filterParam = null;
}
}
if (filterParam == null) {
// If the filter is unspecified, return nothing
qb.appendWhere(" AND 0");
} else {
StringBuilder sb = new StringBuilder();
sb.append(" AND " + Data._ID + " IN (");
sb.append(
"SELECT " + Data._ID +
" FROM " + Tables.DATA +
" WHERE " + DataColumns.MIMETYPE_ID + "=");
sb.append(mDbHelper.get().getMimeTypeIdForEmail());
sb.append(" AND " + Data.DATA1 + " LIKE ");
DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
if (!filterParam.contains("@")) {
sb.append(
" UNION SELECT " + Data._ID +
" FROM " + Tables.DATA +
" WHERE +" + DataColumns.MIMETYPE_ID + "=");
sb.append(mDbHelper.get().getMimeTypeIdForEmail());
sb.append(" AND " + Data.RAW_CONTACT_ID + " IN " +
"(SELECT " + RawContactsColumns.CONCRETE_ID +
" FROM " + Tables.SEARCH_INDEX +
" JOIN " + Tables.RAW_CONTACTS +
" ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
+ "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
" WHERE " + SearchIndexColumns.NAME + " MATCH '");
final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
sb.append(ftsMatchQuery);
sb.append("')");
}
sb.append(")");
qb.appendWhere(sb);
}
groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
if (sortOrder == null) {
final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
sortOrder = accountPromotionSortOrder + ", " + EMAIL_FILTER_SORT_ORDER;
} else {
sortOrder = EMAIL_FILTER_SORT_ORDER;
}
}
break;
}
case POSTALS: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForStructuredPostal());
final boolean removeDuplicates = readBooleanQueryParameter(
uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
if (removeDuplicates) {
groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
// See PHONES for more detail.
addressBookIndexerCountExpression = "DISTINCT "
+ RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
}
break;
}
case POSTALS_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
+ mDbHelper.get().getMimeTypeIdForStructuredPostal());
qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
case RAW_CONTACTS:
case PROFILE_RAW_CONTACTS: {
setTablesAndProjectionMapForRawContacts(qb, uri);
break;
}
case RAW_CONTACTS_ID:
case PROFILE_RAW_CONTACTS_ID: {
long rawContactId = ContentUris.parseId(uri);
setTablesAndProjectionMapForRawContacts(qb, uri);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
case RAW_CONTACTS_DATA:
case PROFILE_RAW_CONTACTS_ID_DATA: {
int segment = match == RAW_CONTACTS_DATA ? 1 : 2;
long rawContactId = Long.parseLong(uri.getPathSegments().get(segment));
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
break;
}
case RAW_CONTACTS_ID_STREAM_ITEMS: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForStreamItems(qb);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=?");
break;
}
case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
long streamItemId = Long.parseLong(uri.getPathSegments().get(3));
setTablesAndProjectionMapForStreamItems(qb);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(streamItemId));
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=? AND " +
StreamItems._ID + "=?");
break;
}
case PROFILE_RAW_CONTACTS_ID_ENTITIES: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(2));
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
setTablesAndProjectionMapForRawEntities(qb, uri);
qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
case DATA:
case PROFILE_DATA: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
break;
}
case DATA_ID:
case PROFILE_DATA_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
case PROFILE_PHOTO: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
break;
}
case PHONE_LOOKUP: {
// Phone lookup cannot be combined with a selection
selection = null;
selectionArgs = null;
if (uri.getBooleanQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false)) {
if (TextUtils.isEmpty(sortOrder)) {
// Default the sort order to something reasonable so we get consistent
// results when callers don't request an ordering
sortOrder = Contacts.DISPLAY_NAME + " ASC";
}
String sipAddress = uri.getPathSegments().size() > 1
? Uri.decode(uri.getLastPathSegment()) : "";
setTablesAndProjectionMapForData(qb, uri, null, false, true);
StringBuilder sb = new StringBuilder();
selectionArgs = mDbHelper.get().buildSipContactQuery(sb, sipAddress);
selection = sb.toString();
} else {
if (TextUtils.isEmpty(sortOrder)) {
// Default the sort order to something reasonable so we get consistent
// results when callers don't request an ordering
sortOrder = " length(lookup.normalized_number) DESC";
}
String number = uri.getPathSegments().size() > 1
? uri.getLastPathSegment() : "";
String numberE164 = PhoneNumberUtils.formatNumberToE164(number,
mDbHelper.get().getCurrentCountryIso());
String normalizedNumber =
PhoneNumberUtils.normalizeNumber(number);
mDbHelper.get().buildPhoneLookupAndContactQuery(
qb, normalizedNumber, numberE164);
qb.setProjectionMap(sPhoneLookupProjectionMap);
// Peek at the results of the first query (which attempts to use fully
// normalized and internationalized numbers for comparison). If no results
// were returned, fall back to doing a match of the trailing 7 digits.
qb.setStrict(true);
boolean foundResult = false;
Cursor cursor = query(mActiveDb.get(), qb, projection, selection, selectionArgs,
sortOrder, groupBy, limit);
try {
if (cursor.getCount() > 0) {
foundResult = true;
return cursor;
} else {
qb = new SQLiteQueryBuilder();
mDbHelper.get().buildMinimalPhoneLookupAndContactQuery(
qb, normalizedNumber);
qb.setProjectionMap(sPhoneLookupProjectionMap);
}
} finally {
if (!foundResult) {
// We'll be returning a different cursor, so close this one.
cursor.close();
}
}
}
break;
}
case GROUPS: {
qb.setTables(Views.GROUPS);
qb.setProjectionMap(sGroupsProjectionMap);
appendAccountFromParameter(qb, uri);
break;
}
case GROUPS_ID: {
qb.setTables(Views.GROUPS);
qb.setProjectionMap(sGroupsProjectionMap);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(Groups._ID + "=?");
break;
}
case GROUPS_SUMMARY: {
final boolean returnGroupCountPerAccount =
readBooleanQueryParameter(uri, Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT,
false);
String tables = Views.GROUPS + " AS " + Tables.GROUPS;
if (hasColumn(projection, Groups.SUMMARY_COUNT)) {
tables = tables + Joins.GROUP_MEMBER_COUNT;
}
qb.setTables(tables);
qb.setProjectionMap(returnGroupCountPerAccount ?
sGroupsSummaryProjectionMapWithGroupCountPerAccount
: sGroupsSummaryProjectionMap);
appendAccountFromParameter(qb, uri);
groupBy = GroupsColumns.CONCRETE_ID;
break;
}
case AGGREGATION_EXCEPTIONS: {
qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
qb.setProjectionMap(sAggregationExceptionsProjectionMap);
break;
}
case AGGREGATION_SUGGESTIONS: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
String filter = null;
if (uri.getPathSegments().size() > 3) {
filter = uri.getPathSegments().get(3);
}
final int maxSuggestions;
if (limit != null) {
maxSuggestions = Integer.parseInt(limit);
} else {
maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
}
ArrayList<AggregationSuggestionParameter> parameters = null;
List<String> query = uri.getQueryParameters("query");
if (query != null && !query.isEmpty()) {
parameters = new ArrayList<AggregationSuggestionParameter>(query.size());
for (String parameter : query) {
int offset = parameter.indexOf(':');
parameters.add(offset == -1
? new AggregationSuggestionParameter(
AggregationSuggestions.PARAMETER_MATCH_NAME,
parameter)
: new AggregationSuggestionParameter(
parameter.substring(0, offset),
parameter.substring(offset + 1)));
}
}
setTablesAndProjectionMapForContacts(qb, uri, projection);
return mAggregator.get().queryAggregationSuggestions(qb, projection, contactId,
maxSuggestions, filter, parameters);
}
case SETTINGS: {
qb.setTables(Tables.SETTINGS);
qb.setProjectionMap(sSettingsProjectionMap);
appendAccountFromParameter(qb, uri);
// When requesting specific columns, this query requires
// late-binding of the GroupMembership MIME-type.
final String groupMembershipMimetypeId = Long.toString(mDbHelper.get()
.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
if (projection != null && projection.length != 0 &&
mDbHelper.get().isInProjection(projection, Settings.UNGROUPED_COUNT)) {
selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
}
if (projection != null && projection.length != 0 &&
mDbHelper.get().isInProjection(
projection, Settings.UNGROUPED_WITH_PHONES)) {
selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
}
break;
}
case STATUS_UPDATES:
case PROFILE_STATUS_UPDATES: {
setTableAndProjectionMapForStatusUpdates(qb, projection);
break;
}
case STATUS_UPDATES_ID: {
setTableAndProjectionMapForStatusUpdates(qb, projection);
selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
break;
}
case SEARCH_SUGGESTIONS: {
return mGlobalSearchSupport.handleSearchSuggestionsQuery(
mActiveDb.get(), uri, projection, limit);
}
case SEARCH_SHORTCUT: {
String lookupKey = uri.getLastPathSegment();
String filter = getQueryParameter(
uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
return mGlobalSearchSupport.handleSearchShortcutRefresh(
mActiveDb.get(), projection, lookupKey, filter);
}
case RAW_CONTACT_ENTITIES:
case PROFILE_RAW_CONTACT_ENTITIES: {
setTablesAndProjectionMapForRawEntities(qb, uri);
break;
}
case RAW_CONTACT_ENTITY_ID: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForRawEntities(qb, uri);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
case PROVIDER_STATUS: {
return queryProviderStatus(uri, projection);
}
case DIRECTORIES : {
qb.setTables(Tables.DIRECTORIES);
qb.setProjectionMap(sDirectoryProjectionMap);
break;
}
case DIRECTORIES_ID : {
long id = ContentUris.parseId(uri);
qb.setTables(Tables.DIRECTORIES);
qb.setProjectionMap(sDirectoryProjectionMap);
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id));
qb.appendWhere(Directory._ID + "=?");
break;
}
case COMPLETE_NAME: {
return completeName(uri, projection);
}
default:
return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
sortOrder, limit);
}
qb.setStrict(true);
Cursor cursor =
query(mActiveDb.get(), qb, projection, selection, selectionArgs, sortOrder, groupBy,
limit);
if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
cursor = bundleLetterCountExtras(cursor, mActiveDb.get(), qb, selection,
selectionArgs, sortOrder, addressBookIndexerCountExpression);
}
if (snippetDeferred) {
cursor = addDeferredSnippetingExtra(cursor);
}
return cursor;
}
private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
String selection, String[] selectionArgs, String sortOrder, String groupBy,
String limit) {
if (projection != null && projection.length == 1
&& BaseColumns._COUNT.equals(projection[0])) {
qb.setProjectionMap(sCountProjectionMap);
}
final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
sortOrder, limit);
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
}
return c;
}
/**
* Creates a single-row cursor containing the current status of the provider.
*/
private Cursor queryProviderStatus(Uri uri, String[] projection) {
MatrixCursor cursor = new MatrixCursor(projection);
RowBuilder row = cursor.newRow();
for (int i = 0; i < projection.length; i++) {
if (ProviderStatus.STATUS.equals(projection[i])) {
row.add(mProviderStatus);
} else if (ProviderStatus.DATA1.equals(projection[i])) {
row.add(mEstimatedStorageRequirement);
}
}
return cursor;
}
/**
* Runs the query with the supplied contact ID and lookup ID. If the query succeeds,
* it returns the resulting cursor, otherwise it returns null and the calling
* method needs to resolve the lookup key and rerun the query.
*/
private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb,
SQLiteDatabase db, Uri uri,
String[] projection, String selection, String[] selectionArgs,
String sortOrder, String groupBy, String limit,
String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) {
String[] args;
if (selectionArgs == null) {
args = new String[2];
} else {
args = new String[selectionArgs.length + 2];
System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
}
args[0] = String.valueOf(contactId);
args[1] = Uri.encode(lookupKey);
lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
groupBy, limit);
if (c.getCount() != 0) {
return c;
}
c.close();
return null;
}
private static final class AddressBookIndexQuery {
public static final String LETTER = "letter";
public static final String TITLE = "title";
public static final String COUNT = "count";
public static final String[] COLUMNS = new String[] {
LETTER, TITLE, COUNT
};
public static final int COLUMN_LETTER = 0;
public static final int COLUMN_TITLE = 1;
public static final int COLUMN_COUNT = 2;
// The first letter of the sort key column is what is used for the index headings.
public static final String SECTION_HEADING = "SUBSTR(%1$s,1,1)";
public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
}
/**
* Computes counts by the address book index titles and adds the resulting tally
* to the returned cursor as a bundle of extras.
*/
private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder,
String countExpression) {
if (!(cursor instanceof AbstractCursor)) {
Log.w(TAG, "Unable to bundle extras. Cursor is not AbstractCursor.");
return cursor;
}
String sortKey;
// The sort order suffix could be something like "DESC".
// We want to preserve it in the query even though we will change
// the sort column itself.
String sortOrderSuffix = "";
if (sortOrder != null) {
int spaceIndex = sortOrder.indexOf(' ');
if (spaceIndex != -1) {
sortKey = sortOrder.substring(0, spaceIndex);
sortOrderSuffix = sortOrder.substring(spaceIndex);
} else {
sortKey = sortOrder;
}
} else {
sortKey = Contacts.SORT_KEY_PRIMARY;
}
String locale = getLocale().toString();
HashMap<String, String> projectionMap = Maps.newHashMap();
String sectionHeading = String.format(Locale.US, AddressBookIndexQuery.SECTION_HEADING,
sortKey);
projectionMap.put(AddressBookIndexQuery.LETTER,
sectionHeading + " AS " + AddressBookIndexQuery.LETTER);
// If "what to count" is not specified, we just count all records.
if (TextUtils.isEmpty(countExpression)) {
countExpression = "*";
}
/**
* Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
* to map the first letter of the sort key to a character that is traditionally
* used in phonebooks to represent that letter. For example, in Korean it will
* be the first consonant in the letter; for Japanese it will be Hiragana rather
* than Katakana.
*/
projectionMap.put(AddressBookIndexQuery.TITLE,
"GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + locale + "')"
+ " AS " + AddressBookIndexQuery.TITLE);
projectionMap.put(AddressBookIndexQuery.COUNT,
"COUNT(" + countExpression + ") AS " + AddressBookIndexQuery.COUNT);
qb.setProjectionMap(projectionMap);
Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
AddressBookIndexQuery.ORDER_BY, null /* having */,
AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
try {
int groupCount = indexCursor.getCount();
String titles[] = new String[groupCount];
int counts[] = new int[groupCount];
int indexCount = 0;
String currentTitle = null;
// Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
// with multiple entries for the same title. The following code
// collapses those duplicates.
for (int i = 0; i < groupCount; i++) {
indexCursor.moveToNext();
String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
titles[indexCount] = currentTitle = title;
counts[indexCount] = count;
indexCount++;
} else {
counts[indexCount - 1] += count;
}
}
if (indexCount < groupCount) {
String[] newTitles = new String[indexCount];
System.arraycopy(titles, 0, newTitles, 0, indexCount);
titles = newTitles;
int[] newCounts = new int[indexCount];
System.arraycopy(counts, 0, newCounts, 0, indexCount);
counts = newCounts;
}
final Bundle bundle = new Bundle();
bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
((AbstractCursor) cursor).setExtras(bundle);
return cursor;
} finally {
indexCursor.close();
}
}
/**
* Returns the contact Id for the contact identified by the lookupKey.
* Robust against changes in the lookup key: if the key has changed, will
* look up the contact by the raw contact IDs or name encoded in the lookup
* key.
*/
public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
ContactLookupKey key = new ContactLookupKey();
ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
long contactId = -1;
if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_PROFILE)) {
// We should already be in a profile database context, so just look up a single contact.
contactId = lookupSingleContactId(db);
}
if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
contactId = lookupContactIdBySourceIds(db, segments);
if (contactId != -1) {
return contactId;
}
}
boolean hasRawContactIds =
lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
if (hasRawContactIds) {
contactId = lookupContactIdByRawContactIds(db, segments);
if (contactId != -1) {
return contactId;
}
}
if (hasRawContactIds
|| lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
contactId = lookupContactIdByDisplayNames(db, segments);
}
return contactId;
}
private long lookupSingleContactId(SQLiteDatabase db) {
Cursor c = db.query(Tables.CONTACTS, new String[] {Contacts._ID},
null, null, null, null, null, "1");
try {
if (c.moveToFirst()) {
return c.getLong(0);
} else {
return -1;
}
} finally {
c.close();
}
}
private interface LookupBySourceIdQuery {
String TABLE = Views.RAW_CONTACTS;
String COLUMNS[] = {
RawContacts.CONTACT_ID,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.ACCOUNT_NAME,
RawContacts.SOURCE_ID
};
int CONTACT_ID = 0;
int ACCOUNT_TYPE_AND_DATA_SET = 1;
int ACCOUNT_NAME = 2;
int SOURCE_ID = 3;
}
private long lookupContactIdBySourceIds(SQLiteDatabase db,
ArrayList<LookupKeySegment> segments) {
StringBuilder sb = new StringBuilder();
sb.append(RawContacts.SOURCE_ID + " IN (");
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
DatabaseUtils.appendEscapedSQLString(sb, segment.key);
sb.append(",");
}
}
sb.setLength(sb.length() - 1); // Last comma
sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
sb.toString(), null, null, null, null);
try {
while (c.moveToNext()) {
String accountTypeAndDataSet =
c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE_AND_DATA_SET);
String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
int accountHashCode =
ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
&& accountHashCode == segment.accountHashCode
&& segment.key.equals(sourceId)) {
segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
break;
}
}
}
} finally {
c.close();
}
return getMostReferencedContactId(segments);
}
private interface LookupByRawContactIdQuery {
String TABLE = Views.RAW_CONTACTS;
String COLUMNS[] = {
RawContacts.CONTACT_ID,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.ACCOUNT_NAME,
RawContacts._ID,
};
int CONTACT_ID = 0;
int ACCOUNT_TYPE_AND_DATA_SET = 1;
int ACCOUNT_NAME = 2;
int ID = 3;
}
private long lookupContactIdByRawContactIds(SQLiteDatabase db,
ArrayList<LookupKeySegment> segments) {
StringBuilder sb = new StringBuilder();
sb.append(RawContacts._ID + " IN (");
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
sb.append(segment.rawContactId);
sb.append(",");
}
}
sb.setLength(sb.length() - 1); // Last comma
sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
sb.toString(), null, null, null, null);
try {
while (c.moveToNext()) {
String accountTypeAndDataSet = c.getString(
LookupByRawContactIdQuery.ACCOUNT_TYPE_AND_DATA_SET);
String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
int accountHashCode =
ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
&& accountHashCode == segment.accountHashCode
&& segment.rawContactId.equals(rawContactId)) {
segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
break;
}
}
}
} finally {
c.close();
}
return getMostReferencedContactId(segments);
}
private interface LookupByDisplayNameQuery {
String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
String COLUMNS[] = {
RawContacts.CONTACT_ID,
RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
RawContacts.ACCOUNT_NAME,
NameLookupColumns.NORMALIZED_NAME
};
int CONTACT_ID = 0;
int ACCOUNT_TYPE_AND_DATA_SET = 1;
int ACCOUNT_NAME = 2;
int NORMALIZED_NAME = 3;
}
private long lookupContactIdByDisplayNames(SQLiteDatabase db,
ArrayList<LookupKeySegment> segments) {
StringBuilder sb = new StringBuilder();
sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
|| segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
DatabaseUtils.appendEscapedSQLString(sb, segment.key);
sb.append(",");
}
}
sb.setLength(sb.length() - 1); // Last comma
sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
+ " AND " + RawContacts.CONTACT_ID + " NOT NULL");
Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
sb.toString(), null, null, null, null);
try {
while (c.moveToNext()) {
String accountTypeAndDataSet =
c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
int accountHashCode =
ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
|| segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
&& accountHashCode == segment.accountHashCode
&& segment.key.equals(name)) {
segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
break;
}
}
}
} finally {
c.close();
}
return getMostReferencedContactId(segments);
}
private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
for (int i = 0; i < segments.size(); i++) {
LookupKeySegment segment = segments.get(i);
if (segment.lookupType == lookupType) {
return true;
}
}
return false;
}
public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
mAggregator.get().updateLookupKeyForRawContact(db, rawContactId);
}
/**
* Returns the contact ID that is mentioned the highest number of times.
*/
private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
Collections.sort(segments);
long bestContactId = -1;
int bestRefCount = 0;
long contactId = -1;
int count = 0;
int segmentCount = segments.size();
for (int i = 0; i < segmentCount; i++) {
LookupKeySegment segment = segments.get(i);
if (segment.contactId != -1) {
if (segment.contactId == contactId) {
count++;
} else {
if (count > bestRefCount) {
bestContactId = contactId;
bestRefCount = count;
}
contactId = segment.contactId;
count = 1;
}
}
}
if (count > bestRefCount) {
return contactId;
} else {
return bestContactId;
}
}
private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
String[] projection) {
setTablesAndProjectionMapForContacts(qb, uri, projection, false);
}
/**
* @param includeDataUsageStat true when the table should include DataUsageStat table.
* Note that this uses INNER JOIN instead of LEFT OUTER JOIN, so some of data in Contacts
* may be dropped.
*/
private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
String[] projection, boolean includeDataUsageStat) {
StringBuilder sb = new StringBuilder();
if (includeDataUsageStat) {
sb.append(Views.DATA_USAGE_STAT + " AS " + Tables.DATA_USAGE_STAT);
sb.append(" INNER JOIN ");
}
sb.append(Views.CONTACTS);
// Just for frequently contacted contacts in Strequent Uri handling.
if (includeDataUsageStat) {
sb.append(" ON (" +
DbQueryUtils.concatenateClauses(
DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0",
RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) +
")");
}
appendContactPresenceJoin(sb, projection, Contacts._ID);
appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
qb.setTables(sb.toString());
qb.setProjectionMap(sContactsProjectionMap);
}
/**
* Finds name lookup records matching the supplied filter, picks one arbitrary match per
* contact and joins that with other contacts tables.
*/
private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
String[] projection, String filter, long directoryId, boolean deferredSnippeting) {
StringBuilder sb = new StringBuilder();
sb.append(Views.CONTACTS);
if (filter != null) {
filter = filter.trim();
}
if (TextUtils.isEmpty(filter) || (directoryId != -1 && directoryId != Directory.DEFAULT)) {
sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + " WHERE 0)");
} else {
appendSearchIndexJoin(sb, uri, projection, filter, deferredSnippeting);
}
appendContactPresenceJoin(sb, projection, Contacts._ID);
appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
qb.setTables(sb.toString());
qb.setProjectionMap(sContactsProjectionWithSnippetMap);
}
private void appendSearchIndexJoin(
StringBuilder sb, Uri uri, String[] projection, String filter,
boolean deferredSnippeting) {
if (snippetNeeded(projection)) {
String[] args = null;
String snippetArgs =
getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
if (snippetArgs != null) {
args = snippetArgs.split(",");
}
String startMatch = args != null && args.length > 0 ? args[0]
: DEFAULT_SNIPPET_ARG_START_MATCH;
String endMatch = args != null && args.length > 1 ? args[1]
: DEFAULT_SNIPPET_ARG_END_MATCH;
String ellipsis = args != null && args.length > 2 ? args[2]
: DEFAULT_SNIPPET_ARG_ELLIPSIS;
int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
: DEFAULT_SNIPPET_ARG_MAX_TOKENS;
appendSearchIndexJoin(
sb, filter, true, startMatch, endMatch, ellipsis, maxTokens,
deferredSnippeting);
} else {
appendSearchIndexJoin(sb, filter, false, null, null, null, 0, false);
}
}
public void appendSearchIndexJoin(StringBuilder sb, String filter,
boolean snippetNeeded, String startMatch, String endMatch, String ellipsis,
int maxTokens, boolean deferredSnippeting) {
boolean isEmailAddress = false;
String emailAddress = null;
boolean isPhoneNumber = false;
String phoneNumber = null;
String numberE164 = null;
// If the query consists of a single word, we can do snippetizing after-the-fact for a
// performance boost.
boolean singleTokenSearch = isSingleWordQuery(filter);
if (filter.indexOf('@') != -1) {
emailAddress = mDbHelper.get().extractAddressFromEmailAddress(filter);
isEmailAddress = !TextUtils.isEmpty(emailAddress);
} else {
isPhoneNumber = isPhoneNumber(filter);
if (isPhoneNumber) {
phoneNumber = PhoneNumberUtils.normalizeNumber(filter);
numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber,
mDbHelper.get().getCountryIso());
}
}
final String SNIPPET_CONTACT_ID = "snippet_contact_id";
sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS " + SNIPPET_CONTACT_ID);
if (snippetNeeded) {
sb.append(", ");
if (isEmailAddress) {
sb.append("ifnull(");
DatabaseUtils.appendEscapedSQLString(sb, startMatch);
sb.append("||(SELECT MIN(" + Email.ADDRESS + ")");
sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS);
sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE ");
DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
sb.append(")||");
DatabaseUtils.appendEscapedSQLString(sb, endMatch);
sb.append(",");
// Optimization for single-token search (do only if requested).
if (singleTokenSearch && deferredSnippeting) {
sb.append(SearchIndexColumns.CONTENT);
} else {
appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
}
sb.append(")");
} else if (isPhoneNumber) {
sb.append("ifnull(");
DatabaseUtils.appendEscapedSQLString(sb, startMatch);
sb.append("||(SELECT MIN(" + Phone.NUMBER + ")");
sb.append(" FROM " +
Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP);
sb.append(" ON " + DataColumns.CONCRETE_ID);
sb.append("=" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID);
sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
sb.append("=" + RawContacts.CONTACT_ID);
sb.append(" AND " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
sb.append(phoneNumber);
sb.append("%'");
if (!TextUtils.isEmpty(numberE164)) {
sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
sb.append(numberE164);
sb.append("%'");
}
sb.append(")||");
DatabaseUtils.appendEscapedSQLString(sb, endMatch);
sb.append(",");
// Optimization for single-token search (do only if requested).
if (singleTokenSearch && deferredSnippeting) {
sb.append(SearchIndexColumns.CONTENT);
} else {
appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
}
sb.append(")");
} else {
final String normalizedFilter = NameNormalizer.normalize(filter);
if (!TextUtils.isEmpty(normalizedFilter)) {
// Optimization for single-token search (do only if requested)..
if (singleTokenSearch && deferredSnippeting) {
sb.append(SearchIndexColumns.CONTENT);
} else {
sb.append("(CASE WHEN EXISTS (SELECT 1 FROM ");
sb.append(Tables.RAW_CONTACTS + " AS rc INNER JOIN ");
sb.append(Tables.NAME_LOOKUP + " AS nl ON (rc." + RawContacts._ID);
sb.append("=nl." + NameLookupColumns.RAW_CONTACT_ID);
sb.append(") WHERE nl." + NameLookupColumns.NORMALIZED_NAME);
sb.append(" GLOB '" + normalizedFilter + "*' AND ");
sb.append("nl." + NameLookupColumns.NAME_TYPE + "=");
sb.append(NameLookupType.NAME_COLLATION_KEY + " AND ");
sb.append(Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
sb.append("=rc." + RawContacts.CONTACT_ID);
sb.append(") THEN NULL ELSE ");
appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
sb.append(" END)");
}
} else {
sb.append("NULL");
}
}
sb.append(" AS " + SearchSnippetColumns.SNIPPET);
}
sb.append(" FROM " + Tables.SEARCH_INDEX);
sb.append(" WHERE ");
sb.append(Tables.SEARCH_INDEX + " MATCH '");
if (isEmailAddress) {
// we know that the emailAddress contains a @. This phrase search should be
// scoped against "content:" only, but unfortunately SQLite doesn't support
// phrases and scoped columns at once. This is fine in this case however, because:
// - We can't erronously match against name, as name is all-hex (so the @ can't match)
// - We can't match against tokens, because phone-numbers can't contain @
final String sanitizedEmailAddress =
emailAddress == null ? "" : sanitizeMatch(emailAddress);
sb.append("\"");
sb.append(sanitizedEmailAddress);
sb.append("*\"");
} else if (isPhoneNumber) {
// normalized version of the phone number (phoneNumber can only have + and digits)
final String phoneNumberCriteria = " OR tokens:" + phoneNumber + "*";
// international version of this number (numberE164 can only have + and digits)
final String numberE164Criteria =
(numberE164 != null && !TextUtils.equals(numberE164, phoneNumber))
? " OR tokens:" + numberE164 + "*"
: "";
// combine all criteria
final String commonCriteria =
phoneNumberCriteria + numberE164Criteria;
// search in content
sb.append(SearchIndexManager.getFtsMatchQuery(filter,
FtsQueryBuilder.getDigitsQueryBuilder(commonCriteria)));
} else {
// general case: not a phone number, not an email-address
sb.append(SearchIndexManager.getFtsMatchQuery(filter,
FtsQueryBuilder.SCOPED_NAME_NORMALIZING));
}
// Omit results in "Other Contacts".
sb.append("' AND " + SNIPPET_CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")");
sb.append(" ON (" + Contacts._ID + "=" + SNIPPET_CONTACT_ID + ")");
}
private static String sanitizeMatch(String filter) {
return filter.replace("'", "").replace("*", "").replace("-", "").replace("\"", "");
}
private void appendSnippetFunction(
StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) {
sb.append("snippet(" + Tables.SEARCH_INDEX + ",");
DatabaseUtils.appendEscapedSQLString(sb, startMatch);
sb.append(",");
DatabaseUtils.appendEscapedSQLString(sb, endMatch);
sb.append(",");
DatabaseUtils.appendEscapedSQLString(sb, ellipsis);
// The index of the column used for the snippet, "content"
sb.append(",1,");
sb.append(maxTokens);
sb.append(")");
}
private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
StringBuilder sb = new StringBuilder();
sb.append(Views.RAW_CONTACTS);
qb.setTables(sb.toString());
qb.setProjectionMap(sRawContactsProjectionMap);
appendAccountFromParameter(qb, uri);
}
private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
qb.setTables(Views.RAW_ENTITIES);
qb.setProjectionMap(sRawEntityProjectionMap);
appendAccountFromParameter(qb, uri);
}
private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
String[] projection, boolean distinct) {
setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, null);
}
private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
String[] projection, boolean distinct, boolean addSipLookupColumns) {
setTablesAndProjectionMapForData(qb, uri, projection, distinct, addSipLookupColumns, null);
}
/**
* @param usageType when non-null {@link Tables#DATA_USAGE_STAT} is joined with the specified
* type.
*/
private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
String[] projection, boolean distinct, Integer usageType) {
setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, usageType);
}
private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
String[] projection, boolean distinct, boolean addSipLookupColumns, Integer usageType) {
StringBuilder sb = new StringBuilder();
sb.append(Views.DATA);
sb.append(" data");
appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
if (usageType != null) {
appendDataUsageStatJoin(sb, usageType, DataColumns.CONCRETE_ID);
}
qb.setTables(sb.toString());
boolean useDistinct = distinct
|| !mDbHelper.get().isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
qb.setDistinct(useDistinct);
final ProjectionMap projectionMap;
if (addSipLookupColumns) {
projectionMap = useDistinct
? sDistinctDataSipLookupProjectionMap : sDataSipLookupProjectionMap;
} else {
projectionMap = useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap;
}
qb.setProjectionMap(projectionMap);
appendAccountFromParameter(qb, uri);
}
private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
String[] projection) {
StringBuilder sb = new StringBuilder();
sb.append(Views.DATA);
sb.append(" data");
appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
qb.setTables(sb.toString());
qb.setProjectionMap(sStatusUpdatesProjectionMap);
}
private void setTablesAndProjectionMapForStreamItems(SQLiteQueryBuilder qb) {
qb.setTables(Views.STREAM_ITEMS);
qb.setProjectionMap(sStreamItemsProjectionMap);
}
private void setTablesAndProjectionMapForStreamItemPhotos(SQLiteQueryBuilder qb) {
qb.setTables(Tables.PHOTO_FILES
+ " JOIN " + Tables.STREAM_ITEM_PHOTOS + " ON ("
+ StreamItemPhotosColumns.CONCRETE_PHOTO_FILE_ID + "="
+ PhotoFilesColumns.CONCRETE_ID
+ ") JOIN " + Tables.STREAM_ITEMS + " ON ("
+ StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "="
+ StreamItemsColumns.CONCRETE_ID + ")"
+ " JOIN " + Tables.RAW_CONTACTS + " ON ("
+ StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+ ")");
qb.setProjectionMap(sStreamItemPhotosProjectionMap);
}
private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
String[] projection) {
StringBuilder sb = new StringBuilder();
sb.append(Views.ENTITIES);
sb.append(" data");
appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID);
appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID);
appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID);
qb.setTables(sb.toString());
qb.setProjectionMap(sEntityProjectionMap);
appendAccountFromParameter(qb, uri);
}
private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
String lastStatusUpdateIdColumn) {
if (mDbHelper.get().isInProjection(projection,
Contacts.CONTACT_STATUS,
Contacts.CONTACT_STATUS_RES_PACKAGE,
Contacts.CONTACT_STATUS_ICON,
Contacts.CONTACT_STATUS_LABEL,
Contacts.CONTACT_STATUS_TIMESTAMP)) {
sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
+ ContactsStatusUpdatesColumns.ALIAS +
" ON (" + lastStatusUpdateIdColumn + "="
+ ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
}
}
private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection,
String dataIdColumn) {
if (mDbHelper.get().isInProjection(projection,
StatusUpdates.STATUS,
StatusUpdates.STATUS_RES_PACKAGE,
StatusUpdates.STATUS_ICON,
StatusUpdates.STATUS_LABEL,
StatusUpdates.STATUS_TIMESTAMP)) {
sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
" ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
+ dataIdColumn + ")");
}
}
private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
" ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + dataIdColumn +
" AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + usageType + ")");
}
private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
String contactIdColumn) {
if (mDbHelper.get().isInProjection(projection,
Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) {
sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
" ON (" + contactIdColumn + " = "
+ AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")");
}
}
private void appendDataPresenceJoin(StringBuilder sb, String[] projection,
String dataIdColumn) {
if (mDbHelper.get().isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) {
sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
" ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")");
}
}
private boolean appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) {
if (directoryId == Directory.DEFAULT) {
qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY);
return true;
} else if (directoryId == Directory.LOCAL_INVISIBLE){
qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY);
return true;
}
return false;
}
private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
if (partialUri) {
// Throw when either account is incomplete
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
}
// Accounts are valid by only checking one parameter, since we've
// already ruled out partial accounts.
final boolean validAccount = !TextUtils.isEmpty(accountName);
if (validAccount) {
String toAppend = RawContacts.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ RawContacts.ACCOUNT_TYPE + "="
+ DatabaseUtils.sqlEscapeString(accountType);
if (dataSet == null) {
toAppend += " AND " + RawContacts.DATA_SET + " IS NULL";
} else {
toAppend += " AND " + RawContacts.DATA_SET + "=" +
DatabaseUtils.sqlEscapeString(dataSet);
}
qb.appendWhere(toAppend);
} else {
qb.appendWhere("1");
}
}
private String appendAccountToSelection(Uri uri, String selection) {
final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
if (partialUri) {
// Throw when either account is incomplete
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
}
// Accounts are valid by only checking one parameter, since we've
// already ruled out partial accounts.
final boolean validAccount = !TextUtils.isEmpty(accountName);
if (validAccount) {
StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ RawContacts.ACCOUNT_TYPE + "="
+ DatabaseUtils.sqlEscapeString(accountType));
if (dataSet == null) {
selectionSb.append(" AND " + RawContacts.DATA_SET + " IS NULL");
} else {
selectionSb.append(" AND " + RawContacts.DATA_SET + "=")
.append(DatabaseUtils.sqlEscapeString(dataSet));
}
if (!TextUtils.isEmpty(selection)) {
selectionSb.append(" AND (");
selectionSb.append(selection);
selectionSb.append(')');
}
return selectionSb.toString();
} else {
return selection;
}
}
/**
* Gets the value of the "limit" URI query parameter.
*
* @return A string containing a non-negative integer, or <code>null</code> if
* the parameter is not set, or is set to an invalid value.
*/
private String getLimit(Uri uri) {
String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
if (limitParam == null) {
return null;
}
// make sure that the limit is a non-negative integer
try {
int l = Integer.parseInt(limitParam);
if (l < 0) {
Log.w(TAG, "Invalid limit parameter: " + limitParam);
return null;
}
return String.valueOf(l);
} catch (NumberFormatException ex) {
Log.w(TAG, "Invalid limit parameter: " + limitParam);
return null;
}
}
@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
if (mode.equals("r")) {
waitForAccess(mReadAccessLatch);
} else {
waitForAccess(mWriteAccessLatch);
}
if (mapsToProfileDb(uri)) {
switchToProfileMode();
return mProfileProvider.openAssetFile(uri, mode);
} else {
switchToContactMode();
return openAssetFileLocal(uri, mode);
}
}
public AssetFileDescriptor openAssetFileLocal(Uri uri, String mode)
throws FileNotFoundException {
// Default active DB to the contacts DB if none has been set.
if (mActiveDb.get() == null) {
if (mode.equals("r")) {
mActiveDb.set(mContactsHelper.getReadableDatabase());
} else {
mActiveDb.set(mContactsHelper.getWritableDatabase());
}
}
int match = sUriMatcher.match(uri);
switch (match) {
case CONTACTS_ID_PHOTO: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
return openPhotoAssetFile(mActiveDb.get(), uri, mode,
Data._ID + "=" + Contacts.PHOTO_ID + " AND " +
RawContacts.CONTACT_ID + "=?",
new String[]{String.valueOf(contactId)});
}
case CONTACTS_ID_DISPLAY_PHOTO: {
if (!mode.equals("r")) {
throw new IllegalArgumentException(
"Display photos retrieved by contact ID can only be read.");
}
long contactId = Long.parseLong(uri.getPathSegments().get(1));
Cursor c = mActiveDb.get().query(Tables.CONTACTS,
new String[]{Contacts.PHOTO_FILE_ID},
Contacts._ID + "=?", new String[]{String.valueOf(contactId)},
null, null, null);
try {
if (c.moveToFirst()) {
long photoFileId = c.getLong(0);
return openDisplayPhotoForRead(photoFileId);
} else {
// No contact for this ID.
throw new FileNotFoundException(uri.toString());
}
} finally {
c.close();
}
}
case PROFILE_DISPLAY_PHOTO: {
if (!mode.equals("r")) {
throw new IllegalArgumentException(
"Display photos retrieved by contact ID can only be read.");
}
Cursor c = mActiveDb.get().query(Tables.CONTACTS,
new String[]{Contacts.PHOTO_FILE_ID}, null, null, null, null, null);
try {
if (c.moveToFirst()) {
long photoFileId = c.getLong(0);
return openDisplayPhotoForRead(photoFileId);
} else {
// No profile record.
throw new FileNotFoundException(uri.toString());
}
} finally {
c.close();
}
}
case CONTACTS_LOOKUP_PHOTO:
case CONTACTS_LOOKUP_ID_PHOTO:
case CONTACTS_LOOKUP_DISPLAY_PHOTO:
case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: {
if (!mode.equals("r")) {
throw new IllegalArgumentException(
"Photos retrieved by contact lookup key can only be read.");
}
List<String> pathSegments = uri.getPathSegments();
int segmentCount = pathSegments.size();
if (segmentCount < 4) {
throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
"Missing a lookup key", uri));
}
boolean forDisplayPhoto = (match == CONTACTS_LOOKUP_ID_DISPLAY_PHOTO
|| match == CONTACTS_LOOKUP_DISPLAY_PHOTO);
String lookupKey = pathSegments.get(2);
String[] projection = new String[]{Contacts.PHOTO_ID, Contacts.PHOTO_FILE_ID};
if (segmentCount == 5) {
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
projection, null, null, null, null, null,
Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
if (c != null) {
try {
c.moveToFirst();
if (forDisplayPhoto) {
long photoFileId =
c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID));
return openDisplayPhotoForRead(photoFileId);
} else {
long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID));
return openPhotoAssetFile(mActiveDb.get(), uri, mode,
Data._ID + "=?", new String[]{String.valueOf(photoId)});
}
} finally {
c.close();
}
}
}
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForContacts(qb, uri, projection);
long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
Cursor c = qb.query(mActiveDb.get(), projection, Contacts._ID + "=?",
new String[]{String.valueOf(contactId)}, null, null, null);
try {
c.moveToFirst();
if (forDisplayPhoto) {
long photoFileId = c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID));
return openDisplayPhotoForRead(photoFileId);
} else {
long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID));
return openPhotoAssetFile(mActiveDb.get(), uri, mode,
Data._ID + "=?", new String[]{String.valueOf(photoId)});
}
} finally {
c.close();
}
}
case RAW_CONTACTS_ID_DISPLAY_PHOTO: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
boolean writeable = !mode.equals("r");
// Find the primary photo data record for this raw contact.
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String[] projection = new String[]{Data._ID, Photo.PHOTO_FILE_ID};
setTablesAndProjectionMapForData(qb, uri, projection, false);
long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
Cursor c = qb.query(mActiveDb.get(), projection,
Data.RAW_CONTACT_ID + "=? AND " + DataColumns.MIMETYPE_ID + "=?",
new String[]{String.valueOf(rawContactId), String.valueOf(photoMimetypeId)},
null, null, Data.IS_PRIMARY + " DESC");
long dataId = 0;
long photoFileId = 0;
try {
if (c.getCount() >= 1) {
c.moveToFirst();
dataId = c.getLong(0);
photoFileId = c.getLong(1);
}
} finally {
c.close();
}
// If writeable, open a writeable file descriptor that we can monitor.
// When the caller finishes writing content, we'll process the photo and
// update the data record.
if (writeable) {
return openDisplayPhotoForWrite(rawContactId, dataId, uri, mode);
} else {
return openDisplayPhotoForRead(photoFileId);
}
}
case DISPLAY_PHOTO: {
long photoFileId = ContentUris.parseId(uri);
if (!mode.equals("r")) {
throw new IllegalArgumentException(
"Display photos retrieved by key can only be read.");
}
return openDisplayPhotoForRead(photoFileId);
}
case DATA_ID: {
long dataId = Long.parseLong(uri.getPathSegments().get(1));
long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
return openPhotoAssetFile(mActiveDb.get(), uri, mode,
Data._ID + "=? AND " + DataColumns.MIMETYPE_ID + "=" + photoMimetypeId,
new String[]{String.valueOf(dataId)});
}
case PROFILE_AS_VCARD: {
// When opening a contact as file, we pass back contents as a
// vCard-encoded stream. We build into a local buffer first,
// then pipe into MemoryFile once the exact size is known.
final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
outputRawContactsAsVCard(uri, localStream, null, null);
return buildAssetFileDescriptor(localStream);
}
case CONTACTS_AS_VCARD: {
// When opening a contact as file, we pass back contents as a
// vCard-encoded stream. We build into a local buffer first,
// then pipe into MemoryFile once the exact size is known.
final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
outputRawContactsAsVCard(uri, localStream, null, null);
return buildAssetFileDescriptor(localStream);
}
case CONTACTS_AS_MULTI_VCARD: {
final String lookupKeys = uri.getPathSegments().get(2);
final String[] loopupKeyList = lookupKeys.split(":");
final StringBuilder inBuilder = new StringBuilder();
Uri queryUri = Contacts.CONTENT_URI;
int index = 0;
// SQLite has limits on how many parameters can be used
// so the IDs are concatenated to a query string here instead
for (String lookupKey : loopupKeyList) {
if (index == 0) {
inBuilder.append("(");
} else {
inBuilder.append(",");
}
// TODO: Figure out what to do if the profile contact is in the list.
long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
inBuilder.append(contactId);
index++;
}
inBuilder.append(')');
final String selection = Contacts._ID + " IN " + inBuilder.toString();
// When opening a contact as file, we pass back contents as a
// vCard-encoded stream. We build into a local buffer first,
// then pipe into MemoryFile once the exact size is known.
final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
outputRawContactsAsVCard(queryUri, localStream, selection, null);
return buildAssetFileDescriptor(localStream);
}
default:
throw new FileNotFoundException(mDbHelper.get().exceptionMessage(
"File does not exist", uri));
}
}
private AssetFileDescriptor openPhotoAssetFile(SQLiteDatabase db, Uri uri, String mode,
String selection, String[] selectionArgs)
throws FileNotFoundException {
if (!"r".equals(mode)) {
throw new FileNotFoundException(mDbHelper.get().exceptionMessage("Mode " + mode
+ " not supported.", uri));
}
String sql =
"SELECT " + Photo.PHOTO + " FROM " + Views.DATA +
" WHERE " + selection;
try {
return makeAssetFileDescriptor(
DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs));
} catch (SQLiteDoneException e) {
// this will happen if the DB query returns no rows (i.e. contact does not exist)
throw new FileNotFoundException(uri.toString());
}
}
/**
* Opens a display photo from the photo store for reading.
* @param photoFileId The display photo file ID
* @return An asset file descriptor that allows the file to be read.
* @throws FileNotFoundException If no photo file for the given ID exists.
*/
private AssetFileDescriptor openDisplayPhotoForRead(long photoFileId)
throws FileNotFoundException {
PhotoStore.Entry entry = mPhotoStore.get().get(photoFileId);
if (entry != null) {
try {
return makeAssetFileDescriptor(
ParcelFileDescriptor.open(new File(entry.path),
ParcelFileDescriptor.MODE_READ_ONLY),
entry.size);
} catch (FileNotFoundException fnfe) {
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
throw fnfe;
}
} else {
scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
throw new FileNotFoundException("No photo file found for ID " + photoFileId);
}
}
/**
* Opens a file descriptor for a photo to be written. When the caller completes writing
* to the file (closing the output stream), the image will be parsed out and processed.
* If processing succeeds, the given raw contact ID's primary photo record will be
* populated with the inserted image (if no primary photo record exists, the data ID can
* be left as 0, and a new data record will be inserted).
* @param rawContactId Raw contact ID this photo entry should be associated with.
* @param dataId Data ID for a photo mimetype that will be updated with the inserted
* image. May be set to 0, in which case the inserted image will trigger creation
* of a new primary photo image data row for the raw contact.
* @param uri The URI being used to access this file.
* @param mode Read/write mode string.
* @return An asset file descriptor the caller can use to write an image file for the
* raw contact.
*/
private AssetFileDescriptor openDisplayPhotoForWrite(long rawContactId, long dataId, Uri uri,
String mode) {
try {
ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
PipeMonitor pipeMonitor = new PipeMonitor(rawContactId, dataId, pipeFds[0]);
pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[]) null);
return new AssetFileDescriptor(pipeFds[1], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
} catch (IOException ioe) {
Log.e(TAG, "Could not create temp image file in mode " + mode);
return null;
}
}
/**
* Async task that monitors the given file descriptor (the read end of a pipe) for
* the writer finishing. If the data from the pipe contains a valid image, the image
* is either inserted into the given raw contact or updated in the given data row.
*/
private class PipeMonitor extends AsyncTask<Object, Object, Object> {
private final ParcelFileDescriptor mDescriptor;
private final long mRawContactId;
private final long mDataId;
private PipeMonitor(long rawContactId, long dataId, ParcelFileDescriptor descriptor) {
mRawContactId = rawContactId;
mDataId = dataId;
mDescriptor = descriptor;
}
@Override
protected Object doInBackground(Object... params) {
AutoCloseInputStream is = new AutoCloseInputStream(mDescriptor);
try {
Bitmap b = BitmapFactory.decodeStream(is);
if (b != null) {
waitForAccess(mWriteAccessLatch);
PhotoProcessor processor = new PhotoProcessor(b, mMaxDisplayPhotoDim,
mMaxThumbnailPhotoDim);
// Store the compressed photo in the photo store.
PhotoStore photoStore = ContactsContract.isProfileId(mRawContactId)
? mProfilePhotoStore
: mContactsPhotoStore;
long photoFileId = photoStore.insert(processor);
// Depending on whether we already had a data row to attach the photo
// to, do an update or insert.
if (mDataId != 0) {
// Update the data record with the new photo.
ContentValues updateValues = new ContentValues();
// Signal that photo processing has already been handled.
updateValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true);
if (photoFileId != 0) {
updateValues.put(Photo.PHOTO_FILE_ID, photoFileId);
}
updateValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
update(ContentUris.withAppendedId(Data.CONTENT_URI, mDataId),
updateValues, null, null);
} else {
// Insert a new primary data record with the photo.
ContentValues insertValues = new ContentValues();
// Signal that photo processing has already been handled.
insertValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true);
insertValues.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
insertValues.put(Data.IS_PRIMARY, 1);
if (photoFileId != 0) {
insertValues.put(Photo.PHOTO_FILE_ID, photoFileId);
}
insertValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
insert(RawContacts.CONTENT_URI.buildUpon()
.appendPath(String.valueOf(mRawContactId))
.appendPath(RawContacts.Data.CONTENT_DIRECTORY).build(),
insertValues);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
}
}
private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
/**
* Returns an {@link AssetFileDescriptor} backed by the
* contents of the given {@link ByteArrayOutputStream}.
*/
private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
try {
stream.flush();
final byte[] byteData = stream.toByteArray();
return makeAssetFileDescriptor(
ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME),
byteData.length);
} catch (IOException e) {
Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString());
return null;
}
}
private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) {
return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH);
}
private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) {
return fd != null ? new AssetFileDescriptor(fd, 0, length) : null;
}
/**
* Output {@link RawContacts} matching the requested selection in the vCard
* format to the given {@link OutputStream}. This method returns silently if
* any errors encountered.
*/
private void outputRawContactsAsVCard(Uri uri, OutputStream stream,
String selection, String[] selectionArgs) {
final Context context = this.getContext();
int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT;
if(uri.getBooleanQueryParameter(
Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) {
vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
}
final VCardComposer composer =
new VCardComposer(context, vcardconfig, false);
Writer writer = null;
final Uri rawContactsUri;
if (mapsToProfileDb(uri)) {
// Pre-authorize the URI, since the caller would have already gone through the
// permission check to get here, but the pre-authorization at the top level wouldn't
// carry over to the raw contact.
rawContactsUri = preAuthorizeUri(RawContactsEntity.PROFILE_CONTENT_URI);
} else {
rawContactsUri = RawContactsEntity.CONTENT_URI;
}
try {
writer = new BufferedWriter(new OutputStreamWriter(stream));
if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) {
Log.w(TAG, "Failed to init VCardComposer");
return;
}
while (!composer.isAfterLast()) {
writer.write(composer.createOneEntry());
}
} catch (IOException e) {
Log.e(TAG, "IOException: " + e);
} finally {
composer.terminate();
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
Log.w(TAG, "IOException during closing output stream: " + e);
}
}
}
}
@Override
public String getType(Uri uri) {
waitForAccess(mReadAccessLatch);
final int match = sUriMatcher.match(uri);
switch (match) {
case CONTACTS:
return Contacts.CONTENT_TYPE;
case CONTACTS_LOOKUP:
case CONTACTS_ID:
case CONTACTS_LOOKUP_ID:
case PROFILE:
return Contacts.CONTENT_ITEM_TYPE;
case CONTACTS_AS_VCARD:
case CONTACTS_AS_MULTI_VCARD:
case PROFILE_AS_VCARD:
return Contacts.CONTENT_VCARD_TYPE;
case CONTACTS_ID_PHOTO:
case CONTACTS_LOOKUP_PHOTO:
case CONTACTS_LOOKUP_ID_PHOTO:
case CONTACTS_ID_DISPLAY_PHOTO:
case CONTACTS_LOOKUP_DISPLAY_PHOTO:
case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO:
case RAW_CONTACTS_ID_DISPLAY_PHOTO:
case DISPLAY_PHOTO:
return "image/jpeg";
case RAW_CONTACTS:
case PROFILE_RAW_CONTACTS:
return RawContacts.CONTENT_TYPE;
case RAW_CONTACTS_ID:
case PROFILE_RAW_CONTACTS_ID:
return RawContacts.CONTENT_ITEM_TYPE;
case DATA:
case PROFILE_DATA:
return Data.CONTENT_TYPE;
case DATA_ID:
long id = ContentUris.parseId(uri);
if (ContactsContract.isProfileId(id)) {
return mProfileHelper.getDataMimeType(id);
} else {
return mContactsHelper.getDataMimeType(id);
}
case PHONES:
return Phone.CONTENT_TYPE;
case PHONES_ID:
return Phone.CONTENT_ITEM_TYPE;
case PHONE_LOOKUP:
return PhoneLookup.CONTENT_TYPE;
case EMAILS:
return Email.CONTENT_TYPE;
case EMAILS_ID:
return Email.CONTENT_ITEM_TYPE;
case POSTALS:
return StructuredPostal.CONTENT_TYPE;
case POSTALS_ID:
return StructuredPostal.CONTENT_ITEM_TYPE;
case AGGREGATION_EXCEPTIONS:
return AggregationExceptions.CONTENT_TYPE;
case AGGREGATION_EXCEPTION_ID:
return AggregationExceptions.CONTENT_ITEM_TYPE;
case SETTINGS:
return Settings.CONTENT_TYPE;
case AGGREGATION_SUGGESTIONS:
return Contacts.CONTENT_TYPE;
case SEARCH_SUGGESTIONS:
return SearchManager.SUGGEST_MIME_TYPE;
case SEARCH_SHORTCUT:
return SearchManager.SHORTCUT_MIME_TYPE;
case DIRECTORIES:
return Directory.CONTENT_TYPE;
case DIRECTORIES_ID:
return Directory.CONTENT_ITEM_TYPE;
case STREAM_ITEMS:
return StreamItems.CONTENT_TYPE;
case STREAM_ITEMS_ID:
return StreamItems.CONTENT_ITEM_TYPE;
case STREAM_ITEMS_ID_PHOTOS:
return StreamItems.StreamItemPhotos.CONTENT_TYPE;
case STREAM_ITEMS_ID_PHOTOS_ID:
return StreamItems.StreamItemPhotos.CONTENT_ITEM_TYPE;
case STREAM_ITEMS_PHOTOS:
throw new UnsupportedOperationException("Not supported for write-only URI " + uri);
default:
return mLegacyApiSupport.getType(uri);
}
}
public String[] getDefaultProjection(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case CONTACTS:
case CONTACTS_LOOKUP:
case CONTACTS_ID:
case CONTACTS_LOOKUP_ID:
case AGGREGATION_SUGGESTIONS:
case PROFILE:
return sContactsProjectionMap.getColumnNames();
case CONTACTS_ID_ENTITIES:
case PROFILE_ENTITIES:
return sEntityProjectionMap.getColumnNames();
case CONTACTS_AS_VCARD:
case CONTACTS_AS_MULTI_VCARD:
case PROFILE_AS_VCARD:
return sContactsVCardProjectionMap.getColumnNames();
case RAW_CONTACTS:
case RAW_CONTACTS_ID:
case PROFILE_RAW_CONTACTS:
case PROFILE_RAW_CONTACTS_ID:
return sRawContactsProjectionMap.getColumnNames();
case DATA_ID:
case PHONES:
case PHONES_ID:
case EMAILS:
case EMAILS_ID:
case POSTALS:
case POSTALS_ID:
case PROFILE_DATA:
return sDataProjectionMap.getColumnNames();
case PHONE_LOOKUP:
return sPhoneLookupProjectionMap.getColumnNames();
case AGGREGATION_EXCEPTIONS:
case AGGREGATION_EXCEPTION_ID:
return sAggregationExceptionsProjectionMap.getColumnNames();
case SETTINGS:
return sSettingsProjectionMap.getColumnNames();
case DIRECTORIES:
case DIRECTORIES_ID:
return sDirectoryProjectionMap.getColumnNames();
default:
return null;
}
}
private class StructuredNameLookupBuilder extends NameLookupBuilder {
public StructuredNameLookupBuilder(NameSplitter splitter) {
super(splitter);
}
@Override
protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
String name) {
mDbHelper.get().insertNameLookup(rawContactId, dataId, lookupType, name);
}
@Override
protected String[] getCommonNicknameClusters(String normalizedName) {
return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
}
}
public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
sb.append("(" +
"SELECT DISTINCT " + RawContacts.CONTACT_ID +
" FROM " + Tables.RAW_CONTACTS +
" JOIN " + Tables.NAME_LOOKUP +
" ON(" + RawContactsColumns.CONCRETE_ID + "="
+ NameLookupColumns.RAW_CONTACT_ID + ")" +
" WHERE normalized_name GLOB '");
sb.append(NameNormalizer.normalize(filterParam));
sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
" IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
}
public boolean isPhoneNumber(String filter) {
boolean atLeastOneDigit = false;
int len = filter.length();
for (int i = 0; i < len; i++) {
char c = filter.charAt(i);
if (c >= '0' && c <= '9') {
atLeastOneDigit = true;
} else if (c != '*' && c != '#' && c != '+' && c != 'N' && c != '.' && c != ';'
&& c != '-' && c != '(' && c != ')' && c != ' ') {
return false;
}
}
return atLeastOneDigit;
}
/**
* Takes components of a name from the query parameters and returns a cursor with those
* components as well as all missing components. There is no database activity involved
* in this so the call can be made on the UI thread.
*/
private Cursor completeName(Uri uri, String[] projection) {
if (projection == null) {
projection = sDataProjectionMap.getColumnNames();
}
ContentValues values = new ContentValues();
DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName)
getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE);
copyQueryParamsToContentValues(values, uri,
StructuredName.DISPLAY_NAME,
StructuredName.PREFIX,
StructuredName.GIVEN_NAME,
StructuredName.MIDDLE_NAME,
StructuredName.FAMILY_NAME,
StructuredName.SUFFIX,
StructuredName.PHONETIC_NAME,
StructuredName.PHONETIC_FAMILY_NAME,
StructuredName.PHONETIC_MIDDLE_NAME,
StructuredName.PHONETIC_GIVEN_NAME
);
handler.fixStructuredNameComponents(values, values);
MatrixCursor cursor = new MatrixCursor(projection);
Object[] row = new Object[projection.length];
for (int i = 0; i < projection.length; i++) {
row[i] = values.get(projection[i]);
}
cursor.addRow(row);
return cursor;
}
private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) {
for (String column : columns) {
String param = uri.getQueryParameter(column);
if (param != null) {
values.put(column, param);
}
}
}
/**
* Inserts an argument at the beginning of the selection arg list.
*/
private String[] insertSelectionArg(String[] selectionArgs, String arg) {
if (selectionArgs == null) {
return new String[] {arg};
} else {
int newLength = selectionArgs.length + 1;
String[] newSelectionArgs = new String[newLength];
newSelectionArgs[0] = arg;
System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
return newSelectionArgs;
}
}
private String[] appendProjectionArg(String[] projection, String arg) {
if (projection == null) {
return null;
}
final int length = projection.length;
String[] newProjection = new String[length + 1];
System.arraycopy(projection, 0, newProjection, 0, length);
newProjection[length] = arg;
return newProjection;
}
protected Account getDefaultAccount() {
AccountManager accountManager = AccountManager.get(getContext());
try {
Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE);
if (accounts != null && accounts.length > 0) {
return accounts[0];
}
} catch (Throwable e) {
Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
}
return null;
}
/**
* Returns true if the specified account type and data set is writable.
*/
protected boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) {
if (accountTypeAndDataSet == null) {
return true;
}
Boolean writable = mAccountWritability.get(accountTypeAndDataSet);
if (writable != null) {
return writable;
}
IContentService contentService = ContentResolver.getContentService();
try {
// TODO(dsantoro): Need to update this logic to allow for sub-accounts.
for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
if (ContactsContract.AUTHORITY.equals(sync.authority) &&
accountTypeAndDataSet.equals(sync.accountType)) {
writable = sync.supportsUploading();
break;
}
}
} catch (RemoteException e) {
Log.e(TAG, "Could not acquire sync adapter types");
}
if (writable == null) {
writable = false;
}
mAccountWritability.put(accountTypeAndDataSet, writable);
return writable;
}
/* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
boolean defaultValue) {
// Manually parse the query, which is much faster than calling uri.getQueryParameter
String query = uri.getEncodedQuery();
if (query == null) {
return defaultValue;
}
int index = query.indexOf(parameter);
if (index == -1) {
return defaultValue;
}
index += parameter.length();
return !matchQueryParameter(query, index, "=0", false)
&& !matchQueryParameter(query, index, "=false", true);
}
private static boolean matchQueryParameter(String query, int index, String value,
boolean ignoreCase) {
int length = value.length();
return query.regionMatches(ignoreCase, index, value, 0, length)
&& (query.length() == index + length || query.charAt(index + length) == '&');
}
/**
* A fast re-implementation of {@link Uri#getQueryParameter}
*/
/* package */ static String getQueryParameter(Uri uri, String parameter) {
String query = uri.getEncodedQuery();
if (query == null) {
return null;
}
int queryLength = query.length();
int parameterLength = parameter.length();
String value;
int index = 0;
while (true) {
index = query.indexOf(parameter, index);
if (index == -1) {
return null;
}
// Should match against the whole parameter instead of its suffix.
// e.g. The parameter "param" must not be found in "some_param=val".
if (index > 0) {
char prevChar = query.charAt(index - 1);
if (prevChar != '?' && prevChar != '&') {
// With "some_param=val1&param=val2", we should find second "param" occurrence.
index += parameterLength;
continue;
}
}
index += parameterLength;
if (queryLength == index) {
return null;
}
if (query.charAt(index) == '=') {
index++;
break;
}
}
int ampIndex = query.indexOf('&', index);
if (ampIndex == -1) {
value = query.substring(index);
} else {
value = query.substring(index, ampIndex);
}
return Uri.decode(value);
}
protected boolean isAggregationUpgradeNeeded() {
if (!mContactAggregator.isEnabled()) {
return false;
}
int version = Integer.parseInt(mContactsHelper.getProperty(
PROPERTY_AGGREGATION_ALGORITHM, "1"));
return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
}
protected void upgradeAggregationAlgorithmInBackground() {
// This upgrade will affect very few contacts, so it can be performed on the
// main thread during the initial boot after an OTA
Log.i(TAG, "Upgrading aggregation algorithm");
int count = 0;
long start = SystemClock.currentThreadTimeMillis();
SQLiteDatabase db = null;
try {
switchToContactMode();
db = mContactsHelper.getWritableDatabase();
mActiveDb.set(db);
db.beginTransaction();
Cursor cursor = db.query(true,
Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2",
new String[]{"r1." + RawContacts._ID},
"r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
" AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
" AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
" AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE +
" AND r1." + RawContacts.DATA_SET + "=r2." + RawContacts.DATA_SET,
null, null, null, null, null);
try {
while (cursor.moveToNext()) {
long rawContactId = cursor.getLong(0);
mContactAggregator.markForAggregation(rawContactId,
RawContacts.AGGREGATION_MODE_DEFAULT, true);
count++;
}
} finally {
cursor.close();
}
mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db);
updateSearchIndexInTransaction();
db.setTransactionSuccessful();
mContactsHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
} finally {
if (db != null) {
db.endTransaction();
}
long end = SystemClock.currentThreadTimeMillis();
Log.i(TAG, "Aggregation algorithm upgraded for " + count
+ " contacts, in " + (end - start) + "ms");
}
}
/* Visible for testing */
boolean isPhone() {
if (!sIsPhoneInitialized) {
sIsPhone = new TelephonyManager(getContext()).isVoiceCapable();
sIsPhoneInitialized = true;
}
return sIsPhone;
}
private boolean handleDataUsageFeedback(Uri uri) {
final long currentTimeMillis = System.currentTimeMillis();
final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
final String[] ids = uri.getLastPathSegment().trim().split(",");
final ArrayList<Long> dataIds = new ArrayList<Long>();
for (String id : ids) {
dataIds.add(Long.valueOf(id));
}
final boolean successful;
if (TextUtils.isEmpty(usageType)) {
Log.w(TAG, "Method for data usage feedback isn't specified. Ignoring.");
successful = false;
} else {
successful = updateDataUsageStat(dataIds, usageType, currentTimeMillis) > 0;
}
// Handle old API. This doesn't affect the result of this entire method.
final String[] questionMarks = new String[ids.length];
Arrays.fill(questionMarks, "?");
final String where = Data._ID + " IN (" + TextUtils.join(",", questionMarks) + ")";
final Cursor cursor = mActiveDb.get().query(
Views.DATA,
new String[] { Data.CONTACT_ID },
where, ids, null, null, null);
try {
while (cursor.moveToNext()) {
mSelectionArgs1[0] = cursor.getString(0);
ContentValues values2 = new ContentValues();
values2.put(Contacts.LAST_TIME_CONTACTED, currentTimeMillis);
mActiveDb.get().update(Tables.CONTACTS, values2, Contacts._ID + "=?",
mSelectionArgs1);
mActiveDb.get().execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
mActiveDb.get().execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
}
} finally {
cursor.close();
}
return successful;
}
/**
* Update {@link Tables#DATA_USAGE_STAT}.
*
* @return the number of rows affected.
*/
@VisibleForTesting
/* package */ int updateDataUsageStat(
List<Long> dataIds, String type, long currentTimeMillis) {
final int typeInt = sDataUsageTypeMap.get(type);
final String where = DataUsageStatColumns.DATA_ID + " =? AND "
+ DataUsageStatColumns.USAGE_TYPE_INT + " =?";
final String[] columns =
new String[] { DataUsageStatColumns._ID, DataUsageStatColumns.TIMES_USED };
final ContentValues values = new ContentValues();
for (Long dataId : dataIds) {
final String[] args = new String[] { dataId.toString(), String.valueOf(typeInt) };
mActiveDb.get().beginTransaction();
try {
final Cursor cursor = mActiveDb.get().query(Tables.DATA_USAGE_STAT, columns, where,
args, null, null, null);
try {
if (cursor.getCount() > 0) {
if (!cursor.moveToFirst()) {
Log.e(TAG,
"moveToFirst() failed while getAccount() returned non-zero.");
} else {
values.clear();
values.put(DataUsageStatColumns.TIMES_USED, cursor.getInt(1) + 1);
values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis);
mActiveDb.get().update(Tables.DATA_USAGE_STAT, values,
DataUsageStatColumns._ID + " =?",
new String[] { cursor.getString(0) });
}
} else {
values.clear();
values.put(DataUsageStatColumns.DATA_ID, dataId);
values.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
values.put(DataUsageStatColumns.TIMES_USED, 1);
values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis);
mActiveDb.get().insert(Tables.DATA_USAGE_STAT, null, values);
}
mActiveDb.get().setTransactionSuccessful();
} finally {
cursor.close();
}
} finally {
mActiveDb.get().endTransaction();
}
}
return dataIds.size();
}
/**
* Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
* associated with a primary account. The primary account should be supplied from applications
* with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
* {@link ContactsContract#PRIMARY_ACCOUNT_TYPE}. Null will be returned when the primary
* account isn't available.
*/
private String getAccountPromotionSortOrder(Uri uri) {
final String primaryAccountName =
uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
final String primaryAccountType =
uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE);
// Data rows associated with primary account should be promoted.
if (!TextUtils.isEmpty(primaryAccountName)) {
StringBuilder sb = new StringBuilder();
sb.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "=");
DatabaseUtils.appendEscapedSQLString(sb, primaryAccountName);
if (!TextUtils.isEmpty(primaryAccountType)) {
sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
DatabaseUtils.appendEscapedSQLString(sb, primaryAccountType);
}
sb.append(" THEN 0 ELSE 1 END)");
return sb.toString();
} else {
return null;
}
}
/**
* Checks the URI for a deferred snippeting request
* @return a boolean indicating if a deferred snippeting request is in the RI
*/
private boolean deferredSnippetingRequested(Uri uri) {
String deferredSnippeting =
getQueryParameter(uri, SearchSnippetColumns.DEFERRED_SNIPPETING_KEY);
return !TextUtils.isEmpty(deferredSnippeting) && deferredSnippeting.equals("1");
}
/**
* Checks if query is a single word or not.
* @return a boolean indicating if the query is one word or not
*/
private boolean isSingleWordQuery(String query) {
return query.split(QUERY_TOKENIZER_REGEX).length == 1;
}
/**
* Checks the projection for a SNIPPET column indicating that a snippet is needed
* @return a boolean indicating if a snippet is needed or not.
*/
private boolean snippetNeeded(String [] projection) {
return mDbHelper.get().isInProjection(projection, SearchSnippetColumns.SNIPPET);
}
}