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