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