/* * 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.settings.SettingsEnums.ACTION_SUPERVISION_MAIN_TOGGLE_OFF import android.app.settings.SettingsEnums.ACTION_SUPERVISION_MAIN_TOGGLE_ON import android.app.supervision.SupervisionManager import android.content.Context import android.content.Intent import android.os.UserHandle 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.overlay.FeatureFactory import com.android.settings.supervision.ipc.PreferenceData import com.android.settingslib.HelpUtils import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.NoOpKeyedObservable 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.PreferenceSummaryProvider import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.preference.forEachRecursively import com.android.settingslib.supervision.SupervisionLog.TAG import com.android.settingslib.widget.MainSwitchPreference import com.android.settingslib.widget.MainSwitchPreferenceBinding import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Main toggle to enable or disable device supervision. */ class SupervisionMainSwitchPreference( context: Context, private val preferenceDataProvider: PreferenceDataProvider, private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : BooleanValuePreference, MainSwitchPreferenceBinding, PreferenceSummaryProvider, Preference.OnPreferenceChangeListener, PreferenceLifecycleProvider { private val supervisionMainSwitchStorage = SupervisionMainSwitchStorage(context) private var preferenceDataMap: Map? = null private lateinit var lifeCycleContext: PreferenceLifecycleContext private var pendingNewValue: Boolean? = null override val key get() = KEY override val title get() = R.string.device_supervision_switch_title override fun getSummary(context: Context): CharSequence? = if (!context.isSupervisingCredentialSet) { context.getString(R.string.device_supervision_switch_no_pin_summary) } else { null } override fun storage(context: Context): KeyValueStore = supervisionMainSwitchStorage override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.DISALLOW override fun getWritePermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.DISALLOW override val sensitivityLevel: Int get() = SensitivityLevel.HIGH_SENSITIVITY override fun onCreate(context: PreferenceLifecycleContext) { lifeCycleContext = context } override fun onResume(context: PreferenceLifecycleContext) { val mainSwitchPreference = context.findPreference(KEY) updateDependentPreferencesEnabledState( mainSwitchPreference, supervisionMainSwitchStorage.getBoolean(KEY)!!, ) val preferenceKeys = buildList { mainSwitchPreference?.parent?.forEachRecursively { if (it.parent?.key == SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1) { add(it.key) } } } context.lifecycleScope.launch { val cachedData = preferenceDataProvider.getCachedPreferenceData(preferenceKeys) if (cachedData.isNotEmpty()) { preferenceDataMap = cachedData updateDependentPreferenceSummary(mainSwitchPreference) } // TODO(b/426048474): when navigating from supervision app back to dashboard, the // settings injection often overrides the first preference data update, here we // are always updating the preference again after fetching fresh data to mitigate the // issue. preferenceDataMap = withContext(coroutineDispatcher) { preferenceDataProvider.getPreferenceData(preferenceKeys) } updateDependentPreferenceSummary(mainSwitchPreference) } } override fun onActivityResult( unused: PreferenceLifecycleContext, requestCode: Int, resultCode: Int, data: Intent?, ): Boolean { if ( requestCode != REQUEST_CODE_SET_UP_SUPERVISION && requestCode != REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS ) { return false } if (resultCode == Activity.RESULT_OK) { val mainSwitchPreference = lifeCycleContext.requirePreference(KEY) // Determine the new switch value based on the request code. // For setup, as setup activity will always set the value to true, // we need to use the pending value. // For confirmation, toggle the current value. val newValue = if (requestCode == REQUEST_CODE_SET_UP_SUPERVISION) { pendingNewValue ?: false } else { !supervisionMainSwitchStorage.getBoolean(KEY)!! } pendingNewValue = null mainSwitchPreference.setChecked(newValue) lifeCycleContext.notifyPreferenceChange(KEY) updateDependentPreferencesEnabledState(mainSwitchPreference, newValue) updateDependentPreferenceSummary(mainSwitchPreference) lifeCycleContext.notifyPreferenceChange(SupervisionPinManagementScreen.KEY) FeatureFactory.featureFactory.metricsFeatureProvider.action( lifeCycleContext, if (newValue) ACTION_SUPERVISION_MAIN_TOGGLE_ON else ACTION_SUPERVISION_MAIN_TOGGLE_OFF, ) } return true } override fun bind(preference: Preference, metadata: PreferenceMetadata) { super.bind(preference, metadata) preference.onPreferenceChangeListener = this } override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { if (newValue !is Boolean) return true // Only perform these checks when the user is turning supervision ON. if (newValue) { val userManager = preference.context.getSystemService(UserManager::class.java) if (userManager != null) { val supervisingProfileHandle: UserHandle? = preference.context.supervisingUserHandle val nonSupervisingProfiles = userManager.userProfiles.filter { it != supervisingProfileHandle } // If more than one profile remains (the main user + another), block enabling // supervision. if (nonSupervisingProfiles.size > 1) { AlertDialog.Builder(preference.context) .setTitle(R.string.supervision_multi_profile_error_title) .setMessage(R.string.supervision_multi_profile_error_message) .setPositiveButton(android.R.string.ok, null) .setNeutralButton(R.string.learn_more, { _, _ -> onLearnMore() }) .show() return false } } } // If supervision is being toggled but either the supervising profile hasn't been // created or the credentials aren't set, launch SetupSupervisionActivity. if (!preference.context.isSupervisingCredentialSet) { pendingNewValue = newValue val intent = Intent(lifeCycleContext, SetupSupervisionActivity::class.java) lifeCycleContext.startActivityForResult(intent, REQUEST_CODE_SET_UP_SUPERVISION, null) return false } // If supervision is already set up, confirm credentials before any change. val intent = Intent(lifeCycleContext, ConfirmSupervisionCredentialsActivity::class.java).apply { putExtra(ConfirmSupervisionCredentialsActivity.EXTRA_FORCE_CONFIRMATION, true) } lifeCycleContext.startActivityForResult( intent, REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, null, ) return false } private fun updateDependentPreferencesEnabledState( preference: Preference?, isChecked: Boolean, ) { preference?.parent?.forEachRecursively { if (it.parent?.key == SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1) { it.isEnabled = isChecked } } } private fun updateDependentPreferenceSummary(preference: Preference?) { preference?.parent?.forEachRecursively { if (it.parent?.key == SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1) { val newSummary = preferenceDataMap?.get(it.key)?.summary if (newSummary != null) { it.summary = newSummary } } } } private fun onLearnMore() { val intent = HelpUtils.getHelpIntent( lifeCycleContext, lifeCycleContext.getString(R.string.supervision_unavailable_learn_more_link), lifeCycleContext::class.java.name, ) if (intent != null) { lifeCycleContext.startActivity(intent) } else { Log.w(TAG, "HelpIntent is null") } } @Suppress("UNCHECKED_CAST") private class SupervisionMainSwitchStorage(private val context: Context) : NoOpKeyedObservable(), KeyValueStore { override fun contains(key: String) = key == KEY override fun getValue(key: String, valueType: Class) = (context.getSystemService(SupervisionManager::class.java)?.isSupervisionEnabled() == true) as T override fun setValue(key: String, valueType: Class, value: T?) { if (key == KEY && value is Boolean) { val supervisionManager = context.getSystemService(SupervisionManager::class.java) supervisionManager?.setSupervisionEnabled(value) } } } companion object { const val KEY = "device_supervision_switch" @VisibleForTesting const val REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS = 0 @VisibleForTesting const val REQUEST_CODE_SET_UP_SUPERVISION = 1 } }