Add tests for DeviceLocalContactsFilterProvider.

Bug 28637652
Change-Id: I7c1773610217765794ad4a3b326a07eca62def47
diff --git a/proguard.flags b/proguard.flags
index f1e609b..d9dad57 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -19,6 +19,7 @@
 -keep class com.android.contacts.common.database.NoNullCursorAsyncQueryHandler { *; }
 -keep class com.android.contacts.common.format.FormatUtils { *; }
 -keep class com.android.contacts.common.format.TextHighlighter { *; }
+-keep class com.android.contacts.common.list.ContactListFilter { *; }
 -keep class com.android.contacts.common.list.ContactListItemView { *; }
 -keep class com.android.contacts.common.list.ContactsSectionIndexer { *; }
 -keep class com.android.contacts.common.location.CountryDetector { *; }
@@ -66,6 +67,11 @@
 -keep class com.android.contacts.common.util.NameConverter { *; }
 -keep class com.android.contacts.common.util.SearchUtil { *; }
 -keep class com.android.contacts.common.util.SearchUtil$* { *; }
+-keep class com.android.contacts.common.util.DeviceAccountFilter { *; }
+-keep class com.android.contacts.common.util.DeviceAccountFilter$* { *; }
+-keep class com.android.contacts.common.util.DeviceAccountPresentationValues { *; }
+-keep class com.android.contacts.common.util.DeviceAccountPresentationValues$* { *; }
+-keep class com.android.contacts.common.util.DeviceLocalContactsFilterProvider { *; }
 -keep class com.android.contacts.ContactsApplication { *; }
 -keep class com.android.contacts.ContactSaveService { *; }
 -keep class com.android.contacts.ContactSaveService$* { *; }
@@ -86,11 +92,9 @@
 -keep class com.google.common.collect.Multimap { *; }
 -keep class com.google.common.collect.Sets { *; }
 
-# Any class or method annotated with NeededForTesting or NeededForReflection.
--keep @com.android.contacts.common.testing.NeededForTesting class *
+# Any class or method annotated with NeededForReflection.
 -keep @com.android.contacts.test.NeededForReflection class *
 -keepclassmembers class * {
-@com.android.contacts.common.testing.NeededForTesting *;
 @com.android.contacts.test.NeededForReflection *;
 }
 
diff --git a/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java b/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java
index 98b865d..1d06a43 100644
--- a/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java
+++ b/src/com/android/contacts/common/util/DeviceLocalContactsFilterProvider.java
@@ -26,8 +26,11 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract;
+import android.support.annotation.Keep;
+import android.support.annotation.VisibleForTesting;
 
 import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.test.NeededForReflection;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -74,17 +77,9 @@
 
     @Override
     public CursorLoader onCreateLoader(int i, Bundle bundle) {
-        final AccountManager accountManager = (AccountManager) mContext
-                .getSystemService(Context.ACCOUNT_SERVICE);
-        final Set<String> knownTypes = new HashSet<>();
-        final Account[] accounts = accountManager.getAccounts();
-        for (Account account : accounts) {
-            if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0) {
-                knownTypes.add(account.type);
-            }
+        if (mKnownAccountTypes == null) {
+            initKnownAccountTypes();
         }
-        mKnownAccountTypes = knownTypes.toArray(new String[knownTypes.size()]);
-
         return new CursorLoader(mContext, getUri(), PROJECTION, getSelection(),
                 getSelectionArgs(), null);
     }
@@ -128,6 +123,25 @@
     public void onLoaderReset(Loader<Cursor> loader) {
     }
 
