Merge "Implemented blocked numbers migration" into nyc-dev
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index d2cda92..6c8d770 100644
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -375,8 +375,9 @@
         int resId = view.getId();
         if (resId == R.id.call_detail_action_block) {
             FilteredNumberCompat
-                    .showBlockNumberDialogFlow(mBlockedNumberId, mNumber, mDetails.countryIso,
-                            mDisplayNumber, R.id.call_detail, getFragmentManager(), this);
+                    .showBlockNumberDialogFlow(mContext.getContentResolver(), mBlockedNumberId,
+                            mNumber, mDetails.countryIso, mDisplayNumber, R.id.call_detail,
+                            getFragmentManager(), this);
         } else if (resId == R.id.call_detail_action_copy) {
             ClipboardUtils.copyText(mContext, null, mNumber, true);
         } else if (resId == R.id.call_detail_action_edit_before_call) {
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index de9bf77..750914b 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -373,8 +373,8 @@
         int resId = item.getItemId();
         if (resId == R.id.context_menu_block_number) {
             FilteredNumberCompat
-                    .showBlockNumberDialogFlow(blockId, number, countryIso, displayNumber,
-                            R.id.floating_action_button_container,
+                    .showBlockNumberDialogFlow(mContext.getContentResolver(), blockId, number,
+                            countryIso, displayNumber, R.id.floating_action_button_container,
                             ((Activity) mContext).getFragmentManager(),
                             mFilteredNumberDialogCallback);
             return true;
diff --git a/src/com/android/dialer/compat/FilteredNumberCompat.java b/src/com/android/dialer/compat/FilteredNumberCompat.java
index 743bf28..c74ff9d 100644
--- a/src/com/android/dialer/compat/FilteredNumberCompat.java
+++ b/src/com/android/dialer/compat/FilteredNumberCompat.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Preconditions;
 
 import android.app.FragmentManager;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.net.Uri;
@@ -35,8 +36,8 @@
 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
 import com.android.dialer.filterednumber.BlockNumberDialogFragment;
 import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback;
+import com.android.dialer.filterednumber.BlockedNumbersMigrator;
 import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment;
-import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment.Listener;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -239,20 +240,24 @@
      * @param callback (optional) The {@link Callback} to call when the block or unblock operation
      * is complete.
      */
-    public static void showBlockNumberDialogFlow(final Integer blockId, final String number,
-            final String countryIso, final String displayNumber, final Integer parentViewId,
+    public static void showBlockNumberDialogFlow(final ContentResolver contentResolver,
+            final Integer blockId, final String number, final String countryIso,
+            final String displayNumber, final Integer parentViewId,
             final FragmentManager fragmentManager, @Nullable final Callback callback) {
         // If the user is blocking a number and isn't using the framework solution when they
         // should be, show the migration dialog
         if (shouldShowMigrationDialog(blockId == null)) {
-            MigrateBlockedNumbersDialogFragment.newInstance(new Listener() {
-                @Override
-                public void onComplete() {
-                    BlockNumberDialogFragment
-                            .show(null, number, countryIso, displayNumber, parentViewId,
-                                    fragmentManager, callback);
-                }
-            }).show(fragmentManager, "MigrateBlockedNumbers");
+            MigrateBlockedNumbersDialogFragment
+                    .newInstance(new BlockedNumbersMigrator(contentResolver),
+                            new BlockedNumbersMigrator.Listener() {
+                                @Override
+                                public void onComplete() {
+                                    BlockNumberDialogFragment
+                                            .show(null, number, countryIso, displayNumber,
+                                                    parentViewId,
+                                                    fragmentManager, callback);
+                                }
+                            }).show(fragmentManager, "MigrateBlockedNumbers");
             return;
         }
         BlockNumberDialogFragment
diff --git a/src/com/android/dialer/filterednumber/BlockedNumbersMigrator.java b/src/com/android/dialer/filterednumber/BlockedNumbersMigrator.java
new file mode 100644
index 0000000..de201d9
--- /dev/null
+++ b/src/com/android/dialer/filterednumber/BlockedNumbersMigrator.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.filterednumber;
+
+import com.google.common.base.Preconditions;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.support.annotation.Nullable;
+
+import com.android.dialer.compat.BlockedNumbersSdkCompat;
+import com.android.dialer.compat.FilteredNumberCompat;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+
+/**
+ * Class which should be used to migrate numbers from {@link FilteredNumberContract} blocking to
+ * {@link android.provider.BlockedNumberContract} blocking.
+ */
+public class BlockedNumbersMigrator {
+
+    /**
+     * 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();
+    }
+
+    private final ContentResolver mContentResolver;
+
+    /**
+     * Creates a new BlockedNumbersMigrate, using the given {@link ContentResolver} to perform
+     * queries against the blocked numbers tables.
+     *
+     * @param contentResolver The ContentResolver
+     * @throws NullPointerException if contentResolver is null
+     */
+    public BlockedNumbersMigrator(ContentResolver contentResolver) {
+        mContentResolver = Preconditions.checkNotNull(contentResolver);
+    }
+
+    /**
+     * 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) {
+        if (!FilteredNumberCompat.canUseNewFiltering()) {
+            return false;
+        }
+        Preconditions.checkNotNull(listener);
+        new AsyncTask<Void, Void, Boolean>() {
+            @Override
+            protected Boolean doInBackground(Void... params) {
+                return migrateToNewBlockingInBackground(mContentResolver);
+            }
+
+            @Override
+            protected void onPostExecute(Boolean isSuccessful) {
+                FilteredNumberCompat.setHasMigratedToNewBlocking(isSuccessful);
+                listener.onComplete();
+            }
+        }.execute();
+        return true;
+    }
+
+    private static boolean migrateToNewBlockingInBackground(ContentResolver resolver) {
+        try (Cursor cursor = resolver.query(FilteredNumber.CONTENT_URI,
+                new String[]{FilteredNumberColumns.NUMBER}, null, null, null)) {
+            if (cursor == null) {
+                return false;
+            }
+
+            while (cursor.moveToNext()) {
+                String originalNumber = cursor
+                        .getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER));
+                if (isNumberInNewBlocking(resolver, originalNumber)) {
+                    continue;
+                }
+                ContentValues values = new ContentValues();
+                values.put(BlockedNumbersSdkCompat.COLUMN_ORIGINAL_NUMBER, originalNumber);
+                resolver.insert(BlockedNumbersSdkCompat.CONTENT_URI, values);
+            }
+            return true;
+        }
+    }
+
+    private static boolean isNumberInNewBlocking(ContentResolver resolver, String originalNumber) {
+        try (Cursor cursor = resolver.query(BlockedNumbersSdkCompat.CONTENT_URI,
+                new String[]{BlockedNumbersSdkCompat._ID},
+                BlockedNumbersSdkCompat.COLUMN_ORIGINAL_NUMBER + " = ?",
+                new String[] {originalNumber}, null)) {
+            return cursor != null && cursor.getCount() != 0;
+        }
+    }
+}
diff --git a/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragment.java b/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragment.java
index 3783519..efcb505 100644
--- a/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragment.java
+++ b/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragment.java
@@ -24,7 +24,6 @@
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.os.Bundle;
-import android.support.annotation.Nullable;
 
 import com.android.dialer.R;
 
@@ -34,31 +33,23 @@
  */
 public class MigrateBlockedNumbersDialogFragment extends DialogFragment {
 
-    /**
-     * Listener for the operation to migrate from
-     * {@link com.android.dialer.database.FilteredNumberContract} blocking to
-     * {@link android.provider.BlockedNumberContract} blocking.
-     */
-    public interface Listener {
-
-        /**
-         * Callback called when the migration operation is finished.
-         */
-        void onComplete();
-    }
-
-    private Listener mMigrationListener;
+    private BlockedNumbersMigrator mBlockedNumbersMigrator;
+    private BlockedNumbersMigrator.Listener mMigrationListener;
 
     /**
      * Creates a new MigrateBlockedNumbersDialogFragment.
      *
-     * @param migrationListener The {@link Listener} to call when the
+     * @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 migrationListener is {@code null}.
+     * @return The new MigrateBlockedNumbersDialogFragment.
+     * @throws NullPointerException if blockedNumbersMigrator or migrationListener are {@code null}.
      */
-    public static DialogFragment newInstance(Listener migrationListener) {
+    public static DialogFragment newInstance(BlockedNumbersMigrator blockedNumbersMigrator,
+            BlockedNumbersMigrator.Listener migrationListener) {
         MigrateBlockedNumbersDialogFragment fragment = new MigrateBlockedNumbersDialogFragment();
+        fragment.mBlockedNumbersMigrator = Preconditions.checkNotNull(blockedNumbersMigrator);
         fragment.mMigrationListener = Preconditions.checkNotNull(migrationListener);
         return fragment;
     }
@@ -79,10 +70,7 @@
         return new OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int which) {
-                // TODO(maxwelb): Perform actual migration, call
-                // FilteredNumberCompat#hasMigratedToNewBlocking
-
-                mMigrationListener.onComplete();
+                mBlockedNumbersMigrator.migrate(mMigrationListener);
             }
         };
     }
@@ -91,6 +79,7 @@
     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/tests/src/com/android/dialer/filterednumber/BlockedNumbersMigratorTest.java b/tests/src/com/android/dialer/filterednumber/BlockedNumbersMigratorTest.java
new file mode 100644
index 0000000..565c206
--- /dev/null
+++ b/tests/src/com/android/dialer/filterednumber/BlockedNumbersMigratorTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.filterednumber;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.android.dialer.compat.FilteredNumberCompat;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class BlockedNumbersMigratorTest extends AndroidTestCase {
+
+    private static final String NUMBER = "6502530000";
+    private static final String NUMBER1 = "6502530001";
+    private static final String NUMBER2 = "6502530002";
+
+    @Mock private BlockedNumbersMigrator.Listener mListener;
+    private final MockContentResolver mContentResolver = new MockContentResolver();
+    private final MockContentProvider mContentProvider = new MockContentProvider();
+    private BlockedNumbersMigrator mMigrator;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        FilteredNumberCompat.setIsEnabledForTest(true);
+        mContentResolver.addProvider(FilteredNumberContract.AUTHORITY, mContentProvider);
+        mContentResolver.addProvider(BlockedNumberContract.AUTHORITY, mContentProvider);
+        mMigrator = new BlockedNumbersMigrator(mContentResolver);
+    }
+
+    public void testConstructor_NullContentResolver() {
+        try {
+            new BlockedNumbersMigrator(null);
+            fail();
+        } catch (NullPointerException e) {}
+    }
+
+    public void testMigrate_M() {
+        if (CompatUtils.isNCompatible()) {
+            return;
+        }
+        assertFalse(mMigrator.migrate(mListener));
+    }
+
+    public void testMigrate_N_Disabled() {
+        if (!CompatUtils.isNCompatible()) {
+            return;
+        }
+        FilteredNumberCompat.setIsEnabledForTest(false);
+        assertFalse(mMigrator.migrate(mListener));
+    }
+
+    public void testMigrate_N_NullListener() {
+        if (!CompatUtils.isNCompatible()) {
+            return;
+        }
+        try {
+            mMigrator.migrate(null);
+            fail();
+        } catch (NullPointerException e) {}
+    }
+
+    public void testMigrate_N() throws InterruptedException {
+        if (!CompatUtils.isNCompatible()) {
+            return;
+        }
+        mContentProvider.expectQuery(FilteredNumber.CONTENT_URI)
+                .withProjection(FilteredNumberColumns.NUMBER).returnRow(NUMBER).returnRow(NUMBER1)
+                .returnRow(NUMBER2);
+
+        setUpNewBlockedNumberExpectations(mContentProvider, NUMBER, 0);
+        setUpNewBlockedNumberExpectations(mContentProvider, NUMBER1, 1);
+        setUpNewBlockedNumberExpectations(mContentProvider, NUMBER2, 2);
+
+        MigrationListener listener = new MigrationListener();
+        assertTrue(mMigrator.migrate(listener));
+        listener.waitForCallback();
+        assertTrue(FilteredNumberCompat.hasMigratedToNewBlocking());
+        mContentProvider.verify();
+    }
+
+    public void testMigrate_N_AlreadyBlocked() throws InterruptedException {
+        if (!CompatUtils.isNCompatible()) {
+            return;
+        }
+        mContentProvider.expectQuery(FilteredNumber.CONTENT_URI)
+                .withProjection(FilteredNumberColumns.NUMBER).returnRow(NUMBER);
+        mContentProvider.expectQuery(BlockedNumbers.CONTENT_URI)
+                .withProjection(BlockedNumbers.COLUMN_ID)
+                .withSelection(BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " = ?", NUMBER).returnRow(0);
+        // No expectation for insert into BlockedNumbers.CONTENT_URI because it's already there
+
+        MigrationListener listener = new MigrationListener();
+        assertTrue(mMigrator.migrate(listener));
+        listener.waitForCallback();
+        assertTrue(FilteredNumberCompat.hasMigratedToNewBlocking());
+        mContentProvider.verify();
+    }
+
+    private void setUpNewBlockedNumberExpectations(MockContentProvider contentProvider,
+            String number, int returnId) {
+        contentProvider.expectQuery(BlockedNumbers.CONTENT_URI)
+                .withProjection(BlockedNumbers.COLUMN_ID)
+                .withSelection(BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " = ?", number).returnEmptyCursor();
+        contentProvider.expectInsert(BlockedNumbers.CONTENT_URI,
+                createBlockedNumberInsertValues(number),
+                ContentUris.withAppendedId(BlockedNumbers.CONTENT_URI, returnId));
+    }
+
+    private ContentValues createBlockedNumberInsertValues(String number) {
+        ContentValues values = new ContentValues();
+        values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number);
+        return values;
+    }
+
+    private static class MigrationListener implements BlockedNumbersMigrator.Listener {
+
+        private final CountDownLatch mOnCompleteCalled = new CountDownLatch(1);
+
+        @Override
+        public void onComplete() {
+            mOnCompleteCalled.countDown();
+        }
+
+        public void waitForCallback() throws InterruptedException {
+            if (!mOnCompleteCalled.await(5000, TimeUnit.MILLISECONDS)) {
+                throw new IllegalStateException("Waiting on callback timed out.");
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentInstrumentationTest.java b/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentInstrumentationTest.java
index 4cf648a..16b6f3f 100644
--- a/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentInstrumentationTest.java
+++ b/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentInstrumentationTest.java
@@ -18,6 +18,7 @@
 
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.AlertDialog;
 import android.app.DialogFragment;
@@ -25,7 +26,7 @@
 import android.test.ActivityInstrumentationTestCase2;
 
 import com.android.dialer.DialtactsActivity;
-import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment.Listener;
+import com.android.dialer.filterednumber.BlockedNumbersMigrator.Listener;
 
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -39,6 +40,7 @@
 
     private static final String SHOW_TAG = "ShowTag";
 
+    @Mock private BlockedNumbersMigrator mBlockedNumbersMigrator;
     @Mock private Listener mListener;
     private DialtactsActivity mActivity;
     private DialogFragment mMigrateDialogFragment;
@@ -52,7 +54,8 @@
         super.setUp();
         MockitoAnnotations.initMocks(this);
         mActivity = getActivity();
-        mMigrateDialogFragment = MigrateBlockedNumbersDialogFragment.newInstance(mListener);
+        mMigrateDialogFragment = MigrateBlockedNumbersDialogFragment
+                .newInstance(mBlockedNumbersMigrator, mListener);
         getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
@@ -67,7 +70,7 @@
     }
 
     public void testDialogPositiveButtonPress() {
-        doNothing().when(mListener).onComplete();
+        when(mBlockedNumbersMigrator.migrate(mListener)).thenReturn(true);
         getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
@@ -78,6 +81,6 @@
         getInstrumentation().waitForIdleSync();
         // Dialog was dismissed
         assertNull(mMigrateDialogFragment.getDialog());
-        verify(mListener).onComplete();
+        verify(mBlockedNumbersMigrator).migrate(mListener);
     }
 }
diff --git a/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentTest.java b/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentTest.java
index c8c7820..1b419ce 100644
--- a/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentTest.java
+++ b/tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentTest.java
@@ -19,7 +19,10 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment.Listener;
+import com.android.dialer.filterednumber.BlockedNumbersMigrator.Listener;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 
 /**
@@ -28,18 +31,31 @@
 @SmallTest
 public class MigrateBlockedNumbersDialogFragmentTest extends AndroidTestCase {
 
-    public void testNewInstance_NullMigrationListener() {
+    @Mock private BlockedNumbersMigrator mBlockedNumbersMigrator;
+    @Mock private Listener mListener;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+    }
+
+    public void testNewInstance_NullMigrator() {
         try {
-            MigrateBlockedNumbersDialogFragment.newInstance(null);
+            MigrateBlockedNumbersDialogFragment.newInstance(null, mListener);
+            fail();
+        } catch (NullPointerException e) {}
+    }
+
+    public void testNewInstance_NullListener() {
+        try {
+            MigrateBlockedNumbersDialogFragment.newInstance(mBlockedNumbersMigrator, null);
             fail();
         } catch (NullPointerException e) {}
     }
 
     public void testNewInstance_WithListener() {
-        assertNotNull(MigrateBlockedNumbersDialogFragment.newInstance(
-                new Listener() {
-                    @Override
-                    public void onComplete() {}
-                }));
+        assertNotNull(MigrateBlockedNumbersDialogFragment.newInstance(mBlockedNumbersMigrator,
+                mListener));
     }
 }