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

Commit 75890ea6 authored by Jacky Wang's avatar Jacky Wang
Browse files

Refine SupervisionPromoFooterPreference

Bug: 399497788
Flag: android.app.supervision.flags.enable_supervision_settings_screen
Test: atest SupervisionPromoFooterPreferenceTest
Change-Id: Id232b18d08ca0b602d0558373110cc0554241aee
parent f0ff4bf6
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@
package com.android.settings.supervision

import com.android.settings.supervision.ipc.PreferenceData
import kotlinx.coroutines.Deferred

/**
 * Interface for providing preference data.
@@ -36,5 +35,5 @@ interface PreferenceDataProvider {
     * @return A map where the keys are the requested keys, and the values are the corresponding
     *   [PreferenceData] objects.
     */
    suspend fun getPreferenceData(keys: List<String>): Deferred<Map<String, PreferenceData>>
    suspend fun getPreferenceData(keys: List<String>): Map<String, PreferenceData>
}
+40 −31
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.settings.supervision

import android.content.Context
import android.content.Intent
import androidx.preference.Preference
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.PreferenceLifecycleProvider
import com.android.settingslib.metadata.PreferenceMetadata
@@ -24,19 +25,31 @@ import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.PreferenceTitleProvider
import com.android.settingslib.preference.PreferenceBinding
import com.android.settingslib.widget.CardPreference
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/** A bottom banner promoting other supervision features offered by the supervision app. */
class SupervisionPromoFooterPreference(private val preferenceDataProvider: PreferenceDataProvider) :
class SupervisionPromoFooterPreference(
    private val preferenceDataProvider: PreferenceDataProvider,
    private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
) :
    PreferenceMetadata,
    PreferenceBinding,
    PreferenceLifecycleProvider,
    PreferenceTitleProvider,
    PreferenceSummaryProvider {

    /** Whether [intent] holds an initialized value. */
    private var initialized = false

    // Operation to be performed when the preference is clicked
    private var intent: Intent? = null

    override val key: String
        get() = KEY

    // TODO(b/399497788): Remove this and get title from supervision app
    override fun getTitle(context: Context) = "Full parental controls"

@@ -44,44 +57,40 @@ class SupervisionPromoFooterPreference(private val preferenceDataProvider: Prefe
    override fun getSummary(context: Context) =
        "Set up an account for your kid & help them manage it (required for kids under [AOC])"

    override fun intent(context: Context): Intent? = intent

    override val key: String
        get() = KEY

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

    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
        super.bind(preference, metadata)
        if (initialized) {
            preference.intent = intent
            preference.isVisible = intent != null
        }
    }

    override fun onResume(context: PreferenceLifecycleContext) {
        super.onResume(context)
        val preference = context.findPreference<CardPreference>(KEY)
        if (preference != null) {
        context.lifecycleScope.launch {
            // TODO(b/399497788) Get title & summary from supervision app.
            val preferenceData =
                    preferenceDataProvider.getPreferenceData(listOf(KEY)).await()[KEY]
                intent = intent ?: Intent()
                intent?.setAction(preferenceData?.action)
                intent?.setPackage(preferenceData?.targetPackage)
                // Hide the preference if the target package can not respond to the action
                if (!canTargetPackageRespondToAction(preference.context)) {
                    preference.isVisible = false
                withContext(coroutineDispatcher) {
                    preferenceDataProvider.getPreferenceData(listOf(KEY))[KEY]
                }
            initialized = true
            val targetIntent =
                Intent(preferenceData?.action).apply {
                    `package` = preferenceData?.targetPackage
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                }
            if (targetIntent.isValid(context)) intent = targetIntent
            context.notifyPreferenceChange(KEY)
        }
    }

    private fun canTargetPackageRespondToAction(context: Context): Boolean {
        if (intent?.action == null || intent?.`package` == null) {
            return false
        }

        intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        val activities =
            context.packageManager.queryIntentActivitiesAsUser(intent!!, 0, context.userId)
        return activities.isNotEmpty()
    }
    private fun Intent.isValid(context: Context) =
        action != null &&
            `package` != null &&
            context.packageManager.queryIntentActivitiesAsUser(this, 0, context.userId).isNotEmpty()

    companion object {
        const val KEY = "supervision_promo_footer"
        const val KEY = "promo_footer"
    }
}
+5 −7
Original line number Diff line number Diff line
@@ -21,8 +21,6 @@ import android.util.Log
import com.android.internal.R
import com.android.settings.supervision.PreferenceDataProvider
import com.android.settingslib.ipc.MessengerServiceClient
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred

/**
 * A specialized [MessengerServiceClient] for interacting with the system supervision service.
@@ -55,17 +53,17 @@ class SupervisionMessengerClient(context: Context) :
     * @return A map of preference data, where the keys are the preference keys and the values are
     *   [PreferenceData] objects, or an empty map if an error occurs.
     */
    override suspend fun getPreferenceData(
        keys: List<String>
    ): Deferred<Map<String, PreferenceData>> =
    override suspend fun getPreferenceData(keys: List<String>): Map<String, PreferenceData> =
        try {
            invoke(supervisionPackageName, PreferenceDataApi(), PreferenceDataRequest(keys = keys))
                .await()
        } catch (e: Exception) {
            Log.e("MessengerService", "Error fetching Preference data from supervision app", e)
            CompletableDeferred(mapOf())
            Log.e(TAG, "Error fetching Preference data from supervision app", e)
            mapOf()
        }

    companion object {
        private const val TAG = "SupervisionMessengerClient"
        private const val SUPERVISION_MESSENGER_SERVICE_BIND_ACTION =
            "android.app.supervision.action.SUPERVISION_MESSENGER_SERVICE"
    }
+68 −120
Original line number Diff line number Diff line
@@ -16,80 +16,55 @@
package com.android.settings.supervision

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.settings.supervision.SupervisionPromoFooterPreference.Companion.KEY
import com.android.settings.supervision.ipc.PreferenceData
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.getPreferenceSummary
import com.android.settingslib.metadata.getPreferenceTitle
import com.android.settingslib.widget.CardPreference
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class SupervisionPromoFooterPreferenceTest {
    private val context: Context = ApplicationProvider.getApplicationContext()
    private val preference = CardPreference(context)

    @get:Rule val mocks: MockitoRule = MockitoJUnit.rule()
    private var preferenceData: PreferenceData? = null

    private val testDispatcher = UnconfinedTestDispatcher()
    private val testScope = TestScope(testDispatcher)
    private lateinit var preference: CardPreference
    private lateinit var context: Context
    private lateinit var lifecycleCoroutineScope: LifecycleCoroutineScope

    @Mock private lateinit var preferenceLifecycleContext: PreferenceLifecycleContext

    @Mock private lateinit var preferenceDataProvider: PreferenceDataProvider

    @Mock private lateinit var mockPackageManager: PackageManager

    @Before
    fun setup() {
        context = spy(InstrumentationRegistry.getInstrumentation().context)
        whenever(context.packageManager).thenReturn(mockPackageManager)
        preference = CardPreference(context)
        lifecycleCoroutineScope = TestLifecycleOwner().lifecycleScope
        whenever(preferenceLifecycleContext.findPreference<Preference>(any()))
            .thenReturn(preference)
        whenever(preferenceLifecycleContext.lifecycleScope).thenReturn(lifecycleCoroutineScope)
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    private val mockPackageManager: PackageManager = mock()
    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
@@ -114,121 +89,94 @@ class SupervisionPromoFooterPreferenceTest {
    @Test
    fun onResume_actionIsNull_preferenceIsHidden() =
        testScope.runTest {
            val promoPreference = SupervisionPromoFooterPreference(preferenceDataProvider)
            promoPreference.bind(preference, mock())

            whenever(preferenceDataProvider.getPreferenceData(any())).thenAnswer {
                CompletableDeferred(
                    mapOf(
                        SupervisionPromoFooterPreference.KEY to
                            PreferenceData(targetPackage = "test.package")
                    )
                )
            }
            val promoPreference =
                SupervisionPromoFooterPreference(preferenceDataProvider, testDispatcher)
            preferenceData = PreferenceData(targetPackage = "test.package")

            promoPreference.onResume(preferenceLifecycleContext)

            assertFalse(preference.isVisible)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY) // will trigger binding
            promoPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
            verify(mockPackageManager, never())
                .queryIntentActivitiesAsUser(any<Intent>(), any<Int>(), any<Int>())
                .queryIntentActivitiesAsUser(any(), any<Int>(), any<Int>())
        }

    @Test
    fun onResume_packageIsNull_preferenceIsHidden() =
        testScope.runTest {
            val promoPreference = SupervisionPromoFooterPreference(preferenceDataProvider)
            promoPreference.bind(preference, mock())

            whenever(preferenceDataProvider.getPreferenceData(any())).thenAnswer {
                CompletableDeferred(
                    mapOf(
                        SupervisionPromoFooterPreference.KEY to
                            PreferenceData(action = "Test Action")
                    )
                )
            }
            val promoPreference =
                SupervisionPromoFooterPreference(preferenceDataProvider, testDispatcher)
            preferenceData = PreferenceData(action = "Test Action")

            promoPreference.onResume(preferenceLifecycleContext)

            assertFalse(preference.isVisible)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY) // will trigger binding
            promoPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
            verify(mockPackageManager, never())
                .queryIntentActivitiesAsUser(any<Intent>(), any<Int>(), any<Int>())
                .queryIntentActivitiesAsUser(any(), any<Int>(), any<Int>())
        }

    @Test
    fun onResume_emptyPreferenceData_preferenceIsHidden() =
        testScope.runTest {
            val promoPreference = SupervisionPromoFooterPreference(preferenceDataProvider)
            promoPreference.bind(preference, mock())

            whenever(preferenceDataProvider.getPreferenceData(any())).thenAnswer {
                CompletableDeferred(mapOf<String, PreferenceData>())
            }
            val promoPreference =
                SupervisionPromoFooterPreference(preferenceDataProvider, testDispatcher)
            preferenceData = null

            promoPreference.onResume(preferenceLifecycleContext)

            assertFalse(preference.isVisible)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY) // will trigger binding
            promoPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
            verify(mockPackageManager, never())
                .queryIntentActivitiesAsUser(any<Intent>(), any<Int>(), any<Int>())
                .queryIntentActivitiesAsUser(any(), any<Int>(), any<Int>())
        }

    @Test
    fun onResume_noActivitiesCanHandleIntent_preferenceIsHidden() =
        testScope.runTest {
            val promoPreference = SupervisionPromoFooterPreference(preferenceDataProvider)
            promoPreference.bind(preference, mock())

            whenever(preferenceDataProvider.getPreferenceData(any())).thenAnswer {
                CompletableDeferred(
                    mapOf(
                        SupervisionPromoFooterPreference.KEY to
                            PreferenceData(action = "Test Action", targetPackage = "test.package")
                    )
                )
            }
            val promoPreference =
                SupervisionPromoFooterPreference(preferenceDataProvider, testDispatcher)
            preferenceData = PreferenceData(action = "Test Action", targetPackage = "test.package")

            whenever(
                    mockPackageManager.queryIntentActivitiesAsUser(
                        any<Intent>(),
                        any<Int>(),
                        any<Int>(),
                    )
                )
            mockPackageManager.stub {
                on { queryIntentActivitiesAsUser(any(), any<Int>(), any<Int>()) }
                    .thenReturn(emptyList())
            }

            promoPreference.onResume(preferenceLifecycleContext)

            assertFalse(preference.isVisible)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY) // will trigger binding
            promoPreference.bind(preference, mock())

            assertThat(preference.isVisible).isFalse()
        }

    @Test
    fun onResume_validIntent_hasActivityToHandleIntent_preferenceIsVisible_validIntentCreated() =
        testScope.runTest {
            val promoPreference = SupervisionPromoFooterPreference(preferenceDataProvider)
            promoPreference.bind(preference, mock())

            whenever(preferenceDataProvider.getPreferenceData(any())).thenAnswer {
                CompletableDeferred(
                    mapOf(
                        SupervisionPromoFooterPreference.KEY to
                            PreferenceData(action = "Test Action", targetPackage = "test.package")
                    )
                )
            }
            val promoPreference =
                SupervisionPromoFooterPreference(preferenceDataProvider, testDispatcher)
            preferenceData = PreferenceData(action = "Test Action", targetPackage = "test.package")

            whenever(
                    mockPackageManager.queryIntentActivitiesAsUser(
                        any<Intent>(),
                        any<Int>(),
                        any<Int>(),
                    )
                )
            mockPackageManager.stub {
                on { queryIntentActivitiesAsUser(any(), any<Int>(), any<Int>()) }
                    .thenReturn(listOf(ResolveInfo()))
            }

            promoPreference.onResume(preferenceLifecycleContext)

            assertTrue(preference.isVisible)
            assertEquals("Test Action", promoPreference.intent(context)?.action)
            assertEquals("test.package", promoPreference.intent(context)?.`package`)
            verify(preferenceLifecycleContext).notifyPreferenceChange(KEY) // will trigger binding
            promoPreference.bind(preference, mock())

            assertThat(preference.isVisible).isTrue()
            val intent = preference.intent!!
            assertThat(intent.action).isEqualTo("Test Action")
            assertThat(intent.`package`).isEqualTo("test.package")
        }
}