/* * Copyright (C) 2018 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.settings.slices; import static android.content.ContentResolver.SCHEME_CONTENT; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.slice.SliceManager; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.StrictMode; import android.provider.SettingsSlicesContract; import android.util.ArraySet; import com.android.settings.bluetooth.BluetoothSliceBuilder; import com.android.settings.location.LocationSliceBuilder; import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeToggleController; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.shadow.ShadowThreadUtils; import com.android.settings.wifi.WifiSliceBuilder; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import androidx.slice.Slice; /** * TODO Investigate using ShadowContentResolver.registerProviderInternal(String, ContentProvider) */ @RunWith(SettingsRobolectricTestRunner.class) @Config(shadows = ShadowThreadUtils.class) public class SettingsSliceProviderTest { private static final String KEY = "KEY"; private static final String INTENT_PATH = SettingsSlicesContract.PATH_SETTING_INTENT + "/" + KEY; private static final String TITLE = "title"; private static final String SUMMARY = "summary"; private static final String SCREEN_TITLE = "screen title"; private static final String FRAGMENT_NAME = "fragment name"; private static final int ICON = 1234; // I declare a thumb war private static final Uri URI = Uri.parse("content://com.android.settings.slices/test"); private static final String PREF_CONTROLLER = FakeToggleController.class.getName(); private Context mContext; private SettingsSliceProvider mProvider; private SQLiteDatabase mDb; private SliceManager mManager; private static final List SPECIAL_CASE_PLATFORM_URIS = Arrays.asList( WifiSliceBuilder.WIFI_URI, BluetoothSliceBuilder.BLUETOOTH_URI, LocationSliceBuilder.LOCATION_URI ); private static final List SPECIAL_CASE_OEM_URIS = Arrays.asList( ZenModeSliceBuilder.ZEN_MODE_URI ); @Before public void setUp() { mContext = spy(RuntimeEnvironment.application); mProvider = spy(new SettingsSliceProvider()); ShadowStrictMode.reset(); mProvider.mSliceWeakDataCache = new HashMap<>(); mProvider.mSliceDataCache = new HashMap<>(); mProvider.mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(mContext); when(mProvider.getContext()).thenReturn(mContext); mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); SlicesDatabaseHelper.getInstance(mContext).setIndexedState(); mManager = mock(SliceManager.class); when(mContext.getSystemService(SliceManager.class)).thenReturn(mManager); when(mManager.getPinnedSlices()).thenReturn(Collections.emptyList()); } @After public void cleanUp() { ShadowThreadUtils.reset(); DatabaseTestUtils.clearDb(mContext); } @Test public void testInitialSliceReturned_emptySlice() { insertSpecialCase(KEY); final Uri uri = SliceBuilderUtils.getUri(INTENT_PATH, false); Slice slice = mProvider.onBindSlice(uri); assertThat(slice.getUri()).isEqualTo(uri); assertThat(slice.getItems()).isEmpty(); } @Test public void testLoadSlice_returnsSliceFromAccessor() { insertSpecialCase(KEY); final Uri uri = SliceBuilderUtils.getUri(INTENT_PATH, false); mProvider.loadSlice(uri); SliceData data = mProvider.mSliceWeakDataCache.get(uri); assertThat(data.getKey()).isEqualTo(KEY); assertThat(data.getTitle()).isEqualTo(TITLE); } @Test public void loadSlice_registersIntentFilter() { insertSpecialCase(KEY); final Uri uri = SliceBuilderUtils.getUri(INTENT_PATH, false); mProvider.loadSlice(uri); verify(mProvider).registerIntentToUri(eq(FakeToggleController.INTENT_FILTER), eq(uri)); } @Test public void testLoadSlice_doesNotCacheWithoutPin() { insertSpecialCase(KEY); final Uri uri = SliceBuilderUtils.getUri(INTENT_PATH, false); mProvider.loadSlice(uri); SliceData data = mProvider.mSliceDataCache.get(uri); assertThat(data).isNull(); } @Test public void testLoadSlice_cachesWithPin() { insertSpecialCase(KEY); final Uri uri = SliceBuilderUtils.getUri(INTENT_PATH, false); when(mManager.getPinnedSlices()).thenReturn(Arrays.asList(uri)); mProvider.loadSlice(uri); SliceData data = mProvider.mSliceDataCache.get(uri); assertThat(data.getKey()).isEqualTo(KEY); assertThat(data.getTitle()).isEqualTo(TITLE); } @Test public void testLoadSlice_cachedEntryRemovedOnBuild() { SliceData data = getDummyData(); mProvider.mSliceWeakDataCache.put(data.getUri(), data); mProvider.onBindSlice(data.getUri()); insertSpecialCase(data.getKey()); SliceData cachedData = mProvider.mSliceWeakDataCache.get(data.getUri()); assertThat(cachedData).isNull(); } @Test public void onBindSlice_mainThread_shouldNotOverrideStrictMode() { ShadowThreadUtils.setIsMainThread(true); final StrictMode.ThreadPolicy oldThreadPolicy = StrictMode.getThreadPolicy(); SliceData data = getDummyData(); mProvider.mSliceWeakDataCache.put(data.getUri(), data); mProvider.onBindSlice(data.getUri()); final StrictMode.ThreadPolicy newThreadPolicy = StrictMode.getThreadPolicy(); assertThat(newThreadPolicy.toString()).isEqualTo(oldThreadPolicy.toString()); } @Test @Config(shadows = ShadowStrictMode.class) public void onBindSlice_backgroundThread_shouldOverrideStrictMode() { ShadowThreadUtils.setIsMainThread(false); SliceData data = getDummyData(); mProvider.mSliceWeakDataCache.put(data.getUri(), data); mProvider.onBindSlice(data.getUri()); assertThat(ShadowStrictMode.isThreadPolicyOverridden()).isTrue(); } @Test public void onBindSlice_requestsBlockedSlice_retunsNull() { final String blockedKey = "blocked_key"; final Set blockedSet = new ArraySet<>(); blockedSet.add(blockedKey); doReturn(blockedSet).when(mProvider).getBlockedKeys(); final Uri blockedUri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(blockedKey) .build(); final Slice slice = mProvider.onBindSlice(blockedUri); assertThat(slice).isNull(); } @Test public void testLoadSlice_cachedEntryRemovedOnUnpin() { SliceData data = getDummyData(); mProvider.mSliceDataCache.put(data.getUri(), data); mProvider.onSliceUnpinned(data.getUri()); insertSpecialCase(data.getKey()); SliceData cachedData = mProvider.mSliceWeakDataCache.get(data.getUri()); assertThat(cachedData).isNull(); } @Test public void getDescendantUris_fullActionUri_returnsSelf() { final Uri uri = SliceBuilderUtils.getUri( SettingsSlicesContract.PATH_SETTING_ACTION + "/key", true); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactly(uri); } @Test public void getDescendantUris_fullIntentUri_returnsSelf() { final Uri uri = SliceBuilderUtils.getUri( SettingsSlicesContract.PATH_SETTING_ACTION + "/key", true); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactly(uri); } @Test public void getDescendantUris_wrongPath_returnsEmpty() { final Uri uri = SliceBuilderUtils.getUri("invalid_path", true); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).isEmpty(); } @Test public void getDescendantUris_invalidPath_returnsEmpty() { final String key = "platform_key"; insertSpecialCase(key, true /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .appendPath("invalid") .build(); final Collection descendants = mProvider.onGetSliceDescendants(uri); descendants.removeAll(SPECIAL_CASE_OEM_URIS); assertThat(descendants).isEmpty(); } @Test public void getDescendantUris_platformSlice_doesNotReturnOEMSlice() { insertSpecialCase("oem_key", false /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .build(); final Collection descendants = mProvider.onGetSliceDescendants(uri); descendants.removeAll(SPECIAL_CASE_PLATFORM_URIS); assertThat(descendants).isEmpty(); } @Test public void getDescendantUris_oemSlice_doesNotReturnPlatformSlice() { insertSpecialCase("platform_key", true /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .build(); final Collection descendants = mProvider.onGetSliceDescendants(uri); descendants.removeAll(SPECIAL_CASE_OEM_URIS); assertThat(descendants).isEmpty(); } @Test public void getDescendantUris_oemSlice_returnsOEMUriDescendant() { final String key = "oem_key"; insertSpecialCase(key, false /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .build(); final Collection expectedUris = new HashSet<>(); expectedUris.addAll(SPECIAL_CASE_OEM_URIS); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(key) .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test public void getDescendantUris_oemSliceNoPath_returnsOEMUriDescendant() { final String key = "oem_key"; insertSpecialCase(key, false /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .build(); final Collection expectedUris = new HashSet<>(); expectedUris.addAll(SPECIAL_CASE_OEM_URIS); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(key) .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test public void getDescendantUris_platformSlice_returnsPlatformUriDescendant() { final String key = "platform_key"; insertSpecialCase(key, true /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .build(); final Collection expectedUris = new HashSet<>(); expectedUris.addAll(SPECIAL_CASE_PLATFORM_URIS); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(key) .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test public void getDescendantUris_platformSliceNoPath_returnsPlatformUriDescendant() { final String key = "platform_key"; insertSpecialCase(key, true /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .build(); final Collection expectedUris = new HashSet<>(); expectedUris.addAll(SPECIAL_CASE_PLATFORM_URIS); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(key) .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test public void getDescendantUris_noAuthorityNorPath_returnsAllUris() { final String platformKey = "platform_key"; final String oemKey = "oemKey"; insertSpecialCase(platformKey, true /* isPlatformSlice */); insertSpecialCase(oemKey, false /* isPlatformSlice */); final Uri uri = new Uri.Builder() .scheme(SCHEME_CONTENT) .build(); final Collection expectedUris = new HashSet<>(); expectedUris.addAll(SPECIAL_CASE_PLATFORM_URIS); expectedUris.addAll(SPECIAL_CASE_OEM_URIS); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(platformKey) .build()); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(oemKey) .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test public void bindSlice_wifiSlice_returnsWifiSlice() { final Slice wifiSlice = mProvider.onBindSlice(WifiSliceBuilder.WIFI_URI); assertThat(wifiSlice.getUri()).isEqualTo(WifiSliceBuilder.WIFI_URI); } @Test public void onSlicePinned_noIntentRegistered_specialCaseUri_doesNotCrash() { final Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(SettingsSlicesContract.KEY_LOCATION) .build(); mProvider.onSlicePinned(uri); } private void insertSpecialCase(String key) { insertSpecialCase(key, true); } private void insertSpecialCase(String key, boolean isPlatformSlice) { ContentValues values = new ContentValues(); values.put(SlicesDatabaseHelper.IndexColumns.KEY, key); values.put(SlicesDatabaseHelper.IndexColumns.TITLE, TITLE); values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, "s"); values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, "s"); values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234); values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, "test"); values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, PREF_CONTROLLER); values.put(SlicesDatabaseHelper.IndexColumns.PLATFORM_SLICE, isPlatformSlice); values.put(SlicesDatabaseHelper.IndexColumns.SLICE_TYPE, SliceData.SliceType.INTENT); mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); } private static SliceData getDummyData() { return new SliceData.Builder() .setKey(KEY) .setTitle(TITLE) .setSummary(SUMMARY) .setScreenTitle(SCREEN_TITLE) .setIcon(ICON) .setFragmentName(FRAGMENT_NAME) .setUri(URI) .setPreferenceControllerClassName(PREF_CONTROLLER) .build(); } @Implements(value = StrictMode.class, inheritImplementationMethods = true) public static class ShadowStrictMode { private static int sSetThreadPolicyCount; @Resetter public static void reset() { sSetThreadPolicyCount = 0; } @Implementation public static void setThreadPolicy(final StrictMode.ThreadPolicy policy) { sSetThreadPolicyCount++; } public static boolean isThreadPolicyOverridden() { return sSetThreadPolicyCount != 0; } } }