Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.providers.contacts;
     17 
     18 import android.content.ContentProvider;
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentProviderResult;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.IContentProvider;
     25 import android.content.OperationApplicationException;
     26 import android.content.UriMatcher;
     27 import android.database.Cursor;
     28 import android.database.sqlite.SQLiteDatabase;
     29 import android.database.sqlite.SQLiteQueryBuilder;
     30 import android.net.Uri;
     31 import android.os.Binder;
     32 import android.provider.ContactsContract;
     33 import android.provider.ContactsContract.MetadataSync;
     34 import android.provider.ContactsContract.MetadataSyncState;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 import com.android.common.content.ProjectionMap;
     38 import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
     39 import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncStateColumns;
     40 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     41 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
     42 import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
     43 import com.android.providers.contacts.util.SelectionBuilder;
     44 import com.android.providers.contacts.util.UserUtils;
     45 import com.google.common.annotations.VisibleForTesting;
     46 
     47 import java.util.ArrayList;
     48 import java.util.Arrays;
     49 import java.util.Map;
     50 
     51 import static com.android.providers.contacts.ContactsProvider2.getLimit;
     52 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
     53 
     54 /**
     55  * Simple content provider to handle directing contact metadata specific calls.
     56  */
     57 public class ContactMetadataProvider extends ContentProvider {
     58     private static final String TAG = "ContactMetadata";
     59     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
     60     private static final int METADATA_SYNC = 1;
     61     private static final int METADATA_SYNC_ID = 2;
     62     private static final int SYNC_STATE = 3;
     63 
     64     private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     65 
     66     static {
     67         sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync", METADATA_SYNC);
     68         sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync/#", METADATA_SYNC_ID);
     69         sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync_state", SYNC_STATE);
     70     }
     71 
     72     private static final Map<String, String> sMetadataProjectionMap = ProjectionMap.builder()
     73             .add(MetadataSync._ID)
     74             .add(MetadataSync.RAW_CONTACT_BACKUP_ID)
     75             .add(MetadataSync.ACCOUNT_TYPE)
     76             .add(MetadataSync.ACCOUNT_NAME)
     77             .add(MetadataSync.DATA_SET)
     78             .add(MetadataSync.DATA)
     79             .add(MetadataSync.DELETED)
     80             .build();
     81 
     82     private static final Map<String, String> sSyncStateProjectionMap =ProjectionMap.builder()
     83             .add(MetadataSyncState._ID)
     84             .add(MetadataSyncState.ACCOUNT_TYPE)
     85             .add(MetadataSyncState.ACCOUNT_NAME)
     86             .add(MetadataSyncState.DATA_SET)
     87             .add(MetadataSyncState.STATE)
     88             .build();
     89 
     90     private ContactsDatabaseHelper mDbHelper;
     91     private ContactsProvider2 mContactsProvider;
     92 
     93     private String mAllowedPackage;
     94 
     95     @Override
     96     public boolean onCreate() {
     97         final Context context = getContext();
     98         mDbHelper = getDatabaseHelper(context);
     99         final IContentProvider iContentProvider = context.getContentResolver().acquireProvider(
    100                 ContactsContract.AUTHORITY);
    101         final ContentProvider provider = ContentProvider.coerceToLocalContentProvider(
    102                 iContentProvider);
    103         mContactsProvider = (ContactsProvider2) provider;
    104 
    105         mAllowedPackage = getContext().getResources().getString(R.string.metadata_sync_pacakge);
    106         return true;
    107     }
    108 
    109     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
    110         return ContactsDatabaseHelper.getInstance(context);
    111     }
    112 
    113     @VisibleForTesting
    114     protected void setDatabaseHelper(final ContactsDatabaseHelper helper) {
    115         mDbHelper = helper;
    116     }
    117 
    118     @Override
    119     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    120             String sortOrder) {
    121 
    122         ensureCaller();
    123 
    124         if (VERBOSE_LOGGING) {
    125             Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
    126                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
    127                     "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
    128                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
    129         }
    130         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    131         String limit = getLimit(uri);
    132 
    133         final SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    134 
    135         final int match = sURIMatcher.match(uri);
    136         switch (match) {
    137             case METADATA_SYNC:
    138                 setTablesAndProjectionMapForMetadata(qb);
    139                 break;
    140 
    141             case METADATA_SYNC_ID: {
    142                 setTablesAndProjectionMapForMetadata(qb);
    143                 selectionBuilder.addClause(getEqualityClause(MetadataSync._ID,
    144                         ContentUris.parseId(uri)));
    145                 break;
    146             }
    147 
    148             case SYNC_STATE:
    149                 setTablesAndProjectionMapForSyncState(qb);
    150                 break;
    151             default:
    152                 throw new IllegalArgumentException("Unknown URL " + uri);
    153         }
    154 
    155         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    156         return qb.query(db, projection, selectionBuilder.build(), selectionArgs, null,
    157                 null, sortOrder, limit);
    158     }
    159 
    160     @Override
    161     public String getType(Uri uri) {
    162         int match = sURIMatcher.match(uri);
    163         switch (match) {
    164             case METADATA_SYNC:
    165                 return MetadataSync.CONTENT_TYPE;
    166             case METADATA_SYNC_ID:
    167                 return MetadataSync.CONTENT_ITEM_TYPE;
    168             case SYNC_STATE:
    169                 return MetadataSyncState.CONTENT_TYPE;
    170             default:
    171                 throw new IllegalArgumentException("Unknown URI: " + uri);
    172         }
    173     }
    174 
    175     @Override
    176     /**
    177      * Insert or update if the raw is already existing.
    178      */
    179     public Uri insert(Uri uri, ContentValues values) {
    180 
    181         ensureCaller();
    182 
    183         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    184         db.beginTransactionNonExclusive();
    185         try {
    186             final int matchedUriId = sURIMatcher.match(uri);
    187             switch (matchedUriId) {
    188                 case METADATA_SYNC:
    189                     // Insert the new entry, and also parse the data column to update related
    190                     // tables.
    191                     final long metadataSyncId = updateOrInsertDataToMetadataSync(db, uri, values);
    192                     db.setTransactionSuccessful();
    193                     return ContentUris.withAppendedId(uri, metadataSyncId);
    194                 case SYNC_STATE:
    195                     replaceAccountInfoByAccountId(uri, values);
    196                     final Long syncStateId = db.replace(
    197                             Tables.METADATA_SYNC_STATE, MetadataSyncColumns.ACCOUNT_ID, values);
    198                     db.setTransactionSuccessful();
    199                     return ContentUris.withAppendedId(uri, syncStateId);
    200                 default:
    201                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    202                             "Calling contact metadata insert on an unknown/invalid URI", uri));
    203             }
    204         } finally {
    205             db.endTransaction();
    206         }
    207     }
    208 
    209     @Override
    210     public int delete(Uri uri, String selection, String[] selectionArgs) {
    211 
    212         ensureCaller();
    213 
    214         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    215         db.beginTransactionNonExclusive();
    216         try {
    217             final int matchedUriId = sURIMatcher.match(uri);
    218             int numDeletes = 0;
    219             switch (matchedUriId) {
    220                 case METADATA_SYNC:
    221                     Cursor c = db.query(Views.METADATA_SYNC, new String[]{MetadataSync._ID},
    222                             selection, selectionArgs, null, null, null);
    223                     try {
    224                         while (c.moveToNext()) {
    225                             final long contactMetadataId = c.getLong(0);
    226                             numDeletes += db.delete(Tables.METADATA_SYNC,
    227                                     MetadataSync._ID + "=" + contactMetadataId, null);
    228                         }
    229                     } finally {
    230                         c.close();
    231                     }
    232                     db.setTransactionSuccessful();
    233                     return numDeletes;
    234                 case SYNC_STATE:
    235                     c = db.query(Views.METADATA_SYNC_STATE, new String[]{MetadataSyncState._ID},
    236                             selection, selectionArgs, null, null, null);
    237                     try {
    238                         while (c.moveToNext()) {
    239                             final long stateId = c.getLong(0);
    240                             numDeletes += db.delete(Tables.METADATA_SYNC_STATE,
    241                                     MetadataSyncState._ID + "=" + stateId, null);
    242                         }
    243                     } finally {
    244                         c.close();
    245                     }
    246                     db.setTransactionSuccessful();
    247                     return numDeletes;
    248                 default:
    249                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    250                             "Calling contact metadata delete on an unknown/invalid URI", uri));
    251             }
    252         } finally {
    253             db.endTransaction();
    254         }
    255     }
    256 
    257     @Override
    258     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    259 
    260         ensureCaller();
    261 
    262         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    263         db.beginTransactionNonExclusive();
    264         try {
    265             final int matchedUriId = sURIMatcher.match(uri);
    266             switch (matchedUriId) {
    267                 // Do not support update metadata sync by update() method. Please use insert().
    268                 case SYNC_STATE:
    269                     // Only support update by account.
    270                     final Long accountId = replaceAccountInfoByAccountId(uri, values);
    271                     if (accountId == null) {
    272                         throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    273                                 "Invalid identifier is found for accountId", uri));
    274                     }
    275                     values.put(MetadataSyncColumns.ACCOUNT_ID, accountId);
    276                     // Insert a new row if it doesn't exist.
    277                     db.replace(Tables.METADATA_SYNC_STATE, null, values);
    278                     db.setTransactionSuccessful();
    279                     return 1;
    280                 default:
    281                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    282                             "Calling contact metadata update on an unknown/invalid URI", uri));
    283             }
    284         } finally {
    285             db.endTransaction();
    286         }
    287     }
    288 
    289     @Override
    290     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
    291             throws OperationApplicationException {
    292 
    293         ensureCaller();
    294 
    295         if (VERBOSE_LOGGING) {
    296             Log.v(TAG, "applyBatch: " + operations.size() + " ops");
    297         }
    298         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    299         db.beginTransactionNonExclusive();
    300         try {
    301             ContentProviderResult[] results = super.applyBatch(operations);
    302             db.setTransactionSuccessful();
    303             return results;
    304         } finally {
    305             db.endTransaction();
    306         }
    307     }
    308 
    309     @Override
    310     public int bulkInsert(Uri uri, ContentValues[] values) {
    311 
    312         ensureCaller();
    313 
    314         if (VERBOSE_LOGGING) {
    315             Log.v(TAG, "bulkInsert: " + values.length + " inserts");
    316         }
    317         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
    318         db.beginTransactionNonExclusive();
    319         try {
    320             final int numValues = super.bulkInsert(uri, values);
    321             db.setTransactionSuccessful();
    322             return numValues;
    323         } finally {
    324             db.endTransaction();
    325         }
    326     }
    327 
    328     private void setTablesAndProjectionMapForMetadata(SQLiteQueryBuilder qb){
    329         qb.setTables(Views.METADATA_SYNC);
    330         qb.setProjectionMap(sMetadataProjectionMap);
    331         qb.setStrict(true);
    332     }
    333 
    334     private void setTablesAndProjectionMapForSyncState(SQLiteQueryBuilder qb){
    335         qb.setTables(Views.METADATA_SYNC_STATE);
    336         qb.setProjectionMap(sSyncStateProjectionMap);
    337         qb.setStrict(true);
    338     }
    339 
    340     /**
    341      * Insert or update a non-deleted entry to MetadataSync table, and also parse the data column
    342      * to update related tables for the raw contact.
    343      * Returns new upserted metadataSyncId.
    344      */
    345     private long updateOrInsertDataToMetadataSync(SQLiteDatabase db, Uri uri, ContentValues values) {
    346         final int matchUri = sURIMatcher.match(uri);
    347         if (matchUri != METADATA_SYNC) {
    348             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    349                     "Calling contact metadata insert or update on an unknown/invalid URI", uri));
    350         }
    351 
    352         // Don't insert or update a deleted metadata.
    353         Integer deleted = values.getAsInteger(MetadataSync.DELETED);
    354         if (deleted != null && deleted != 0) {
    355             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    356                     "Cannot insert or update deleted metadata:" + values.toString(), uri));
    357         }
    358 
    359         // Check if data column is empty or null.
    360         final String data = values.getAsString(MetadataSync.DATA);
    361         if (TextUtils.isEmpty(data)) {
    362             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    363                     "Data column cannot be empty.", uri));
    364         }
    365 
    366         // Update or insert for backupId and account info.
    367         final Long accountId = replaceAccountInfoByAccountId(uri, values);
    368         final String rawContactBackupId = values.getAsString(
    369                 MetadataSync.RAW_CONTACT_BACKUP_ID);
    370         // TODO (tingtingw): Consider a corner case: if there's raw with the same accountId and
    371         // backupId, but deleted=1, (Deleted should be synced up to server and hard-deleted, but
    372         // may be delayed.) In this case, should we not override it with delete=0? or should this
    373         // be prevented by sync adapter side?.
    374         deleted = 0; // Only insert or update non-deleted metadata
    375         if (accountId == null) {
    376             // Do nothing, just return.
    377             return 0;
    378         }
    379         if (rawContactBackupId == null) {
    380             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    381                     "Invalid identifier is found: accountId=" + accountId + "; " +
    382                             "rawContactBackupId=" + rawContactBackupId, uri));
    383         }
    384 
    385         // Update if it exists, otherwise insert.
    386         final long metadataSyncId = mDbHelper.upsertMetadataSync(
    387                 rawContactBackupId, accountId, data, deleted);
    388         if (metadataSyncId <= 0) {
    389             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    390                     "Metadata upsertion failed. Values= " + values.toString(), uri));
    391         }
    392 
    393         // Parse the data column and update other tables.
    394         // Data field will never be empty or null, since contacts prefs and usage stats
    395         // have default values.
    396         final MetadataEntry metadataEntry = MetadataEntryParser.parseDataToMetaDataEntry(data);
    397         mContactsProvider.updateFromMetaDataEntry(db, metadataEntry);
    398 
    399         return metadataSyncId;
    400     }
    401 
    402     /**
    403      *  Replace account_type, account_name and data_set with account_id. If a valid account_id
    404      *  cannot be found for this combination, return null.
    405      */
    406     private Long replaceAccountInfoByAccountId(Uri uri, ContentValues values) {
    407         String accountName = values.getAsString(MetadataSync.ACCOUNT_NAME);
    408         String accountType = values.getAsString(MetadataSync.ACCOUNT_TYPE);
    409         String dataSet = values.getAsString(MetadataSync.DATA_SET);
    410         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
    411         if (partialUri) {
    412             // Throw when either account is incomplete.
    413             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
    414                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
    415         }
    416 
    417         final AccountWithDataSet account = AccountWithDataSet.get(
    418                 accountName, accountType, dataSet);
    419 
    420         final Long id = mDbHelper.getAccountIdOrNull(account);
    421         if (id == null) {
    422             return null;
    423         }
    424 
    425         values.put(MetadataSyncColumns.ACCOUNT_ID, id);
    426         // Only remove the account information once the account ID is extracted (since these
    427         // fields are actually used by resolveAccountWithDataSet to extract the relevant ID).
    428         values.remove(MetadataSync.ACCOUNT_NAME);
    429         values.remove(MetadataSync.ACCOUNT_TYPE);
    430         values.remove(MetadataSync.DATA_SET);
    431 
    432         return id;
    433     }
    434 
    435     @VisibleForTesting
    436     void ensureCaller() {
    437         final String caller = getCallingPackage();
    438         if (mAllowedPackage.equals(caller)) {
    439             return; // Okay.
    440         }
    441         throw new SecurityException("Caller " + caller + " can't access ContactMetadataProvider");
    442     }
    443 }
    444