/* * Copyright (C) 2017 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.dialer.phonelookup.database; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * {@link ContentProvider} for the PhoneLookupHistory. * *

Operations may run against the entire table using the URI: * *

 *   content://com.android.dialer.phonelookuphistory/PhoneLookupHistory
 * 
* *

Or against an individual row keyed by normalized number where the number is the last component * in the URI path, for example: * *

 *     content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+11234567890
 * 
*/ public class PhoneLookupHistoryContentProvider extends ContentProvider { /** * Can't use {@link UriMatcher} because it doesn't support empty values, and numbers can be empty. */ @Retention(RetentionPolicy.SOURCE) @IntDef({UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE, UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE}) private @interface UriType { // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory int PHONE_LOOKUP_HISTORY_TABLE_CODE = 1; // For operations against: // content://com.android.dialer.phonelookuphistory/PhoneLookupHistory?number=123 int PHONE_LOOKUP_HISTORY_TABLE_ID_CODE = 2; } private PhoneLookupHistoryDatabaseHelper databaseHelper; private final ThreadLocal applyingBatch = new ThreadLocal<>(); /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */ private boolean isApplyingBatch() { return applyingBatch.get() != null && applyingBatch.get(); } @Override public boolean onCreate() { databaseHelper = new PhoneLookupHistoryDatabaseHelper(getContext()); return true; } @Nullable @Override public Cursor query( @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(PhoneLookupHistory.TABLE); @UriType int uriType = uriType(uri); switch (uriType) { case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: queryBuilder.appendWhere( PhoneLookupHistory.NORMALIZED_NUMBER + "=" + DatabaseUtils.sqlEscapeString( Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)))); // fall through case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); if (cursor == null) { LogUtil.w("PhoneLookupHistoryContentProvider.query", "cursor was null"); return null; } cursor.setNotificationUri( getContext().getContentResolver(), PhoneLookupHistory.CONTENT_URI); return cursor; default: throw new IllegalArgumentException("Unknown uri: " + uri); } } @Nullable @Override public String getType(@NonNull Uri uri) { return PhoneLookupHistory.CONTENT_ITEM_TYPE; } @Nullable @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { // Javadoc states values is not nullable, even though it is annotated as such (a bug)! Assert.checkArgument(values != null); SQLiteDatabase database = databaseHelper.getWritableDatabase(); @UriType int uriType = uriType(uri); switch (uriType) { case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: Assert.checkArgument( values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER) != null, "You must specify a normalized number when inserting"); break; case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: String normalizedNumberFromUri = Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)); String normalizedNumberFromValues = values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER); Assert.checkArgument( normalizedNumberFromValues == null || normalizedNumberFromValues.equals(normalizedNumberFromUri), "NORMALIZED_NUMBER from values %s does not match normalized number from URI: %s", LogUtil.sanitizePhoneNumber(normalizedNumberFromValues), LogUtil.sanitizePhoneNumber(normalizedNumberFromUri)); if (normalizedNumberFromValues == null) { values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumberFromUri); } break; default: throw new IllegalArgumentException("Unknown uri: " + uri); } // Note: The id returned for a successful insert isn't actually part of the table. long id = database.insert(PhoneLookupHistory.TABLE, null, values); if (id < 0) { LogUtil.w( "PhoneLookupHistoryContentProvider.insert", "error inserting row with number: %s", LogUtil.sanitizePhoneNumber(values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER))); return null; } Uri insertedUri = PhoneLookupHistory.contentUriForNumber( values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER)); if (!isApplyingBatch()) { notifyChange(insertedUri); } return insertedUri; } @Override public int delete( @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); @UriType int uriType = uriType(uri); switch (uriType) { case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: break; case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: Assert.checkArgument(selection == null, "Do not specify selection when deleting by number"); Assert.checkArgument( selectionArgs == null, "Do not specify selection args when deleting by number"); String number = Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)); Assert.checkArgument(!TextUtils.isEmpty(number), "error parsing number from uri: %s", uri); selection = PhoneLookupHistory.NORMALIZED_NUMBER + "= ?"; selectionArgs = new String[] {number}; break; default: throw new IllegalArgumentException("Unknown uri: " + uri); } int rows = database.delete(PhoneLookupHistory.TABLE, selection, selectionArgs); if (rows == 0) { LogUtil.w("PhoneLookupHistoryContentProvider.delete", "no rows deleted"); return rows; } if (!isApplyingBatch()) { notifyChange(uri); } return rows; } /** * Note: If the normalized number is included as part of the URI (for example, * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update * operation will actually be a "replace" operation, inserting a new row if one does not already * exist. * *

All columns in an existing row will be replaced which means you must specify all required * columns in {@code values} when using this method. */ @Override public int update( @NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { // Javadoc states values is not nullable, even though it is annotated as such (a bug)! Assert.checkArgument(values != null); SQLiteDatabase database = databaseHelper.getWritableDatabase(); @UriType int uriType = uriType(uri); switch (uriType) { case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs); if (rows == 0) { LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated"); return rows; } if (!isApplyingBatch()) { notifyChange(uri); } return rows; case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: Assert.checkArgument( !values.containsKey(PhoneLookupHistory.NORMALIZED_NUMBER), "Do not specify number in values when updating by number"); Assert.checkArgument(selection == null, "Do not specify selection when updating by ID"); Assert.checkArgument( selectionArgs == null, "Do not specify selection args when updating by ID"); String normalizedNumber = Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM)); values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber); long result = database.replace(PhoneLookupHistory.TABLE, null, values); Assert.checkArgument(result != -1, "replacing PhoneLookupHistory row failed"); if (!isApplyingBatch()) { notifyChange(uri); } return 1; default: throw new IllegalArgumentException("Unknown uri: " + uri); } } /** * {@inheritDoc} * *

Note: When applyBatch is used with the PhoneLookupHistory, only a single notification for * the content URI is generated, not individual notifications for each affected URI. */ @NonNull @Override public ContentProviderResult[] applyBatch(@NonNull ArrayList operations) throws OperationApplicationException { ContentProviderResult[] results = new ContentProviderResult[operations.size()]; if (operations.isEmpty()) { return results; } SQLiteDatabase database = databaseHelper.getWritableDatabase(); try { applyingBatch.set(true); database.beginTransaction(); for (int i = 0; i < operations.size(); i++) { ContentProviderOperation operation = operations.get(i); @UriType int uriType = uriType(operation.getUri()); switch (uriType) { case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE: case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: ContentProviderResult result = operation.apply(this, results, i); if (operation.isInsert()) { if (result.uri == null) { throw new OperationApplicationException("error inserting row"); } } else if (result.count == 0) { throw new OperationApplicationException("error applying operation"); } results[i] = result; break; default: throw new IllegalArgumentException("Unknown uri: " + operation.getUri()); } } database.setTransactionSuccessful(); } finally { applyingBatch.set(false); database.endTransaction(); } notifyChange(PhoneLookupHistory.CONTENT_URI); return results; } private void notifyChange(Uri uri) { getContext().getContentResolver().notifyChange(uri, null); } @UriType private int uriType(Uri uri) { Assert.checkArgument(uri.getAuthority().equals(PhoneLookupHistoryContract.AUTHORITY)); List pathSegments = uri.getPathSegments(); Assert.checkArgument(pathSegments.size() == 1); Assert.checkArgument(pathSegments.get(0).equals(PhoneLookupHistory.TABLE)); return uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM) == null ? UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE : UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE; } }