Home | History | Annotate | Download | only in blocking
      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 
     17 package com.android.dialer.blocking;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.AsyncQueryHandler;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.database.Cursor;
     24 import android.database.DatabaseUtils;
     25 import android.database.sqlite.SQLiteDatabaseCorruptException;
     26 import android.net.Uri;
     27 import android.os.Build.VERSION_CODES;
     28 import android.support.annotation.Nullable;
     29 import android.support.annotation.VisibleForTesting;
     30 import android.support.v4.os.UserManagerCompat;
     31 import android.telephony.PhoneNumberUtils;
     32 import android.text.TextUtils;
     33 import com.android.dialer.common.Assert;
     34 import com.android.dialer.common.LogUtil;
     35 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
     36 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
     37 import java.util.Map;
     38 import java.util.concurrent.ConcurrentHashMap;
     39 
     40 public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler {
     41 
     42   public static final int INVALID_ID = -1;
     43   // Id used to replace null for blocked id since ConcurrentHashMap doesn't allow null key/value.
     44   @VisibleForTesting static final int BLOCKED_NUMBER_CACHE_NULL_ID = -1;
     45 
     46   @VisibleForTesting
     47   static final Map<String, Integer> blockedNumberCache = new ConcurrentHashMap<>();
     48 
     49   private static final int NO_TOKEN = 0;
     50   private final Context context;
     51 
     52   public FilteredNumberAsyncQueryHandler(Context context) {
     53     super(context.getContentResolver());
     54     this.context = context;
     55   }
     56 
     57   @Override
     58   protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
     59     try {
     60       if (cookie != null) {
     61         ((Listener) cookie).onQueryComplete(token, cookie, cursor);
     62       }
     63     } finally {
     64       if (cursor != null) {
     65         cursor.close();
     66       }
     67     }
     68   }
     69 
     70   @Override
     71   protected void onInsertComplete(int token, Object cookie, Uri uri) {
     72     if (cookie != null) {
     73       ((Listener) cookie).onInsertComplete(token, cookie, uri);
     74     }
     75   }
     76 
     77   @Override
     78   protected void onUpdateComplete(int token, Object cookie, int result) {
     79     if (cookie != null) {
     80       ((Listener) cookie).onUpdateComplete(token, cookie, result);
     81     }
     82   }
     83 
     84   @Override
     85   protected void onDeleteComplete(int token, Object cookie, int result) {
     86     if (cookie != null) {
     87       ((Listener) cookie).onDeleteComplete(token, cookie, result);
     88     }
     89   }
     90 
     91   void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) {
     92     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
     93       listener.onHasBlockedNumbers(false);
     94       return;
     95     }
     96     startQuery(
     97         NO_TOKEN,
     98         new Listener() {
     99           @Override
    100           protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    101             listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0);
    102           }
    103         },
    104         FilteredNumberCompat.getContentUri(context, null),
    105         new String[] {FilteredNumberCompat.getIdColumnName(context)},
    106         FilteredNumberCompat.useNewFiltering(context)
    107             ? null
    108             : FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER,
    109         null,
    110         null);
    111   }
    112 
    113   /**
    114    * Checks if the given number is blocked, calling the given {@link OnCheckBlockedListener} with
    115    * the id for the blocked number, {@link #INVALID_ID}, or {@code null} based on the result of the
    116    * check.
    117    */
    118   public void isBlockedNumber(
    119       final OnCheckBlockedListener listener, @Nullable final String number, String countryIso) {
    120     if (number == null) {
    121       listener.onCheckComplete(INVALID_ID);
    122       return;
    123     }
    124     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
    125       listener.onCheckComplete(null);
    126       return;
    127     }
    128     Integer cachedId = blockedNumberCache.get(number);
    129     if (cachedId != null) {
    130       if (listener == null) {
    131         return;
    132       }
    133       if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
    134         cachedId = null;
    135       }
    136       listener.onCheckComplete(cachedId);
    137       return;
    138     }
    139 
    140     if (!UserManagerCompat.isUserUnlocked(context)) {
    141       LogUtil.i(
    142           "FilteredNumberAsyncQueryHandler.isBlockedNumber",
    143           "Device locked in FBE mode, cannot access blocked number database");
    144       listener.onCheckComplete(INVALID_ID);
    145       return;
    146     }
    147 
    148     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    149     String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
    150     if (TextUtils.isEmpty(formattedNumber)) {
    151       listener.onCheckComplete(INVALID_ID);
    152       blockedNumberCache.put(number, INVALID_ID);
    153       return;
    154     }
    155 
    156     startQuery(
    157         NO_TOKEN,
    158         new Listener() {
    159           @Override
    160           protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    161             /*
    162              * In the frameworking blocking, numbers can be blocked in both e164 format
    163              * and not, resulting in multiple rows being returned for this query. For
    164              * example, both '16502530000' and '6502530000' can exist at the same time
    165              * and will be returned by this query.
    166              */
    167             if (cursor == null || cursor.getCount() == 0) {
    168               blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
    169               listener.onCheckComplete(null);
    170               return;
    171             }
    172             cursor.moveToFirst();
    173             // New filtering doesn't have a concept of type
    174             if (!FilteredNumberCompat.useNewFiltering(context)
    175                 && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
    176                     != FilteredNumberTypes.BLOCKED_NUMBER) {
    177               blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
    178               listener.onCheckComplete(null);
    179               return;
    180             }
    181             Integer blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
    182             blockedNumberCache.put(number, blockedId);
    183             listener.onCheckComplete(blockedId);
    184           }
    185         },
    186         FilteredNumberCompat.getContentUri(context, null),
    187         FilteredNumberCompat.filter(
    188             new String[] {
    189               FilteredNumberCompat.getIdColumnName(context),
    190               FilteredNumberCompat.getTypeColumnName(context)
    191             }),
    192         getIsBlockedNumberSelection(e164Number != null) + " = ?",
    193         new String[] {formattedNumber},
    194         null);
    195   }
    196 
    197   /**
    198    * Synchronously check if this number has been blocked.
    199    *
    200    * @return blocked id.
    201    */
    202   @TargetApi(VERSION_CODES.M)
    203   @Nullable
    204   public Integer getBlockedIdSynchronous(@Nullable String number, String countryIso) {
    205     Assert.isWorkerThread();
    206     if (number == null) {
    207       return null;
    208     }
    209     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
    210       return null;
    211     }
    212     Integer cachedId = blockedNumberCache.get(number);
    213     if (cachedId != null) {
    214       if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
    215         cachedId = null;
    216       }
    217       return cachedId;
    218     }
    219 
    220     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    221     String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
    222     if (TextUtils.isEmpty(formattedNumber)) {
    223       return null;
    224     }
    225 
    226     try (Cursor cursor =
    227         context
    228             .getContentResolver()
    229             .query(
    230                 FilteredNumberCompat.getContentUri(context, null),
    231                 FilteredNumberCompat.filter(
    232                     new String[] {
    233                       FilteredNumberCompat.getIdColumnName(context),
    234                       FilteredNumberCompat.getTypeColumnName(context)
    235                     }),
    236                 getIsBlockedNumberSelection(e164Number != null) + " = ?",
    237                 new String[] {formattedNumber},
    238                 null)) {
    239       /*
    240        * In the frameworking blocking, numbers can be blocked in both e164 format
    241        * and not, resulting in multiple rows being returned for this query. For
    242        * example, both '16502530000' and '6502530000' can exist at the same time
    243        * and will be returned by this query.
    244        */
    245       if (cursor == null || cursor.getCount() == 0) {
    246         blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
    247         return null;
    248       }
    249       cursor.moveToFirst();
    250       int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
    251       blockedNumberCache.put(number, blockedId);
    252       return blockedId;
    253     } catch (SecurityException e) {
    254       LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronous", null, e);
    255       return null;
    256     }
    257   }
    258 
    259   @VisibleForTesting
    260   public void clearCache() {
    261     blockedNumberCache.clear();
    262   }
    263 
    264   /*
    265    * TODO: b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a
    266    * temporary workaround, determine which column of the database to query based on whether the
    267    * number is e164 or not.
    268    */
    269   private String getIsBlockedNumberSelection(boolean isE164Number) {
    270     if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) {
    271       return FilteredNumberCompat.getOriginalNumberColumnName(context);
    272     }
    273     return FilteredNumberCompat.getE164NumberColumnName(context);
    274   }
    275 
    276   public void blockNumber(
    277       final OnBlockNumberListener listener, String number, @Nullable String countryIso) {
    278     blockNumber(listener, null, number, countryIso);
    279   }
    280 
    281   /** Add a number manually blocked by the user. */
    282   public void blockNumber(
    283       final OnBlockNumberListener listener,
    284       @Nullable String normalizedNumber,
    285       String number,
    286       @Nullable String countryIso) {
    287     blockNumber(
    288         listener,
    289         FilteredNumberCompat.newBlockNumberContentValues(
    290             context, number, normalizedNumber, countryIso));
    291   }
    292 
    293   /**
    294    * Block a number with specified ContentValues. Can be manually added or a restored row from
    295    * performing the 'undo' action after unblocking.
    296    */
    297   public void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
    298     blockedNumberCache.clear();
    299     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
    300       listener.onBlockComplete(null);
    301       return;
    302     }
    303     startInsert(
    304         NO_TOKEN,
    305         new Listener() {
    306           @Override
    307           public void onInsertComplete(int token, Object cookie, Uri uri) {
    308             if (listener != null) {
    309               listener.onBlockComplete(uri);
    310             }
    311           }
    312         },
    313         FilteredNumberCompat.getContentUri(context, null),
    314         values);
    315   }
    316 
    317   /**
    318    * Unblocks the number with the given id.
    319    *
    320    * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
    321    *     unblocked.
    322    * @param id The id of the number to unblock.
    323    */
    324   public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) {
    325     if (id == null) {
    326       throw new IllegalArgumentException("Null id passed into unblock");
    327     }
    328     unblock(listener, FilteredNumberCompat.getContentUri(context, id));
    329   }
    330 
    331   /**
    332    * Removes row from database.
    333    *
    334    * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
    335    *     unblocked.
    336    * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}.
    337    */
    338   public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) {
    339     blockedNumberCache.clear();
    340     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
    341       if (listener != null) {
    342         listener.onUnblockComplete(0, null);
    343       }
    344       return;
    345     }
    346     startQuery(
    347         NO_TOKEN,
    348         new Listener() {
    349           @Override
    350           public void onQueryComplete(int token, Object cookie, Cursor cursor) {
    351             int rowsReturned = cursor == null ? 0 : cursor.getCount();
    352             if (rowsReturned != 1) {
    353               throw new SQLiteDatabaseCorruptException(
    354                   "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected.");
    355             }
    356             cursor.moveToFirst();
    357             final ContentValues values = new ContentValues();
    358             DatabaseUtils.cursorRowToContentValues(cursor, values);
    359             values.remove(FilteredNumberCompat.getIdColumnName(context));
    360 
    361             startDelete(
    362                 NO_TOKEN,
    363                 new Listener() {
    364                   @Override
    365                   public void onDeleteComplete(int token, Object cookie, int result) {
    366                     if (listener != null) {
    367                       listener.onUnblockComplete(result, values);
    368                     }
    369                   }
    370                 },
    371                 uri,
    372                 null,
    373                 null);
    374           }
    375         },
    376         uri,
    377         null,
    378         null,
    379         null,
    380         null);
    381   }
    382 
    383   public interface OnCheckBlockedListener {
    384 
    385     /**
    386      * Invoked after querying if a number is blocked.
    387      *
    388      * @param id The ID of the row if blocked, null otherwise.
    389      */
    390     void onCheckComplete(Integer id);
    391   }
    392 
    393   public interface OnBlockNumberListener {
    394 
    395     /**
    396      * Invoked after inserting a blocked number.
    397      *
    398      * @param uri The uri of the newly created row.
    399      */
    400     void onBlockComplete(Uri uri);
    401   }
    402 
    403   public interface OnUnblockNumberListener {
    404 
    405     /**
    406      * Invoked after removing a blocked number
    407      *
    408      * @param rows The number of rows affected (expected value 1).
    409      * @param values The deleted data (used for restoration).
    410      */
    411     void onUnblockComplete(int rows, ContentValues values);
    412   }
    413 
    414   interface OnHasBlockedNumbersListener {
    415 
    416     /**
    417      * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false}
    418      *     otherwise.
    419      */
    420     void onHasBlockedNumbers(boolean hasBlockedNumbers);
    421   }
    422 
    423   /** Methods for FilteredNumberAsyncQueryHandler result returns. */
    424   private abstract static class Listener {
    425 
    426     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {}
    427 
    428     protected void onInsertComplete(int token, Object cookie, Uri uri) {}
    429 
    430     protected void onUpdateComplete(int token, Object cookie, int result) {}
    431 
    432     protected void onDeleteComplete(int token, Object cookie, int result) {}
    433   }
    434 }
    435