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

Commit e554174d authored by Zhou Liu's avatar Zhou Liu Committed by Android (Google) Code Review
Browse files

Merge "[Device Supervision] Add footnote for AoC to dashboard" into main

parents c5c145af 4ddacec6
Loading
Loading
Loading
Loading
+85 −0
Original line number Diff line number Diff line
/*
 * 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.util.Log
import androidx.preference.Preference
import com.android.settings.supervision.ipc.PreferenceData
import com.android.settings.widget.FooterPreferenceBinding
import com.android.settings.widget.FooterPreferenceMetadata
import com.android.settingslib.HelpUtils
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.PreferenceLifecycleProvider
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.supervision.SupervisionLog
import com.android.settingslib.widget.FooterPreference
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/** Displays a footnote for Age of Consent (AoC) information. */
class SupervisionAocFooterPreference(
    private val preferenceDataProvider: PreferenceDataProvider,
    private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : FooterPreferenceMetadata, FooterPreferenceBinding, PreferenceLifecycleProvider {
    override val key: String
        get() = KEY

    private var preferenceData: PreferenceData? = null

    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
        super.bind(preference, metadata)
        val footerPreference = preference as FooterPreference
        val context = preference.context

        preference.isVisible =
            (preferenceData?.isVisible ?: false) &&
                preferenceData?.title != null &&
                preferenceData?.learnMoreLink != null
        preference.title = preferenceData?.title

        footerPreference.setLearnMoreAction {
            val intent =
                HelpUtils.getHelpIntent(
                    context,
                    preferenceData?.learnMoreLink,
                    context::class.java.name,
                )

            if (intent != null) {
                context.startActivity(intent)
            } else {
                Log.w(SupervisionLog.TAG, "HelpIntent is null")
            }
        }
    }

    override fun onResume(context: PreferenceLifecycleContext) {
        context.lifecycleScope.launch {
            preferenceData =
                withContext(coroutineDispatcher) {
                    preferenceDataProvider.getPreferenceData(listOf(KEY))[KEY]
                }

            context.notifyPreferenceChange(KEY)
        }
    }

    companion object {
        const val KEY = "aoc_footer"
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -76,6 +76,7 @@ class SupervisionDashboardScreen : PreferenceScreenCreator, PreferenceLifecycleP
            }
            +SupervisionPinManagementScreen.KEY order 100
            +SupervisionPromoFooterPreference(supervisionClient) order 300
            +SupervisionAocFooterPreference(supervisionClient) order 400
        }

    private fun getSupervisionClient(context: Context) =
+11 −0
Original line number Diff line number Diff line
@@ -32,6 +32,9 @@ import android.os.Bundle
 * @property trailingIcon Optional trailing icon resource ID.
 * @property targetPackage Optional target package name for limiting the applications that can
 *   perform the action.
 * @property isVisible Optional boolean indicating whether the preference should be visible,
 *   defaults to true.
 * @property learnMoreLink Optional link to a help center article for more information.
 */
