blob: a4e46d129b1d53905a85ec449face368ab8464a1 [file] [log] [blame]
/*
* 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<String, PreferenceData>? = 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<Preference>(KEY)
updateDependentPreferencesEnabledState(
mainSwitchPreference,
supervisionMainSwitchStorage.getBoolean(KEY)!!,
)
val preferenceKeys =
buildList<String> {
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<MainSwitchPreference>(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<String>(), KeyValueStore {
override fun contains(key: String) = key == KEY
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
(context.getSystemService(SupervisionManager::class.java)?.isSupervisionEnabled() ==
true)
as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, 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
}
}