summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/blocking
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/dialer/blocking
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
Diffstat (limited to 'java/com/android/dialer/blocking')
-rw-r--r--java/com/android/dialer/blocking/AndroidManifest.xml13
-rw-r--r--java/com/android/dialer/blocking/BlockNumberDialogFragment.java328
-rw-r--r--java/com/android/dialer/blocking/BlockReportSpamDialogs.java305
-rw-r--r--java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java110
-rw-r--r--java/com/android/dialer/blocking/BlockedNumbersMigrator.java159
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java428
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberCompat.java320
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberProvider.java176
-rw-r--r--java/com/android/dialer/blocking/FilteredNumbersUtil.java380
-rw-r--r--java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java113
-rw-r--r--java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.pngbin0 -> 478 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.pngbin0 -> 240 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.pngbin0 -> 312 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.pngbin0 -> 335 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.pngbin0 -> 174 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.pngbin0 -> 240 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.pngbin0 -> 665 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.pngbin0 -> 272 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.pngbin0 -> 973 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.pngbin0 -> 522 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.pngbin0 -> 1295 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.pngbin0 -> 450 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.pngbin0 -> 649 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable/blocked_contact.xml36
-rw-r--r--java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml36
-rw-r--r--java/com/android/dialer/blocking/res/values/colors.xml24
-rw-r--r--java/com/android/dialer/blocking/res/values/dimens.xml18
-rw-r--r--java/com/android/dialer/blocking/res/values/strings.xml122
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
new file mode 100644
index 000000000..2ccc89d24
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..dc0c995c1
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..919a872e0
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png
Binary files differ
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
new file mode 100644
index 000000000..ec1b33f0e
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..70b82d6c1
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..dc0c995c1
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png
Binary files differ
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
new file mode 100644
index 000000000..7aba97b65
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..18e7764ab
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..aed766804
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png
Binary files differ
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
new file mode 100644
index 000000000..fddfa54b8
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..aed766804
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..f7cfacbd4
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png
Binary files differ
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
new file mode 100644
index 000000000..0378d1bed
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..855e59015
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png
Binary files differ
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
new file mode 100644
index 000000000..7ef0d7afc
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png
Binary files differ
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>