/* * 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 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 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 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"); } }