ContactMetadataProvider.java revision 56bb2f6d67417270a6ef0cb1cbb24ae2c313c4be
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 */
16package com.android.providers.contacts;
17
18import android.content.ContentProvider;
19import android.content.ContentProviderOperation;
20import android.content.ContentProviderResult;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.IContentProvider;
25import android.content.OperationApplicationException;
26import android.content.UriMatcher;
27import android.database.Cursor;
28import android.database.sqlite.SQLiteDatabase;
29import android.database.sqlite.SQLiteQueryBuilder;
30import android.net.Uri;
31import android.os.Binder;
32import android.provider.ContactsContract;
33import android.provider.ContactsContract.MetadataSync;
34import android.provider.ContactsContract.MetadataSyncState;
35import android.text.TextUtils;
36import android.util.Log;
37import com.android.common.content.ProjectionMap;
38import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncColumns;
39import com.android.providers.contacts.ContactsDatabaseHelper.MetadataSyncStateColumns;
40import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
41import com.android.providers.contacts.ContactsDatabaseHelper.Views;
42import com.android.providers.contacts.MetadataEntryParser.MetadataEntry;
43import com.android.providers.contacts.util.SelectionBuilder;
44import com.android.providers.contacts.util.UserUtils;
45import com.google.common.annotations.VisibleForTesting;
46
47import java.util.ArrayList;
48import java.util.Arrays;
49import java.util.Map;
50
51import static com.android.providers.contacts.ContactsProvider2.getLimit;
52import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
53
54/**
55 * Simple content provider to handle directing contact metadata specific calls.
56 */
57public 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.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
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
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
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
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
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