Home | History | Annotate | Download | only in database
      1 /*
      2  * Copyright (C) 2017 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 
     17 package com.android.dialer.phonelookup.database;
     18 
     19 import android.content.ContentProvider;
     20 import android.content.ContentProviderOperation;
     21 import android.content.ContentProviderResult;
     22 import android.content.ContentValues;
     23 import android.content.OperationApplicationException;
     24 import android.content.UriMatcher;
     25 import android.database.Cursor;
     26 import android.database.DatabaseUtils;
     27 import android.database.sqlite.SQLiteDatabase;
     28 import android.database.sqlite.SQLiteQueryBuilder;
     29 import android.net.Uri;
     30 import android.support.annotation.IntDef;
     31 import android.support.annotation.NonNull;
     32 import android.support.annotation.Nullable;
     33 import com.android.dialer.common.Assert;
     34 import com.android.dialer.common.LogUtil;
     35 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract;
     36 import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory;
     37 import java.lang.annotation.Retention;
     38 import java.lang.annotation.RetentionPolicy;
     39 import java.util.ArrayList;
     40 import java.util.List;
     41 
     42 /**
     43  * {@link ContentProvider} for the PhoneLookupHistory.
     44  *
     45  * <p>Operations may run against the entire table using the URI:
     46  *
     47  * <pre>
     48  *   content://com.android.dialer.phonelookuphistory/PhoneLookupHistory
     49  * </pre>
     50  *
     51  * <p>Or against an individual row keyed by normalized number where the number is the last component
     52  * in the URI path, for example:
     53  *
     54  * <pre>
     55  *     content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+11234567890
     56  * </pre>
     57  */
     58 public class PhoneLookupHistoryContentProvider extends ContentProvider {
     59 
     60   /**
     61    * Can't use {@link UriMatcher} because it doesn't support empty values, and numbers can be empty.
     62    */
     63   @Retention(RetentionPolicy.SOURCE)
     64   @IntDef({UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE, UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE})
     65   private @interface UriType {
     66     // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory
     67     int PHONE_LOOKUP_HISTORY_TABLE_CODE = 1;
     68     // For operations against:
     69     // content://com.android.dialer.phonelookuphistory/PhoneLookupHistory?number=123
     70     int PHONE_LOOKUP_HISTORY_TABLE_ID_CODE = 2;
     71   }
     72 
     73   private PhoneLookupHistoryDatabaseHelper databaseHelper;
     74 
     75   private final ThreadLocal<Boolean> applyingBatch = new ThreadLocal<>();
     76 
     77   /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */
     78   private boolean isApplyingBatch() {
     79     return applyingBatch.get() != null && applyingBatch.get();
     80   }
     81 
     82   @Override
     83   public boolean onCreate() {
     84     databaseHelper = new PhoneLookupHistoryDatabaseHelper(getContext());
     85     return true;
     86   }
     87 
     88   @Nullable
     89   @Override
     90   public Cursor query(
     91       @NonNull Uri uri,
     92       @Nullable String[] projection,
     93       @Nullable String selection,
     94       @Nullable String[] selectionArgs,
     95       @Nullable String sortOrder) {
     96     SQLiteDatabase db = databaseHelper.getReadableDatabase();
     97     SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
     98     queryBuilder.setTables(PhoneLookupHistory.TABLE);
     99     @UriType int uriType = uriType(uri);
    100     switch (uriType) {
    101       case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE:
    102         queryBuilder.appendWhere(
    103             PhoneLookupHistory.NORMALIZED_NUMBER
    104                 + "="
    105                 + DatabaseUtils.sqlEscapeString(
    106                     Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM))));
    107         // fall through
    108       case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE:
    109         Cursor cursor =
    110             queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
    111         if (cursor == null) {
    112           LogUtil.w("PhoneLookupHistoryContentProvider.query", "cursor was null");
    113           return null;
    114         }
    115         cursor.setNotificationUri(
    116             getContext().getContentResolver(), PhoneLookupHistory.CONTENT_URI);
    117         return cursor;
    118       default:
    119         throw new IllegalArgumentException("Unknown uri: " + uri);
    120     }
    121   }
    122 
    123   @Nullable
    124   @Override
    125   public String getType(@NonNull Uri uri) {
    126     return PhoneLookupHistory.CONTENT_ITEM_TYPE;
    127   }
    128 
    129   @Nullable
    130   @Override
    131   public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    132     // Javadoc states values is not nullable, even though it is annotated as such (a bug)!
    133     Assert.checkArgument(values != null);
    134 
    135     SQLiteDatabase database = databaseHelper.getWritableDatabase();
    136     @UriType int uriType = uriType(uri);
    137     switch (uriType) {
    138       case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE:
    139         Assert.checkArgument(
    140             values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER) != null,
    141             "You must specify a normalized number when inserting");
    142         break;
    143       case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE:
    144         String normalizedNumberFromUri =
    145             Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM));
    146         String normalizedNumberFromValues =
    147             values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER);
    148         Assert.checkArgument(
    149             normalizedNumberFromValues == null
    150                 || normalizedNumberFromValues.equals(normalizedNumberFromUri),
    151             "NORMALIZED_NUMBER from values %s does not match normalized number from URI: %s",
    152             LogUtil.sanitizePhoneNumber(normalizedNumberFromValues),
    153             LogUtil.sanitizePhoneNumber(normalizedNumberFromUri));
    154         if (normalizedNumberFromValues == null) {
    155           values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumberFromUri);
    156         }
    157         break;
    158       default:
    159         throw new IllegalArgumentException("Unknown uri: " + uri);
    160     }
    161     // Note: The id returned for a successful insert isn't actually part of the table.
    162     long id = database.insert(PhoneLookupHistory.TABLE, null, values);
    163     if (id < 0) {
    164       LogUtil.w(
    165           "PhoneLookupHistoryContentProvider.insert",
    166           "error inserting row with number: %s",
    167           LogUtil.sanitizePhoneNumber(values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER)));
    168       return null;
    169     }
    170     Uri insertedUri =
    171         PhoneLookupHistory.contentUriForNumber(
    172             values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER));
    173     if (!isApplyingBatch()) {
    174       notifyChange(insertedUri);
    175     }
    176     return insertedUri;
    177   }
    178 
    179   @Override
    180   public int delete(
    181       @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
    182     SQLiteDatabase database = databaseHelper.getWritableDatabase();
    183     @UriType int uriType = uriType(uri);
    184     switch (uriType) {
    185       case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE:
    186         break;
    187       case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE:
    188         Assert.checkArgument(selection == null, "Do not specify selection when deleting by number");
    189         Assert.checkArgument(
    190             selectionArgs == null, "Do not specify selection args when deleting by number");
    191         String number = Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM));
    192         Assert.checkArgument(
    193             number != null, "error parsing number from uri: %s", LogUtil.sanitizePii(uri));
    194         selection = PhoneLookupHistory.NORMALIZED_NUMBER + "= ?";
    195         selectionArgs = new String[] {number};
    196         break;
    197       default:
    198         throw new IllegalArgumentException("Unknown uri: " + uri);
    199     }
    200     int rows = database.delete(PhoneLookupHistory.TABLE, selection, selectionArgs);
    201     if (rows == 0) {
    202       LogUtil.w("PhoneLookupHistoryContentProvider.delete", "no rows deleted");
    203       return rows;
    204     }
    205     if (!isApplyingBatch()) {
    206       notifyChange(uri);
    207     }
    208     return rows;
    209   }
    210 
    211   /**
    212    * Note: If the normalized number is included as part of the URI (for example,
    213    * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update
    214    * operation will actually be a "replace" operation, inserting a new row if one does not already
    215    * exist.
    216    *
    217    * <p>All columns in an existing row will be replaced which means you must specify all required
    218    * columns in {@code values} when using this method.
    219    */
    220   @Override
    221   public int update(
    222       @NonNull Uri uri,
    223       @Nullable ContentValues values,
    224       @Nullable String selection,
    225       @Nullable String[] selectionArgs) {
    226     // Javadoc states values is not nullable, even though it is annotated as such (a bug)!
    227     Assert.checkArgument(values != null);
    228 
    229     SQLiteDatabase database = databaseHelper.getWritableDatabase();
    230     @UriType int uriType = uriType(uri);
    231     switch (uriType) {
    232       case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE:
    233         int rows = database.update(PhoneLookupHistory.TABLE, values, selection, selectionArgs);
    234         if (rows == 0) {
    235           LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated");
    236           return rows;
    237         }
    238         if (!isApplyingBatch()) {
    239           notifyChange(uri);
    240         }
    241         return rows;
    242       case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE:
    243         Assert.checkArgument(
    244             !values.containsKey(PhoneLookupHistory.NORMALIZED_NUMBER),
    245             "Do not specify number in values when updating by number");
    246         Assert.checkArgument(selection == null, "Do not specify selection when updating by ID");
    247         Assert.checkArgument(
    248             selectionArgs == null, "Do not specify selection args when updating by ID");
    249 
    250         String normalizedNumber =
    251             Uri.decode(uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM));
    252         values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber);
    253         long result = database.replace(PhoneLookupHistory.TABLE, null, values);
    254         Assert.checkArgument(result != -1, "replacing PhoneLookupHistory row failed");
    255         if (!isApplyingBatch()) {
    256           notifyChange(uri);
    257         }
    258         return 1;
    259       default:
    260         throw new IllegalArgumentException("Unknown uri: " + uri);
    261     }
    262   }
    263 
    264   /**
    265    * {@inheritDoc}
    266    *
    267    * <p>Note: When applyBatch is used with the PhoneLookupHistory, only a single notification for
    268    * the content URI is generated, not individual notifications for each affected URI.
    269    */
    270   @NonNull
    271   @Override
    272   public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations)
    273       throws OperationApplicationException {
    274     ContentProviderResult[] results = new ContentProviderResult[operations.size()];
    275     if (operations.isEmpty()) {
    276       return results;
    277     }
    278 
    279     SQLiteDatabase database = databaseHelper.getWritableDatabase();
    280     try {
    281       applyingBatch.set(true);
    282       database.beginTransaction();
    283       for (int i = 0; i < operations.size(); i++) {
    284         ContentProviderOperation operation = operations.get(i);
    285         @UriType int uriType = uriType(operation.getUri());
    286         switch (uriType) {
    287           case UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE:
    288           case UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE:
    289             ContentProviderResult result = operation.apply(this, results, i);
    290             if (operation.isInsert()) {
    291               if (result.uri == null) {
    292                 throw new OperationApplicationException("error inserting row");
    293               }
    294             } else if (result.count == 0) {
    295               throw new OperationApplicationException("error applying operation");
    296             }
    297             results[i] = result;
    298             break;
    299           default:
    300             throw new IllegalArgumentException("Unknown uri: " + operation.getUri());
    301         }
    302       }
    303       database.setTransactionSuccessful();
    304     } finally {
    305       applyingBatch.set(false);
    306       database.endTransaction();
    307     }
    308     notifyChange(PhoneLookupHistory.CONTENT_URI);
    309     return results;
    310   }
    311 
    312   private void notifyChange(Uri uri) {
    313     getContext().getContentResolver().notifyChange(uri, null);
    314   }
    315 
    316   @UriType
    317   private int uriType(Uri uri) {
    318     Assert.checkArgument(uri.getAuthority().equals(PhoneLookupHistoryContract.AUTHORITY));
    319     List<String> pathSegments = uri.getPathSegments();
    320     Assert.checkArgument(pathSegments.size() == 1);
    321     Assert.checkArgument(pathSegments.get(0).equals(PhoneLookupHistory.TABLE));
    322     return uri.getQueryParameter(PhoneLookupHistory.NUMBER_QUERY_PARAM) == null
    323         ? UriType.PHONE_LOOKUP_HISTORY_TABLE_CODE
    324         : UriType.PHONE_LOOKUP_HISTORY_TABLE_ID_CODE;
    325   }
    326 }
    327