data class PreferenceData(
    val icon: Int? = null,
@@ -40,6 +43,8 @@ data class PreferenceData(
    var action: String? = null,
    var trailingIcon: Int? = null,
    var targetPackage: String? = null,
    var isVisible: Boolean = true,
    var learnMoreLink: String? = null,
) {
    constructor(
        bundle: Bundle
@@ -50,6 +55,8 @@ data class PreferenceData(
        action = bundle.getString(ACTION),
        trailingIcon = bundle.getInt(ACTION_ICON, -1).takeIf { it != -1 },
        targetPackage = bundle.getString(TARGET_PACKAGE),
        isVisible = bundle.getBoolean(IS_VISIBLE, true),
        learnMoreLink = bundle.getString(LEARN_MORE_LINK),
    )

    fun toBundle(): Bundle {
@@ -60,6 +67,8 @@ data class PreferenceData(
            action?.let { putString(ACTION, it) }
            trailingIcon?.let { putInt(ACTION_ICON, it) }
            targetPackage?.let { putString(TARGET_PACKAGE, it) }
            putBoolean(IS_VISIBLE, isVisible)
            learnMoreLink?.let { putString(LEARN_MORE_LINK, it) }
        }
    }

@@ -70,5 +79,7 @@ data class PreferenceData(
        private const val ACTION = "action"
        private const val ACTION_ICON = "trailing_icon"
        private const val TARGET_PACKAGE = "target_package"
        private const val IS_VISIBLE = "is_visible"
        private const val LEARN_MORE_LINK = "learn_more_link"
    }
}
+140 −0
Original line number Diff line number Diff line
/*
 * 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.content.ContextWrapper
import android.content.pm.PackageManager
import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.supervision.SupervisionAocFooterPreference.Companion.KEY
import com.android.settings.supervision.ipc.PreferenceData
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.widget.FooterPreference
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class SupervisionAocFooterPreferenceTest {
    private val mockPackageManager: PackageManager = mock()
    private val context: Context =
        object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
            override fun getPackageManager() = mockPackageManager
        }
    private val preference = FooterPreference(context)

    private var preferenceData: PreferenceData? = null

    private val testDispatcher = UnconfinedTestDispatcher()
    private val testScope = TestScope(testDispatcher)

    private val preferenceLifecycleContext: PreferenceLifecycleContext = mock {
        on { lifecycleScope }.thenReturn(testScope)
        on { packageManager }.thenReturn(mockPackageManager)
        on { findPreference<Preference>(any()) }.thenReturn(preference)
    }
    private val preferenceDataProvider: PreferenceDataProvider = mock {
        onBlocking { getPreferenceData(any()) }
            .thenAnswer {
                when (preferenceData) {
                    null -> mapOf<String, PreferenceData>()
                    else -> mapOf(KEY to preferenceData)
                }
            }
    }

    @Test
    fun onResume_allGood_titleIsSet_preferenceIsVisible() =
        testScope.runTest {
            val title = "test title"
            val learnMoreLink = "https://google.com/learnMore"
            preferenceData =
                PreferenceData(title = title, learnMoreLink = learnMoreLink, isVisible = true)

            val aosFooterPreference =
                SupervisionAocFooterPreference(preferenceDataProvider, testDispatcher)

            aosFooterPreference.onResume(preferenceLifecycleContext)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY)
            aosFooterPreference.bind(preference, mock())

            assertThat(preference.isVisible).isTrue()
            assertThat(preference.title).isEqualTo(title)
        }

    @Test
    fun onResume_notVisible_preferenceIsHidden() =
        testScope.runTest {
            val title = "test title"
            val learnMoreLink = "https://google.com/learnMore"
            preferenceData =
                PreferenceData(title = title, learnMoreLink = learnMoreLink, isVisible = false)

            val aosFooterPreference =
                SupervisionAocFooterPreference(preferenceDataProvider, testDispatcher)

            aosFooterPreference.onResume(preferenceLifecycleContext)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY)
            aosFooterPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
            assertThat(preference.title).isEqualTo(title)
        }

    @Test
    fun onResume_noLearnMoreLink_preferenceIsHidden() =
        testScope.runTest {
            val title = "test title"
            preferenceData = PreferenceData(title = title, isVisible = false)

            val aosFooterPreference =
                SupervisionAocFooterPreference(preferenceDataProvider, testDispatcher)

            aosFooterPreference.onResume(preferenceLifecycleContext)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY)
            aosFooterPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
            assertThat(preference.title).isEqualTo(title)
        }

    @Test
    fun onResume_noTitle_preferenceIsHidden() =
        testScope.runTest {
            val learnMoreLink = "https://google.com/learnMore"
            preferenceData = PreferenceData(learnMoreLink = learnMoreLink, isVisible = false)

            val aosFooterPreference =
                SupervisionAocFooterPreference(preferenceDataProvider, testDispatcher)

            aosFooterPreference.onResume(preferenceLifecycleContext)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY)
            aosFooterPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
        }
}
+2 −0
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ class PreferenceDataApiTest {
                        action = "android.settings.SUPERVISION_UPGRADE",
                        trailingIcon = 2,
                        targetPackage = "com.google.android.gms.kids",
                        isVisible = false,
                        learnMoreLink = "https://support.google.com/families/answer/1234567",
                    ),
            )
        val encoded = preferenceDataApi.responseCodec.encode(response)