+    @Keep
+    @VisibleForTesting
+    public void setKnownAccountTypes(String... accountTypes) {
+        mKnownAccountTypes = accountTypes;
+    }
+
+    private void initKnownAccountTypes() {
+        final AccountManager accountManager = (AccountManager) mContext
+                .getSystemService(Context.ACCOUNT_SERVICE);
+        final Set<String> knownTypes = new HashSet<>();
+        final Account[] accounts = accountManager.getAccounts();
+        for (Account account : accounts) {
+            if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0) {
+                knownTypes.add(account.type);
+            }
+        }
+        mKnownAccountTypes = knownTypes.toArray(new String[knownTypes.size()]);
+    }
+
     private Uri getUri() {
         final Uri.Builder builder = ContactsContract.RawContacts.CONTENT_URI.buildUpon();
         if (mKnownAccountTypes == null || mKnownAccountTypes.length == 0) {
diff --git a/tests/src/com/android/contacts/common/util/DeviceLocalContactsFilterProviderTests.java b/tests/src/com/android/contacts/common/util/DeviceLocalContactsFilterProviderTests.java
new file mode 100644
index 0000000..d776ab8
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/DeviceLocalContactsFilterProviderTests.java
@@ -0,0 +1,221 @@
+/*
+ * 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.contacts.common.util;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.Nullable;
+import android.test.LoaderTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.mockito.Mockito.when;
+
+@SmallTest
+public class DeviceLocalContactsFilterProviderTests extends LoaderTestCase {
+
+    // Basic smoke test that just checks that it doesn't throw when loading from CP2. We don't
+    // care what CP2 actually contains for this.
+    public void testShouldNotCrash() {
+        final DeviceLocalContactsFilterProvider sut = new DeviceLocalContactsFilterProvider(
+                getContext(), DeviceAccountFilter.ONLY_NULL);
+        final CursorLoader loader = sut.onCreateLoader(0, null);
+        getLoaderResultSynchronously(loader);
+        // We didn't throw so it passed
+    }
+
+    public void testCreatesNoFiltersIfNoRawContactsHaveDeviceAccountType() {
+        final DeviceLocalContactsFilterProvider sut = createWithFilterAndLoaderResult(
+                DeviceAccountFilter.ONLY_NULL, queryResult(
+                        "user", "com.example",
+                        "user", "com.example",
+                        "user", "com.example"));
+        sut.setKnownAccountTypes("com.example");
+
+        doLoad(sut);
+
+        assertEquals(0, sut.getListFilters().size());
+    }
+
+    public void testCreatesOneFilterForDeviceAccount() {
+        final DeviceLocalContactsFilterProvider sut = createWithFilterAndLoaderResult(
+                DeviceAccountFilter.ONLY_NULL, queryResult(
+                        "user", "com.example",
+                        "user", "com.example",
+                        null, null,
+                        "user", "com.example",
+                        null, null));
+        sut.setKnownAccountTypes("com.example");
+
+        doLoad(sut);
+
+        assertEquals(1, sut.getListFilters().size());
+        assertEquals(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
+                sut.getListFilters().get(0).filterType);
+    }
+
+    public void testCreatesOneFilterForEachDeviceAccount() {
+         final DeviceLocalContactsFilterProvider sut = createWithFilterAndLoaderResult(
+                 filterAllowing(null, "vnd.sec.contact.phone", "vnd.sec.contact.sim"), queryResult(
+                         "sim_account", "vnd.sec.contact.sim",
+                         "user", "com.example",
+                         "user", "com.example",
+                         "phone_account", "vnd.sec.contact.phone",
+                         null, null,
+                         "phone_account", "vnd.sec.contact.phone",
+                         "user", "com.example",
+                         null, null,
+                         "sim_account", "vnd.sec.contact.sim",
+                         "sim_account_2", "vnd.sec.contact.sim"
+                 ));
+        sut.setKnownAccountTypes("com.example");
+
+        doLoad(sut);
+
+        assertEquals(4, sut.getListFilters().size());
+    }
+
+    public void testFilterIsUpdatedWhenLoaderReloads() {
+        final FakeContactsProvider provider = new FakeContactsProvider();
+        final DeviceLocalContactsFilterProvider sut = new DeviceLocalContactsFilterProvider(
+                createStubContextWithContactsProvider(provider), DeviceAccountFilter.ONLY_NULL);
+        sut.setKnownAccountTypes("com.example");
+
+        provider.setNextQueryResult(queryResult(
+                null, null,
+                "user", "com.example",
+                "user", "com.example"
+        ));
+        doLoad(sut);
+
+        assertFalse(sut.getListFilters().isEmpty());
+
+        provider.setNextQueryResult(queryResult(
+                "user", "com.example",
+                "user", "com.example"
+        ));
+        doLoad(sut);
+
+        assertTrue(sut.getListFilters().isEmpty());
+    }
+
+    public void testDoesNotCreateFiltersForKnownAccounts() {
+        final DeviceLocalContactsFilterProvider sut = new DeviceLocalContactsFilterProvider(
+                getContext(), DeviceAccountFilter.ONLY_NULL);
+        sut.setKnownAccountTypes("com.example", "maybe_syncable_device_account_type");
+
+        final CursorLoader loader = sut.onCreateLoader(0, null);
+
+        // The filtering is done at the DB level rather than in the code so just verify that
+        // selection is about right.
+        assertTrue("Loader selection is wrong", loader.getSelection().contains("NOT IN (?,?)"));
+        assertEquals("com.example", loader.getSelectionArgs()[0]);
+        assertEquals("maybe_syncable_device_account_type", loader.getSelectionArgs()[1]);
+    }
+
+    private void doLoad(DeviceLocalContactsFilterProvider loaderCallbacks) {
+        final CursorLoader loader = loaderCallbacks.onCreateLoader(0, null);
+        final Cursor cursor = getLoaderResultSynchronously(loader);
+        loaderCallbacks.onLoadFinished(loader, cursor);
+    }
+
+    private DeviceLocalContactsFilterProvider createWithFilterAndLoaderResult(
+            DeviceAccountFilter filter, Cursor cursor) {
+        final DeviceLocalContactsFilterProvider result = new DeviceLocalContactsFilterProvider(
+                createStubContextWithContentQueryResult(cursor), filter);
+        return result;
+    }
+
+    private Context createStubContextWithContentQueryResult(final Cursor cursor) {
+        return createStubContextWithContactsProvider(new FakeContactsProvider(cursor));
+    }
+
+    private Context createStubContextWithContactsProvider(ContentProvider contactsProvider) {
+        final MockContentResolver resolver = new MockContentResolver();
+        resolver.addProvider(ContactsContract.AUTHORITY, contactsProvider);
+
+        final Context context = Mockito.mock(MockContext.class);
+        when(context.getContentResolver()).thenReturn(resolver);
+
+        // The loader pulls out the application context instead of usign the context directly
+        when(context.getApplicationContext()).thenReturn(context);
+
+        return context;
+    }
+
+    private Cursor queryResult(String... typeNamePairs) {
+        final MatrixCursor cursor = new MatrixCursor(new String[]
+                { RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE });
+        for (int i = 0; i < typeNamePairs.length; i += 2) {
+            cursor.newRow().add(typeNamePairs[i]).add(typeNamePairs[i+1]);
+        }
+        return cursor;
+    }
+
+    private DeviceAccountFilter filterAllowing(String... accountTypes) {
+        final Set<String> allowed = new HashSet<>(Arrays.asList(accountTypes));
+        return new DeviceAccountFilter() {
+            @Override
+            public boolean isDeviceAccountType(String accountType) {
+                return allowed.contains(accountType);
+            }
+        };
+    }
+
+    private static class FakeContactsProvider extends MockContentProvider {
+        public Cursor mNextQueryResult;
+
+        public FakeContactsProvider() {}
+
+        public FakeContactsProvider(Cursor result) {
+            mNextQueryResult = result;
+        }
+
+        public void setNextQueryResult(Cursor result) {
+            mNextQueryResult = result;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return query(uri, projection, selection, selectionArgs, sortOrder, null);
+        }
+
+        @Nullable
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder, CancellationSignal cancellationSignal) {
+            return mNextQueryResult;
+        }
+    }
+}