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

Commit 6c4424da authored by Clara Thomas's avatar Clara Thomas Committed by Android (Google) Code Review
Browse files

Merge "This change implements the Delete PIN flow" into main

parents cbfecec3 ccf4cfd9
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -14473,8 +14473,21 @@ Data usage charges may apply.</string>
    <string name="supervision_full_screen_pin_verification_title">Enter supervision PIN</string>
    <!-- Title on activity to choose PIN for supervision setup [CHAR LIMIT=NONE] -->
    <string name="supervision_choose_your_pin_header">Set a parental controls PIN</string>
    <!-- Title on activity to confirm PIN for supervision setup [CHAR LIMIT=NONE] -->
    <string name="supervision_confirm_your_pin_header">Confirm PIN</string>
    <string name="supervision_pin_reset_success_toast">PIN updated</string>
    <!-- Title on dialog that confirms whether user wants to delete their supervision PIN [CHAR LIMIT=NONE] -->
    <string name="supervision_delete_pin_confirm_header">Delete PIN?</string>
    <!-- Message in dialog that confirms whether user wants to delete their supervision PIN [CHAR LIMIT=NONE] -->
    <string name="supervision_delete_pin_confirm_message">This will delete your current PIN for all users on this device and disable controls you have set up. This action can\u2019t be undone.</string>
    <!-- Title on dialog that informs the user that they need to disable supervision before deleting the PIN [CHAR LIMIT=NONE] -->
    <string name="supervision_delete_pin_supervision_enabled_header">Pause controls on other profiles first</string>
    <!-- Message in dialog that informs the user that they need to disable supervision before deleting the PIN [CHAR LIMIT=NONE] -->
    <string name="supervision_delete_pin_supervision_enabled_message">To proceed with deleting this PIN and resetting supervision settings, please pause controls on all other profiles.</string>
    <!-- Title on dialog that informs the user that an error occurred while trying to delete the PIN [CHAR LIMIT=NONE] -->
    <string name="supervision_delete_pin_error_header">PIN can not be deleted</string>
    <!-- Message in dialog that informs the user that an error occurred while trying to delete the PIN [CHAR LIMIT=NONE] -->
    <string name="supervision_delete_pin_error_message">An error occurred while trying to delete the PIN.</string>
    <string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string>
    <!-- Light theme customization for lottie illustration. Not translatable. -->
+107 −2
Original line number Diff line number Diff line
@@ -15,14 +15,34 @@
 */
package com.android.settings.supervision

import android.app.settings.SettingsEnums
import android.app.supervision.SupervisionManager
import android.content.Context
import android.os.UserManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.core.SubSettingLauncher
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.PreferenceLifecycleProvider
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.preference.PreferenceBinding
import com.android.settingslib.supervision.SupervisionLog.TAG

/**
 * Setting on PIN Management screen (Settings > Supervision > Manage Pin) that invokes the flow to
 * delete the device PIN.
 */
