blob: 3cf7df2ce26541b9555200a14680bf79b0cb09b4 [file] [log] [blame]
/*
* Copyright (C) 2015 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 android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.IContentProvider;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
import android.provider.ContactsContract;
import android.provider.ContactsContract.MetadataSync;
import android.provider.ContactsContract.MetadataSyncState;
import android.text.TextUtils;
import android.util.Log;
import com.android.common.content.ProjectionMap;
import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncStateColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.android.providers.contacts.ContactsDatabaseHelper.Views;
import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.UserUtils;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import static com.android.providers.contacts.ContactsProvider2.getLimit;
import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
/**
* Simple content provider to handle directing contact metadata specific calls.
*/
public class ContactMetadataProvider extends ContentProvider {
private static final String TAG = "ContactMetadata";
private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
private static final int METADATA_SYNC = 1;
private static final int METADATA_SYNC_ID = 2;
private static final int SYNC_STATE = 3;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync", METADATA_SYNC);
sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync/#", METADATA_SYNC_ID);
sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync_state", SYNC_STATE);
}
private static final Map<String, String> sMetadataProjectionMap = ProjectionMap.builder()
.add(MetadataSync._ID)
.add(MetadataSync.RAW_CONTACT_BACKUP_ID)
.add(MetadataSync.ACCOUNT_TYPE)
.add(MetadataSync.ACCOUNT_NAME)
.add(MetadataSync.DATA_SET)
.add(MetadataSync.DATA)
.add(MetadataSync.DELETED)
.build();
private static final Map<String, String> sSyncStateProjectionMap =ProjectionMap.builder()
.add(MetadataSyncState._ID)
.add(MetadataSyncState.ACCOUNT_TYPE)
.add(MetadataSyncState.ACCOUNT_NAME)
.add(MetadataSyncState.DATA_SET)
.add(MetadataSyncState.STATE)
.build();
private ContactsDatabaseHelper mDbHelper;
private ContactsProvider2 mContactsProvider;
private String mAllowedPackage;
@Override
public boolean onCreate() {
final Context context = getContext();
mDbHelper = getDatabaseHelper(context);
final IContentProvider iContentProvider = context.getContentResolver().acquireProvider(
ContactsContract.AUTHORITY);
final ContentProvider provider = ContentProvider.coerceToLocalContentProvider(
iContentProvider);
mContactsProvider = (ContactsProvider2) provider;
mAllowedPackage = getContext().getResources().getString(R.string.metadata_sync_pacakge);
return true;
}
protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
return ContactsDatabaseHelper.getInstance(context);
}
@VisibleForTesting
protected void setDatabaseHelper(final ContactsDatabaseHelper helper) {
mDbHelper = helper;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
ensureCaller();
if (VERBOSE_LOGGING) {
Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String limit = getLimit(uri);
final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
final int match = sURIMatcher.match(uri);
switch (match) {
case METADATA_SYNC:
setTablesAndProjectionMapForMetadata(qb);
break;
case METADATA_SYNC_ID: {
setTablesAndProjectionMapForMetadata(qb);
selectionBuilder.addClause(getEqualityClause(MetadataSync._ID,
ContentUris.parseId(uri)));
break;
}
case SYNC_STATE:
setTablesAndProjectionMapForSyncState(qb);
break;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
return qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
null, sortOrder, limit);
}
@Override
public String getType(Uri uri) {
int match = sURIMatcher.match(uri);
switch (match) {
case METADATA_SYNC:
return MetadataSync.CONTENT_TYPE;
case METADATA_SYNC_ID:
return MetadataSync.CONTENT_ITEM_TYPE;
case SYNC_STATE:
return MetadataSyncState.CONTENT_TYPE;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
@Override
/**
* Insert or update if the raw is already existing.
*/
public Uri insert(Uri uri, ContentValues values) {
ensureCaller();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
final int matchedUriId = sURIMatcher.match(uri);
switch (matchedUriId) {
case METADATA_SYNC:
// Insert the new entry, and also parse the data column to update related
// tables.
final long metadataSyncId = updateOrInsertDataToMetadataSync(db, uri, values);
db.setTransactionSuccessful();
return ContentUris.withAppendedId(uri, metadataSyncId);
case SYNC_STATE:
replaceAccountInfoByAccountId(uri, values);
final Long syncStateId = db.replace(
Tables.METADATA_SYNC_STATE, MetadataSyncColumns.ACCOUNT_ID, values);
db.setTransactionSuccessful();
return ContentUris.withAppendedId(uri, syncStateId);
default:
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Calling contact metadata insert on an unknown/invalid URI", uri));
}
} finally {
db.endTransaction();
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
ensureCaller();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
final int matchedUriId = sURIMatcher.match(uri);
int numDeletes = 0;
switch (matchedUriId) {
case METADATA_SYNC:
Cursor c = db.query(Views.METADATA_SYNC, new String[]{MetadataSync._ID},
selection, selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
final long contactMetadataId = c.getLong(0);
numDeletes += db.delete(Tables.METADATA_SYNC,
MetadataSync._ID + "=" + contactMetadataId, null);
}
} finally {
c.close();
}
db.setTransactionSuccessful();
return numDeletes;
case SYNC_STATE:
c = db.query(Views.METADATA_SYNC_STATE, new String[]{MetadataSyncState._ID},
selection, selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
final long stateId = c.getLong(0);
numDeletes += db.delete(Tables.METADATA_SYNC_STATE,
MetadataSyncState._ID + "=" + stateId, null);
}
} finally {
c.close();
}
db.setTransactionSuccessful();
return numDeletes;
default:
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Calling contact metadata delete on an unknown/invalid URI", uri));
}
} finally {
db.endTransaction();
}
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
ensureCaller();
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
final int matchedUriId = sURIMatcher.match(uri);
switch (matchedUriId) {
// Do not support update metadata sync by update() method. Please use insert().
case SYNC_STATE:
// Only support update by account.
final Long accountId = replaceAccountInfoByAccountId(uri, values);
if (accountId == null) {
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Invalid identifier is found for accountId", uri));
}
values.put(MetadataSyncColumns.ACCOUNT_ID, accountId);
// Insert a new row if it doesn't exist.
db.replace(Tables.METADATA_SYNC_STATE, null, values);
db.setTransactionSuccessful();
return 1;
default:
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Calling contact metadata update on an unknown/invalid URI", uri));
}
} finally {
db.endTransaction();
}
}
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
ensureCaller();
if (VERBOSE_LOGGING) {
Log.v(TAG, "applyBatch: " + operations.size() + " ops");
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
ContentProviderResult[] results = super.applyBatch(operations);
db.setTransactionSuccessful();
return results;
} finally {
db.endTransaction();
}
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
ensureCaller();
if (VERBOSE_LOGGING) {
Log.v(TAG, "bulkInsert: " + values.length + " inserts");
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransactionNonExclusive();
try {
final int numValues = super.bulkInsert(uri, values);
db.setTransactionSuccessful();
return numValues;
} finally {
db.endTransaction();
}
}
private void setTablesAndProjectionMapForMetadata(SQLiteQueryBuilder qb){
qb.setTables(Views.METADATA_SYNC);
qb.setProjectionMap(sMetadataProjectionMap);
qb.setStrict(true);
}
private void setTablesAndProjectionMapForSyncState(SQLiteQueryBuilder qb){
qb.setTables(Views.METADATA_SYNC_STATE);
qb.setProjectionMap(sSyncStateProjectionMap);
qb.setStrict(true);
}
/**
* Insert or update a non-deleted entry to MetadataSync table, and also parse the data column
* to update related tables for the raw contact.
* Returns new upserted metadataSyncId.
*/
private long updateOrInsertDataToMetadataSync(SQLiteDatabase db, Uri uri, ContentValues values) {
final int matchUri = sURIMatcher.match(uri);
if (matchUri != METADATA_SYNC) {
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Calling contact metadata insert or update on an unknown/invalid URI", uri));
}
// Don't insert or update a deleted metadata.
Integer deleted = values.getAsInteger(MetadataSync.DELETED);
if (deleted != null && deleted != 0) {
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Cannot insert or update deleted metadata:" + values.toString(), uri));
}
// Check if data column is empty or null.
final String data = values.getAsString(MetadataSync.DATA);
if (TextUtils.isEmpty(data)) {
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Data column cannot be empty.", uri));
}
// Update or insert for backupId and account info.
final Long accountId = replaceAccountInfoByAccountId(uri, values);
final String rawContactBackupId = values.getAsString(
MetadataSync.RAW_CONTACT_BACKUP_ID);
// TODO (tingtingw): Consider a corner case: if there's raw with the same accountId and
// backupId, but deleted=1, (Deleted should be synced up to server and hard-deleted, but
// may be delayed.) In this case, should we not override it with delete=0? or should this
// be prevented by sync adapter side?.
deleted = 0; // Only insert or update non-deleted metadata
if (accountId == null) {
// Do nothing, just return.
return 0;
}
if (rawContactBackupId == null) {
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Invalid identifier is found: accountId=" + accountId + "; " +
"rawContactBackupId=" + rawContactBackupId, uri));
}
// Update if it exists, otherwise insert.
final long metadataSyncId = mDbHelper.upsertMetadataSync(
rawContactBackupId, accountId, data, deleted);
if (metadataSyncId <= 0) {
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Metadata upsertion failed. Values= " + values.toString(), uri));
}
// Parse the data column and update other tables.
// Data field will never be empty or null, since contacts prefs and usage stats
// have default values.
final MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(data);
mContactsProvider.updateFromMetaDataEntry(db, metadataEntry);
return metadataSyncId;
}
/**
* Replace account_type, account_name and data_set with account_id. If a valid account_id
* cannot be found for this combination, return null.
*/
private Long replaceAccountInfoByAccountId(Uri uri, ContentValues values) {
String accountName = values.getAsString(MetadataSync.ACCOUNT_NAME);
String accountType = values.getAsString(MetadataSync.ACCOUNT_TYPE);
String dataSet = values.getAsString(MetadataSync.DATA_SET);
final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
if (partialUri) {
// Throw when either account is incomplete.
throw new IllegalArgumentException(mDbHelper.exceptionMessage(
"Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
}
final AccountWithDataSet account = AccountWithDataSet.get(
accountName, accountType, dataSet);
final Long id = mDbHelper.getAccountIdOrNull(account);
if (id == null) {
return null;
}
values.put(MetadataSyncColumns.ACCOUNT_ID, id);
// Only remove the account information once the account ID is extracted (since these
// fields are actually used by resolveAccountWithDataSet to extract the relevant ID).
values.remove(MetadataSync.ACCOUNT_NAME);
values.remove(MetadataSync.ACCOUNT_TYPE);
values.remove(MetadataSync.DATA_SET);
return id;
}
@VisibleForTesting
void ensureCaller() {
final String caller = getCallingPackage();
if (mAllowedPackage.equals(caller)) {
return; // Okay.
}
throw new SecurityException("Caller " + caller + " can't access ContactMetadataProvider");
}
}