1 /*
2  * Copyright (C) 2016 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.compat;
18 
19 import com.google.common.base.MoreObjects;
20 import com.google.common.base.Preconditions;
21 
22 import android.app.FragmentManager;
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.net.Uri;
29 import android.os.UserManager;
30 import android.preference.PreferenceManager;
31 import android.support.annotation.Nullable;
32 import android.telecom.TelecomManager;
33 import android.telephony.PhoneNumberUtils;
34 import android.util.Log;
35 
36 import com.android.contacts.common.compat.CompatUtils;
37 import com.android.contacts.common.compat.TelecomManagerUtil;
38 import com.android.contacts.common.testing.NeededForTesting;
39 import com.android.dialer.DialerApplication;
40 import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
41 import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
42 import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
43 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
44 import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
45 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
46 import com.android.dialer.filterednumber.BlockNumberDialogFragment;
47 import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback;
48 import com.android.dialer.filterednumber.BlockedNumbersMigrator;
49 import com.android.dialer.filterednumber.BlockedNumbersSettingsActivity;
50 import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment;
51 import com.android.dialerbind.ObjectFactory;
52 
53 import java.util.ArrayList;
54 import java.util.List;
55 
56 /**
57  * Compatibility class to encapsulate logic to switch between call blocking using
58  * {@link com.android.dialer.database.FilteredNumberContract} and using
59  * {@link android.provider.BlockedNumberContract}. This class should be used rather than explicitly
60  * referencing columns from either contract class in situations where both blocking solutions may be
61  * used.
62  */
63 public class FilteredNumberCompat {
64 
65     private static final String TAG = "FilteredNumberCompat";
66 
67     protected static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking";
68 
69     private static Boolean isEnabledForTest;
70 
71     private static Context contextForTest;
72 
73     /**
74      * @return The column name for ID in the filtered number database.
75      */
getIdColumnName()76     public static String getIdColumnName() {
77         return useNewFiltering() ? BlockedNumbersSdkCompat._ID : FilteredNumberColumns._ID;
78     }
79 
80     /**
81      * @return The column name for type in the filtered number database. Will be {@code null} for
82      * the framework blocking implementation.
83      */
84     @Nullable
getTypeColumnName()85     public static String getTypeColumnName() {
86         return useNewFiltering() ? null : FilteredNumberColumns.TYPE;
87     }
88 
89     /**
90      * @return The column name for source in the filtered number database. Will be {@code null} for
91      * the framework blocking implementation
92      */
93     @Nullable
getSourceColumnName()94     public static String getSourceColumnName() {
95         return useNewFiltering() ? null : FilteredNumberColumns.SOURCE;
96     }
97 
98     /**
99      * @return The column name for the original number in the filtered number database.
100      */
getOriginalNumberColumnName()101     public static String getOriginalNumberColumnName() {
102         return useNewFiltering() ? BlockedNumbersSdkCompat.COLUMN_ORIGINAL_NUMBER
103                 : FilteredNumberColumns.NUMBER;
104     }
105 
106     /**
107      * @return The column name for country iso in the filtered number database. Will be {@code null}
108      * the framework blocking implementation
109      */
110     @Nullable
getCountryIsoColumnName()111     public static String getCountryIsoColumnName() {
112         return useNewFiltering() ? null : FilteredNumberColumns.COUNTRY_ISO;
113     }
114 
115     /**
116      * @return The column name for the e164 formatted number in the filtered number database.
117      */
getE164NumberColumnName()118     public static String getE164NumberColumnName() {
119         return useNewFiltering() ? BlockedNumbersSdkCompat.E164_NUMBER
120                 : FilteredNumberColumns.NORMALIZED_NUMBER;
121     }
122 
123     /**
124      * @return {@code true} if the current SDK version supports using new filtering, {@code false}
125      * otherwise.
126      */
canUseNewFiltering()127     public static boolean canUseNewFiltering() {
128         if (isEnabledForTest != null) {
129             return CompatUtils.isNCompatible() && isEnabledForTest;
130         }
131         return CompatUtils.isNCompatible() && ObjectFactory
132                 .isNewBlockingEnabled(DialerApplication.getContext());
133     }
134 
135     /**
136      * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary
137      * migration has been performed, {@code false} otherwise.
138      */
useNewFiltering()139     public static boolean useNewFiltering() {
140         return canUseNewFiltering() && hasMigratedToNewBlocking();
141     }
142 
143     /**
144      * @return {@code true} if the user has migrated to use
145      * {@link android.provider.BlockedNumberContract} blocking, {@code false} otherwise.
146      */
hasMigratedToNewBlocking()147     public static boolean hasMigratedToNewBlocking() {
148         return PreferenceManager.getDefaultSharedPreferences(DialerApplication.getContext())
149                 .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false);
150     }
151 
152     /**
153      * Called to inform this class whether the user has fully migrated to use
154      * {@link android.provider.BlockedNumberContract} blocking or not.
155      *
156      * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise.
157      */
158     @NeededForTesting
setHasMigratedToNewBlocking(boolean hasMigrated)159     public static void setHasMigratedToNewBlocking(boolean hasMigrated) {
160         PreferenceManager.getDefaultSharedPreferences(
161                 MoreObjects.firstNonNull(contextForTest, DialerApplication.getContext())).edit()
162                 .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated).apply();
163     }
164 
165     @NeededForTesting
setIsEnabledForTest(Boolean isEnabled)166     public static void setIsEnabledForTest(Boolean isEnabled) {
167         isEnabledForTest = isEnabled;
168     }
169 
170     @NeededForTesting
setContextForTest(Context context)171     public static void setContextForTest(Context context) {
172         contextForTest = context;
173     }
174 
175     /**
176      * Gets the content {@link Uri} for number filtering.
177      *
178      * @param id The optional id to append with the base content uri.
179      * @return The Uri for number filtering.
180      */
getContentUri(@ullable Integer id)181     public static Uri getContentUri(@Nullable Integer id) {
182         if (id == null) {
183             return getBaseUri();
184         }
185         return ContentUris.withAppendedId(getBaseUri(), id);
186     }
187 
188 
getBaseUri()189     private static Uri getBaseUri() {
190         return useNewFiltering() ? BlockedNumbersSdkCompat.CONTENT_URI : FilteredNumber.CONTENT_URI;
191     }
192 
193     /**
194      * Removes any null column names from the given projection array. This method is intended to be
195      * used to strip out any column names that aren't available in every version of number blocking.
196      * Example:
197      * {@literal
198      *   getContext().getContentResolver().query(
199      *       someUri,
200      *       // Filtering ensures that no non-existant columns are queried
201      *       FilteredNumberCompat.filter(new String[] {FilteredNumberCompat.getIdColumnName(),
202      *           FilteredNumberCompat.getTypeColumnName()},
203      *       FilteredNumberCompat.getE164NumberColumnName() + " = ?",
204      *       new String[] {e164Number});
205      * }
206      *
207      * @param projection The projection array.
208      * @return The filtered projection array.
209      */
210     @Nullable
filter(@ullable String[] projection)211     public static String[] filter(@Nullable String[] projection) {
212         if (projection == null) {
213             return null;
214         }
215         List<String> filtered = new ArrayList<>();
216         for (String column : projection) {
217             if (column != null) {
218                 filtered.add(column);
219             }
220         }
221         return filtered.toArray(new String[filtered.size()]);
222     }
223 
224     /**
225      * Creates a new {@link ContentValues} suitable for inserting in the filtered number table.
226      *
227      * @param number The unformatted number to insert.
228      * @param e164Number (optional) The number to insert formatted to E164 standard.
229      * @param countryIso (optional) The country iso to use to format the number.
230      * @return The ContentValues to insert.
231      * @throws NullPointerException If number is null.
232      */
newBlockNumberContentValues(String number, @Nullable String e164Number, @Nullable String countryIso)233     public static ContentValues newBlockNumberContentValues(String number,
234             @Nullable String e164Number, @Nullable String countryIso) {
235         ContentValues contentValues = new ContentValues();
236         contentValues.put(getOriginalNumberColumnName(), Preconditions.checkNotNull(number));
237         if (!useNewFiltering()) {
238             if (e164Number == null) {
239                 e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
240             }
241             contentValues.put(getE164NumberColumnName(), e164Number);
242             contentValues.put(getCountryIsoColumnName(), countryIso);
243             contentValues.put(getTypeColumnName(), FilteredNumberTypes.BLOCKED_NUMBER);
244             contentValues.put(getSourceColumnName(), FilteredNumberSources.USER);
245         }
246         return contentValues;
247     }
248 
249     /**
250      * Shows the flow of {@link android.app.DialogFragment}s for blocking or unblocking numbers.
251      *
252      * @param blockId The id into the blocked numbers database.
253      * @param number The number to block or unblock.
254      * @param countryIso The countryIso used to format the given number.
255      * @param displayNumber The form of the number to block, suitable for displaying.
256      * @param parentViewId The id for the containing view of the Dialog.
257      * @param fragmentManager The {@link FragmentManager} used to show fragments.
258      * @param callback (optional) The {@link Callback} to call when the block or unblock operation
259      * is complete.
260      */
showBlockNumberDialogFlow(final ContentResolver contentResolver, final Integer blockId, final String number, final String countryIso, final String displayNumber, final Integer parentViewId, final FragmentManager fragmentManager, @Nullable final Callback callback)261     public static void showBlockNumberDialogFlow(final ContentResolver contentResolver,
262             final Integer blockId, final String number, final String countryIso,
263             final String displayNumber, final Integer parentViewId,
264             final FragmentManager fragmentManager, @Nullable final Callback callback) {
265         Log.i(TAG, "showBlockNumberDialogFlow - start");
266         // If the user is blocking a number and isn't using the framework solution when they
267         // should be, show the migration dialog
268         if (shouldShowMigrationDialog(blockId == null)) {
269             Log.i(TAG, "showBlockNumberDialogFlow - showing migration dialog");
270             MigrateBlockedNumbersDialogFragment
271                     .newInstance(new BlockedNumbersMigrator(contentResolver), newMigrationListener(
272                             DialerApplication.getContext().getContentResolver(), number, countryIso,
273                             displayNumber, parentViewId, fragmentManager, callback))
274                     .show(fragmentManager, "MigrateBlockedNumbers");
275             return;
276         }
277         Log.i(TAG, "showBlockNumberDialogFlow - showing block number dialog");
278         BlockNumberDialogFragment
279                 .show(blockId, number, countryIso, displayNumber, parentViewId, fragmentManager,
280                         callback);
281     }
282 
shouldShowMigrationDialog(boolean isBlocking)283     private static boolean shouldShowMigrationDialog(boolean isBlocking) {
284         return isBlocking && canUseNewFiltering() && !hasMigratedToNewBlocking();
285     }
286 
newMigrationListener( final ContentResolver contentResolver, final String number, final String countryIso, final String displayNumber, final Integer parentViewId, final FragmentManager fragmentManager, @Nullable final Callback callback)287     private static BlockedNumbersMigrator.Listener newMigrationListener(
288             final ContentResolver contentResolver, final String number, final String countryIso,
289             final String displayNumber, final Integer parentViewId,
290             final FragmentManager fragmentManager, @Nullable final Callback callback) {
291         return new BlockedNumbersMigrator.Listener() {
292             @Override
293             public void onComplete() {
294                 Log.i(TAG, "showBlockNumberDialogFlow - listener showing block number dialog");
295                 if (!hasMigratedToNewBlocking()) {
296                     Log.i(TAG, "showBlockNumberDialogFlow - migration failed");
297                     return;
298                 }
299                 /*
300                  * Edge case to cover here: if the user initiated the migration workflow with a
301                  * number that's already blocked in the framework, don't show the block number
302                  * dialog. Doing so would allow them to block the same number twice, causing a
303                  * crash.
304                  */
305                 new FilteredNumberAsyncQueryHandler(contentResolver).isBlockedNumber(
306                         new OnCheckBlockedListener() {
307                             @Override
308                             public void onCheckComplete(Integer id) {
309                                 if (id != null) {
310                                     Log.i(TAG,
311                                             "showBlockNumberDialogFlow - number already blocked");
312                                     return;
313                                 }
314                                 Log.i(TAG, "showBlockNumberDialogFlow - need to block number");
315                                 BlockNumberDialogFragment
316                                         .show(null, number, countryIso, displayNumber, parentViewId,
317                                                 fragmentManager, callback);
318                             }
319                         }, number, countryIso);
320             }
321         };
322     }
323 
324     /**
325      * Creates the {@link Intent} which opens the blocked numbers management interface.
326      *
327      * @param context The {@link Context}.
328      * @return The intent.
329      */
330     public static Intent createManageBlockedNumbersIntent(Context context) {
331         if (canUseNewFiltering() && hasMigratedToNewBlocking()) {
332             return TelecomManagerUtil.createManageBlockedNumbersIntent(
333                     (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE));
334         }
335         return new Intent(context, BlockedNumbersSettingsActivity.class);
336     }
337 
338     /**
339      * Method used to determine if block operations are possible.
340      *
341      * @param context The {@link Context}.
342      * @return {@code true} if the app and user can block numbers, {@code false} otherwise.
343      */
344     public static boolean canAttemptBlockOperations(Context context) {
345         if (!CompatUtils.isNCompatible()) {
346             // Dialer blocking, must be primary user
347             return UserManagerCompat.isSystemUser(
348                     (UserManager) context.getSystemService(Context.USER_SERVICE));
349         }
350 
351         // Great Wall blocking, must be primary user and the default or system dialer
352         // TODO(maxwelb): check that we're the default or system Dialer
353         return BlockedNumbersSdkCompat.canCurrentUserBlockNumbers(context);
354     }
355 
356     /**
357      * Used to determine if the call blocking settings can be opened.
358      *
359      * @param context The {@link Context}.
360      * @return {@code true} if the current user can open the call blocking settings, {@code false}
361      * otherwise.
362      */
363     public static boolean canCurrentUserOpenBlockSettings(Context context) {
364         if (!CompatUtils.isNCompatible()) {
365             // Dialer blocking, must be primary user
366             return UserManagerCompat.isSystemUser(
367                     (UserManager) context.getSystemService(Context.USER_SERVICE));
368         }
369         // BlockedNumberContract blocking, verify through Contract API
370         return BlockedNumbersSdkCompat.canCurrentUserBlockNumbers(context);
371     }
372 }
373