| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.dialer.filterednumber; |
| |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.os.AsyncTask; |
| import android.preference.PreferenceManager; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.Settings; |
| import android.telephony.PhoneNumberUtils; |
| import android.text.TextUtils; |
| import android.widget.Toast; |
| |
| import com.android.contacts.common.testing.NeededForTesting; |
| import com.android.contacts.common.util.PermissionsUtil; |
| import com.android.dialer.R; |
| import com.android.dialer.compat.FilteredNumberCompat; |
| import com.android.dialer.database.FilteredNumberAsyncQueryHandler; |
| import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener; |
| import com.android.dialer.database.FilteredNumberContract.FilteredNumber; |
| import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; |
| import com.android.dialer.logging.InteractionEvent; |
| import com.android.dialer.logging.Logger; |
| |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Utility to help with tasks related to filtered numbers. |
| */ |
| public class FilteredNumbersUtil { |
| |
| // Disable incoming call blocking if there was a call within the past 2 days. |
| private static final long RECENT_EMERGENCY_CALL_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 2; |
| |
| // Pref key for storing the time of end of the last emergency call in milliseconds after epoch. |
| protected static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms"; |
| |
| // Pref key for storing whether a notification has been dispatched to notify the user that call |
| // blocking has been disabled because of a recent emergency call. |
| protected static final String NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY = |
| "notified_call_blocking_disabled_by_emergency_call"; |
| |
| public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking"; |
| public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10; |
| |
| /** |
| * Used for testing to specify that a custom threshold should be used instead of the default. |
| * This custom threshold will only be used when setting this log tag to VERBOSE: |
| * |
| * adb shell setprop log.tag.DebugEmergencyCall VERBOSE |
| * |
| */ |
| @NeededForTesting |
| private static final String DEBUG_EMERGENCY_CALL_TAG = "DebugEmergencyCall"; |
| |
| /** |
| * Used for testing to specify the custom threshold value, in milliseconds for whether an |
| * emergency call is "recent". The default value will be used if this custom threshold is less |
| * than zero. For example, to set this threshold to 60 seconds: |
| * |
| * adb shell settings put system dialer_emergency_call_threshold_ms 60000 |
| * |
| */ |
| @NeededForTesting |
| private static final String RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY = |
| "dialer_emergency_call_threshold_ms"; |
| |
| public interface CheckForSendToVoicemailContactListener { |
| public void onComplete(boolean hasSendToVoicemailContact); |
| } |
| |
| public interface ImportSendToVoicemailContactsListener { |
| public void onImportComplete(); |
| } |
| |
| private static class ContactsQuery { |
| static final String[] PROJECTION = { |
| Contacts._ID |
| }; |
| |
| static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; |
| |
| static final int ID_COLUMN_INDEX = 0; |
| } |
| |
| public static class PhoneQuery { |
| static final String[] PROJECTION = { |
| Contacts._ID, |
| Phone.NORMALIZED_NUMBER, |
| Phone.NUMBER |
| }; |
| |
| static final int ID_COLUMN_INDEX = 0; |
| static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1; |
| static final int NUMBER_COLUMN_INDEX = 2; |
| |
| static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; |
| } |
| |
| /** |
| * Checks if there exists a contact with {@code Contacts.SEND_TO_VOICEMAIL} set to true. |
| */ |
| public static void checkForSendToVoicemailContact( |
| final Context context, final CheckForSendToVoicemailContactListener listener) { |
| final AsyncTask task = new AsyncTask<Object, Void, Boolean>() { |
| @Override |
| public Boolean doInBackground(Object[] params) { |
| if (context == null || !PermissionsUtil.hasContactsPermissions(context)) { |
| return false; |
| } |
| |
| final Cursor cursor = context.getContentResolver().query( |
| Contacts.CONTENT_URI, |
| ContactsQuery.PROJECTION, |
| ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE, |
| null, |
| null); |
| |
| boolean hasSendToVoicemailContacts = false; |
| if (cursor != null) { |
| try { |
| hasSendToVoicemailContacts = cursor.getCount() > 0; |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return hasSendToVoicemailContacts; |
| } |
| |
| @Override |
| public void onPostExecute(Boolean hasSendToVoicemailContact) { |
| if (listener != null) { |
| listener.onComplete(hasSendToVoicemailContact); |
| } |
| } |
| }; |
| task.execute(); |
| } |
| |
| /** |
| * Blocks all the phone numbers of any contacts marked as SEND_TO_VOICEMAIL, then clears the |
| * SEND_TO_VOICEMAIL flag on those contacts. |
| */ |
| public static void importSendToVoicemailContacts( |
| final Context context, final ImportSendToVoicemailContactsListener listener) { |
| Logger.logInteraction(InteractionEvent.IMPORT_SEND_TO_VOICEMAIL); |
| final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler = |
| new FilteredNumberAsyncQueryHandler(context.getContentResolver()); |
| |
| final AsyncTask<Object, Void, Boolean> task = new AsyncTask<Object, Void, Boolean>() { |
| @Override |
| public Boolean doInBackground(Object[] params) { |
| if (context == null) { |
| return false; |
| } |
| |
| // Get the phone number of contacts marked as SEND_TO_VOICEMAIL. |
| final Cursor phoneCursor = context.getContentResolver().query( |
| Phone.CONTENT_URI, |
| PhoneQuery.PROJECTION, |
| PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE, |
| null, |
| null); |
| |
| if (phoneCursor == null) { |
| return false; |
| } |
| |
| try { |
| while (phoneCursor.moveToNext()) { |
| final String normalizedNumber = phoneCursor.getString( |
| PhoneQuery.NORMALIZED_NUMBER_COLUMN_INDEX); |
| final String number = phoneCursor.getString( |
| PhoneQuery.NUMBER_COLUMN_INDEX); |
| if (normalizedNumber != null) { |
| // Block the phone number of the contact. |
| mFilteredNumberAsyncQueryHandler.blockNumber( |
| null, normalizedNumber, number, null); |
| } |
| } |
| } finally { |
| phoneCursor.close(); |
| } |
| |
| // Clear SEND_TO_VOICEMAIL on all contacts. The setting has been imported to Dialer. |
| ContentValues newValues = new ContentValues(); |
| newValues.put(Contacts.SEND_TO_VOICEMAIL, 0); |
| context.getContentResolver().update( |
| Contacts.CONTENT_URI, |
| newValues, |
| ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE, |
| null); |
| |
| return true; |
| } |
| |
| @Override |
| public void onPostExecute(Boolean success) { |
| if (success) { |
| if (listener != null) { |
| listener.onImportComplete(); |
| } |
| } else if (context != null) { |
| String toastStr = context.getString(R.string.send_to_voicemail_import_failed); |
| Toast.makeText(context, toastStr, Toast.LENGTH_SHORT).show(); |
| } |
| } |
| }; |
| task.execute(); |
| } |
| |
| /** |
| * WARNING: This method should NOT be executed on the UI thread. |
| * Use {@code FilteredNumberAsyncQueryHandler} to asynchronously check if a number is blocked. |
| */ |
| public static boolean shouldBlockVoicemail( |
| Context context, String number, String countryIso, long voicemailDateMs) { |
| final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); |
| if (TextUtils.isEmpty(normalizedNumber)) { |
| return false; |
| } |
| |
| if (hasRecentEmergencyCall(context)) { |
| return false; |
| } |
| |
| final Cursor cursor = context.getContentResolver().query( |
| FilteredNumber.CONTENT_URI, |
| new String[] { |
| FilteredNumberColumns.CREATION_TIME |
| }, |
| FilteredNumberColumns.NORMALIZED_NUMBER + "=?", |
| new String[] { normalizedNumber }, |
| null); |
| if (cursor == null) { |
| return false; |
| } |
| try { |
| /* |
| * Block if number is found and it was added before this voicemail was received. |
| * The VVM's date is reported with precision to the minute, even though its |
| * magnitude is in milliseconds, so we perform the comparison in minutes. |
| */ |
| return cursor.moveToFirst() && |
| TimeUnit.MINUTES.convert(voicemailDateMs, TimeUnit.MILLISECONDS) >= |
| TimeUnit.MINUTES.convert(cursor.getLong(0), TimeUnit.MILLISECONDS); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| public static boolean hasRecentEmergencyCall(Context context) { |
| if (context == null) { |
| return false; |
| } |
| |
| Long lastEmergencyCallTime = PreferenceManager.getDefaultSharedPreferences(context) |
| .getLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, 0); |
| if (lastEmergencyCallTime == 0) { |
| return false; |
| } |
| |
| return (System.currentTimeMillis() - lastEmergencyCallTime) |
| < getRecentEmergencyCallThresholdMs(context); |
| } |
| |
| public static void recordLastEmergencyCallTime(Context context) { |
| if (context == null) { |
| return; |
| } |
| |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, System.currentTimeMillis()) |
| .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false) |
| .apply(); |
| |
| maybeNotifyCallBlockingDisabled(context); |
| } |
| |
| public static void maybeNotifyCallBlockingDisabled(final Context context) { |
| // The Dialer is not responsible for this notification after migrating |
| if (FilteredNumberCompat.useNewFiltering()) { |
| return; |
| } |
| // Skip if the user has already received a notification for the most recent emergency call. |
| if (PreferenceManager.getDefaultSharedPreferences(context) |
| .getBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)) { |
| return; |
| } |
| |
| // If the user has blocked numbers, notify that call blocking is temporarily disabled. |
| FilteredNumberAsyncQueryHandler queryHandler = |
| new FilteredNumberAsyncQueryHandler(context.getContentResolver()); |
| queryHandler.hasBlockedNumbers(new OnHasBlockedNumbersListener() { |
| @Override |
| public void onHasBlockedNumbers(boolean hasBlockedNumbers) { |
| if (context == null || !hasBlockedNumbers) { |
| return; |
| } |
| |
| NotificationManager notificationManager = (NotificationManager) |
| context.getSystemService(Context.NOTIFICATION_SERVICE); |
| Notification.Builder builder = new Notification.Builder(context) |
| .setSmallIcon(R.drawable.ic_block_24dp) |
| .setContentTitle(context.getString( |
| R.string.call_blocking_disabled_notification_title)) |
| .setContentText(context.getString( |
| R.string.call_blocking_disabled_notification_text)) |
| .setAutoCancel(true); |
| |
| final Intent contentIntent = |
| new Intent(context, BlockedNumbersSettingsActivity.class); |
| builder.setContentIntent(PendingIntent.getActivity( |
| context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT)); |
| |
| notificationManager.notify( |
| CALL_BLOCKING_NOTIFICATION_TAG, |
| CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID, |
| builder.build()); |
| |
| // Record that the user has been notified for this emergency call. |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, true) |
| .apply(); |
| } |
| }); |
| } |
| |
| public static boolean canBlockNumber(Context context, String number, String countryIso) { |
| final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); |
| return !TextUtils.isEmpty(normalizedNumber) |
| && !PhoneNumberUtils.isEmergencyNumber(normalizedNumber); |
| } |
| |
| private static long getRecentEmergencyCallThresholdMs(Context context) { |
| if (android.util.Log.isLoggable( |
| DEBUG_EMERGENCY_CALL_TAG, android.util.Log.VERBOSE)) { |
| long thresholdMs = Settings.System.getLong( |
| context.getContentResolver(), |
| RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY, 0); |
| return thresholdMs > 0 ? thresholdMs : RECENT_EMERGENCY_CALL_THRESHOLD_MS; |
| } else { |
| return RECENT_EMERGENCY_CALL_THRESHOLD_MS; |
| } |
| } |
| } |