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

Commit aee2de00 authored by Zhou Liu's avatar Zhou Liu
Browse files

[Device Supervision] Top level entry point routing

Bug: b/407056097
Test: atest TopLevelSupervisionPreferenceControllerTest
Flag: android.app.supervision.flags.enable_supervision_settings_screen
Change-Id: Ie2fe1cfed66a8f01f51efe10bd6f2a3175749249
parent eff614f8
Loading
Loading
Loading
Loading
+77 −4
Original line number Diff line number Diff line
@@ -17,12 +17,85 @@ package com.android.settings.supervision

import android.app.supervision.flags.Flags
import android.content.Context
import android.content.Intent
import androidx.preference.Preference
import com.android.settings.applications.AppStoreUtil.getAppStoreLink
import com.android.settings.applications.AppStoreUtil.getInstallerPackageName
import com.android.settings.core.BasePreferenceController
import com.android.settings.supervision.ipc.SupervisionMessengerClient.Companion.SUPERVISION_MESSENGER_SERVICE_BIND_ACTION

/** Controller for the top level Supervision settings Preference item. */
class TopLevelSupervisionPreferenceController(context: Context, key: String) :
    BasePreferenceController(context, key) {
class TopLevelSupervisionPreferenceController(
    private val context: Context,
    private val key: String,
) : BasePreferenceController(context, key) {
    private val supervisionPackage =
        SupervisionHelper.getInstance(context).getSupervisionPackageName()

    override fun getAvailabilityStatus(): Int =
        if (Flags.enableSupervisionSettingsScreen()) AVAILABLE else UNSUPPORTED_ON_DEVICE
    private var missingAppStoreLink = false

    private var redirectIntent: Intent? = null

    override fun handlePreferenceTreeClick(preference: Preference?): Boolean {
        if (preference?.key.equals(key) && redirectIntent != null) {
            context.startActivity(redirectIntent)
            return true
        }
        return super.handlePreferenceTreeClick(preference)
    }

    override fun updateState(preference: Preference?) {
        super.updateState(preference)
        if (!hasNecessarySupervisionComponent() && missingAppStoreLink) {
            preference?.isEnabled = false
        }
    }

    override fun getAvailabilityStatus(): Int {
        if (!Flags.enableSupervisionSettingsScreen() || supervisionPackage == null)
            return UNSUPPORTED_ON_DEVICE

        // Try to navigate to app store if supervision app with necessary component is not installed
        if (!hasNecessarySupervisionComponent()) {
            val installerPackageName = getInstallerPackageName(context, supervisionPackage)
            val appStoreLinkIntent =
                installerPackageName?.let {
                    getAppStoreLink(context, installerPackageName, supervisionPackage)
                }
            if (appStoreLinkIntent == null) {
                missingAppStoreLink = true
                return AVAILABLE
            }
            missingAppStoreLink = false
            redirectIntent = appStoreLinkIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }

        if (hasRedirect()) {
            redirectIntent = Intent(SETTINGS_REDIRECT_ACTION).setPackage(supervisionPackage)
        }

        return AVAILABLE
    }

    private fun hasNecessarySupervisionComponent(): Boolean {
        val intent =
            Intent(SUPERVISION_MESSENGER_SERVICE_BIND_ACTION).setPackage(supervisionPackage)

        return supervisionPackage != null &&
            context.packageManager.queryIntentServices(intent, 0).isNotEmpty()
    }

    private fun hasRedirect(): Boolean {
        val intent = Intent(SETTINGS_REDIRECT_ACTION).setPackage(supervisionPackage)
        return supervisionPackage != null &&
            context.packageManager
                .queryIntentActivitiesAsUser(intent, 0, context.userId)
                .isNotEmpty()
    }

    companion object {
        // Supervision app should declare an intent-filter with this action to redirect the settings
        // navigation target.
        const val SETTINGS_REDIRECT_ACTION = "android.app.supervision.action.VIEW_SETTINGS"
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -65,7 +65,7 @@ class SupervisionMessengerClient(context: Context) :
        }

    companion object {
        private const val SUPERVISION_MESSENGER_SERVICE_BIND_ACTION =
        const val SUPERVISION_MESSENGER_SERVICE_BIND_ACTION =
            "android.app.supervision.action.SUPERVISION_MESSENGER_SERVICE"
    }
}
+179 −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.app.Activity
import android.app.role.RoleManager
import android.content.Context
import android.content.Intent
import android.content.pm.InstallSourceInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import androidx.preference.Preference
import com.android.settings.core.BasePreferenceController.AVAILABLE
import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
import com.android.settings.supervision.TopLevelSupervisionPreferenceController.Companion.SETTINGS_REDIRECT_ACTION
import com.android.settings.supervision.ipc.SupervisionMessengerClient.Companion.SUPERVISION_MESSENGER_SERVICE_BIND_ACTION
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argThat
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.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class TopLevelSupervisionPreferenceControllerTest {
    private val mockRoleManager = mock<RoleManager>()
    private val mockPackageManager = mock<PackageManager>()
    private val context =
        spy(Robolectric.buildActivity(Activity::class.java).get()) {
            on { getSystemService(Context.ROLE_SERVICE) }.thenReturn(mockRoleManager)
            on { packageManager }.thenReturn(mockPackageManager)
        }

    private val preference = Preference(context)

    @Before
    fun setUp() {
        SupervisionHelper.sInstance = null
        preference.key = PREFERENCE_KEY
    }

    @Test
    fun supervisionPackageNameIsNull_returnUnsupported() {
        mockRoleManager.stub {
            on { getRoleHolders(RoleManager.ROLE_SYSTEM_SUPERVISION) }.thenReturn(listOf<String>())
        }

        val preferenceController = TopLevelSupervisionPreferenceController(context, PREFERENCE_KEY)
        verify(mockRoleManager).getRoleHolders(any())
        assertThat(preferenceController.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
    }

    @Test
    fun noNecessaryComponent_noAppStoreLink_preferenceDisabled() {
        mockRoleManager.stub {
            on { getRoleHolders(RoleManager.ROLE_SYSTEM_SUPERVISION) }
                .thenReturn(listOf(SUPERVISION_PACKAGE_NAME))
        }

        mockPackageManager.stub {
            on {
                    queryIntentServices(
                        intentMatcher(SUPERVISION_MESSENGER_SERVICE_BIND_ACTION),
                        any<Int>(),
                    )
                }
                .thenReturn(listOf())
        }
        mockPackageManager.stub {
            on { getInstallSourceInfo(any()) }.thenReturn(InstallSourceInfo(null, null, null, null))
        }

        val preferenceController = TopLevelSupervisionPreferenceController(context, PREFERENCE_KEY)

        assertThat(preferenceController.availabilityStatus).isEqualTo(AVAILABLE)
        preferenceController.handlePreferenceTreeClick(preference)
        preferenceController.updateState(preference)

        assertThat(preference.isEnabled).isFalse()
        verify(context, never()).startActivity(any())
    }

    @Test
    fun hasNecessaryComponent_isFullySupervised_launchFullSupervision() {
        mockRoleManager.stub {
            on { getRoleHolders(RoleManager.ROLE_SYSTEM_SUPERVISION) }
                .thenReturn(listOf(SUPERVISION_PACKAGE_NAME))
        }

        mockPackageManager.stub {
            on {
                    queryIntentServices(
                        intentMatcher(SUPERVISION_MESSENGER_SERVICE_BIND_ACTION),
                        any<Int>(),
                    )
                }
                .thenReturn(listOf(ResolveInfo()))
        }
        mockPackageManager.stub {
            on {
                    queryIntentActivitiesAsUser(
                        intentMatcher(SETTINGS_REDIRECT_ACTION),
                        any<Int>(),
                        any<Int>(),
                    )
                }
                .thenReturn(listOf(ResolveInfo()))
        }

        val preferenceController = TopLevelSupervisionPreferenceController(context, PREFERENCE_KEY)

        assertThat(preferenceController.availabilityStatus).isEqualTo(AVAILABLE)
        preferenceController.handlePreferenceTreeClick(preference)

        verify(context).startActivity(intentMatcher(SETTINGS_REDIRECT_ACTION))
    }

    @Test
    fun hasNecessaryComponent_isNotFullySupervised_returnAvailable() {
        mockRoleManager.stub {
            on { getRoleHolders(RoleManager.ROLE_SYSTEM_SUPERVISION) }
                .thenReturn(listOf(SUPERVISION_PACKAGE_NAME))
        }

        mockPackageManager.stub {
            on {
                    queryIntentServices(
                        intentMatcher(SUPERVISION_MESSENGER_SERVICE_BIND_ACTION),
                        any<Int>(),
                    )
                }
                .thenReturn(listOf(ResolveInfo()))
        }
        mockPackageManager.stub {
            on {
                    queryIntentActivitiesAsUser(
                        intentMatcher(SETTINGS_REDIRECT_ACTION),
                        any<Int>(),
                        any<Int>(),
                    )
                }
                .thenReturn(listOf())
        }

        val preferenceController = TopLevelSupervisionPreferenceController(context, PREFERENCE_KEY)

        assertThat(preferenceController.availabilityStatus).isEqualTo(AVAILABLE)

        preferenceController.handlePreferenceTreeClick(preference)
        verify(context, never()).startActivity(any())
    }

    private fun intentMatcher(action: String) = argThat<Intent> { this.action == action }

    private companion object {
        const val SUPERVISION_PACKAGE_NAME = "com.android.supervision"
        const val PREFERENCE_KEY = "test_key"
    }
}