diff options
3 files changed, 252 insertions, 13 deletions
diff --git a/proguard.flags b/proguard.flags index f1e609b9a..d9dad57b2 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 98b865d7e..1d06a4317 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.database.Cursor; 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 @@ public class DeviceLocalContactsFilterProvider @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 class DeviceLocalContactsFilterProvider 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 000000000..d776ab8ee --- /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; + } + } +} |