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.content.AsyncQueryHandler;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.database.DatabaseUtils;
24 import android.database.sqlite.SQLiteDatabaseCorruptException;
25 import android.net.Uri;
26 import android.support.annotation.Nullable;
27 import android.support.annotation.VisibleForTesting;
28 import android.support.v4.os.UserManagerCompat;
29 import android.telephony.PhoneNumberUtils;
30 import android.text.TextUtils;
31 import com.android.dialer.common.Assert;
32 import com.android.dialer.common.LogUtil;
33 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
34 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
35 import java.util.Map;
36 import java.util.concurrent.ConcurrentHashMap;
37 
38 /** TODO(calderwoodra): documentation */
39 @Deprecated
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   @Nullable
getBlockedIdSynchronous(@ullable String number, String countryIso)203   public Integer getBlockedIdSynchronous(@Nullable String number, String countryIso) {
204     Assert.isWorkerThread();
205     if (number == null) {
206       return null;
207     }
208     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
209       return null;
210     }
211     Integer cachedId = blockedNumberCache.get(number);
212     if (cachedId != null) {
213       if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
214         cachedId = null;
215       }
216       return cachedId;
217     }
218 
219     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
220     String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
221     if (TextUtils.isEmpty(formattedNumber)) {
222       return null;
223     }
224 
225     try (Cursor cursor =
226         context
227             .getContentResolver()
228             .query(
229                 FilteredNumberCompat.getContentUri(context, null),
230                 FilteredNumberCompat.filter(
231                     new String[] {
232                       FilteredNumberCompat.getIdColumnName(context),
233                       FilteredNumberCompat.getTypeColumnName(context)
234                     }),
235                 getIsBlockedNumberSelection(e164Number != null) + " = ?",
236                 new String[] {formattedNumber},
237                 null)) {
238       /*
239        * In the frameworking blocking, numbers can be blocked in both e164 format
240        * and not, resulting in multiple rows being returned for this query. For
241        * example, both '16502530000' and '6502530000' can exist at the same time
242        * and will be returned by this query.
243        */
244       if (cursor == null || cursor.getCount() == 0) {
245         blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
246         return null;
247       }
248       cursor.moveToFirst();
249       int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
250       blockedNumberCache.put(number, blockedId);
251       return blockedId;
252     } catch (SecurityException e) {
253       LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronous", null, e);
254       return null;
255     }
256   }
257 
258   @VisibleForTesting
clearCache()259   public void clearCache() {
260     blockedNumberCache.clear();
261   }
262 
263   /*
264    * TODO(maxwelb): a bug, non-e164 numbers can be blocked in the new form of blocking. As a
265    * temporary workaround, determine which column of the database to query based on whether the
266    * number is e164 or not.
267    */
getIsBlockedNumberSelection(boolean isE164Number)268   private String getIsBlockedNumberSelection(boolean isE164Number) {
269     if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) {
270       return FilteredNumberCompat.getOriginalNumberColumnName(context);
271     }
272     return FilteredNumberCompat.getE164NumberColumnName(context);
273   }
274 
blockNumber( final OnBlockNumberListener listener, String number, @Nullable String countryIso)275   public void blockNumber(
276       final OnBlockNumberListener listener, String number, @Nullable String countryIso) {
277     blockNumber(listener, null, number, countryIso);
278   }
279 
280   /** Add a number manually blocked by the user. */
blockNumber( final OnBlockNumberListener listener, @Nullable String normalizedNumber, String number, @Nullable String countryIso)281   public void blockNumber(
282       final OnBlockNumberListener listener,
283       @Nullable String normalizedNumber,
284       String number,
285       @Nullable String countryIso) {
286     blockNumber(
287         listener,
288         FilteredNumberCompat.newBlockNumberContentValues(
289             context, number, normalizedNumber, countryIso));
290   }
291 
292   /**
293    * Block a number with specified ContentValues. Can be manually added or a restored row from
294    * performing the 'undo' action after unblocking.
295    */
blockNumber(final OnBlockNumberListener listener, ContentValues values)296   public void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
297     blockedNumberCache.clear();
298     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
299       if (listener != null) {
300         listener.onBlockComplete(null);
301       }
302       return;
303     }
304     startInsert(
305         NO_TOKEN,
306         new Listener() {
307           @Override
308           public void onInsertComplete(int token, Object cookie, Uri uri) {
309             if (listener != null) {
310               listener.onBlockComplete(uri);
311             }
312           }
313         },
314         FilteredNumberCompat.getContentUri(context, null),
315         values);
316   }
317 
318   /**
319    * Unblocks the number with the given id.
320    *
321    * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
322    *     unblocked.
323    * @param id The id of the number to unblock.
324    */
unblock(@ullable final OnUnblockNumberListener listener, Integer id)325   public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) {
326     if (id == null) {
327       throw new IllegalArgumentException("Null id passed into unblock");
328     }
329     unblock(listener, FilteredNumberCompat.getContentUri(context, id));
330   }
331 
332   /**
333    * Removes row from database.
334    *
335    * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
336    *     unblocked.
337    * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}.
338    */
unblock(@ullable final OnUnblockNumberListener listener, final Uri uri)339   public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) {
340     blockedNumberCache.clear();
341     if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
342       if (listener != null) {
343         listener.onUnblockComplete(0, null);
344       }
345       return;
346     }
347     startQuery(
348         NO_TOKEN,
349         new Listener() {
350           @Override
351           public void onQueryComplete(int token, Object cookie, Cursor cursor) {
352             int rowsReturned = cursor == null ? 0 : cursor.getCount();
353             if (rowsReturned != 1) {
354               throw new SQLiteDatabaseCorruptException(
355                   "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected.");
356             }
357             cursor.moveToFirst();
358             final ContentValues values = new ContentValues();
359             DatabaseUtils.cursorRowToContentValues(cursor, values);
360             values.remove(FilteredNumberCompat.getIdColumnName(context));
361 
362             startDelete(
363                 NO_TOKEN,
364                 new Listener() {
365                   @Override
366                   public void onDeleteComplete(int token, Object cookie, int result) {
367                     if (listener != null) {
368                       listener.onUnblockComplete(result, values);
369                     }
370                   }
371                 },
372                 uri,
373                 null,
374                 null);
375           }
376         },
377         uri,
378         null,
379         null,
380         null,
381         null);
382   }
383 
384   /** TODO(calderwoodra): documentation */
385   public interface OnCheckBlockedListener {
386 
387     /**
388      * Invoked after querying if a number is blocked.
389      *
390      * @param id The ID of the row if blocked, null otherwise.
391      */
onCheckComplete(Integer id)392     void onCheckComplete(Integer id);
393   }
394 
395   /** TODO(calderwoodra): documentation */
396   public interface OnBlockNumberListener {
397 
398     /**
399      * Invoked after inserting a blocked number.
400      *
401      * @param uri The uri of the newly created row.
402      */
onBlockComplete(Uri uri)403     void onBlockComplete(Uri uri);
404   }
405 
406   /** TODO(calderwoodra): documentation */
407   public interface OnUnblockNumberListener {
408 
409     /**
410      * Invoked after removing a blocked number
411      *
412      * @param rows The number of rows affected (expected value 1).
413      * @param values The deleted data (used for restoration).
414      */
onUnblockComplete(int rows, ContentValues values)415     void onUnblockComplete(int rows, ContentValues values);
416   }
417 
418   /** TODO(calderwoodra): documentation */
419   interface OnHasBlockedNumbersListener {
420 
421     /**
422      * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false}
423      *     otherwise.
424      */
onHasBlockedNumbers(boolean hasBlockedNumbers)425     void onHasBlockedNumbers(boolean hasBlockedNumbers);
426   }
427 
428   /** Methods for FilteredNumberAsyncQueryHandler result returns. */
429   private abstract static class Listener {
430 
onQueryComplete(int token, Object cookie, Cursor cursor)431     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {}
432 
onInsertComplete(int token, Object cookie, Uri uri)433     protected void onInsertComplete(int token, Object cookie, Uri uri) {}
434 
onUpdateComplete(int token, Object cookie, int result)435     protected void onUpdateComplete(int token, Object cookie, int result) {}
436 
onDeleteComplete(int token, Object cookie, int result)437     protected void onDeleteComplete(int token, Object cookie, int result) {}
438   }
439 }
440