Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bd8546ed authored by Jacky Wang's avatar Jacky Wang Committed by Android (Google) Code Review
Browse files

Merge "[Supervision] Refine SupervisionSafeSearchPreference" into main

parents 5115d208 246ca3bd
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -27,12 +27,11 @@ import com.android.settingslib.datastore.SettingsStore
/** Datastore of the safe search preference. */
@Suppress("UNCHECKED_CAST")
class SupervisionSafeSearchDataStore(
    private val context: Context,
    context: Context,
    private val settingsStore: SettingsStore = SettingsSecureStore.get(context),
) : AbstractKeyedDataObservable<String>(), KeyedObserver<String>, KeyValueStore {
    override fun contains(key: String) =
        key == SupervisionSearchFilterOnPreference.KEY ||
            key == SupervisionSearchFilterOffPreference.KEY

    override fun contains(key: String) = settingsStore.contains(SEARCH_CONTENT_FILTERS_ENABLED)

    override fun <T : Any> getValue(key: String, valueType: Class<T>): T? {
        val settingValue = settingsStore.getInt(SEARCH_CONTENT_FILTERS_ENABLED)
+21 −27
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@
package com.android.settings.supervision

import android.app.Activity
import android.app.settings.SettingsEnums
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResult
@@ -24,48 +25,42 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.annotation.VisibleForTesting
import androidx.preference.Preference
import com.android.settings.R
import com.android.settingslib.datastore.Permissions
import com.android.settings.metrics.PreferenceActionMetricsProvider
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.metadata.BooleanValuePreference
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.PreferenceLifecycleProvider
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.metadata.SensitivityLevel
import com.android.settingslib.preference.PreferenceBinding
import com.android.settingslib.preference.forEachRecursively
import com.android.settingslib.preference.BooleanValuePreferenceBinding
import com.android.settingslib.supervision.SupervisionIntentProvider
import com.android.settingslib.widget.SelectorWithWidgetPreference

/** Base class of web content filters Search filter preferences. */
sealed class SupervisionSafeSearchPreference(
    protected val dataStore: SupervisionSafeSearchDataStore
    private val dataStore: SupervisionSafeSearchDataStore
) :
    BooleanValuePreference,
    BooleanValuePreferenceBinding,
    PreferenceActionMetricsProvider,
    SelectorWithWidgetPreference.OnClickListener,
    PreferenceBinding,
    PreferenceLifecycleProvider {

    private lateinit var lifeCycleContext: PreferenceLifecycleContext

    private lateinit var supervisionCredentialLauncher: ActivityResultLauncher<Intent>

    override fun storage(context: Context) = dataStore

    override fun getReadPermissions(context: Context) = Permissions.EMPTY
    override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions()

    override fun getWritePermissions(context: Context) = Permissions.EMPTY
    override fun getWritePermissions(context: Context) = SettingsSecureStore.getWritePermissions()

    override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) =
        ReadWritePermit.ALLOW

    override fun getWritePermit(
        context: Context,
        value: Boolean?,
        callingPid: Int,
        callingUid: Int,
    ) = ReadWritePermit.DISALLOW

    override val sensitivityLevel
        get() = SensitivityLevel.NO_SENSITIVITY
    override fun getWritePermit(context: Context, callingPid: Int, callingUid: Int) =
        ReadWritePermit.DISALLOW

    override fun createWidget(context: Context) = SelectorWithWidgetPreference(context)

@@ -85,21 +80,14 @@ sealed class SupervisionSafeSearchPreference(

    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
        super.bind(preference, metadata)
        (preference as SelectorWithWidgetPreference).also {
            it.isChecked = (dataStore.getBoolean(it.key) == true)
            it.setOnClickListener(this)
        }
        (preference as SelectorWithWidgetPreference).setOnClickListener(this)
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun onConfirmCredentials(result: ActivityResult) {
        if (result.resultCode == Activity.RESULT_OK) {
            val preference = lifeCycleContext.findPreference<SelectorWithWidgetPreference>(key)
            preference?.parent?.forEachRecursively {
                if (it is SelectorWithWidgetPreference) {
                    it.isChecked = it.key == key
                }
            }
            // Update checked state with dataStore also works but it will bypass metrics logging
            lifeCycleContext.requirePreference<SelectorWithWidgetPreference>(key).isChecked = true
        }
    }
}
@@ -117,6 +105,9 @@ class SupervisionSearchFilterOnPreference(dataStore: SupervisionSafeSearchDataSt
    override val summary
        get() = R.string.supervision_web_content_filters_search_filter_on_summary

    override val preferenceActionMetrics: Int
        get() = SettingsEnums.ACTION_SUPERVISION_SEARCH_FILTER_ON

    companion object {
        const val KEY = "web_content_filters_search_filter_on"
    }
@@ -132,6 +123,9 @@ class SupervisionSearchFilterOffPreference(dataStore: SupervisionSafeSearchDataS
    override val title
        get() = R.string.supervision_web_content_filters_search_filter_off_title

    override val preferenceActionMetrics: Int
        get() = SettingsEnums.ACTION_SUPERVISION_SEARCH_FILTER_OFF

    companion object {
        const val KEY = "web_content_filters_search_filter_off"
    }
+52 −83
Original line number Diff line number Diff line
@@ -20,9 +20,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.provider.Settings
import android.provider.Settings.Secure.SEARCH_CONTENT_FILTERS_ENABLED
import android.provider.Settings.SettingNotFoundException
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
@@ -30,46 +28,53 @@ import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.testutils.MetricsRule
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.metadata.PreferenceLifecycleContext
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.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions

@RunWith(AndroidJUnit4::class)
class SupervisionSafeSearchPreferenceTest {
    @get:Rule val metricsRule = MetricsRule()

    private val context: Context = ApplicationProvider.getApplicationContext()
    private val dataStore = SupervisionSafeSearchDataStore(context)
    private val searchFilterOffPreference = SupervisionSearchFilterOffPreference(dataStore)
    private val searchFilterOnPreference = SupervisionSearchFilterOnPreference(dataStore)

    private val mockActivityResultLauncher: ActivityResultLauncher<Intent> = mock()
    private val mockPackageManager: PackageManager = mock {
        on { queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt()) } doReturn
            listOf(ResolveInfo())
    }
    private val mockLifeCycleContext: PreferenceLifecycleContext = mock {
        on { packageManager } doReturn mockPackageManager
        on { registerForActivityResult(any<StartActivityForResult>(), any()) } doReturn
            mockActivityResultLauncher
    }

    private lateinit var mockLifeCycleContext: PreferenceLifecycleContext
    private lateinit var mockActivityResultLauncher: ActivityResultLauncher<Intent>
    private lateinit var mockPackageManager: PackageManager
    private lateinit var dataStore: SupervisionSafeSearchDataStore
    private lateinit var searchFilterOffPreference: SupervisionSearchFilterOffPreference
    private lateinit var searchFilterOnPreference: SupervisionSearchFilterOnPreference
    private var dataStoreValue: Int?
        get() = SettingsSecureStore.get(context).getInt(SEARCH_CONTENT_FILTERS_ENABLED)
        set(value) = SettingsSecureStore.get(context).setInt(SEARCH_CONTENT_FILTERS_ENABLED, value)

    @Before
    fun setUp() {
        dataStore = SupervisionSafeSearchDataStore(context)
        mockLifeCycleContext = mock(PreferenceLifecycleContext::class.java)
        mockActivityResultLauncher =
            mock(ActivityResultLauncher::class.java) as ActivityResultLauncher<Intent>
        mockPackageManager = mock(PackageManager::class.java)
        mockConfirmSupervisionCredentialsActivity()
        searchFilterOffPreference = SupervisionSearchFilterOffPreference(dataStore)
        searchFilterOffPreference.onCreate(mockLifeCycleContext)
        searchFilterOnPreference = SupervisionSearchFilterOnPreference(dataStore)
        searchFilterOnPreference.onCreate(mockLifeCycleContext)
    }

@@ -93,24 +98,22 @@ class SupervisionSafeSearchPreferenceTest {

    @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()
        dataStoreValue = null
        assertThat(searchFilterOnPreference.createWidget().isChecked).isFalse()
        assertThat(searchFilterOffPreference.createWidget().isChecked).isTrue()
    }

    @Test
    fun filterOnIsChecked_whenPreviouslyEnabled() {
        Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 1)
        assertThat(getFilterOffWidget().isChecked).isFalse()
        assertThat(getFilterOnWidget().isChecked).isTrue()
        dataStoreValue = 1
        assertThat(searchFilterOnPreference.createWidget().isChecked).isTrue()
        assertThat(searchFilterOffPreference.createWidget().isChecked).isFalse()
    }

    @Test
    fun clickFilterOn_failedToEnablesFilter_activityFailed() {
        Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 0)
        val filterOnWidget = getFilterOnWidget()
        dataStoreValue = 0
        val filterOnWidget = searchFilterOnPreference.createWidget()
        assertThat(filterOnWidget.isChecked).isFalse()

        filterOnWidget.performClick()
@@ -119,96 +122,62 @@ class SupervisionSafeSearchPreferenceTest {
            ActivityResult(Activity.RESULT_CANCELED, null)
        )

        assertThat(
                Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED)
            )
            .isEqualTo(0)
        verifyNoInteractions(metricsRule.metricsFeatureProvider)
        assertThat(dataStoreValue).isEqualTo(0)
        assertThat(filterOnWidget.isChecked).isFalse()
    }

    @Test
    fun clickFilterOn_unresolvedIntent_activityNotLaunched() {
        `when`(mockPackageManager.queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt()))
            .thenReturn(emptyList<ResolveInfo>())
        mockPackageManager.stub {
            on { queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt()) } doReturn listOf()
        }

        Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 0)
        val filterOnWidget = getFilterOnWidget()
        dataStoreValue = 0
        val filterOnWidget = searchFilterOnPreference.createWidget()
        assertThat(filterOnWidget.isChecked).isFalse()

        filterOnWidget.performClick()

        verify(mockActivityResultLauncher, never()).launch(any())
        assertThat(
                Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED)
            )
            .isEqualTo(0)
        verifyNoInteractions(metricsRule.metricsFeatureProvider)
        assertThat(dataStoreValue).isEqualTo(0)
        assertThat(filterOnWidget.isChecked).isFalse()
    }

    @Test
    fun clickFilterOn_enablesFilter() {
        Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, -1)
        val filterOnWidget = getFilterOnWidget()
        dataStoreValue = -1
        val filterOnWidget = searchFilterOnPreference.createWidget()
        assertThat(filterOnWidget.isChecked).isFalse()

        filterOnWidget.performClick()
        verifyConfirmSupervisionCredentialsActivity()
        searchFilterOnPreference.onConfirmCredentials(ActivityResult(Activity.RESULT_OK, null))

        assertThat(
                Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED)
            )
            .isEqualTo(1)
        assertThat(dataStoreValue).isEqualTo(1)
        assertThat(filterOnWidget.isChecked).isTrue()
        verify(metricsRule.metricsFeatureProvider).changed(0, searchFilterOnPreference.key, 1)
    }

    @Test
    fun clickFilterOff_disablesFilter() {
        Settings.Secure.putInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED, 1)
        val filterOffWidget = getFilterOffWidget()
        dataStoreValue = 1
        val filterOffWidget = searchFilterOffPreference.createWidget()
        assertThat(filterOffWidget.isChecked).isFalse()

        filterOffWidget.performClick()
        verifyConfirmSupervisionCredentialsActivity()
        searchFilterOffPreference.onConfirmCredentials(ActivityResult(Activity.RESULT_OK, null))

        assertThat(
                Settings.Secure.getInt(context.getContentResolver(), SEARCH_CONTENT_FILTERS_ENABLED)
            )
            .isEqualTo(0)
        assertThat(dataStoreValue).isEqualTo(0)
        assertThat(filterOffWidget.isChecked).isTrue()
        verify(metricsRule.metricsFeatureProvider).changed(0, searchFilterOffPreference.key, 1)
    }

    private fun getFilterOnWidget(): SelectorWithWidgetPreference {
        val widget: SelectorWithWidgetPreference =
            searchFilterOnPreference.createAndBindWidget(context)
        mockLifeCycleContext.stub {
            on { findPreference<Preference>(SupervisionSearchFilterOnPreference.KEY) } doReturn
                widget
            on { requirePreference<Preference>(SupervisionSearchFilterOnPreference.KEY) } doReturn
                widget
        }
        return widget
    }

    private fun getFilterOffWidget(): SelectorWithWidgetPreference {
        val widget: SelectorWithWidgetPreference =
            searchFilterOffPreference.createAndBindWidget(context)
        mockLifeCycleContext.stub {
            on { findPreference<Preference>(SupervisionSearchFilterOffPreference.KEY) } doReturn
                widget
            on { requirePreference<Preference>(SupervisionSearchFilterOffPreference.KEY) } doReturn
                widget
        }
        return widget
    }

    private fun mockConfirmSupervisionCredentialsActivity() {
        `when`(mockPackageManager.queryIntentActivitiesAsUser(any<Intent>(), anyInt(), anyInt()))
            .thenReturn(listOf(ResolveInfo()))
        `when`(mockLifeCycleContext.packageManager).thenReturn(mockPackageManager)
        `when`(mockLifeCycleContext.registerForActivityResult(any<StartActivityForResult>(), any()))
            .thenReturn(mockActivityResultLauncher)
    private fun SupervisionSafeSearchPreference.createWidget() =
        createAndBindWidget<SelectorWithWidgetPreference>(context).also { widget ->
            mockLifeCycleContext.stub { on { requirePreference<Preference>(key) } doReturn widget }
        }

    private fun verifyConfirmSupervisionCredentialsActivity() {
+7 −3
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.settings.R
import com.android.settings.supervision.ipc.SupervisionMessengerClient
import com.android.settings.testutils.SettingsStoreRule
import com.android.settingslib.ipc.MessengerServiceRule
import com.android.settingslib.widget.FooterPreference
import com.android.settingslib.widget.SelectorWithWidgetPreference
@@ -47,6 +48,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowPackageManager

@RunWith(AndroidJUnit4::class)
@@ -56,9 +58,9 @@ class SupervisionWebContentFiltersScreenTest {
    private val supervisionWebContentFiltersScreen = SupervisionWebContentFiltersScreen()
    private lateinit var shadowPackageManager: ShadowPackageManager

    @get:Rule val setFlagsRule = SetFlagsRule()

    @get:Rule
    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
    @get:Rule(order = 1) val settingsStoreRule = SettingsStoreRule()
    @get:Rule(order = 2)
    val serviceRule =
        MessengerServiceRule<SupervisionMessengerClient>(
            TestSupervisionMessengerService::class.java
@@ -192,6 +194,7 @@ class SupervisionWebContentFiltersScreenTest {
                    Activity.RESULT_OK,
                    null,
                )
                ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

                assertThat(searchFilterOnWidget.isChecked).isTrue()
                assertThat(searchFilterOffWidget.isChecked).isFalse()
@@ -224,6 +227,7 @@ class SupervisionWebContentFiltersScreenTest {
                    Activity.RESULT_CANCELED,
                    null,
                )
                ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

                assertThat(searchFilterOnPreference.isChecked).isFalse()
                assertThat(searchFilterOffPreference.isChecked).isTrue()