• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
FilteredNumberAsyncQueryHandler(Context context)52   public FilteredNumberAsyncQueryHandler(Context context) {
53     super(context.getContentResolver());
54     this.context = context;
55   }
56 
57   @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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
onInsertComplete(int token, Object cookie, Uri uri)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
onUpdateComplete(int token, Object cookie, int result)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
onDeleteComplete(int token, Object cookie, int result)85   protected void onDeleteComplete(int token, Object cookie, int result) {
86     if (cookie != null) {
87       ((Listener) cookie).onDeleteComplete(token, cookie, result);
88     }
89   }
90 
hasBlockedNumbers(final OnHasBlockedNumbersListener listener)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    */
isBlockedNumber( final OnCheckBlockedListener listener, @Nullable final String number, String countryIso)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
getBlockedIdSynchronous(@ullable String number, String countryIso)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
clearCache()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    */
getIsBlockedNumberSelection(boolean isE164Number)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 
blockNumber( final OnBlockNumberListener listener, String number, @Nullable String countryIso)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. */
blockNumber( final OnBlockNumberListener listener, @Nullable String normalizedNumber, String number, @Nullable String countryIso)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    */
blockNumber(final OnBlockNumberListener listener, ContentValues values)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    */
unblock(@ullable final OnUnblockNumberListener listener, Integer id)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    */
unblock(@ullable final OnUnblockNumberListener listener, final Uri uri)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      */
onCheckComplete(Integer id)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      */
onBlockComplete(Uri uri)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      */
onUnblockComplete(int rows, ContentValues values)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      */
onHasBlockedNumbers(boolean hasBlockedNumbers)420     void onHasBlockedNumbers(boolean hasBlockedNumbers);
421   }
422 
423   /** Methods for FilteredNumberAsyncQueryHandler result returns. */
424   private abstract static class Listener {
425 
onQueryComplete(int token, Object cookie, Cursor cursor)426     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {}
427 
onInsertComplete(int token, Object cookie, Uri uri)428     protected void onInsertComplete(int token, Object cookie, Uri uri) {}
429 
onUpdateComplete(int token, Object cookie, int result)430     protected void onUpdateComplete(int token, Object cookie, int result) {}
431 
onDeleteComplete(int token, Object cookie, int result)432     protected void onDeleteComplete(int token, Object cookie, int result) {}
433   }
434 }
435