diff --git a/src/com/android/settings/supervision/SupervisionSafeSearchDataStore.kt b/src/com/android/settings/supervision/SupervisionSafeSearchDataStore.kt new file mode 100644 index 00000000000..ffae5cfdd3c --- /dev/null +++ b/src/com/android/settings/supervision/SupervisionSafeSearchDataStore.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 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.supervision + +import android.content.Context +import android.provider.Settings.Secure.SEARCH_CONTENT_FILTERS_ENABLED +import com.android.settingslib.datastore.AbstractKeyedDataObservable +import com.android.settingslib.datastore.HandlerExecutor +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.datastore.SettingsSecureStore +import com.android.settingslib.datastore.SettingsStore + +/** Datastore of the safe search preference. */ +@Suppress("UNCHECKED_CAST") +class SupervisionSafeSearchDataStore( + private val context: Context, + private val settingsStore: SettingsStore = SettingsSecureStore.get(context), +) : AbstractKeyedDataObservable(), KeyedObserver, KeyValueStore { + override fun contains(key: String) = + key == SupervisionSearchFilterOnPreference.KEY || + key == SupervisionSearchFilterOffPreference.KEY + + override fun getValue(key: String, valueType: Class): T? { + val settingValue = (settingsStore.getBoolean(SEARCH_CONTENT_FILTERS_ENABLED) == true) + return when (key) { + SupervisionSearchFilterOffPreference.KEY -> !settingValue + SupervisionSearchFilterOnPreference.KEY -> settingValue + else -> null + } + as T? + } + + override fun setValue(key: String, valueType: Class, value: T?) { + if (value !is Boolean) return + when (key) { + SupervisionSearchFilterOffPreference.KEY -> + settingsStore.setBoolean(SEARCH_CONTENT_FILTERS_ENABLED, !value) + SupervisionSearchFilterOnPreference.KEY -> + settingsStore.setBoolean(SEARCH_CONTENT_FILTERS_ENABLED, value) + } + } + + override fun onFirstObserverAdded() { + // observe the underlying storage key + settingsStore.addObserver(SEARCH_CONTENT_FILTERS_ENABLED, this, HandlerExecutor.main) + } + + override fun onKeyChanged(key: String, reason: Int) { + // forward data change to preference hierarchy key + notifyChange(SupervisionSearchFilterOffPreference.KEY, reason) + notifyChange(SupervisionSearchFilterOnPreference.KEY, reason) + } + + override fun onLastObserverRemoved() { + settingsStore.removeObserver(SEARCH_CONTENT_FILTERS_ENABLED, this) + } +} diff --git a/src/com/android/settings/supervision/SupervisionSafeSearchPreference.kt b/src/com/android/settings/supervision/SupervisionSafeSearchPreference.kt index 617a3451207..337753e23fb 100644 --- a/src/com/android/settings/supervision/SupervisionSafeSearchPreference.kt +++ b/src/com/android/settings/supervision/SupervisionSafeSearchPreference.kt @@ -18,9 +18,7 @@ package com.android.settings.supervision import android.content.Context import androidx.preference.Preference import com.android.settings.R -import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.Permissions -import com.android.settingslib.datastore.SettingsSecureStore import com.android.settingslib.metadata.BooleanValuePreference import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.ReadWritePermit @@ -30,9 +28,10 @@ import com.android.settingslib.preference.forEachRecursively import com.android.settingslib.widget.SelectorWithWidgetPreference /** Base class of web content filters SafeSearch preferences. */ -sealed class SupervisionSafeSearchPreference : - BooleanValuePreference, SelectorWithWidgetPreference.OnClickListener, PreferenceBinding { - override fun storage(context: Context): KeyValueStore = SettingsSecureStore.get(context) +sealed class SupervisionSafeSearchPreference( + protected val dataStore: SupervisionSafeSearchDataStore +) : BooleanValuePreference, SelectorWithWidgetPreference.OnClickListener, PreferenceBinding { + override fun storage(context: Context) = dataStore override fun getReadPermissions(context: Context) = Permissions.EMPTY @@ -64,15 +63,15 @@ sealed class SupervisionSafeSearchPreference : override fun bind(preference: Preference, metadata: PreferenceMetadata) { super.bind(preference, metadata) (preference as SelectorWithWidgetPreference).also { - // TODO(b/401568995): Set the isChecked value using stored values. - it.isChecked = (it.key == SupervisionSearchFilterOffPreference.KEY) + it.isChecked = (dataStore.getBoolean(it.key) == true) it.setOnClickListener(this) } } } /** The SafeSearch filter on preference. */ -class SupervisionSearchFilterOnPreference : SupervisionSafeSearchPreference() { +class SupervisionSearchFilterOnPreference(dataStore: SupervisionSafeSearchDataStore) : + SupervisionSafeSearchPreference(dataStore) { override val key get() = KEY @@ -89,7 +88,8 @@ class SupervisionSearchFilterOnPreference : SupervisionSafeSearchPreference() { } /** The SafeSearch filter off preference. */ -class SupervisionSearchFilterOffPreference : SupervisionSafeSearchPreference() { +class SupervisionSearchFilterOffPreference(dataStore: SupervisionSafeSearchDataStore) : + SupervisionSafeSearchPreference(dataStore) { override val key get() = KEY diff --git a/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt b/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt index 994165a8b36..5e5a9384e33 100644 --- a/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt +++ b/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt @@ -56,8 +56,9 @@ class SupervisionWebContentFiltersScreen : PreferenceScreenCreator { R.string.supervision_web_content_filters_search_title, ) += { - +SupervisionSearchFilterOnPreference() - +SupervisionSearchFilterOffPreference() + val dataStore = SupervisionSafeSearchDataStore(context) + +SupervisionSearchFilterOnPreference(dataStore) + +SupervisionSearchFilterOffPreference(dataStore) } } diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSearchPreferenceTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSearchPreferenceTest.kt index 371cca3f2b5..991b1f0646f 100644 --- a/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSearchPreferenceTest.kt +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSearchPreferenceTest.kt @@ -16,20 +16,33 @@ package com.android.settings.supervision import android.content.Context +import android.provider.Settings +import android.provider.Settings.Secure.SEARCH_CONTENT_FILTERS_ENABLED +import android.provider.Settings.SettingNotFoundException import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R +import com.android.settingslib.preference.createAndBindWidget +import com.android.settingslib.widget.SelectorWithWidgetPreference import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SupervisionSafeSearchPreferenceTest { private val context: Context = ApplicationProvider.getApplicationContext() + private lateinit var dataStore: SupervisionSafeSearchDataStore + private lateinit var searchFilterOffPreference: SupervisionSearchFilterOffPreference + private lateinit var searchFilterOnPreference: SupervisionSearchFilterOnPreference - private val searchFilterOnPreference = SupervisionSearchFilterOnPreference() - - private val searchFilterOffPreference = SupervisionSearchFilterOffPreference() + @Before + fun setUp() { + dataStore = SupervisionSafeSearchDataStore(context) + searchFilterOffPreference = SupervisionSearchFilterOffPreference(dataStore) + searchFilterOnPreference = SupervisionSearchFilterOnPreference(dataStore) + } @Test fun getTitle_filterOn() { @@ -37,7 +50,6 @@ class SupervisionSafeSearchPreferenceTest { .isEqualTo(R.string.supervision_web_content_filters_search_filter_on_title) } - @Test fun getSummary_filterOn() { assertThat(searchFilterOnPreference.summary) @@ -53,8 +65,60 @@ class SupervisionSafeSearchPreferenceTest { @Test fun getSummary_filterOff() { assertThat(searchFilterOffPreference.summary) - .isEqualTo( - R.string.supervision_web_content_filters_search_filter_off_summary + .isEqualTo(R.string.supervision_web_content_filters_search_filter_off_summary) + } + + @Test + fun filterOffIsChecked_whenNoValueIsSet() { + assertThrows(SettingNotFoundException::class.java) { + Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED) + } + assertThat(getFilterOnWidget().isChecked).isFalse() + assertThat(getFilterOffWidget().isChecked).isTrue() + } + + @Test + fun filterOnIsChecked_whenPreviouslyEnabled() { + Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 1) + assertThat(getFilterOffWidget().isChecked).isFalse() + assertThat(getFilterOnWidget().isChecked).isTrue() + } + + @Test + fun clickBlockExplicitSites_enablesFilter() { + Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 0) + val filterOnWidget = getFilterOnWidget() + assertThat(filterOnWidget.isChecked).isFalse() + + filterOnWidget.performClick() + + assertThat( + Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED) ) + .isEqualTo(1) + assertThat(filterOnWidget.isChecked).isTrue() + } + + @Test + fun clickAllowAllSites_disablesFilter() { + Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 1) + val filterOffWidget = getFilterOffWidget() + assertThat(filterOffWidget.isChecked).isFalse() + + filterOffWidget.performClick() + + assertThat( + Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED) + ) + .isEqualTo(0) + assertThat(filterOffWidget.isChecked).isTrue() + } + + private fun getFilterOnWidget(): SelectorWithWidgetPreference { + return searchFilterOnPreference.createAndBindWidget(context) + } + + private fun getFilterOffWidget(): SelectorWithWidgetPreference { + return searchFilterOffPreference.createAndBindWidget(context) } } diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionWebContentFiltersScreenTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionWebContentFiltersScreenTest.kt index 31bdbd29405..17830d4140b 100644 --- a/tests/robotests/src/com/android/settings/supervision/SupervisionWebContentFiltersScreenTest.kt +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionWebContentFiltersScreenTest.kt @@ -89,4 +89,28 @@ class SupervisionWebContentFiltersScreenTest { assertThat(allowAllSitesPreference.isChecked).isFalse() } } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WEB_CONTENT_FILTERS_SCREEN) + fun switchSafeSearchPreferences() { + FragmentScenario.launchInContainer(supervisionWebContentFiltersScreen.fragmentClass()) + .onFragment { fragment -> + val searchFilterOffPreference = + fragment.findPreference( + SupervisionSearchFilterOffPreference.KEY + )!! + val searchFilterOnPreference = + fragment.findPreference( + SupervisionSearchFilterOnPreference.KEY + )!! + + assertThat(searchFilterOffPreference.isChecked).isTrue() + assertThat(searchFilterOnPreference.isChecked).isFalse() + + searchFilterOnPreference.performClick() + + assertThat(searchFilterOnPreference.isChecked).isTrue() + assertThat(searchFilterOffPreference.isChecked).isFalse() + } + } }