1 /*
2  * Copyright (C) 2018 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.ContentProviderOperation;
20 import android.content.ContentProviderResult;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.OperationApplicationException;
25 import android.database.Cursor;
26 import android.os.RemoteException;
27 import android.provider.BlockedNumberContract;
28 import android.provider.BlockedNumberContract.BlockedNumbers;
29 import android.support.annotation.Nullable;
30 import android.telephony.PhoneNumberUtils;
31 import android.util.ArrayMap;
32 import com.android.dialer.common.concurrent.DialerExecutorComponent;
33 import com.android.dialer.common.database.Selection;
34 import com.google.common.collect.ImmutableCollection;
35 import com.google.common.collect.ImmutableMap;
36 import com.google.common.util.concurrent.ListenableFuture;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Map;
40 
41 /** Blocks and unblocks number. */
42 public final class Blocking {
43 
Blocking()44   private Blocking() {}
45 
46   /**
47    * Thrown when blocking cannot be performed because dialer is not the default dialer, or the
48    * current user is not a primary user.
49    *
50    * <p>Blocking is only allowed on the primary user (the first user added). Primary user cannot be
51    * easily checked because {@link
52    * android.provider.BlockedNumberContract#canCurrentUserBlockNumbers(Context)} is a slow IPC, and
53    * UserManager.isPrimaryUser() is a system API. Since secondary users are rare cases this class
54    * choose to ignore the check and let callers handle the failure later.
55    */
56   public static final class BlockingFailedException extends Exception {
BlockingFailedException(Throwable cause)57     BlockingFailedException(Throwable cause) {
58       super(cause);
59     }
60   }
61 
62   /**
63    * Block a list of numbers.
64    *
65    * @param countryIso the current location used to guess the country code of the number if not
66    *     available. If {@code null} and {@code number} does not have a country code, only the
67    *     original number will be blocked.
68    * @throws BlockingFailedException in the returned future if the operation failed.
69    */
block( Context context, ImmutableCollection<String> numbers, @Nullable String countryIso)70   public static ListenableFuture<Void> block(
71       Context context, ImmutableCollection<String> numbers, @Nullable String countryIso) {
72     return DialerExecutorComponent.get(context)
73         .backgroundExecutor()
74         .submit(
75             () -> {
76               ArrayList<ContentProviderOperation> operations = new ArrayList<>();
77               for (String number : numbers) {
78                 ContentValues values = new ContentValues();
79                 values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number);
80                 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
81                 if (e164Number != null) {
82                   values.put(BlockedNumbers.COLUMN_E164_NUMBER, e164Number);
83                 }
84                 operations.add(
85                     ContentProviderOperation.newInsert(BlockedNumbers.CONTENT_URI)
86                         .withValues(values)
87                         .build());
88               }
89               applyBatchOps(context.getContentResolver(), operations);
90               return null;
91             });
92   }
93 
94   /**
95    * Unblock a list of number.
96    *
97    * @param countryIso the current location used to guess the country code of the number if not
98    *     available. If {@code null} and {@code number} does not have a country code, only the
99    *     original number will be unblocked.
100    * @throws BlockingFailedException in the returned future if the operation failed.
101    */
unblock( Context context, ImmutableCollection<String> numbers, @Nullable String countryIso)102   public static ListenableFuture<Void> unblock(
103       Context context, ImmutableCollection<String> numbers, @Nullable String countryIso) {
104     return DialerExecutorComponent.get(context)
105         .backgroundExecutor()
106         .submit(
107             () -> {
108               ArrayList<ContentProviderOperation> operations = new ArrayList<>();
109               for (String number : numbers) {
110                 Selection selection =
111                     Selection.column(BlockedNumbers.COLUMN_ORIGINAL_NUMBER).is("=", number);
112                 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
113                 if (e164Number != null) {
114                   selection =
115                       selection
116                           .buildUpon()
117                           .or(
118                               Selection.column(BlockedNumbers.COLUMN_E164_NUMBER)
119                                   .is("=", e164Number))
120                           .build();
121                 }
122                 operations.add(
123                     ContentProviderOperation.newDelete(BlockedNumbers.CONTENT_URI)
124                         .withSelection(selection.getSelection(), selection.getSelectionArgs())
125                         .build());
126               }
127               applyBatchOps(context.getContentResolver(), operations);
128               return null;
129             });
130   }
131 
132   /**
133    * Get blocked numbers from a list of number.
134    *
135    * @param countryIso the current location used to guess the country code of the number if not
136    *     available. If {@code null} and {@code number} does not have a country code, only the
137    *     original number will be used to check blocked status.
138    * @throws BlockingFailedException in the returned future if the operation failed.
139    */
140   public static ListenableFuture<ImmutableMap<String, Boolean>> isBlocked(
141       Context context, ImmutableCollection<String> numbers, @Nullable String countryIso) {
142     return DialerExecutorComponent.get(context)
143         .backgroundExecutor()
144         .submit(
145             () -> {
146               Map<String, Boolean> blockedStatus = new ArrayMap<>();
147               List<String> e164Numbers = new ArrayList<>();
148 
149               for (String number : numbers) {
150                 // Initialize as unblocked
151                 blockedStatus.put(number, false);
152                 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
153                 if (e164Number != null) {
154                   e164Numbers.add(e164Number);
155                 }
156               }
157 
158               Selection selection =
159                   Selection.builder()
160                       .or(Selection.column(BlockedNumbers.COLUMN_ORIGINAL_NUMBER).in(numbers))
161                       .or(Selection.column(BlockedNumbers.COLUMN_E164_NUMBER).in(e164Numbers))
162                       .build();
163 
164               try (Cursor cursor =
165                   context
166                       .getContentResolver()
167                       .query(
168                           BlockedNumbers.CONTENT_URI,
169                           new String[] {BlockedNumbers.COLUMN_ORIGINAL_NUMBER},
170                           selection.getSelection(),
171                           selection.getSelectionArgs(),
172                           null)) {
173                 if (cursor == null) {
174                   return ImmutableMap.copyOf(blockedStatus);
175                 }
176                 while (cursor.moveToNext()) {
177                   // Update blocked status
178                   blockedStatus.put(cursor.getString(0), true);
179                 }
180               }
181               return ImmutableMap.copyOf(blockedStatus);
182             });
183   }
184 
185   private static ContentProviderResult[] applyBatchOps(
186       ContentResolver resolver, ArrayList<ContentProviderOperation> ops)
187       throws BlockingFailedException {
188     try {
189       return resolver.applyBatch(BlockedNumberContract.AUTHORITY, ops);
190     } catch (RemoteException | OperationApplicationException | SecurityException e) {
191       throw new BlockingFailedException(e);
192     }
193   }
194 }
195