class SupervisionDeletePinPreference : PreferenceMetadata {
class SupervisionDeletePinPreference() :
    PreferenceMetadata,
    PreferenceBinding,
    PreferenceLifecycleProvider,
    Preference.OnPreferenceClickListener {

    private lateinit var lifeCycleContext: PreferenceLifecycleContext

    override val key: String
        get() = KEY

@@ -32,7 +52,92 @@ class SupervisionDeletePinPreference : PreferenceMetadata {
    override val summary: Int
        get() = R.string.supervision_delete_pin_preference_summary

    // TODO(b/406082832): Implements the delete PIN flow in settings.
    override fun onCreate(context: PreferenceLifecycleContext) {
        lifeCycleContext = context
    }

    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
        super.bind(preference, metadata)
        preference.onPreferenceClickListener = this
    }

    override fun onPreferenceClick(preference: Preference): Boolean {
        showDeletionDialog(preference.context)
        return true
    }

    @VisibleForTesting
    fun showDeletionDialog(context: Context) {
        val builder = AlertDialog.Builder(context)

        val supervisionManager = context.getSystemService(SupervisionManager::class.java)
        val userManager = context.getSystemService(UserManager::class.java)

        if (supervisionManager == null || userManager == null) {
            // TODO(b/415995161): Improve error handling
            builder.setTitle(R.string.supervision_delete_pin_error_header)
                .setMessage(R.string.supervision_delete_pin_error_message)
                .setPositiveButton(R.string.okay, null)
        } else if (areAnyUsersExceptCurrentSupervised(supervisionManager, userManager)) {
            builder.setTitle(R.string.supervision_delete_pin_supervision_enabled_header)
                .setMessage(R.string.supervision_delete_pin_supervision_enabled_message)
                .setPositiveButton(R.string.okay, null)
        } else {
            builder.setTitle(R.string.supervision_delete_pin_confirm_header)
                .setMessage(R.string.supervision_delete_pin_confirm_message)
                .setPositiveButton(R.string.delete) { _, _ -> onConfirmDeleteClick() }
                .setNegativeButton(R.string.cancel, null)
        }
        val dialog = builder.create()
        dialog.setCanceledOnTouchOutside(false)
        dialog.show()
    }

    private fun showErrorDialog(context: Context) {
        // TODO(b/415995161): Improve error handling
        AlertDialog.Builder(context)
            .setTitle(R.string.supervision_delete_pin_error_header)
            .setMessage(R.string.supervision_delete_pin_error_message)
            .setPositiveButton(R.string.okay, null)
            .create().show()
    }

    /** Returns whether any users except the current user are supervised on this device. */
    @VisibleForTesting
    fun areAnyUsersExceptCurrentSupervised(
        supervisionManager: SupervisionManager,
        userManager: UserManager): Boolean {
        return userManager.users.any {
            lifeCycleContext.userId != it.id && supervisionManager.isSupervisionEnabledForUser(it.id)
        }
    }

    @VisibleForTesting
    fun onConfirmDeleteClick() {
        val userManager = lifeCycleContext.getSystemService(UserManager::class.java)
        val supervisionManager = lifeCycleContext.getSystemService(SupervisionManager::class.java)
        if (userManager == null || supervisionManager == null) {
            Log.e(TAG, "Can't delete supervision data; system services cannot be found.")
            return
        }
        val supervisingUser = lifeCycleContext.supervisingUserHandle
        if (supervisingUser == null) {
            Log.e(TAG, "Can't delete supervision data; supervising user does not exist.")
            return
        }
        if (userManager.removeUser(supervisingUser)) {
            supervisionManager.isSupervisionEnabled = false
            supervisionManager.supervisionRecoveryInfo = null
            lifeCycleContext.notifyPreferenceChange(KEY)
            SubSettingLauncher(lifeCycleContext)
                .setDestination(SupervisionDashboardFragment::class.java.name)
                .setSourceMetricsCategory(SettingsEnums.SUPERVISION_DASHBOARD)
                .launch()
        } else {
            Log.e(TAG, "Can't delete supervision data; unable to delete supervising profile.")
            showErrorDialog(lifeCycleContext)
        }
    }

    companion object {
        const val KEY = "supervision_delete_pin"
+202 −4
Original line number Diff line number Diff line
@@ -15,29 +15,227 @@
 */
package com.android.settings.supervision

import android.app.supervision.SupervisionManager
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.UserInfo
import android.os.Bundle
import android.os.UserHandle
import android.os.UserManager
import android.os.UserManager.USER_TYPE_FULL_SECONDARY
import android.os.UserManager.USER_TYPE_FULL_SYSTEM
import android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
@Config(shadows = [ShadowAlertDialogCompat::class])
class SupervisionDeletePinPreferenceTest {
    private val context: Context = ApplicationProvider.getApplicationContext()

    private val supervisionDeletePinPreference = SupervisionDeletePinPreference()
    private val appContext: Context = ApplicationProvider.getApplicationContext()
    private val mockSupervisionManager = mock<SupervisionManager>()
    private val mockUserManager = mock<UserManager>()
    private var startedIntent: Intent? = null
    private var notifiedKey: String? = null
    private val context =
        object : ContextWrapper(appContext) {
            override fun getSystemService(name: String): Any =
                when (name) {
                    getSystemServiceName(SupervisionManager::class.java) -> mockSupervisionManager
                    getSystemServiceName(UserManager::class.java) -> mockUserManager
                    else -> super.getSystemService(name)
                }
            override fun startActivity(intent: Intent) {
                startedIntent = intent
            }
        }
    private val preference = SupervisionDeletePinPreference()
    private val widget = preference.createWidget(context)
    // This object is created explicitly instead of mocked in order to preserve access to the
    // original context in test.
    private val lifeCycleContext = object : PreferenceLifecycleContext(context)  {
        override val lifecycleScope: LifecycleCoroutineScope
            get() = mock {} // unused
        override val fragmentManager: FragmentManager
            get() = mock {} // unused
        override val childFragmentManager: FragmentManager
            get() = mock {} // unused

        override fun <T> findPreference(key: String): T? {
            if (key == SupervisionDeletePinPreference.KEY) {
                return widget as T?
            }
            return null
        }
        override fun <T : Any> requirePreference(key: String) = findPreference<T>(key)!!
        override fun getKeyValueStore(key: String): KeyValueStore? = null
        override fun notifyPreferenceChange(key: String) {
            notifiedKey = key
        }
        @Suppress("DEPRECATION")
        override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {}
        override fun <I, O> registerForActivityResult(
            contract: ActivityResultContract<I, O>,
            callback: ActivityResultCallback<O>
        ): ActivityResultLauncher<I> {
           return mock {} // unused
        }
    }

    @Before
    fun setUp() {
        preference.onCreate(lifeCycleContext)
        context.setTheme(R.style.Theme_AppCompat) // Needed for AlertDialog creation
        startedIntent = null
        notifiedKey = null
    }

    @Test
    fun getTitle() {
        assertThat(supervisionDeletePinPreference.title)
        assertThat(preference.title)
            .isEqualTo(R.string.supervision_delete_pin_preference_title)
    }

    @Test
    fun getSummary() {
        assertThat(supervisionDeletePinPreference.summary)
        assertThat(preference.summary)
            .isEqualTo(R.string.supervision_delete_pin_preference_summary)
    }

    @Test
    fun showDeletionDialog_currentUserSupervised_showsConfirmation() {
        mockUserManager.stub {
            on { users } doReturn listOf(MAIN_USER, SECONDARY_USER, SUPERVISING_PROFILE)
        }
        mockSupervisionManager.stub {
            on { isSupervisionEnabledForUser(MAIN_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SECONDARY_USER_ID) } doReturn false
            on { isSupervisionEnabledForUser(SUPERVISING_USER_ID) } doReturn false
        }

        preference.showDeletionDialog(context)
        assertAlertDialogHasMessage(R.string.supervision_delete_pin_confirm_message)
    }

    @Test
    fun showDeletionDialog_secondaryUserSupervised_showsSupervisionEnabledWarning() {
        mockUserManager.stub {
            on { users } doReturn listOf(MAIN_USER, SECONDARY_USER, SUPERVISING_PROFILE)
        }
        mockSupervisionManager.stub {
            on { isSupervisionEnabledForUser(MAIN_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SECONDARY_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SUPERVISING_USER_ID) } doReturn false
        }

        preference.showDeletionDialog(context)
        assertAlertDialogHasMessage(R.string.supervision_delete_pin_supervision_enabled_message)
    }

    @Test
    fun areAnyUsersSupervisedExceptCurrent_currentUserSupervised_returnsFalse() {
        mockUserManager.stub {
            on { users } doReturn listOf(MAIN_USER, SECONDARY_USER, SUPERVISING_PROFILE)
        }
        mockSupervisionManager.stub {
            on { isSupervisionEnabledForUser(MAIN_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SECONDARY_USER_ID) } doReturn false
            on { isSupervisionEnabledForUser(SUPERVISING_USER_ID) } doReturn false
        }

        assertThat(preference.areAnyUsersExceptCurrentSupervised(
            mockSupervisionManager, mockUserManager)).isFalse()
    }

    @Test
    fun areAnyUsersSupervisedExceptCurrent_secondaryUserSupervised_returnsTrue() {
        mockUserManager.stub {
            on { users } doReturn listOf(MAIN_USER, SECONDARY_USER, SUPERVISING_PROFILE)
        }
        mockSupervisionManager.stub {
            on { isSupervisionEnabledForUser(MAIN_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SECONDARY_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SUPERVISING_USER_ID) } doReturn false
        }

        assertThat(preference.areAnyUsersExceptCurrentSupervised(
            mockSupervisionManager, mockUserManager)).isTrue()
    }

    @Test
    fun onConfirmDeleteClick_currentUserSupervised_deletesSupervisionData() {
        mockUserManager.stub {
            on { users } doReturn listOf(MAIN_USER, SECONDARY_USER, SUPERVISING_PROFILE)
            on { removeUser(UserHandle(SUPERVISING_USER_ID)) } doReturn true
        }
        mockSupervisionManager.stub {
            on { isSupervisionEnabledForUser(MAIN_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SECONDARY_USER_ID) } doReturn false
            on { isSupervisionEnabledForUser(SUPERVISING_USER_ID) } doReturn false
        }

        preference.onConfirmDeleteClick()
        verify(mockSupervisionManager).supervisionRecoveryInfo = eq(null)
        verify(mockSupervisionManager).isSupervisionEnabled = eq(false)
        verify(mockUserManager).removeUser(eq(UserHandle(SUPERVISING_USER_ID)))
        assertThat(startedIntent).isNotNull()
        assertThat(notifiedKey).isEqualTo(SupervisionDeletePinPreference.KEY)
    }

    @Test
    fun onConfirmDeleteClick_removeUserFails_doesNotDeleteSupervisionData() {
        mockUserManager.stub {
            on { users } doReturn listOf(MAIN_USER, SECONDARY_USER, SUPERVISING_PROFILE)
            on { removeUser(UserHandle(SUPERVISING_USER_ID)) } doReturn false
        }
        mockSupervisionManager.stub {
            on { isSupervisionEnabledForUser(MAIN_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SECONDARY_USER_ID) } doReturn true
            on { isSupervisionEnabledForUser(SUPERVISING_USER_ID) } doReturn false
        }

        preference.onConfirmDeleteClick()
        verify(mockSupervisionManager, never()).supervisionRecoveryInfo
        verify(mockSupervisionManager, never()).isSupervisionEnabled = eq(false)
        assertThat(startedIntent).isNull()
        assertAlertDialogHasMessage(R.string.supervision_delete_pin_error_message)
    }

    private fun assertAlertDialogHasMessage(resId: Int) {
        val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
        val shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog)
        assertThat(shadowDialog.message).isEqualTo(appContext.getString(resId))
    }

    companion object {
        private const val MAIN_USER_ID = 0
        private const val SECONDARY_USER_ID = 1
        private const val SUPERVISING_USER_ID = 10
        private val MAIN_USER = UserInfo(MAIN_USER_ID, "Main", null, 0, USER_TYPE_FULL_SYSTEM)
        private val SECONDARY_USER =
            UserInfo(SECONDARY_USER_ID, "Secondary", null, 0, USER_TYPE_FULL_SECONDARY)
        private val SUPERVISING_PROFILE =
            UserInfo(SUPERVISING_USER_ID, "Supervising", null, 0, USER_TYPE_PROFILE_SUPERVISING)
    }
}