| /* | |
| * Copyright (C) 2024 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.security | |
| import android.app.Activity.RESULT_OK | |
| import android.content.Intent | |
| import android.content.pm.PackageManager | |
| import android.security.advancedprotection.AdvancedProtectionManager | |
| import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPORT_DIALOG_FEATURE | |
| import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPORT_DIALOG_TYPE | |
| import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G | |
| import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSECURE_WIFI_AUTOJOIN | |
| import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES | |
| import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_ENABLE_MTE | |
| import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION | |
| import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_DISABLED_SETTING | |
| import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_UNKNOWN | |
| import android.util.Log | |
| import android.view.WindowManager | |
| import androidx.annotation.VisibleForTesting | |
| import androidx.compose.material3.Icon | |
| import androidx.compose.material3.MaterialTheme | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.ui.res.painterResource | |
| import androidx.compose.ui.text.LinkAnnotation | |
| import androidx.compose.ui.text.SpanStyle | |
| import androidx.compose.ui.text.buildAnnotatedString | |
| import androidx.compose.ui.text.style.TextDecoration | |
| import androidx.compose.ui.text.withStyle | |
| import com.android.settings.R | |
| import com.android.settingslib.spa.SpaDialogWindowTypeActivity | |
| import com.android.settingslib.spa.widget.dialog.AlertDialogButton | |
| import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogContent | |
| import com.android.settingslib.wifi.WifiUtils.Companion.DIALOG_WINDOW_TYPE | |
| class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { | |
| @Composable | |
| override fun Content() { | |
| val featureId = getIntentFeatureId() | |
| val supportButton = getSupportButtonIfExists() | |
| val configuration = androidx.compose.ui.platform.LocalConfiguration.current | |
| if (featureId == FEATURE_ID_DISALLOW_INSECURE_WIFI_AUTOJOIN) { | |
| // Remove shield icon if very large font in landscape mode | |
| val dialogIcon: @Composable (() -> Unit)? = | |
| if (shouldShowIcon(configuration)) { | |
| { | |
| Icon( | |
| painter = painterResource(R.drawable.ic_settings_safety_center), | |
| contentDescription = null, | |
| ) | |
| } | |
| } else null | |
| // Custom dialog for Wi-Fi Autojoin | |
| SettingsAlertDialogContent( | |
| confirmButton = | |
| AlertDialogButton(getString(R.string.wifi_autojoin_disabled_positive_button)) { | |
| finish() | |
| logDialogShown(learnMoreClicked = false) | |
| }, | |
| dismissButton = | |
| AlertDialogButton(getString(R.string.wifi_autojoin_disabled_negative_button)) { | |
| setResult(RESULT_OK) | |
| finish() | |
| logDialogShown(learnMoreClicked = false) | |
| }, | |
| title = getString(R.string.wifi_autojoin_disabled_title), | |
| icon = dialogIcon, | |
| text = { WifiAutojoinDisabledText(supportButton?.onClick) }, | |
| ) | |
| } else { | |
| // Default dialog for all other features | |
| SettingsAlertDialogContent( | |
| confirmButton = | |
| AlertDialogButton(getString(R.string.okay)) { | |
| finish() | |
| logDialogShown(learnMoreClicked = false) | |
| }, | |
| dismissButton = | |
| supportButton?.let { button -> | |
| AlertDialogButton(button.text) { | |
| button.onClick() | |
| finish() | |
| } | |
| }, | |
| title = getString(R.string.disabled_by_advanced_protection_title), | |
| icon = { | |
| Icon( | |
| painter = painterResource(R.drawable.ic_settings_safety_center), | |
| contentDescription = null, | |
| ) | |
| }, | |
| text = { Text(getDialogMessage()) }, | |
| ) | |
| } | |
| } | |
| /** | |
| * Creates a composable body text. If a valid onClickAction is provided, it appends a clickable | |
| * "Learn more" link that executes that action. | |
| */ | |
| @Composable | |
| private fun WifiAutojoinDisabledText(onClickAction: (() -> Unit)?) { | |
| val annotatedText = buildAnnotatedString { | |
| append(getString(R.string.wifi_autojoin_disabled_body)) | |
| if (onClickAction != null) { | |
| append(" ") | |
| pushLink( | |
| LinkAnnotation.Clickable( | |
| tag = "LEARN_MORE", | |
| linkInteractionListener = { _ -> onClickAction() }, | |
| ) | |
| ) | |
| withStyle( | |
| style = | |
| SpanStyle( | |
| color = MaterialTheme.colorScheme.primary, | |
| textDecoration = TextDecoration.Underline, | |
| ) | |
| ) { | |
| append(getString(R.string.learn_more)) | |
| } | |
| pop() | |
| } | |
| } | |
| Text( | |
| text = annotatedText, | |
| style = | |
| MaterialTheme.typography.bodyMedium.copy( | |
| color = MaterialTheme.colorScheme.onSurfaceVariant | |
| ), | |
| ) | |
| } | |
| private fun getDialogMessage(): String { | |
| val featureId = getIntentFeatureId() | |
| val type = getIntentDialogueType() | |
| val messageId = | |
| when (type) { | |
| SUPPORT_DIALOG_TYPE_DISABLED_SETTING -> { | |
| if (featureIdsWithSettingOn.contains(featureId)) { | |
| R.string.disabled_by_advanced_protection_setting_is_on_message | |
| } else if (featureIdsWithSettingOff.contains(featureId)) { | |
| R.string.disabled_by_advanced_protection_setting_is_off_message | |
| } else { | |
| defaultMessageId | |
| } | |
| } | |
| SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION -> { | |
| R.string.disabled_by_advanced_protection_action_message | |
| } | |
| else -> defaultMessageId | |
| } | |
| return getString(messageId) | |
| } | |
| @VisibleForTesting | |
| fun getSupportButtonIfExists(): AlertDialogButton? { | |
| try { | |
| val helpIntentUri = | |
| getString( | |
| com.android.internal.R.string | |
| .config_help_url_action_disabled_by_advanced_protection | |
| ) | |
| val helpIntent = Intent.parseUri(helpIntentUri, Intent.URI_INTENT_SCHEME) | |
| if (helpIntent == null) return null | |
| helpIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | |
| val helpActivityInfo = | |
| packageManager.resolveActivity(helpIntent, /* flags */ 0)?.activityInfo | |
| if (helpActivityInfo == null) return null | |
| return AlertDialogButton( | |
| getString(R.string.disabled_by_advanced_protection_help_button_title) | |
| ) { | |
| startActivity(helpIntent) | |
| logDialogShown(learnMoreClicked = true) | |
| } | |
| } catch (e: Exception) { | |
| Log.w(TAG, "Tried to set up help button, but this exception was thrown: ${e.message}") | |
| } | |
| return null | |
| } | |
| private fun logDialogShown(learnMoreClicked: Boolean) { | |
| // We should always have this permission, but just in case we don't, we should not log. | |
| if ( | |
| checkSelfPermission(android.Manifest.permission.MANAGE_ADVANCED_PROTECTION_MODE) != | |
| PackageManager.PERMISSION_GRANTED | |
| ) { | |
| return | |
| } | |
| this.getSystemService(AdvancedProtectionManager::class.java) | |
| .logDialogShown(getIntentFeatureId(), getIntentDialogueType(), learnMoreClicked) | |
| } | |
| override fun getDialogWindowType(): Int? = | |
| if (intent.hasExtra(DIALOG_WINDOW_TYPE)) { | |
| intent.getIntExtra(DIALOG_WINDOW_TYPE, WindowManager.LayoutParams.TYPE_APPLICATION) | |
| } else null | |
| private fun getIntentFeatureId(): Int { | |
| return intent.getIntExtra(EXTRA_SUPPORT_DIALOG_FEATURE, -1) | |
| } | |
| private fun getIntentDialogueType(): Int { | |
| return intent.getIntExtra(EXTRA_SUPPORT_DIALOG_TYPE, SUPPORT_DIALOG_TYPE_UNKNOWN) | |
| } | |
| /** Determines if the icon should be shown based on screen real estate. */ | |
| @VisibleForTesting | |
| fun shouldShowIcon(configuration: android.content.res.Configuration): Boolean { | |
| val isLandscape = | |
| configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE | |
| if (!isLandscape) return true | |
| val isFontTooLarge = configuration.fontScale > A11Y_FONT_SCALE_THRESHOLD | |
| val isScreenTooShort = configuration.screenHeightDp < MIN_HEIGHT_FOR_ICON_DP | |
| return !(isFontTooLarge || isScreenTooShort) | |
| } | |
| private companion object { | |
| const val TAG = "AdvancedProtectionDlg" | |
| const val A11Y_FONT_SCALE_THRESHOLD = 1.1f | |
| const val MIN_HEIGHT_FOR_ICON_DP = 400 | |
| val defaultMessageId = R.string.disabled_by_advanced_protection_action_message | |
| val featureIdsWithSettingOn = setOf(FEATURE_ID_DISALLOW_CELLULAR_2G, FEATURE_ID_ENABLE_MTE) | |
| val featureIdsWithSettingOff = setOf(FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES) | |
| } | |
| } |