diff options
Diffstat (limited to 'java/com/android/dialer/blocking')
30 files changed, 2568 insertions, 0 deletions
diff --git a/java/com/android/dialer/blocking/AndroidManifest.xml b/java/com/android/dialer/blocking/AndroidManifest.xml new file mode 100644 index 000000000..08d243988 --- /dev/null +++ b/java/com/android/dialer/blocking/AndroidManifest.xml @@ -0,0 +1,13 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.dialer.blocking"> + + <application android:theme="@style/Theme.AppCompat"> + + <provider + android:authorities="com.android.dialer.blocking.filterednumberprovider" + android:exported="false" + android:multiprocess="false" + android:name="com.android.dialer.blocking.FilteredNumberProvider"/> + + </application> +</manifest> diff --git a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java new file mode 100644 index 000000000..c405b2fe7 --- /dev/null +++ b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java @@ -0,0 +1,328 @@ +/* + * 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.blocking; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.view.View; +import android.widget.Toast; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnBlockNumberListener; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnUnblockNumberListener; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.InteractionEvent; +import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker; + +/** + * Fragment for confirming and enacting blocking/unblocking a number. Also invokes snackbar + * providing undo functionality. + */ +public class BlockNumberDialogFragment extends DialogFragment { + + private static final String BLOCK_DIALOG_FRAGMENT = "BlockNumberDialog"; + private static final String ARG_BLOCK_ID = "argBlockId"; + private static final String ARG_NUMBER = "argNumber"; + private static final String ARG_COUNTRY_ISO = "argCountryIso"; + private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber"; + private static final String ARG_PARENT_VIEW_ID = "parentViewId"; + private String mNumber; + private String mDisplayNumber; + private String mCountryIso; + private FilteredNumberAsyncQueryHandler mHandler; + private View mParentView; + private VisualVoicemailEnabledChecker mVoicemailEnabledChecker; + private Callback mCallback; + + public static BlockNumberDialogFragment show( + Integer blockId, + String number, + String countryIso, + String displayNumber, + Integer parentViewId, + FragmentManager fragmentManager, + Callback callback) { + final BlockNumberDialogFragment newFragment = + BlockNumberDialogFragment.newInstance( + blockId, number, countryIso, displayNumber, parentViewId); + + newFragment.setCallback(callback); + newFragment.show(fragmentManager, BlockNumberDialogFragment.BLOCK_DIALOG_FRAGMENT); + return newFragment; + } + + private static BlockNumberDialogFragment newInstance( + Integer blockId, + String number, + String countryIso, + String displayNumber, + Integer parentViewId) { + final BlockNumberDialogFragment fragment = new BlockNumberDialogFragment(); + final Bundle args = new Bundle(); + if (blockId != null) { + args.putInt(ARG_BLOCK_ID, blockId.intValue()); + } + if (parentViewId != null) { + args.putInt(ARG_PARENT_VIEW_ID, parentViewId.intValue()); + } + args.putString(ARG_NUMBER, number); + args.putString(ARG_COUNTRY_ISO, countryIso); + args.putString(ARG_DISPLAY_NUMBER, displayNumber); + fragment.setArguments(args); + return fragment; + } + + public void setFilteredNumberAsyncQueryHandlerForTesting( + FilteredNumberAsyncQueryHandler handler) { + mHandler = handler; + } + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + final boolean isBlocked = getArguments().containsKey(ARG_BLOCK_ID); + + mNumber = getArguments().getString(ARG_NUMBER); + mDisplayNumber = getArguments().getString(ARG_DISPLAY_NUMBER); + mCountryIso = getArguments().getString(ARG_COUNTRY_ISO); + + if (TextUtils.isEmpty(mDisplayNumber)) { + mDisplayNumber = mNumber; + } + + mHandler = new FilteredNumberAsyncQueryHandler(getContext()); + mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getActivity(), null); + // Choose not to update VoicemailEnabledChecker, as checks should already been done in + // all current use cases. + mParentView = getActivity().findViewById(getArguments().getInt(ARG_PARENT_VIEW_ID)); + + CharSequence title; + String okText; + String message; + if (isBlocked) { + title = null; + okText = getString(R.string.unblock_number_ok); + message = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.unblock_number_confirmation_title, mDisplayNumber) + .toString(); + } else { + title = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.block_number_confirmation_title, mDisplayNumber); + okText = getString(R.string.block_number_ok); + if (FilteredNumberCompat.useNewFiltering(getContext())) { + message = getString(R.string.block_number_confirmation_message_new_filtering); + } else if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) { + message = getString(R.string.block_number_confirmation_message_vvm); + } else { + message = getString(R.string.block_number_confirmation_message_no_vvm); + } + } + + AlertDialog.Builder builder = + new AlertDialog.Builder(getActivity()) + .setTitle(title) + .setMessage(message) + .setPositiveButton( + okText, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + if (isBlocked) { + unblockNumber(); + } else { + blockNumber(); + } + } + }) + .setNegativeButton(android.R.string.cancel, null); + return builder.create(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + String e164Number = PhoneNumberUtils.formatNumberToE164(mNumber, mCountryIso); + if (!FilteredNumbersUtil.canBlockNumber(getContext(), e164Number, mNumber)) { + dismiss(); + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.invalidNumber, mDisplayNumber), + Toast.LENGTH_SHORT) + .show(); + } + } + + @Override + public void onPause() { + // Dismiss on rotation. + dismiss(); + mCallback = null; + + super.onPause(); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + private CharSequence getBlockedMessage() { + return ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.snackbar_number_blocked, mDisplayNumber); + } + + private CharSequence getUnblockedMessage() { + return ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.snackbar_number_unblocked, mDisplayNumber); + } + + private int getActionTextColor() { + return getContext().getResources().getColor(R.color.dialer_snackbar_action_text_color); + } + + private void blockNumber() { + final CharSequence message = getBlockedMessage(); + final CharSequence undoMessage = getUnblockedMessage(); + final Callback callback = mCallback; + final int actionTextColor = getActionTextColor(); + final Context applicationContext = getContext().getApplicationContext(); + + final OnUnblockNumberListener onUndoListener = + new OnUnblockNumberListener() { + @Override + public void onUnblockComplete(int rows, ContentValues values) { + Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show(); + if (callback != null) { + callback.onChangeFilteredNumberUndo(); + } + } + }; + + final OnBlockNumberListener onBlockNumberListener = + new OnBlockNumberListener() { + @Override + public void onBlockComplete(final Uri uri) { + final View.OnClickListener undoListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + // Delete the newly created row on 'undo'. + Logger.get(applicationContext) + .logInteraction(InteractionEvent.Type.UNDO_BLOCK_NUMBER); + mHandler.unblock(onUndoListener, uri); + } + }; + + Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG) + .setAction(R.string.block_number_undo, undoListener) + .setActionTextColor(actionTextColor) + .show(); + + if (callback != null) { + callback.onFilterNumberSuccess(); + } + + if (FilteredNumbersUtil.hasRecentEmergencyCall(applicationContext)) { + FilteredNumbersUtil.maybeNotifyCallBlockingDisabled(applicationContext); + } + } + }; + + mHandler.blockNumber(onBlockNumberListener, mNumber, mCountryIso); + } + + private void unblockNumber() { + final CharSequence message = getUnblockedMessage(); + final CharSequence undoMessage = getBlockedMessage(); + final Callback callback = mCallback; + final int actionTextColor = getActionTextColor(); + final Context applicationContext = getContext().getApplicationContext(); + + final OnBlockNumberListener onUndoListener = + new OnBlockNumberListener() { + @Override + public void onBlockComplete(final Uri uri) { + Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show(); + if (callback != null) { + callback.onChangeFilteredNumberUndo(); + } + } + }; + + mHandler.unblock( + new OnUnblockNumberListener() { + @Override + public void onUnblockComplete(int rows, final ContentValues values) { + final View.OnClickListener undoListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + // Re-insert the row on 'undo', with a new ID. + Logger.get(applicationContext) + .logInteraction(InteractionEvent.Type.UNDO_UNBLOCK_NUMBER); + mHandler.blockNumber(onUndoListener, values); + } + }; + + Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG) + .setAction(R.string.block_number_undo, undoListener) + .setActionTextColor(actionTextColor) + .show(); + + if (callback != null) { + callback.onUnfilterNumberSuccess(); + } + } + }, + getArguments().getInt(ARG_BLOCK_ID)); + } + + /** + * Use a callback interface to update UI after success/undo. Favor this approach over other more + * standard paradigms because of the variety of scenarios in which the DialogFragment can be + * invoked (by an Activity, by a fragment, by an adapter, by an adapter list item). Because of + * this, we do NOT support retaining state on rotation, and will dismiss the dialog upon rotation + * instead. + */ + public interface Callback { + + /** Called when a number is successfully added to the set of filtered numbers */ + void onFilterNumberSuccess(); + + /** Called when a number is successfully removed from the set of filtered numbers */ + void onUnfilterNumberSuccess(); + + /** Called when the action of filtering or unfiltering a number is undone */ + void onChangeFilteredNumberUndo(); + } +} diff --git a/java/com/android/dialer/blocking/BlockReportSpamDialogs.java b/java/com/android/dialer/blocking/BlockReportSpamDialogs.java new file mode 100644 index 000000000..b5f81fcc5 --- /dev/null +++ b/java/com/android/dialer/blocking/BlockReportSpamDialogs.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2016 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.blocking; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +/** Helper class for creating block/report dialog fragments. */ +public class BlockReportSpamDialogs { + + public static final String BLOCK_REPORT_SPAM_DIALOG_TAG = "BlockReportSpamDialog"; + public static final String BLOCK_DIALOG_TAG = "BlockDialog"; + public static final String UNBLOCK_DIALOG_TAG = "UnblockDialog"; + public static final String NOT_SPAM_DIALOG_TAG = "NotSpamDialog"; + + /** Creates a dialog with the default cancel button listener (dismisses dialog). */ + private static AlertDialog.Builder createDialogBuilder( + Activity activity, final DialogFragment fragment) { + return new AlertDialog.Builder(activity, R.style.AlertDialogTheme) + .setCancelable(true) + .setNegativeButton( + android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + fragment.dismiss(); + } + }); + } + + /** + * Creates a generic click listener which dismisses the fragment and then calls the actual + * listener. + */ + private static DialogInterface.OnClickListener createGenericOnClickListener( + final DialogFragment fragment, final OnConfirmListener listener) { + return new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + fragment.dismiss(); + listener.onClick(); + } + }; + } + + private static String getBlockMessage(Context context) { + String message; + if (FilteredNumberCompat.useNewFiltering(context)) { + message = context.getString(R.string.block_number_confirmation_message_new_filtering); + } else { + message = context.getString(R.string.block_report_number_alert_details); + } + return message; + } + + /** + * Listener passed to block/report spam dialog for positive click in {@link + * BlockReportSpamDialogFragment}. + */ + public interface OnSpamDialogClickListener { + + /** + * Called when user clicks on positive button in block/report spam dialog. + * + * @param isSpamChecked Whether the spam checkbox is checked. + */ + void onClick(boolean isSpamChecked); + } + + /** Listener passed to all dialogs except the block/report spam dialog for positive click. */ + public interface OnConfirmListener { + + /** Called when user clicks on positive button in the dialog. */ + void onClick(); + } + + /** Contains the common attributes between all block/unblock/report dialog fragments. */ + private static class CommonDialogsFragment extends DialogFragment { + + /** The number to display in the dialog title. */ + protected String mDisplayNumber; + + /** Called when dialog positive button is pressed. */ + protected OnConfirmListener mPositiveListener; + + /** Called when dialog is dismissed. */ + @Nullable protected DialogInterface.OnDismissListener mDismissListener; + + @Override + public void onDismiss(DialogInterface dialog) { + if (mDismissListener != null) { + mDismissListener.onDismiss(dialog); + } + super.onDismiss(dialog); + } + + @Override + public void onPause() { + // The dialog is dismissed onPause, i.e. rotation. + dismiss(); + mDismissListener = null; + mPositiveListener = null; + mDisplayNumber = null; + super.onPause(); + } + } + + /** Dialog for block/report spam with the mark as spam checkbox. */ + public static class BlockReportSpamDialogFragment extends CommonDialogsFragment { + + /** Called when dialog positive button is pressed. */ + private OnSpamDialogClickListener mPositiveListener; + + /** Whether the mark as spam checkbox is checked before displaying the dialog. */ + private boolean mSpamChecked; + + public static DialogFragment newInstance( + String displayNumber, + boolean spamChecked, + OnSpamDialogClickListener positiveListener, + @Nullable DialogInterface.OnDismissListener dismissListener) { + BlockReportSpamDialogFragment fragment = new BlockReportSpamDialogFragment(); + fragment.mSpamChecked = spamChecked; + fragment.mDisplayNumber = displayNumber; + fragment.mPositiveListener = positiveListener; + fragment.mDismissListener = dismissListener; + return fragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + View dialogView = View.inflate(getActivity(), R.layout.block_report_spam_dialog, null); + final CheckBox isSpamCheckbox = + (CheckBox) dialogView.findViewById(R.id.report_number_as_spam_action); + // Listen for changes on the checkbox and update if orientation changes + isSpamCheckbox.setChecked(mSpamChecked); + isSpamCheckbox.setOnCheckedChangeListener( + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mSpamChecked = isChecked; + } + }); + + TextView details = (TextView) dialogView.findViewById(R.id.block_details); + details.setText(getBlockMessage(getContext())); + + AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this); + Dialog dialog = + alertDialogBuilder + .setView(dialogView) + .setTitle(getString(R.string.block_report_number_alert_title, mDisplayNumber)) + .setPositiveButton( + R.string.block_number_ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + mPositiveListener.onClick(isSpamCheckbox.isChecked()); + } + }) + .create(); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + } + + /** Dialog for blocking a number. */ + public static class BlockDialogFragment extends CommonDialogsFragment { + + private boolean isSpamEnabled; + + public static DialogFragment newInstance( + String displayNumber, + boolean isSpamEnabled, + OnConfirmListener positiveListener, + @Nullable DialogInterface.OnDismissListener dismissListener) { + BlockDialogFragment fragment = new BlockDialogFragment(); + fragment.mDisplayNumber = displayNumber; + fragment.mPositiveListener = positiveListener; + fragment.mDismissListener = dismissListener; + fragment.isSpamEnabled = isSpamEnabled; + return fragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + // Return the newly created dialog + AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this); + Dialog dialog = + alertDialogBuilder + .setTitle(getString(R.string.block_number_confirmation_title, mDisplayNumber)) + .setMessage( + isSpamEnabled + ? getString( + R.string.block_number_alert_details, getBlockMessage(getContext())) + : getString(R.string.block_report_number_alert_details)) + .setPositiveButton( + R.string.block_number_ok, createGenericOnClickListener(this, mPositiveListener)) + .create(); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + } + + /** Dialog for unblocking a number. */ + public static class UnblockDialogFragment extends CommonDialogsFragment { + + /** Whether or not the number is spam. */ + private boolean mIsSpam; + + public static DialogFragment newInstance( + String displayNumber, + boolean isSpam, + OnConfirmListener positiveListener, + @Nullable DialogInterface.OnDismissListener dismissListener) { + UnblockDialogFragment fragment = new UnblockDialogFragment(); + fragment.mDisplayNumber = displayNumber; + fragment.mIsSpam = isSpam; + fragment.mPositiveListener = positiveListener; + fragment.mDismissListener = dismissListener; + return fragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + // Return the newly created dialog + AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this); + if (mIsSpam) { + alertDialogBuilder + .setMessage(R.string.unblock_number_alert_details) + .setTitle(getString(R.string.unblock_report_number_alert_title, mDisplayNumber)); + } else { + alertDialogBuilder.setMessage( + getString(R.string.unblock_report_number_alert_title, mDisplayNumber)); + } + Dialog dialog = + alertDialogBuilder + .setPositiveButton( + R.string.unblock_number_ok, createGenericOnClickListener(this, mPositiveListener)) + .create(); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + } + + /** Dialog for reporting a number as not spam. */ + public static class ReportNotSpamDialogFragment extends CommonDialogsFragment { + + public static DialogFragment newInstance( + String displayNumber, + OnConfirmListener positiveListener, + @Nullable DialogInterface.OnDismissListener dismissListener) { + ReportNotSpamDialogFragment fragment = new ReportNotSpamDialogFragment(); + fragment.mDisplayNumber = displayNumber; + fragment.mPositiveListener = positiveListener; + fragment.mDismissListener = dismissListener; + return fragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + // Return the newly created dialog + AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this); + Dialog dialog = + alertDialogBuilder + .setTitle(R.string.report_not_spam_alert_title) + .setMessage(getString(R.string.report_not_spam_alert_details, mDisplayNumber)) + .setPositiveButton( + R.string.report_not_spam_alert_button, + createGenericOnClickListener(this, mPositiveListener)) + .create(); + dialog.setCanceledOnTouchOutside(true); + return dialog; + } + } +} diff --git a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java new file mode 100644 index 000000000..1773e9b84 --- /dev/null +++ b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2016 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.blocking; + +import android.content.Context; +import android.content.SharedPreferences; +import android.support.annotation.NonNull; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener; +import com.android.dialer.common.LogUtil; +import java.util.Objects; + +/** + * Class responsible for checking if the user can be auto-migrated to {@link + * android.provider.BlockedNumberContract} blocking. In order for this to happen, the user cannot + * have any numbers that are blocked in the Dialer solution. + */ +public class BlockedNumbersAutoMigrator { + + static final String HAS_CHECKED_AUTO_MIGRATE_KEY = "checkedAutoMigrate"; + + @NonNull private final Context context; + @NonNull private final SharedPreferences sharedPreferences; + @NonNull private final FilteredNumberAsyncQueryHandler queryHandler; + + /** + * Constructs the BlockedNumbersAutoMigrator with the given {@link SharedPreferences} and {@link + * FilteredNumberAsyncQueryHandler}. + * + * @param sharedPreferences The SharedPreferences used to persist information. + * @param queryHandler The FilteredNumberAsyncQueryHandler used to determine if there are blocked + * numbers. + * @throws NullPointerException if sharedPreferences or queryHandler are null. + */ + public BlockedNumbersAutoMigrator( + @NonNull Context context, + @NonNull SharedPreferences sharedPreferences, + @NonNull FilteredNumberAsyncQueryHandler queryHandler) { + this.context = Objects.requireNonNull(context); + this.sharedPreferences = Objects.requireNonNull(sharedPreferences); + this.queryHandler = Objects.requireNonNull(queryHandler); + } + + /** + * Attempts to perform the auto-migration. Auto-migration will only be attempted once and can be + * performed only when the user has no blocked numbers. As a result of this method, the user will + * be migrated to the framework blocking solution, as determined by {@link + * FilteredNumberCompat#hasMigratedToNewBlocking()}. + */ + public void autoMigrate() { + if (!shouldAttemptAutoMigrate()) { + return; + } + + LogUtil.i("BlockedNumbersAutoMigrator", "attempting to auto-migrate."); + queryHandler.hasBlockedNumbers( + new OnHasBlockedNumbersListener() { + @Override + public void onHasBlockedNumbers(boolean hasBlockedNumbers) { + if (hasBlockedNumbers) { + LogUtil.i("BlockedNumbersAutoMigrator", "not auto-migrating: blocked numbers exist."); + return; + } + LogUtil.i("BlockedNumbersAutoMigrator", "auto-migrating: no blocked numbers."); + FilteredNumberCompat.setHasMigratedToNewBlocking(context, true); + } + }); + } + + private boolean shouldAttemptAutoMigrate() { + if (sharedPreferences.contains(HAS_CHECKED_AUTO_MIGRATE_KEY)) { + LogUtil.v("BlockedNumbersAutoMigrator", "not attempting auto-migrate: already checked once."); + return false; + } + + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + // This may be the case where the user is on the lock screen, so we shouldn't record that the + // migration status was checked. + LogUtil.i( + "BlockedNumbersAutoMigrator", "not attempting auto-migrate: current user can't block"); + return false; + } + LogUtil.i("BlockedNumbersAutoMigrator", "updating state as already checked for auto-migrate."); + sharedPreferences.edit().putBoolean(HAS_CHECKED_AUTO_MIGRATE_KEY, true).apply(); + + if (!FilteredNumberCompat.canUseNewFiltering()) { + LogUtil.i("BlockedNumbersAutoMigrator", "not attempting auto-migrate: not available."); + return false; + } + + if (FilteredNumberCompat.hasMigratedToNewBlocking(context)) { + LogUtil.i("BlockedNumbersAutoMigrator", "not attempting auto-migrate: already migrated."); + return false; + } + return true; + } +} diff --git a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java new file mode 100644 index 000000000..88f474a84 --- /dev/null +++ b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java @@ -0,0 +1,159 @@ +/* + * 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.blocking; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Build.VERSION_CODES; +import android.provider.BlockedNumberContract.BlockedNumbers; +import android.support.annotation.RequiresApi; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.FilteredNumberContract; +import com.android.dialer.database.FilteredNumberContract.FilteredNumber; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import java.util.Objects; + +/** + * Class which should be used to migrate numbers from {@link FilteredNumberContract} blocking to + * {@link android.provider.BlockedNumberContract} blocking. + */ +@TargetApi(VERSION_CODES.M) +public class BlockedNumbersMigrator { + + private final Context context; + + /** + * Creates a new BlockedNumbersMigrate, using the given {@link ContentResolver} to perform queries + * against the blocked numbers tables. + */ + public BlockedNumbersMigrator(Context context) { + this.context = Objects.requireNonNull(context); + } + + @RequiresApi(VERSION_CODES.N) + @TargetApi(VERSION_CODES.N) + private static boolean migrateToNewBlockingInBackground(ContentResolver resolver) { + try (Cursor cursor = + resolver.query( + FilteredNumber.CONTENT_URI, + new String[] {FilteredNumberColumns.NUMBER}, + null, + null, + null)) { + if (cursor == null) { + LogUtil.i( + "BlockedNumbersMigrator.migrateToNewBlockingInBackground", "migrate - cursor was null"); + return false; + } + + LogUtil.i( + "BlockedNumbersMigrator.migrateToNewBlockingInBackground", + "migrate - attempting to migrate " + cursor.getCount() + "numbers"); + + int numMigrated = 0; + while (cursor.moveToNext()) { + String originalNumber = + cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER)); + if (isNumberInNewBlocking(resolver, originalNumber)) { + LogUtil.i( + "BlockedNumbersMigrator.migrateToNewBlockingInBackground", + "migrate - number was already blocked in new blocking"); + continue; + } + ContentValues values = new ContentValues(); + values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, originalNumber); + resolver.insert(BlockedNumbers.CONTENT_URI, values); + ++numMigrated; + } + LogUtil.i( + "BlockedNumbersMigrator.migrateToNewBlockingInBackground", + "migrate - migration complete. " + numMigrated + " numbers migrated."); + return true; + } + } + + @RequiresApi(VERSION_CODES.N) + @TargetApi(VERSION_CODES.N) + private static boolean isNumberInNewBlocking(ContentResolver resolver, String originalNumber) { + try (Cursor cursor = + resolver.query( + BlockedNumbers.CONTENT_URI, + new String[] {BlockedNumbers.COLUMN_ID}, + BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " = ?", + new String[] {originalNumber}, + null)) { + return cursor != null && cursor.getCount() != 0; + } + } + + /** + * Copies all of the numbers in the {@link FilteredNumberContract} block list to the {@link + * android.provider.BlockedNumberContract} block list. + * + * @param listener {@link Listener} called once the migration is complete. + * @return {@code true} if the migrate can be attempted, {@code false} otherwise. + * @throws NullPointerException if listener is null + */ + public boolean migrate(final Listener listener) { + LogUtil.i("BlockedNumbersMigrator.migrate", "migrate - start"); + if (!FilteredNumberCompat.canUseNewFiltering()) { + LogUtil.i("BlockedNumbersMigrator.migrate", "migrate - can't use new filtering"); + return false; + } + Objects.requireNonNull(listener); + new MigratorTask(listener).execute(); + return true; + } + + /** + * Listener for the operation to migrate from {@link FilteredNumberContract} blocking to {@link + * android.provider.BlockedNumberContract} blocking. + */ + public interface Listener { + + /** Called when the migration operation is finished. */ + void onComplete(); + } + + @TargetApi(VERSION_CODES.N) + private class MigratorTask extends AsyncTask<Void, Void, Boolean> { + + private final Listener listener; + + public MigratorTask(Listener listener) { + this.listener = listener; + } + + @Override + protected Boolean doInBackground(Void... params) { + LogUtil.i("BlockedNumbersMigrator.doInBackground", "migrate - start background migration"); + return migrateToNewBlockingInBackground(context.getContentResolver()); + } + + @Override + protected void onPostExecute(Boolean isSuccessful) { + LogUtil.i("BlockedNumbersMigrator.onPostExecute", "migrate - marking migration complete"); + FilteredNumberCompat.setHasMigratedToNewBlocking(context, isSuccessful); + LogUtil.i("BlockedNumbersMigrator.onPostExecute", "migrate - calling listener"); + listener.onComplete(); + } + } +} diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java new file mode 100644 index 000000000..852e7a0ed --- /dev/null +++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java @@ -0,0 +1,428 @@ +/* + * 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.blocking; + +import android.annotation.TargetApi; +import android.content.AsyncQueryHandler; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.os.UserManagerCompat; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler { + + public static final int INVALID_ID = -1; + // Id used to replace null for blocked id since ConcurrentHashMap doesn't allow null key/value. + @VisibleForTesting static final int BLOCKED_NUMBER_CACHE_NULL_ID = -1; + + @VisibleForTesting + static final Map<String, Integer> blockedNumberCache = new ConcurrentHashMap<>(); + + private static final int NO_TOKEN = 0; + private final Context context; + + public FilteredNumberAsyncQueryHandler(Context context) { + super(context.getContentResolver()); + this.context = context; + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cookie != null) { + ((Listener) cookie).onQueryComplete(token, cookie, cursor); + } + } + + @Override + protected void onInsertComplete(int token, Object cookie, Uri uri) { + if (cookie != null) { + ((Listener) cookie).onInsertComplete(token, cookie, uri); + } + } + + @Override + protected void onUpdateComplete(int token, Object cookie, int result) { + if (cookie != null) { + ((Listener) cookie).onUpdateComplete(token, cookie, result); + } + } + + @Override + protected void onDeleteComplete(int token, Object cookie, int result) { + if (cookie != null) { + ((Listener) cookie).onDeleteComplete(token, cookie, result); + } + } + + public void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) { + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + listener.onHasBlockedNumbers(false); + return; + } + startQuery( + NO_TOKEN, + new Listener() { + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0); + } + }, + FilteredNumberCompat.getContentUri(context, null), + new String[] {FilteredNumberCompat.getIdColumnName(context)}, + FilteredNumberCompat.useNewFiltering(context) + ? null + : FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER, + null, + null); + } + + /** + * Checks if the given number is blocked, calling the given {@link OnCheckBlockedListener} with + * the id for the blocked number, {@link #INVALID_ID}, or {@code null} based on the result of the + * check. + */ + public void isBlockedNumber( + final OnCheckBlockedListener listener, @Nullable final String number, String countryIso) { + if (number == null) { + listener.onCheckComplete(INVALID_ID); + return; + } + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + listener.onCheckComplete(null); + return; + } + Integer cachedId = blockedNumberCache.get(number); + if (cachedId != null) { + if (listener == null) { + return; + } + if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { + cachedId = null; + } + listener.onCheckComplete(cachedId); + return; + } + + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); + if (TextUtils.isEmpty(formattedNumber)) { + listener.onCheckComplete(INVALID_ID); + blockedNumberCache.put(number, INVALID_ID); + return; + } + + if (!UserManagerCompat.isUserUnlocked(context)) { + LogUtil.i( + "FilteredNumberAsyncQueryHandler.isBlockedNumber", + "Device locked in FBE mode, cannot access blocked number database"); + listener.onCheckComplete(INVALID_ID); + return; + } + + startQuery( + NO_TOKEN, + new Listener() { + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + /* + * In the frameworking blocking, numbers can be blocked in both e164 format + * and not, resulting in multiple rows being returned for this query. For + * example, both '16502530000' and '6502530000' can exist at the same time + * and will be returned by this query. + */ + if (cursor == null || cursor.getCount() == 0) { + blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); + listener.onCheckComplete(null); + return; + } + cursor.moveToFirst(); + // New filtering doesn't have a concept of type + if (!FilteredNumberCompat.useNewFiltering(context) + && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE)) + != FilteredNumberTypes.BLOCKED_NUMBER) { + blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); + listener.onCheckComplete(null); + return; + } + Integer blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + blockedNumberCache.put(number, blockedId); + listener.onCheckComplete(blockedId); + } + }, + FilteredNumberCompat.getContentUri(context, null), + FilteredNumberCompat.filter( + new String[] { + FilteredNumberCompat.getIdColumnName(context), + FilteredNumberCompat.getTypeColumnName(context) + }), + getIsBlockedNumberSelection(e164Number != null) + " = ?", + new String[] {formattedNumber}, + null); + } + + /** + * Synchronously check if this number has been blocked. + * + * @return blocked id. + */ + @TargetApi(VERSION_CODES.M) + @Nullable + public Integer getBlockedIdSynchronousForCalllogOnly(@Nullable String number, String countryIso) { + Assert.isWorkerThread(); + if (number == null) { + return null; + } + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + return null; + } + Integer cachedId = blockedNumberCache.get(number); + if (cachedId != null) { + if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) { + cachedId = null; + } + return cachedId; + } + + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number); + if (TextUtils.isEmpty(formattedNumber)) { + return null; + } + + try (Cursor cursor = + context + .getContentResolver() + .query( + FilteredNumberCompat.getContentUri(context, null), + FilteredNumberCompat.filter( + new String[] { + FilteredNumberCompat.getIdColumnName(context), + FilteredNumberCompat.getTypeColumnName(context) + }), + getIsBlockedNumberSelection(e164Number != null) + " = ?", + new String[] {formattedNumber}, + null)) { + /* + * In the frameworking blocking, numbers can be blocked in both e164 format + * and not, resulting in multiple rows being returned for this query. For + * example, both '16502530000' and '6502530000' can exist at the same time + * and will be returned by this query. + */ + if (cursor == null || cursor.getCount() == 0) { + blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID); + return null; + } + cursor.moveToFirst(); + int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + blockedNumberCache.put(number, blockedId); + return blockedId; + } catch (SecurityException e) { + LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly", null, e); + return null; + } + } + + @VisibleForTesting + public void clearCache() { + blockedNumberCache.clear(); + } + + /* + * TODO: b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a + * temporary workaround, determine which column of the database to query based on whether the + * number is e164 or not. + */ + private String getIsBlockedNumberSelection(boolean isE164Number) { + if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) { + return FilteredNumberCompat.getOriginalNumberColumnName(context); + } + return FilteredNumberCompat.getE164NumberColumnName(context); + } + + public void blockNumber( + final OnBlockNumberListener listener, String number, @Nullable String countryIso) { + blockNumber(listener, null, number, countryIso); + } + + /** Add a number manually blocked by the user. */ + public void blockNumber( + final OnBlockNumberListener listener, + @Nullable String normalizedNumber, + String number, + @Nullable String countryIso) { + blockNumber( + listener, + FilteredNumberCompat.newBlockNumberContentValues( + context, number, normalizedNumber, countryIso)); + } + + /** + * Block a number with specified ContentValues. Can be manually added or a restored row from + * performing the 'undo' action after unblocking. + */ + public void blockNumber(final OnBlockNumberListener listener, ContentValues values) { + blockedNumberCache.clear(); + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + listener.onBlockComplete(null); + return; + } + startInsert( + NO_TOKEN, + new Listener() { + @Override + public void onInsertComplete(int token, Object cookie, Uri uri) { + if (listener != null) { + listener.onBlockComplete(uri); + } + } + }, + FilteredNumberCompat.getContentUri(context, null), + values); + } + + /** + * Unblocks the number with the given id. + * + * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is + * unblocked. + * @param id The id of the number to unblock. + */ + public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) { + if (id == null) { + throw new IllegalArgumentException("Null id passed into unblock"); + } + unblock(listener, FilteredNumberCompat.getContentUri(context, id)); + } + + /** + * Removes row from database. + * + * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is + * unblocked. + * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}. + */ + public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) { + blockedNumberCache.clear(); + if (!FilteredNumberCompat.canAttemptBlockOperations(context)) { + if (listener != null) { + listener.onUnblockComplete(0, null); + } + return; + } + startQuery( + NO_TOKEN, + new Listener() { + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + int rowsReturned = cursor == null ? 0 : cursor.getCount(); + if (rowsReturned != 1) { + throw new SQLiteDatabaseCorruptException( + "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected."); + } + cursor.moveToFirst(); + final ContentValues values = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(cursor, values); + values.remove(FilteredNumberCompat.getIdColumnName(context)); + + startDelete( + NO_TOKEN, + new Listener() { + @Override + public void onDeleteComplete(int token, Object cookie, int result) { + if (listener != null) { + listener.onUnblockComplete(result, values); + } + } + }, + uri, + null, + null); + } + }, + uri, + null, + null, + null, + null); + } + + public interface OnCheckBlockedListener { + + /** + * Invoked after querying if a number is blocked. + * + * @param id The ID of the row if blocked, null otherwise. + */ + void onCheckComplete(Integer id); + } + + public interface OnBlockNumberListener { + + /** + * Invoked after inserting a blocked number. + * + * @param uri The uri of the newly created row. + */ + void onBlockComplete(Uri uri); + } + + public interface OnUnblockNumberListener { + + /** + * Invoked after removing a blocked number + * + * @param rows The number of rows affected (expected value 1). + * @param values The deleted data (used for restoration). + */ + void onUnblockComplete(int rows, ContentValues values); + } + + public interface OnHasBlockedNumbersListener { + + /** + * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false} + * otherwise. + */ + void onHasBlockedNumbers(boolean hasBlockedNumbers); + } + + /** Methods for FilteredNumberAsyncQueryHandler result returns. */ + private abstract static class Listener { + + protected void onQueryComplete(int token, Object cookie, Cursor cursor) {} + + protected void onInsertComplete(int token, Object cookie, Uri uri) {} + + protected void onUpdateComplete(int token, Object cookie, int result) {} + + protected void onDeleteComplete(int token, Object cookie, int result) {} + } +} diff --git a/java/com/android/dialer/blocking/FilteredNumberCompat.java b/java/com/android/dialer/blocking/FilteredNumberCompat.java new file mode 100644 index 000000000..0ee85d897 --- /dev/null +++ b/java/com/android/dialer/blocking/FilteredNumberCompat.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2016 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.blocking; + +import android.annotation.TargetApi; +import android.app.FragmentManager; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.UserManager; +import android.preference.PreferenceManager; +import android.provider.BlockedNumberContract; +import android.provider.BlockedNumberContract.BlockedNumbers; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.telecom.TelecomManager; +import android.telephony.PhoneNumberUtils; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.FilteredNumberContract.FilteredNumber; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; +import com.android.dialer.telecom.TelecomUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Compatibility class to encapsulate logic to switch between call blocking using {@link + * com.android.dialer.database.FilteredNumberContract} and using {@link + * android.provider.BlockedNumberContract}. This class should be used rather than explicitly + * referencing columns from either contract class in situations where both blocking solutions may be + * used. + */ +public class FilteredNumberCompat { + + private static Boolean canAttemptBlockOperationsForTest; + + @VisibleForTesting + public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking"; + + /** @return The column name for ID in the filtered number database. */ + public static String getIdColumnName(Context context) { + return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID; + } + + /** + * @return The column name for type in the filtered number database. Will be {@code null} for the + * framework blocking implementation. + */ + @Nullable + public static String getTypeColumnName(Context context) { + return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE; + } + + /** + * @return The column name for source in the filtered number database. Will be {@code null} for + * the framework blocking implementation + */ + @Nullable + public static String getSourceColumnName(Context context) { + return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE; + } + + /** @return The column name for the original number in the filtered number database. */ + public static String getOriginalNumberColumnName(Context context) { + return useNewFiltering(context) + ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER + : FilteredNumberColumns.NUMBER; + } + + /** + * @return The column name for country iso in the filtered number database. Will be {@code null} + * the framework blocking implementation + */ + @Nullable + public static String getCountryIsoColumnName(Context context) { + return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO; + } + + /** @return The column name for the e164 formatted number in the filtered number database. */ + public static String getE164NumberColumnName(Context context) { + return useNewFiltering(context) + ? BlockedNumbers.COLUMN_E164_NUMBER + : FilteredNumberColumns.NORMALIZED_NUMBER; + } + + /** + * @return {@code true} if the current SDK version supports using new filtering, {@code false} + * otherwise. + */ + public static boolean canUseNewFiltering() { + return VERSION.SDK_INT >= VERSION_CODES.N; + } + + /** + * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary + * migration has been performed, {@code false} otherwise. + */ + public static boolean useNewFiltering(Context context) { + return canUseNewFiltering() && hasMigratedToNewBlocking(context); + } + + /** + * @return {@code true} if the user has migrated to use {@link + * android.provider.BlockedNumberContract} blocking, {@code false} otherwise. + */ + public static boolean hasMigratedToNewBlocking(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false); + } + + /** + * Called to inform this class whether the user has fully migrated to use {@link + * android.provider.BlockedNumberContract} blocking or not. + * + * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise. + */ + public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated) + .apply(); + } + + /** + * Gets the content {@link Uri} for number filtering. + * + * @param id The optional id to append with the base content uri. + * @return The Uri for number filtering. + */ + public static Uri getContentUri(Context context, @Nullable Integer id) { + if (id == null) { + return getBaseUri(context); + } + return ContentUris.withAppendedId(getBaseUri(context), id); + } + + private static Uri getBaseUri(Context context) { + // Explicit version check to aid static analysis + return useNewFiltering(context) && VERSION.SDK_INT >= VERSION_CODES.N + ? BlockedNumbers.CONTENT_URI + : FilteredNumber.CONTENT_URI; + } + + /** + * Removes any null column names from the given projection array. This method is intended to be + * used to strip out any column names that aren't available in every version of number blocking. + * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that + * no non-existant columns are queried FilteredNumberCompat.filter(new String[] + * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()}, + * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); } + * + * @param projection The projection array. + * @return The filtered projection array. + */ + @Nullable + public static String[] filter(@Nullable String[] projection) { + if (projection == null) { + return null; + } + List<String> filtered = new ArrayList<>(); + for (String column : projection) { + if (column != null) { + filtered.add(column); + } + } + return filtered.toArray(new String[filtered.size()]); + } + + /** + * Creates a new {@link ContentValues} suitable for inserting in the filtered number table. + * + * @param number The unformatted number to insert. + * @param e164Number (optional) The number to insert formatted to E164 standard. + * @param countryIso (optional) The country iso to use to format the number. + * @return The ContentValues to insert. + * @throws NullPointerException If number is null. + */ + public static ContentValues newBlockNumberContentValues( + Context context, String number, @Nullable String e164Number, @Nullable String countryIso) { + ContentValues contentValues = new ContentValues(); + contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number)); + if (!useNewFiltering(context)) { + if (e164Number == null) { + e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + } + contentValues.put(getE164NumberColumnName(context), e164Number); + contentValues.put(getCountryIsoColumnName(context), countryIso); + contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER); + contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER); + } + return contentValues; + } + + /** + * Shows block number migration dialog if necessary. + * + * @param fragmentManager The {@link FragmentManager} used to show fragments. + * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete. + * @return boolean True if migration dialog is shown. + */ + public static boolean maybeShowBlockNumberMigrationDialog( + Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) { + if (shouldShowMigrationDialog(context)) { + LogUtil.i( + "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog", + "maybeShowBlockNumberMigrationDialog - showing migration dialog"); + MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener) + .show(fragmentManager, "MigrateBlockedNumbers"); + return true; + } + return false; + } + + private static boolean shouldShowMigrationDialog(Context context) { + return canUseNewFiltering() && !hasMigratedToNewBlocking(context); + } + + /** + * Creates the {@link Intent} which opens the blocked numbers management interface. + * + * @param context The {@link Context}. + * @return The intent. + */ + public static Intent createManageBlockedNumbersIntent(Context context) { + // Explicit version check to aid static analysis + if (canUseNewFiltering() + && hasMigratedToNewBlocking(context) + && VERSION.SDK_INT >= VERSION_CODES.N) { + return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent(); + } + Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS"); + intent.setPackage(context.getPackageName()); + return intent; + } + + /** + * Method used to determine if block operations are possible. + * + * @param context The {@link Context}. + * @return {@code true} if the app and user can block numbers, {@code false} otherwise. + */ + public static boolean canAttemptBlockOperations(Context context) { + if (canAttemptBlockOperationsForTest != null) { + return canAttemptBlockOperationsForTest; + } + + if (VERSION.SDK_INT < VERSION_CODES.N) { + // Dialer blocking, must be primary user + return context.getSystemService(UserManager.class).isSystemUser(); + } + + // Great Wall blocking, must be primary user and the default or system dialer + // TODO: check that we're the system Dialer + return TelecomUtil.isDefaultDialer(context) + && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context); + } + + static void setCanAttemptBlockOperationsForTest(boolean canAttempt) { + canAttemptBlockOperationsForTest = canAttempt; + } + + /** + * Used to determine if the call blocking settings can be opened. + * + * @param context The {@link Context}. + * @return {@code true} if the current user can open the call blocking settings, {@code false} + * otherwise. + */ + public static boolean canCurrentUserOpenBlockSettings(Context context) { + if (VERSION.SDK_INT < VERSION_CODES.N) { + // Dialer blocking, must be primary user + return context.getSystemService(UserManager.class).isSystemUser(); + } + // BlockedNumberContract blocking, verify through Contract API + return TelecomUtil.isDefaultDialer(context) + && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context); + } + + /** + * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it + * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't + * available, using this method ensures that the Dialer doesn't crash when on that screen. + * + * @param context The {@link Context}. + * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an + * exception was thrown. + */ + @TargetApi(VERSION_CODES.N) + private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) { + try { + return BlockedNumberContract.canCurrentUserBlockNumbers(context); + } catch (Exception e) { + LogUtil.e( + "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers", + "Exception while querying BlockedNumberContract", + e); + return false; + } + } +} diff --git a/java/com/android/dialer/blocking/FilteredNumberProvider.java b/java/com/android/dialer/blocking/FilteredNumberProvider.java new file mode 100644 index 000000000..5d369038c --- /dev/null +++ b/java/com/android/dialer/blocking/FilteredNumberProvider.java @@ -0,0 +1,176 @@ +/* + * 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.blocking; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.database.FilteredNumberContract; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; + +/** Filtered number content provider. */ +public class FilteredNumberProvider extends ContentProvider { + + private static final int FILTERED_NUMBERS_TABLE = 1; + private static final int FILTERED_NUMBERS_TABLE_ID = 2; + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + private static final String TAG = FilteredNumberProvider.class.getSimpleName(); + private DialerDatabaseHelper mDialerDatabaseHelper; + + @Override + public boolean onCreate() { + mDialerDatabaseHelper = Database.get(getContext()).getDatabaseHelper(getContext()); + if (mDialerDatabaseHelper == null) { + return false; + } + sUriMatcher.addURI( + FilteredNumberContract.AUTHORITY, + FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE, + FILTERED_NUMBERS_TABLE); + sUriMatcher.addURI( + FilteredNumberContract.AUTHORITY, + FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE + "/#", + FILTERED_NUMBERS_TABLE_ID); + return true; + } + + @Override + public Cursor query( + Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + final SQLiteDatabase db = mDialerDatabaseHelper.getReadableDatabase(); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + qb.setTables(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE); + final int match = sUriMatcher.match(uri); + switch (match) { + case FILTERED_NUMBERS_TABLE: + break; + case FILTERED_NUMBERS_TABLE_ID: + qb.appendWhere(FilteredNumberColumns._ID + "=" + ContentUris.parseId(uri)); + break; + default: + throw new IllegalArgumentException("Unknown uri: " + uri); + } + final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null); + if (c != null) { + c.setNotificationUri( + getContext().getContentResolver(), FilteredNumberContract.FilteredNumber.CONTENT_URI); + } else { + Log.d(TAG, "CURSOR WAS NULL"); + } + return c; + } + + @Override + public String getType(Uri uri) { + return FilteredNumberContract.FilteredNumber.CONTENT_ITEM_TYPE; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase(); + setDefaultValues(values); + long id = db.insert(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, null, values); + if (id < 0) { + return null; + } + notifyChange(uri); + return ContentUris.withAppendedId(uri, id); + } + + @VisibleForTesting + protected long getCurrentTimeMs() { + return System.currentTimeMillis(); + } + + private void setDefaultValues(ContentValues values) { + if (values.getAsString(FilteredNumberColumns.COUNTRY_ISO) == null) { + values.put(FilteredNumberColumns.COUNTRY_ISO, GeoUtil.getCurrentCountryIso(getContext())); + } + if (values.getAsInteger(FilteredNumberColumns.TIMES_FILTERED) == null) { + values.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 0); + } + if (values.getAsLong(FilteredNumberColumns.CREATION_TIME) == null) { + values.put(FilteredNumberColumns.CREATION_TIME, getCurrentTimeMs()); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + switch (match) { + case FILTERED_NUMBERS_TABLE: + break; + case FILTERED_NUMBERS_TABLE_ID: + selection = getSelectionWithId(selection, ContentUris.parseId(uri)); + break; + default: + throw new IllegalArgumentException("Unknown uri: " + uri); + } + int rows = + db.delete(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, selection, selectionArgs); + if (rows > 0) { + notifyChange(uri); + } + return rows; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + switch (match) { + case FILTERED_NUMBERS_TABLE: + break; + case FILTERED_NUMBERS_TABLE_ID: + selection = getSelectionWithId(selection, ContentUris.parseId(uri)); + break; + default: + throw new IllegalArgumentException("Unknown uri: " + uri); + } + int rows = + db.update( + DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, values, selection, selectionArgs); + if (rows > 0) { + notifyChange(uri); + } + return rows; + } + + private String getSelectionWithId(String selection, long id) { + if (TextUtils.isEmpty(selection)) { + return FilteredNumberContract.FilteredNumberColumns._ID + "=" + id; + } else { + return selection + "AND " + FilteredNumberContract.FilteredNumberColumns._ID + "=" + id; + } + } + + private void notifyChange(Uri uri) { + getContext().getContentResolver().notifyChange(uri, null); + } +} diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java new file mode 100644 index 000000000..61ecf1886 --- /dev/null +++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java @@ -0,0 +1,380 @@ +/* + * 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.blocking; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +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.support.annotation.Nullable; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.widget.Toast; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.FilteredNumberContract.FilteredNumber; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.InteractionEvent; +import com.android.dialer.util.PermissionsUtil; +import java.util.concurrent.TimeUnit; + +/** Utility to help with tasks related to filtered numbers. */ +public class FilteredNumbersUtil { + + public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking"; + public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10; + // 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"; + // 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; + + /** + * 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: + * + * <p>adb shell settings put system dialer_emergency_call_threshold_ms 60000 + */ + private static final String RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY = + "dialer_emergency_call_threshold_ms"; + + /** 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.get(context).logInteraction(InteractionEvent.Type.IMPORT_SEND_TO_VOICEMAIL); + final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler = + new FilteredNumberAsyncQueryHandler(context); + + 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 long getLastEmergencyCallTimeMillis(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, 0); + } + + public static boolean hasRecentEmergencyCall(Context context) { + if (context == null) { + return false; + } + + Long lastEmergencyCallTime = getLastEmergencyCallTimeMillis(context); + 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(context)) { + 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); + 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); + + builder.setContentIntent( + PendingIntent.getActivity( + context, + 0, + FilteredNumberCompat.createManageBlockedNumbersIntent(context), + 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(); + } + }); + } + + /** + * @param e164Number The e164 formatted version of the number, or {@code null} if such a format + * doesn't exist. + * @param number The number to attempt blocking. + * @return {@code true} if the number can be blocked, {@code false} otherwise. + */ + public static boolean canBlockNumber(Context context, String e164Number, String number) { + String blockableNumber = getBlockableNumber(context, e164Number, number); + return !TextUtils.isEmpty(blockableNumber) + && !PhoneNumberUtils.isEmergencyNumber(blockableNumber); + } + + /** + * @param e164Number The e164 formatted version of the number, or {@code null} if such a format + * doesn't exist.. + * @param number The number to attempt blocking. + * @return The version of the given number that can be blocked with the current blocking solution. + */ + @Nullable + public static String getBlockableNumber( + Context context, @Nullable String e164Number, String number) { + if (!FilteredNumberCompat.useNewFiltering(context)) { + return e164Number; + } + return TextUtils.isEmpty(e164Number) ? number : e164Number; + } + + private static long getRecentEmergencyCallThresholdMs(Context context) { + if (LogUtil.isVerboseEnabled()) { + 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; + } + } + + public interface CheckForSendToVoicemailContactListener { + + void onComplete(boolean hasSendToVoicemailContact); + } + + public interface ImportSendToVoicemailContactsListener { + + 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 { + + public static final String[] PROJECTION = {Contacts._ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER}; + + public static final int ID_COLUMN_INDEX = 0; + public static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1; + public static final int NUMBER_COLUMN_INDEX = 2; + + public static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; + } +} diff --git a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java new file mode 100644 index 000000000..76e50b38e --- /dev/null +++ b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 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.blocking; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.content.DialogInterface.OnShowListener; +import android.os.Bundle; +import android.view.View; +import com.android.dialer.blocking.BlockedNumbersMigrator.Listener; +import java.util.Objects; + +/** + * Dialog fragment shown to users when they need to migrate to use {@link + * android.provider.BlockedNumberContract} for blocking. + */ +public class MigrateBlockedNumbersDialogFragment extends DialogFragment { + + private BlockedNumbersMigrator mBlockedNumbersMigrator; + private BlockedNumbersMigrator.Listener mMigrationListener; + + /** + * Creates a new MigrateBlockedNumbersDialogFragment. + * + * @param blockedNumbersMigrator The {@link BlockedNumbersMigrator} which will be used to migrate + * the numbers. + * @param migrationListener The {@link BlockedNumbersMigrator.Listener} to call when the migration + * is complete. + * @return The new MigrateBlockedNumbersDialogFragment. + * @throws NullPointerException if blockedNumbersMigrator or migrationListener are {@code null}. + */ + public static DialogFragment newInstance( + BlockedNumbersMigrator blockedNumbersMigrator, + BlockedNumbersMigrator.Listener migrationListener) { + MigrateBlockedNumbersDialogFragment fragment = new MigrateBlockedNumbersDialogFragment(); + fragment.mBlockedNumbersMigrator = Objects.requireNonNull(blockedNumbersMigrator); + fragment.mMigrationListener = Objects.requireNonNull(migrationListener); + return fragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreateDialog(savedInstanceState); + AlertDialog dialog = + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.migrate_blocked_numbers_dialog_title) + .setMessage(R.string.migrate_blocked_numbers_dialog_message) + .setPositiveButton(R.string.migrate_blocked_numbers_dialog_allow_button, null) + .setNegativeButton(R.string.migrate_blocked_numbers_dialog_cancel_button, null) + .create(); + // The Dialog's buttons aren't available until show is called, so an OnShowListener + // is used to set the positive button callback. + dialog.setOnShowListener( + new OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + final AlertDialog alertDialog = (AlertDialog) dialog; + alertDialog + .getButton(AlertDialog.BUTTON_POSITIVE) + .setOnClickListener(newPositiveButtonOnClickListener(alertDialog)); + } + }); + return dialog; + } + + /* + * Creates a new View.OnClickListener to be used as the positive button in this dialog. The + * OnClickListener will grey out the dialog's positive and negative buttons while the migration + * is underway, and close the dialog once the migrate is complete. + */ + private View.OnClickListener newPositiveButtonOnClickListener(final AlertDialog alertDialog) { + return new View.OnClickListener() { + @Override + public void onClick(View v) { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); + mBlockedNumbersMigrator.migrate( + new Listener() { + @Override + public void onComplete() { + alertDialog.dismiss(); + mMigrationListener.onComplete(); + } + }); + } + }; + } + + @Override + public void onPause() { + // The dialog is dismissed and state is cleaned up onPause, i.e. rotation. + dismiss(); + mBlockedNumbersMigrator = null; + mMigrationListener = null; + super.onPause(); + } +} diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..2ccc89d24 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png Binary files differnew file mode 100644 index 000000000..dc0c995c1 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png Binary files differnew file mode 100644 index 000000000..919a872e0 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..ec1b33f0e --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png Binary files differnew file mode 100644 index 000000000..70b82d6c1 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png Binary files differnew file mode 100644 index 000000000..dc0c995c1 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..7aba97b65 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png Binary files differnew file mode 100644 index 000000000..18e7764ab --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png Binary files differnew file mode 100644 index 000000000..aed766804 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..fddfa54b8 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png Binary files differnew file mode 100644 index 000000000..aed766804 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png Binary files differnew file mode 100644 index 000000000..f7cfacbd4 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png Binary files differnew file mode 100644 index 000000000..0378d1bed --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png Binary files differnew file mode 100644 index 000000000..855e59015 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png Binary files differnew file mode 100644 index 000000000..7ef0d7afc --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png diff --git a/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml b/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml new file mode 100644 index 000000000..09d7989e8 --- /dev/null +++ b/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ 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 + --> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item> + <shape android:shape="oval"> + <solid android:color="@color/blocked_contact_background"/> + <size + android:height="24dp" + android:width="24dp"/> + </shape> + </item> + + <item + android:drawable="@drawable/ic_report_24dp" + android:gravity="center" + android:height="18dp" + android:width="18dp"/> + +</layer-list> diff --git a/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml b/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml new file mode 100644 index 000000000..82e8d80b3 --- /dev/null +++ b/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2008 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="25dp" + android:orientation="vertical"> + <TextView + android:id="@+id/block_details" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:text="@string/block_report_number_alert_details" + android:textColor="@color/block_report_spam_primary_text_color" + android:textSize="@dimen/blocked_report_spam_primary_text_size"/> + + <CheckBox + android:id="@+id/report_number_as_spam_action" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/checkbox_report_as_spam_action" + android:textSize="@dimen/blocked_report_spam_primary_text_size"/> +</LinearLayout> diff --git a/java/com/android/dialer/blocking/res/values/colors.xml b/java/com/android/dialer/blocking/res/values/colors.xml new file mode 100644 index 000000000..d1a567d9e --- /dev/null +++ b/java/com/android/dialer/blocking/res/values/colors.xml @@ -0,0 +1,24 @@ +<!-- + ~ Copyright (C) 2012 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 +--> +<resources> + + <!-- 87% black --> + <color name="block_report_spam_primary_text_color">#de000000</color> + + <!-- Note, this is also used by InCallUi. --> + <color name="blocked_contact_background">#A52714</color> + +</resources> diff --git a/java/com/android/dialer/blocking/res/values/dimens.xml b/java/com/android/dialer/blocking/res/values/dimens.xml new file mode 100644 index 000000000..cd7cfe2fd --- /dev/null +++ b/java/com/android/dialer/blocking/res/values/dimens.xml @@ -0,0 +1,18 @@ +<!-- + ~ Copyright (C) 2012 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 +--> +<resources> + <dimen name="blocked_report_spam_primary_text_size">16sp</dimen> +</resources> diff --git a/java/com/android/dialer/blocking/res/values/strings.xml b/java/com/android/dialer/blocking/res/values/strings.xml new file mode 100644 index 000000000..8abff4561 --- /dev/null +++ b/java/com/android/dialer/blocking/res/values/strings.xml @@ -0,0 +1,122 @@ +<!-- + ~ Copyright (C) 2016 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 + --> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Title for dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=30]--> + <string name="migrate_blocked_numbers_dialog_title">New, simplified blocking</string> + + <!-- Body text for dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=NONE]--> + <string name="migrate_blocked_numbers_dialog_message">To better protect you, Phone needs to change how blocking works. Your blocked numbers will now stop both calls and texts and may be shared with other apps.</string> + + <!-- Positive confirmation button for the dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=NONE]--> + <string name="migrate_blocked_numbers_dialog_allow_button">Allow</string> + + <!-- Do not translate --> + <string name="migrate_blocked_numbers_dialog_cancel_button">@android:string/cancel</string> + + <!-- Confirmation dialog title for blocking a number. [CHAR LIMIT=NONE] --> + <string name="block_number_confirmation_title">Block + <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>?</string> + + <!-- Confirmation dialog message for blocking a number with visual voicemail active. + [CHAR LIMIT=NONE] --> + <string name="block_number_confirmation_message_vvm"> + Calls from this number will be blocked and voicemails will be automatically deleted. + </string> + + <!-- Confirmation dialog message for blocking a number with no visual voicemail. + [CHAR LIMIT=NONE] --> + <string name="block_number_confirmation_message_no_vvm"> + Calls from this number will be blocked, but the caller may still be able to leave you voicemails. + </string> + + <!-- Confirmation dialog message for blocking a number with new filtering enabled. + [CHAR LIMIT=NONE] --> + <string name="block_number_confirmation_message_new_filtering"> + You will no longer receive calls or texts from this number. + </string> + + <!-- Block number alert dialog button [CHAR LIMIT=32] --> + <string name="block_number_ok">BLOCK</string> + + <!-- Confirmation dialog for unblocking a number. [CHAR LIMIT=NONE] --> + <string name="unblock_number_confirmation_title">Unblock + <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>?</string> + + <!-- Unblock number alert dialog button [CHAR LIMIT=32] --> + <string name="unblock_number_ok">UNBLOCK</string> + + <!-- Error message shown when user tries to add invalid number to the block list. + [CHAR LIMIT=64] --> + <string name="invalidNumber"><xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> + is invalid.</string> + + <!-- Text for snackbar to undo blocking a number. [CHAR LIMIT=64] --> + <string name="snackbar_number_blocked"> + <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> blocked</string> + + <!-- Text for snackbar to undo unblocking a number. [CHAR LIMIT=64] --> + <string name="snackbar_number_unblocked"> + <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> + unblocked</string> + + <!-- Text for undo button in snackbar for blocking/unblocking number. [CHAR LIMIT=10] --> + <string name="block_number_undo">UNDO</string> + + <!-- Error toast message for when send to voicemail import fails. [CHAR LIMIT=40] --> + <string name="send_to_voicemail_import_failed">Import failed</string> + + <!-- Title of notification telling the user that call blocking has been temporarily disabled. + [CHAR LIMIT=56] --> + <string name="call_blocking_disabled_notification_title"> + Call blocking disabled for 48 hours + </string> + + <!-- Text for notification which provides the reason that call blocking has been temporarily + disabled. Namely, we disable call blocking after an emergency call in case of return + phone calls made by emergency services. [CHAR LIMIT=64] --> + <string name="call_blocking_disabled_notification_text"> + Disabled because an emergency call was made. + </string> + + <!-- Title of alert dialog after clicking on Block/report as spam. [CHAR LIMIT=100] --> + <string name="block_report_number_alert_title">Block <xliff:g id="number">%1$s</xliff:g>?</string> + + <!-- Text in alert dialog after clicking on Block/report as spam. [CHAR LIMIT=100] --> + <string name="block_report_number_alert_details">You will no longer receive calls from this number.</string> + + <!-- Text in alert dialog after clicking on Block. [CHAR LIMIT=100] --> + <string name="block_number_alert_details"><xliff:g id="text">%1$s</xliff:g> This call will be reported as spam.</string> + + <!-- Text in alert dialog after clicking on Unblock. [CHAR LIMIT=100] --> + <string name="unblock_number_alert_details">This number will be unblocked and reported as not spam. Future calls won\'t be identified as spam.</string> + + <!-- Title of alert dialog after clicking on Unblock. [CHAR LIMIT=100] --> + <string name="unblock_report_number_alert_title">Unblock <xliff:g id="number">%1$s</xliff:g>?</string> + + <!-- Report not spam number alert dialog button [CHAR LIMIT=32] --> + <string name="report_not_spam_alert_button">Report</string> + + <!-- Title of alert dialog after clicking on Report as not spam. [CHAR LIMIT=100] --> + <string name="report_not_spam_alert_title">Report a mistake?</string> + + <!-- Text in alert dialog after clicking on Report as not spam. [CHAR LIMIT=100] --> + <string name="report_not_spam_alert_details">Future calls from <xliff:g id="number">%1$s</xliff:g> will no longer be identified as spam.</string> + + <!-- Label for checkbox in the Alert dialog to allow the user to report the number as spam as well. [CHAR LIMIT=30] --> + <string name="checkbox_report_as_spam_action">Report call as spam</string> + +</resources> |