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 { sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync", METADATA_SYNC)67 sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync", METADATA_SYNC); sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync/#", METADATA_SYNC_ID)68 sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync/#", METADATA_SYNC_ID); sURIMatcher.addURI(MetadataSync.METADATA_AUTHORITY, "metadata_sync_state", SYNC_STATE)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 onCreate()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 getDatabaseHelper(final Context context)109 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 110 return ContactsDatabaseHelper.getInstance(context); 111 } 112 113 @VisibleForTesting setDatabaseHelper(final ContactsDatabaseHelper helper)114 protected void setDatabaseHelper(final ContactsDatabaseHelper helper) { 115 mDbHelper = helper; 116 } 117 118 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 getType(Uri uri)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 */ insert(Uri uri, ContentValues values)179 public Uri insert(Uri uri, ContentValues values) { 180 181 ensureCaller(); 182 183 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 184 db.beginTransaction(); 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 delete(Uri uri, String selection, String[] selectionArgs)210 public int delete(Uri uri, String selection, String[] selectionArgs) { 211 212 ensureCaller(); 213 214 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 215 db.beginTransaction(); 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 update(Uri uri, ContentValues values, String selection, String[] selectionArgs)258 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 259 260 ensureCaller(); 261 262 final SQLiteDatabase db = mDbHelper.getWritableDatabase(); 263 db.beginTransaction(); 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 applyBatch(ArrayList<ContentProviderOperation> operations)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.beginTransaction(); 300 try { 301 ContentProviderResult[] results = super.applyBatch(operations); 302 db.setTransactionSuccessful(); 303 return results; 304 } finally { 305 db.endTransaction(); 306 } 307 } 308 309 @Override bulkInsert(Uri uri, ContentValues[] values)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.beginTransaction(); 319 try { 320 final int numValues = super.bulkInsert(uri, values); 321 db.setTransactionSuccessful(); 322 return numValues; 323 } finally { 324 db.endTransaction(); 325 } 326 } 327 setTablesAndProjectionMapForMetadata(SQLiteQueryBuilder qb)328 private void setTablesAndProjectionMapForMetadata(SQLiteQueryBuilder qb){ 329 qb.setTables(Views.METADATA_SYNC); 330 qb.setProjectionMap(sMetadataProjectionMap); 331 qb.setStrict(true); 332 } 333 setTablesAndProjectionMapForSyncState(SQLiteQueryBuilder qb)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 */ updateOrInsertDataToMetadataSync(SQLiteDatabase db, Uri uri, ContentValues values)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 */ replaceAccountInfoByAccountId(Uri uri, ContentValues values)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 ensureCaller()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