diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt index cea2b4410c7..eda9d7a9704 100644 --- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt +++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt @@ -38,17 +38,10 @@ import androidx.compose.ui.res.stringResource import androidx.navigation.NavType import androidx.navigation.navArgument import com.android.settings.R -import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeDisplayNames -import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeSelectedOptionsState -import com.android.settings.network.apn.ApnTypes.APN_TYPES_OPTIONS -import com.android.settings.network.apn.ApnTypes.APN_TYPE_MMS -import com.android.settings.network.apn.ApnTypes.getApnTypeSelectedOptionsState -import com.android.settings.network.apn.ApnTypes.updateApnType import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.compose.LocalNavController import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox -import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox import com.android.settingslib.spa.widget.editor.SettingsOutlinedTextField import com.android.settingslib.spa.widget.editor.SettingsTextFieldPassword import com.android.settingslib.spa.widget.preference.SwitchPreference @@ -79,7 +72,7 @@ object ApnEditPageProvider : SettingsPageProvider { val uriString = arguments!!.getString(URI) val uriInit = Uri.parse(String(Base64.getDecoder().decode(uriString))) val subId = arguments.getInt(SUB_ID) - val apnDataInit = getApnDataInit(arguments, LocalContext.current, uriInit, subId) + val apnDataInit = getApnDataInit(arguments, LocalContext.current, uriInit, subId) ?: return val apnDataCur = remember { mutableStateOf(apnDataInit) } @@ -101,12 +94,7 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur val context = LocalContext.current val authTypeOptions = stringArrayResource(R.array.apn_auth_entries).toList() val apnProtocolOptions = stringArrayResource(R.array.apn_protocol_entries).toList() - val networkTypeSelectedOptionsState = remember { - getNetworkTypeSelectedOptionsState(apnData.networkType) - } - var apnTypeSelectedOptionsState = remember { - getApnTypeSelectedOptionsState(apnData.apnType) - } + var apnTypeMmsSelected by remember { mutableStateOf(false) } val navController = LocalNavController.current var valid: String? RegularScaffold( @@ -114,11 +102,6 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur actions = { if (!apnData.customizedConfig.readOnlyApn) { Button(onClick = { - apnData = apnData.copy( - networkType = ApnNetworkTypes.getNetworkType( - networkTypeSelectedOptionsState - ) - ) valid = validateAndSaveApnData( apnDataInit, apnData, @@ -193,27 +176,12 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur label = stringResource(R.string.apn_server), enabled = apnData.serverEnabled ) { apnData = apnData.copy(server = it) } - SettingsExposedDropdownMenuCheckBox( - label = stringResource(R.string.apn_type), - options = APN_TYPES_OPTIONS, - selectedOptionsState = apnTypeSelectedOptionsState, - enabled = apnData.apnTypeEnabled, - errorMessage = validateAPNType( - apnData.validEnabled, apnData.apnType, - apnData.customizedConfig.readOnlyApnTypes, context - ) - ) { - val apnType = updateApnType( - apnTypeSelectedOptionsState, - apnData.customizedConfig.defaultApnTypes, - apnData.customizedConfig.readOnlyApnTypes - ) - apnTypeSelectedOptionsState = getApnTypeSelectedOptionsState(apnType) - apnData = apnData.copy( - apnType = apnType - ) - } - if (apnTypeSelectedOptionsState.contains(APN_TYPES_OPTIONS.indexOf(APN_TYPE_MMS))) { + ApnTypeCheckBox( + apnData = apnData, + onTypeChanged = { apnData = apnData.copy(apnType = it) }, + onMmsSelectedChanged = { apnTypeMmsSelected = it }, + ) + if (apnTypeMmsSelected) { SettingsOutlinedTextField( value = apnData.mmsc, label = stringResource(R.string.apn_mmsc), @@ -249,13 +217,7 @@ fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState, uriInit: Ur selectedOptionIndex = apnData.apnRoaming, enabled = apnData.apnRoamingEnabled ) { apnData = apnData.copy(apnRoaming = it) } - SettingsExposedDropdownMenuCheckBox( - label = stringResource(R.string.network_type), - options = getNetworkTypeDisplayNames(), - selectedOptionsState = networkTypeSelectedOptionsState, - emptyVal = stringResource(R.string.network_type_unspecified), - enabled = apnData.networkTypeEnabled - ) {} + ApnNetworkTypeCheckBox(apnData) { apnData = apnData.copy(networkType = it) } SwitchPreference( object : SwitchPreferenceModel { override val title = context.resources.getString(R.string.carrier_enabled) diff --git a/src/com/android/settings/network/apn/ApnNetworkTypeCheckBox.kt b/src/com/android/settings/network/apn/ApnNetworkTypeCheckBox.kt new file mode 100644 index 00000000000..bc85f5582b7 --- /dev/null +++ b/src/com/android/settings/network/apn/ApnNetworkTypeCheckBox.kt @@ -0,0 +1,41 @@ +/* + * 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.network.apn + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox + +@Composable +fun ApnNetworkTypeCheckBox(apnData: ApnData, onNetworkTypeChanged: (Long) -> Unit) { + val options = remember { ApnNetworkTypes.getNetworkTypeOptions() } + val selectedStateMap = remember { + ApnNetworkTypes.networkTypeToSelectedStateMap(options, apnData.networkType) + } + SettingsDropdownCheckBox( + label = stringResource(R.string.network_type), + options = options, + emptyText = stringResource(R.string.network_type_unspecified), + enabled = apnData.networkTypeEnabled, + ) { + onNetworkTypeChanged( + ApnNetworkTypes.selectedStateMapToNetworkType(options, selectedStateMap) + ) + } +} diff --git a/src/com/android/settings/network/apn/ApnNetworkTypes.kt b/src/com/android/settings/network/apn/ApnNetworkTypes.kt index 0ccd33a8210..e7a93b3cb79 100644 --- a/src/com/android/settings/network/apn/ApnNetworkTypes.kt +++ b/src/com/android/settings/network/apn/ApnNetworkTypes.kt @@ -17,8 +17,9 @@ package com.android.settings.network.apn import android.telephony.TelephonyManager -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.snapshots.SnapshotStateMap +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption object ApnNetworkTypes { private val Types = listOf( @@ -39,32 +40,40 @@ object ApnNetworkTypes { TelephonyManager.NETWORK_TYPE_NR, ) - fun getNetworkTypeDisplayNames(): List = - Types.map { TelephonyManager.getNetworkTypeName(it) } + fun getNetworkTypeOptions(): List = + Types.map { SettingsDropdownCheckOption(TelephonyManager.getNetworkTypeName(it)) } /** * Gets the selected Network type Selected Options according to network type. * @param networkType Initialized network type bitmask, often multiple network type options may * be included. */ - fun getNetworkTypeSelectedOptionsState(networkType: Long): SnapshotStateList { - val networkTypeSelectedOptionsState = mutableStateListOf() + fun networkTypeToSelectedStateMap( + options: List, + networkType: Long, + ): SnapshotStateMap { + val stateMap = mutableStateMapOf() Types.forEachIndexed { index, type -> if (networkType and TelephonyManager.getBitMaskForNetworkType(type) != 0L) { - networkTypeSelectedOptionsState.add(index) + stateMap[options[index]] = true } } - return networkTypeSelectedOptionsState + return stateMap } /** * Gets the network type according to the selected Network type Selected Options. - * @param networkTypeSelectedOptionsState the selected Network type Selected Options. + * @param stateMap the selected Network type Selected Options. */ - fun getNetworkType(networkTypeSelectedOptionsState: SnapshotStateList): Long { + fun selectedStateMapToNetworkType( + options: List, + stateMap: SnapshotStateMap, + ): Long { var networkType = 0L - networkTypeSelectedOptionsState.forEach { option -> - networkType = networkType or TelephonyManager.getBitMaskForNetworkType(Types[option]) + options.forEachIndexed { index, option -> + if (stateMap[option] == true) { + networkType = networkType or TelephonyManager.getBitMaskForNetworkType(Types[index]) + } } return networkType } diff --git a/src/com/android/settings/network/apn/ApnRepository.kt b/src/com/android/settings/network/apn/ApnRepository.kt index 226698310c1..2d41976b239 100644 --- a/src/com/android/settings/network/apn/ApnRepository.kt +++ b/src/com/android/settings/network/apn/ApnRepository.kt @@ -18,6 +18,7 @@ package com.android.settings.network.apn import android.content.ContentValues import android.content.Context +import android.database.Cursor import android.net.Uri import android.provider.Telephony import android.telephony.SubscriptionManager @@ -27,26 +28,7 @@ import com.android.settings.R import com.android.settingslib.utils.ThreadUtils import java.util.Locale -const val NAME_INDEX = 1 -const val APN_INDEX = 2 -const val PROXY_INDEX = 3 -const val PORT_INDEX = 4 -const val USER_INDEX = 5 -const val SERVER_INDEX = 6 -const val PASSWORD_INDEX = 7 -const val MMSC_INDEX = 8 -const val MMSPROXY_INDEX = 9 -const val MMSPORT_INDEX = 10 -const val AUTH_TYPE_INDEX = 11 -const val TYPE_INDEX = 12 -const val PROTOCOL_INDEX = 13 -const val CARRIER_ENABLED_INDEX = 14 -const val NETWORK_TYPE_INDEX = 15 -const val ROAMING_PROTOCOL_INDEX = 16 -const val EDITED_INDEX = 17 -const val USER_EDITABLE_INDEX = 18 - -val sProjection = arrayOf( +val Projection = arrayOf( Telephony.Carriers._ID, // 0 Telephony.Carriers.NAME, // 1 Telephony.Carriers.APN, // 2 @@ -68,7 +50,7 @@ val sProjection = arrayOf( Telephony.Carriers.USER_EDITABLE, // 18 ) -const val TAG = "ApnRepository" +private const val TAG = "ApnRepository" /** * Query apn related information based on uri. @@ -79,56 +61,39 @@ const val TAG = "ApnRepository" fun getApnDataFromUri(uri: Uri, context: Context): ApnData { var apnData = ApnData() val contentResolver = context.contentResolver - val apnProtocolOptions = context.resources.getStringArray(R.array.apn_protocol_entries).toList() contentResolver.query( uri, - sProjection, + Projection, null /* selection */, null /* selectionArgs */, null /* sortOrder */ ).use { cursor -> if (cursor != null && cursor.moveToFirst()) { - val name = cursor.getString(NAME_INDEX) - val apn = cursor.getString(APN_INDEX) - val proxy = cursor.getString(PROXY_INDEX) - val port = cursor.getString(PORT_INDEX) - val userName = cursor.getString(USER_INDEX) - val server = cursor.getString(SERVER_INDEX) - val passWord = cursor.getString(PASSWORD_INDEX) - val mmsc = cursor.getString(MMSC_INDEX) - val mmsProxy = cursor.getString(MMSPROXY_INDEX) - val mmsPort = cursor.getString(MMSPORT_INDEX) - val authType = cursor.getInt(AUTH_TYPE_INDEX) - val apnType = cursor.getString(TYPE_INDEX) - val apnProtocol = convertProtocol2Options(cursor.getString(PROTOCOL_INDEX), context) - val apnRoaming = - convertProtocol2Options(cursor.getString(ROAMING_PROTOCOL_INDEX), context) - val apnEnable = cursor.getInt(CARRIER_ENABLED_INDEX) == 1 - val networkType = cursor.getLong(NETWORK_TYPE_INDEX) - - val edited = cursor.getInt(EDITED_INDEX) - val userEditable = cursor.getInt(USER_EDITABLE_INDEX) - - apnData = apnData.copy( - name = name, - apn = apn, - proxy = proxy, - port = port, - userName = userName, - passWord = passWord, - server = server, - mmsc = mmsc, - mmsProxy = mmsProxy, - mmsPort = mmsPort, - authType = authType, - apnType = apnType, - apnProtocol = apnProtocolOptions.indexOf(apnProtocol), - apnRoaming = apnProtocolOptions.indexOf(apnRoaming), - apnEnable = apnEnable, - networkType = networkType, - edited = edited, - userEditable = userEditable, + apnData = ApnData( + id = cursor.getInt(Telephony.Carriers._ID), + name = cursor.getString(Telephony.Carriers.NAME), + apn = cursor.getString(Telephony.Carriers.APN), + proxy = cursor.getString(Telephony.Carriers.PROXY), + port = cursor.getString(Telephony.Carriers.PORT), + userName = cursor.getString(Telephony.Carriers.USER), + passWord = cursor.getString(Telephony.Carriers.PASSWORD), + server = cursor.getString(Telephony.Carriers.SERVER), + mmsc = cursor.getString(Telephony.Carriers.MMSC), + mmsProxy = cursor.getString(Telephony.Carriers.MMSPROXY), + mmsPort = cursor.getString(Telephony.Carriers.MMSPORT), + authType = cursor.getInt(Telephony.Carriers.AUTH_TYPE), + apnType = cursor.getString(Telephony.Carriers.TYPE), + apnProtocol = context.convertProtocol2Options( + cursor.getString(Telephony.Carriers.PROTOCOL) + ), + apnRoaming = context.convertProtocol2Options( + cursor.getString(Telephony.Carriers.ROAMING_PROTOCOL) + ), + apnEnable = cursor.getInt(Telephony.Carriers.CARRIER_ENABLED) == 1, + networkType = cursor.getLong(Telephony.Carriers.NETWORK_TYPE_BITMASK), + edited = cursor.getInt(Telephony.Carriers.EDITED_STATUS), + userEditable = cursor.getInt(Telephony.Carriers.USER_EDITABLE), ) } } @@ -138,42 +103,23 @@ fun getApnDataFromUri(uri: Uri, context: Context): ApnData { return apnData } +private fun Cursor.getString(columnName: String) = getString(getColumnIndexOrThrow(columnName)) +private fun Cursor.getInt(columnName: String) = getInt(getColumnIndexOrThrow(columnName)) +private fun Cursor.getLong(columnName: String) = getLong(getColumnIndexOrThrow(columnName)) + /** - * Returns The UI choice (e.g., "IPv4/IPv6") corresponding to the given - * raw value of the protocol preference (e.g., "IPV4V6"). If unknown, - * return null. - * - * @return UI choice + * Returns The UI choice index corresponding to the given raw value of the protocol preference + * (e.g., "IPV4V6"). + * If unknown, return -1. */ -private fun convertProtocol2Options(raw: String, context: Context): String { - val apnProtocolOptions = context.resources.getStringArray(R.array.apn_protocol_entries).toList() - val apnProtocolValues = context.resources.getStringArray(R.array.apn_protocol_values).toList() - var uRaw = raw.uppercase(Locale.getDefault()) - uRaw = if (uRaw == "IPV4") "IP" else uRaw - val protocolIndex = apnProtocolValues.indexOf(uRaw) - return if (protocolIndex == -1) { - "" - } else { - try { - apnProtocolOptions[protocolIndex] - } catch (e: ArrayIndexOutOfBoundsException) { - "" - } - } +private fun Context.convertProtocol2Options(protocol: String): Int { + var normalizedProtocol = protocol.uppercase(Locale.getDefault()) + if (normalizedProtocol == "IPV4") normalizedProtocol = "IP" + return resources.getStringArray(R.array.apn_protocol_values).indexOf(normalizedProtocol) } -fun convertOptions2Protocol(protocolIndex: Int, context: Context): String { - val apnProtocolValues = context.resources.getStringArray(R.array.apn_protocol_values).toList() - return if (protocolIndex == -1) { - "" - } else { - try { - apnProtocolValues[protocolIndex] - } catch (e: ArrayIndexOutOfBoundsException) { - "" - } - } -} +fun Context.convertOptions2Protocol(protocolIndex: Int): String = + resources.getStringArray(R.array.apn_protocol_values).getOrElse(protocolIndex) { "" } fun updateApnDataToDatabase( newApn: Boolean, @@ -183,13 +129,13 @@ fun updateApnDataToDatabase( ) { ThreadUtils.postOnBackgroundThread { if (newApn) { - // Add a new apn to the database + Log.d(TAG, "Adding an new APN to the database $uriInit $values") val newUri = context.contentResolver.insert(uriInit, values) if (newUri == null) { Log.e(TAG, "Can't add a new apn to database $uriInit") } } else { - // Update the existing apn + Log.d(TAG, "Updating an existing APN to the database $uriInit $values") context.contentResolver.update( uriInit, values, null /* where */, null /* selection Args */ ) @@ -210,9 +156,12 @@ private val NonDuplicatedKeys = setOf( ) fun isItemExist(apnData: ApnData, context: Context): String? { - val contentValueMap = apnData.getContentValueMap(context).filterKeys { it in NonDuplicatedKeys } - val list = contentValueMap.entries.toList() - val selection = list.joinToString(" AND ") { "${it.key} = ?" } + val selectionMap = apnData.getContentValueMap(context).filterKeys { it in NonDuplicatedKeys } + .mapKeys { "${it.key} = ?" } + .toMutableMap() + if (apnData.id != -1) selectionMap += "${Telephony.Carriers._ID} != ?" to apnData.id + val list = selectionMap.entries.toList() + val selection = list.joinToString(" AND ") { it.key } val selectionArgs: Array = list.map { it.value.toString() }.toTypedArray() context.contentResolver.query( Uri.withAppendedPath(Telephony.Carriers.SIM_APN_URI, apnData.subId.toString()), diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt index 02e2814e749..ab16f1ca5d2 100644 --- a/src/com/android/settings/network/apn/ApnStatus.kt +++ b/src/com/android/settings/network/apn/ApnStatus.kt @@ -22,19 +22,14 @@ import android.net.Uri import android.os.Bundle import android.provider.Telephony import android.telephony.CarrierConfigManager -import android.text.TextUtils import android.util.Log -import com.android.internal.util.ArrayUtils import com.android.settings.R -import com.android.settings.network.apn.ApnTypes.APN_TYPES -import com.android.settings.network.apn.ApnTypes.APN_TYPE_ALL -import com.android.settings.network.apn.ApnTypes.APN_TYPE_EMERGENCY -import com.android.settings.network.apn.ApnTypes.APN_TYPE_IA -import com.android.settings.network.apn.ApnTypes.APN_TYPE_IMS -import com.android.settings.network.apn.ApnTypes.APN_TYPE_MCX -import java.util.Locale +import com.android.settings.network.apn.ApnTypes.getPreSelectedApnType + +private const val TAG = "ApnStatus" data class ApnData( + val id: Int = -1, val name: String = "", val apn: String = "", val proxy: String = "", @@ -86,8 +81,8 @@ data class ApnData( Telephony.Carriers.MMSPROXY to mmsProxy, Telephony.Carriers.MMSPORT to mmsPort, Telephony.Carriers.AUTH_TYPE to authType, - Telephony.Carriers.PROTOCOL to convertOptions2Protocol(apnProtocol, context), - Telephony.Carriers.ROAMING_PROTOCOL to convertOptions2Protocol(apnRoaming, context), + Telephony.Carriers.PROTOCOL to context.convertOptions2Protocol(apnProtocol), + Telephony.Carriers.ROAMING_PROTOCOL to context.convertOptions2Protocol(apnRoaming), Telephony.Carriers.TYPE to apnType, Telephony.Carriers.NETWORK_TYPE_BITMASK to networkType, Telephony.Carriers.CARRIER_ENABLED to apnEnable, @@ -105,7 +100,7 @@ data class CustomizedConfig( val isAddApnAllowed: Boolean = true, val readOnlyApnTypes: List = emptyList(), val readOnlyApnFields: List = emptyList(), - val defaultApnTypes: List = emptyList(), + val defaultApnTypes: List? = null, val defaultApnProtocol: String = "", val defaultApnRoamingProtocol: String = "", ) @@ -118,19 +113,18 @@ data class CustomizedConfig( * * @return Initialized CustomizedConfig information. */ -fun getApnDataInit(arguments: Bundle, context: Context, uriInit: Uri, subId: Int): ApnData { - - val uriType = arguments.getString(URI_TYPE)!! +fun getApnDataInit(arguments: Bundle, context: Context, uriInit: Uri, subId: Int): ApnData? { + val uriType = arguments.getString(URI_TYPE) ?: return null if (!uriInit.isPathPrefixMatch(Telephony.Carriers.CONTENT_URI)) { Log.e(TAG, "Insert request not for carrier table. Uri: $uriInit") - return ApnData() //TODO: finish + return null } var apnDataInit = when (uriType) { EDIT_URL -> getApnDataFromUri(uriInit, context) INSERT_URL -> ApnData() - else -> ApnData() //TODO: finish + else -> return null } if (uriType == INSERT_URL) { @@ -143,13 +137,18 @@ fun getApnDataInit(arguments: Bundle, context: Context, uriInit: Uri, subId: Int apnDataInit = apnDataInit.copy(customizedConfig = getCarrierCustomizedConfig(apnDataInit, configManager)) + if (apnDataInit.newApn) { + apnDataInit = apnDataInit.copy( + apnType = getPreSelectedApnType(apnDataInit.customizedConfig) + ) + } + apnDataInit = apnDataInit.copy( apnEnableEnabled = context.resources.getBoolean(R.bool.config_allow_edit_carrier_enabled) ) // TODO: mIsCarrierIdApn - disableInit(apnDataInit) - return apnDataInit + return disableInit(apnDataInit) } /** @@ -199,53 +198,7 @@ fun validateApnData(apnData: ApnData, context: Context): String? { if (errorMsg == null) { errorMsg = isItemExist(apnData, context) } - if (errorMsg == null) { - errorMsg = validateAPNType( - true, - apnData.apnType, - apnData.customizedConfig.readOnlyApnTypes, - context - ) - } - return errorMsg -} - -private fun getUserEnteredApnType(apnType: String, readOnlyApnTypes: List): String { - // if user has not specified a type, map it to "ALL APN TYPES THAT ARE NOT READ-ONLY" - // but if user enter empty type, map it just for default - var userEnteredApnType = apnType - if (userEnteredApnType != "") userEnteredApnType = - userEnteredApnType.trim { it <= ' ' } - if (TextUtils.isEmpty(userEnteredApnType) || APN_TYPE_ALL == userEnteredApnType) { - userEnteredApnType = getEditableApnType(readOnlyApnTypes) - } - Log.d( - TAG, "getUserEnteredApnType: changed apn type to editable apn types: " - + userEnteredApnType - ) - return userEnteredApnType -} - -private fun getEditableApnType(readOnlyApnTypes: List): String { - val editableApnTypes = StringBuilder() - var first = true - for (apnType in APN_TYPES) { - // add APN type if it is not read-only and is not wild-cardable - if (!readOnlyApnTypes.contains(apnType) - && apnType != APN_TYPE_IA - && apnType != APN_TYPE_EMERGENCY - && apnType != APN_TYPE_MCX - && apnType != APN_TYPE_IMS - ) { - if (first) { - first = false - } else { - editableApnTypes.append(",") - } - editableApnTypes.append(apnType) - } - } - return editableApnTypes.toString() + return errorMsg?.apply { Log.d(TAG, "APN data not valid, reason: $this") } } /** @@ -258,6 +211,10 @@ fun getCarrierCustomizedConfig( apnInit: ApnData, configManager: CarrierConfigManager ): CustomizedConfig { + fun log(message: String) { + Log.d(TAG, "getCarrierCustomizedConfig: $message") + } + val b = configManager.getConfigForSubId( apnInit.subId, CarrierConfigManager.KEY_READ_ONLY_APN_TYPES_STRING_ARRAY, @@ -270,72 +227,61 @@ fun getCarrierCustomizedConfig( val customizedConfig = CustomizedConfig( readOnlyApnTypes = b.getStringArray( CarrierConfigManager.KEY_READ_ONLY_APN_TYPES_STRING_ARRAY - )?.toList() ?: emptyList(), readOnlyApnFields = b.getStringArray( + )?.toList() ?: emptyList(), + readOnlyApnFields = b.getStringArray( CarrierConfigManager.KEY_READ_ONLY_APN_FIELDS_STRING_ARRAY - )?.toList() ?: emptyList(), defaultApnTypes = b.getStringArray( + )?.toList() ?: emptyList(), + defaultApnTypes = b.getStringArray( CarrierConfigManager.KEY_APN_SETTINGS_DEFAULT_APN_TYPES_STRING_ARRAY - )?.toList() ?: emptyList(), defaultApnProtocol = b.getString( + )?.toList(), + defaultApnProtocol = b.getString( CarrierConfigManager.Apn.KEY_SETTINGS_DEFAULT_PROTOCOL_STRING - ) ?: "", defaultApnRoamingProtocol = b.getString( + ) ?: "", + defaultApnRoamingProtocol = b.getString( CarrierConfigManager.Apn.KEY_SETTINGS_DEFAULT_ROAMING_PROTOCOL_STRING - ) ?: "", isAddApnAllowed = b.getBoolean(CarrierConfigManager.KEY_ALLOW_ADDING_APNS_BOOL) + ) ?: "", + isAddApnAllowed = b.getBoolean(CarrierConfigManager.KEY_ALLOW_ADDING_APNS_BOOL), ) - if (!ArrayUtils.isEmpty(customizedConfig.readOnlyApnTypes)) { - Log.d( - TAG, - "getCarrierCustomizedConfig: read only APN type: " + customizedConfig.readOnlyApnTypes.joinToString( - ", " - ) - ) + if (customizedConfig.readOnlyApnTypes.isNotEmpty()) { + log("read only APN type: " + customizedConfig.readOnlyApnTypes) } - if (!ArrayUtils.isEmpty(customizedConfig.defaultApnTypes)) { - Log.d( - TAG, - "getCarrierCustomizedConfig: default apn types: " + customizedConfig.defaultApnTypes.joinToString( - ", " - ) - ) + customizedConfig.defaultApnTypes?.takeIf { it.isNotEmpty() }?.let { + log("default apn types: $it") } - if (!TextUtils.isEmpty(customizedConfig.defaultApnProtocol)) { - Log.d( - TAG, - "getCarrierCustomizedConfig: default apn protocol: ${customizedConfig.defaultApnProtocol}" - ) + if (customizedConfig.defaultApnProtocol.isNotEmpty()) { + log("default apn protocol: ${customizedConfig.defaultApnProtocol}") } - if (!TextUtils.isEmpty(customizedConfig.defaultApnRoamingProtocol)) { - Log.d( - TAG, - "getCarrierCustomizedConfig: default apn roaming protocol: ${customizedConfig.defaultApnRoamingProtocol}" - ) + if (customizedConfig.defaultApnRoamingProtocol.isNotEmpty()) { + log("default apn roaming protocol: ${customizedConfig.defaultApnRoamingProtocol}") } if (!customizedConfig.isAddApnAllowed) { - Log.d(TAG, "getCarrierCustomizedConfig: not allow to add new APN") + log("not allow to add new APN") } return customizedConfig } -fun disableInit(apnDataInit: ApnData): ApnData { - var apnData = apnDataInit - val isUserEdited = apnDataInit.edited == Telephony.Carriers.USER_EDITED - Log.d(TAG, "disableInit: EDITED $isUserEdited") +private fun ApnData.isReadOnly(): Boolean { + Log.d(TAG, "isReadOnly: edited $edited") + if (edited == Telephony.Carriers.USER_EDITED) return false // if it's not a USER_EDITED apn, check if it's read-only - if (!isUserEdited && (apnDataInit.userEditable == 0 - || apnTypesMatch(apnDataInit.customizedConfig.readOnlyApnTypes, apnDataInit.apnType)) - ) { + return userEditable == 0 || + ApnTypes.isApnTypeReadOnly(apnType, customizedConfig.readOnlyApnTypes) +} + +fun disableInit(apnDataInit: ApnData): ApnData { + if (apnDataInit.isReadOnly()) { Log.d(TAG, "disableInit: read-only APN") - apnData = - apnDataInit.copy(customizedConfig = apnDataInit.customizedConfig.copy(readOnlyApn = true)) - apnData = disableAllFields(apnData) - } else if (!ArrayUtils.isEmpty(apnData.customizedConfig.readOnlyApnFields)) { - Log.d( - TAG, - "disableInit: mReadOnlyApnFields ${ - apnData.customizedConfig.readOnlyApnFields.joinToString(", ") - })" + val apnData = apnDataInit.copy( + customizedConfig = apnDataInit.customizedConfig.copy(readOnlyApn = true) ) - apnData = disableFields(apnData.customizedConfig.readOnlyApnFields, apnData) + return disableAllFields(apnData) } - return apnData + val readOnlyApnFields = apnDataInit.customizedConfig.readOnlyApnFields + if (readOnlyApnFields.isNotEmpty()) { + Log.d(TAG, "disableInit: readOnlyApnFields $readOnlyApnFields)") + return disableFields(readOnlyApnFields, apnDataInit) + } + return apnDataInit } /** @@ -402,23 +348,6 @@ private fun disableByFieldName(apnField: String, apnDataInit: ApnData): ApnData return apnData } -private fun apnTypesMatch(apnTypeList: List, apnType: String): Boolean { - val normalizeApnTypeList = apnTypeList.map(::normalizeApnType) - return hasAllApns(normalizeApnTypeList) || - apnType.split(",").map(::normalizeApnType).all { it in normalizeApnTypeList } -} - -fun hasAllApns(apnTypes: List): Boolean { - if (APN_TYPE_ALL in apnTypes) { - Log.d(TAG, "hasAllApns: true because apnTypes.contains(APN_TYPE_ALL)") - return true - } - return APN_TYPES.all { it in apnTypes } -} - -private fun normalizeApnType(apnType: String): String = - apnType.trim().lowercase(Locale.getDefault()) - fun deleteApn(uri: Uri, context: Context) { val contentResolver = context.contentResolver contentResolver.delete(uri, null, null) @@ -439,24 +368,3 @@ fun validateAPN(validEnabled: Boolean, apn: String, context: Context): String? { return if (validEnabled && (apn == "")) context.resources.getString(R.string.error_apn_empty) else null } - -fun validateAPNType( - validEnabled: Boolean, - apnType: String, - readOnlyApnTypes: List, - context: Context -): String? { - // if carrier does not allow editing certain apn types, make sure type does not include those - if (validEnabled && !ArrayUtils.isEmpty(readOnlyApnTypes) - && apnTypesMatch( - readOnlyApnTypes, - getUserEnteredApnType(apnType, readOnlyApnTypes) - ) - ) { - return String.format( - context.resources.getString(R.string.error_adding_apn_type), - readOnlyApnTypes.joinToString(", ") - ) - } - return null -} \ No newline at end of file diff --git a/src/com/android/settings/network/apn/ApnTypeCheckBox.kt b/src/com/android/settings/network/apn/ApnTypeCheckBox.kt new file mode 100644 index 00000000000..4d0659c659c --- /dev/null +++ b/src/com/android/settings/network/apn/ApnTypeCheckBox.kt @@ -0,0 +1,53 @@ +/* + * 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.network.apn + +import android.telephony.data.ApnSetting +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.network.apn.ApnTypes.toApnType +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox + +@Composable +fun ApnTypeCheckBox( + apnData: ApnData, + onTypeChanged: (String) -> Unit, + onMmsSelectedChanged: (Boolean) -> Unit, +) { + val context = LocalContext.current + val apnTypeOptions = remember { + ApnTypes.getOptions(context, apnData.apnType, apnData.customizedConfig.readOnlyApnTypes) + } + + fun updateMmsSelected() { + val apnTypeOptionMms = apnTypeOptions.single { it.text == ApnSetting.TYPE_MMS_STRING } + onMmsSelectedChanged(apnTypeOptionMms.selected.value) + } + LaunchedEffect(Unit) { updateMmsSelected() } + SettingsDropdownCheckBox( + label = stringResource(R.string.apn_type), + options = apnTypeOptions, + enabled = apnData.apnTypeEnabled, + ) { + onTypeChanged(apnTypeOptions.toApnType()) + updateMmsSelected() + } +} diff --git a/src/com/android/settings/network/apn/ApnTypes.kt b/src/com/android/settings/network/apn/ApnTypes.kt index d3dbe38b426..2c8fa2a51a0 100644 --- a/src/com/android/settings/network/apn/ApnTypes.kt +++ b/src/com/android/settings/network/apn/ApnTypes.kt @@ -16,128 +16,112 @@ package com.android.settings.network.apn -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList +import android.content.Context +import android.telephony.data.ApnSetting +import android.util.Log +import android.widget.Toast +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import com.android.settings.R +import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption object ApnTypes { - /** - * APN types for data connections. These are usage categories for an APN - * entry. One APN entry may support multiple APN types, eg, a single APN - * may service regular internet traffic ("default") as well as MMS-specific - * connections.

- * APN_TYPE_ALL is a special type to indicate that this APN entry can - * service all data connections. - */ - const val APN_TYPE_ALL = "*" + private const val TAG = "ApnTypes" - /** APN type for default data traffic */ - const val APN_TYPE_DEFAULT = "default" - - /** APN type for MMS traffic */ - const val APN_TYPE_MMS = "mms" - - /** APN type for SUPL assisted GPS */ - const val APN_TYPE_SUPL = "supl" - - /** APN type for DUN traffic */ - const val APN_TYPE_DUN = "dun" - - /** APN type for HiPri traffic */ - const val APN_TYPE_HIPRI = "hipri" - - /** APN type for FOTA */ - const val APN_TYPE_FOTA = "fota" - - /** APN type for IMS */ - const val APN_TYPE_IMS = "ims" - - /** APN type for CBS */ - const val APN_TYPE_CBS = "cbs" - - /** APN type for IA Initial Attach APN */ - const val APN_TYPE_IA = "ia" - - /** APN type for Emergency PDN. This is not an IA apn, but is used - * for access to carrier services in an emergency call situation. */ - const val APN_TYPE_EMERGENCY = "emergency" - - /** APN type for Mission Critical Services */ - const val APN_TYPE_MCX = "mcx" - - /** APN type for XCAP */ - const val APN_TYPE_XCAP = "xcap" - - /** APN type for VSIM */ - const val APN_TYPE_VSIM = "vsim" - - /** APN type for BIP */ - const val APN_TYPE_BIP = "bip" - - /** APN type for ENTERPRISE */ - const val APN_TYPE_ENTERPRISE = "enterprise" - - val APN_TYPES = arrayOf( - APN_TYPE_DEFAULT, - APN_TYPE_MMS, - APN_TYPE_SUPL, - APN_TYPE_DUN, - APN_TYPE_HIPRI, - APN_TYPE_FOTA, - APN_TYPE_IMS, - APN_TYPE_CBS, - APN_TYPE_IA, - APN_TYPE_EMERGENCY, - APN_TYPE_MCX, - APN_TYPE_XCAP, - APN_TYPE_VSIM, - APN_TYPE_BIP, - APN_TYPE_ENTERPRISE + private val APN_TYPES = arrayOf( + ApnSetting.TYPE_DEFAULT_STRING, + ApnSetting.TYPE_MMS_STRING, + ApnSetting.TYPE_SUPL_STRING, + ApnSetting.TYPE_DUN_STRING, + ApnSetting.TYPE_HIPRI_STRING, + ApnSetting.TYPE_FOTA_STRING, + ApnSetting.TYPE_IMS_STRING, + ApnSetting.TYPE_CBS_STRING, + ApnSetting.TYPE_IA_STRING, + ApnSetting.TYPE_EMERGENCY_STRING, + ApnSetting.TYPE_MCX_STRING, + ApnSetting.TYPE_XCAP_STRING, + ApnSetting.TYPE_VSIM_STRING, + ApnSetting.TYPE_BIP_STRING, + ApnSetting.TYPE_ENTERPRISE_STRING, ) - val APN_TYPES_OPTIONS = listOf(APN_TYPE_ALL) + APN_TYPES - - fun getApnTypeSelectedOptionsState(apnType: String): SnapshotStateList { - val apnTypeSelectedOptionsState = mutableStateListOf() - if (apnType.contains(APN_TYPE_ALL)) - APN_TYPES_OPTIONS.forEachIndexed { index, _ -> - apnTypeSelectedOptionsState.add(index) - } - else { - APN_TYPES_OPTIONS.forEachIndexed { index, type -> - if (apnType.contains(type)) { - apnTypeSelectedOptionsState.add(index) - } - } - if (apnTypeSelectedOptionsState.size == APN_TYPES.size) - apnTypeSelectedOptionsState.add(APN_TYPES_OPTIONS.indexOf(APN_TYPE_ALL)) + private fun splitToList(apnType: String): List { + val types = apnType.split(',').map { it.trim().toLowerCase(Locale.current) } + if (ApnSetting.TYPE_ALL_STRING in types || APN_TYPES.all { it in types }) { + return listOf(ApnSetting.TYPE_ALL_STRING) } - return apnTypeSelectedOptionsState + return APN_TYPES.filter { it in types } } - fun updateApnType( - apnTypeSelectedOptionsState: SnapshotStateList, - defaultApnTypes: List, - readOnlyApnTypes: List - ): String { - val apnType = apnTypeSelectedOptionsState.joinToString { APN_TYPES_OPTIONS[it] } - if (apnType.contains(APN_TYPE_ALL)) return APN_TYPE_ALL - return if (apnType == "" && defaultApnTypes.isNotEmpty()) - getEditableApnType(defaultApnTypes, readOnlyApnTypes) - else - apnType + fun isApnTypeReadOnly(apnType: String, readOnlyTypes: List): Boolean { + val apnTypes = splitToList(apnType) + return ApnSetting.TYPE_ALL_STRING in readOnlyTypes || + ApnSetting.TYPE_ALL_STRING in apnTypes && readOnlyTypes.isNotEmpty() || + apnTypes.any { it in readOnlyTypes } } - private fun getEditableApnType( - defaultApnTypes: List, - readOnlyApnTypes: List - ): String { - return defaultApnTypes.filterNot { apnType -> - readOnlyApnTypes.contains(apnType) || apnType in listOf( - APN_TYPE_IA, - APN_TYPE_EMERGENCY, - APN_TYPE_MCX, - APN_TYPE_IMS, + fun getOptions(context: Context, apnType: String, readOnlyTypes: List) = buildList { + val apnTypes = splitToList(apnType) + add( + context.createSettingsDropdownCheckOption( + text = ApnSetting.TYPE_ALL_STRING, + isSelectAll = true, + changeable = readOnlyTypes.isEmpty(), + selected = ApnSetting.TYPE_ALL_STRING in apnTypes, ) - }.joinToString() + ) + for (type in APN_TYPES) { + add( + context.createSettingsDropdownCheckOption( + text = type, + changeable = ApnSetting.TYPE_ALL_STRING !in readOnlyTypes && + type !in readOnlyTypes, + selected = ApnSetting.TYPE_ALL_STRING in apnTypes || type in apnTypes, + ) + ) + } + }.also { Log.d(TAG, "APN Type options: $it") } + + private fun Context.createSettingsDropdownCheckOption( + text: String, + isSelectAll: Boolean = false, + changeable: Boolean, + selected: Boolean, + ) = SettingsDropdownCheckOption( + text = text, + isSelectAll = isSelectAll, + changeable = changeable, + selected = mutableStateOf(selected), + ) { + if (!changeable) { + val message = resources.getString(R.string.error_adding_apn_type, text) + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } } -} \ No newline at end of file + + fun List.toApnType(): String { + val (selectAllOptions, regularOptions) = partition { it.isSelectAll } + for (selectAllOption in selectAllOptions) { + if (selectAllOption.selected.value) return ApnSetting.TYPE_ALL_STRING + } + return regularOptions.filter { it.selected.value }.joinToString(",") { it.text } + } + + private val NotPreSelectedTypes = setOf( + ApnSetting.TYPE_IMS_STRING, + ApnSetting.TYPE_IA_STRING, + ApnSetting.TYPE_EMERGENCY_STRING, + ApnSetting.TYPE_MCX_STRING, + ) + + fun getPreSelectedApnType(customizedConfig: CustomizedConfig): String = + (customizedConfig.defaultApnTypes + ?: defaultPreSelectedApnTypes(customizedConfig.readOnlyApnTypes)) + .joinToString(",") + + private fun defaultPreSelectedApnTypes(readOnlyApnTypes: List) = + if (ApnSetting.TYPE_ALL_STRING in readOnlyApnTypes) emptyList() + else APN_TYPES.filter { it !in readOnlyApnTypes + NotPreSelectedTypes } +} diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnRepositoryTest.kt index ec3b75470b0..415531882a2 100644 --- a/tests/spa_unit/src/com/android/settings/network/apn/ApnRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnRepositoryTest.kt @@ -58,7 +58,7 @@ class ApnRepositoryTest { @Test fun getApnDataFromUri() { // mock out resources and the feature provider - val cursor = MatrixCursor(sProjection) + val cursor = MatrixCursor(Projection) cursor.addRow( arrayOf( 0, @@ -82,7 +82,7 @@ class ApnRepositoryTest { 1, ) ) - whenever(contentResolver.query(uri, sProjection, null, null, null)).thenReturn(cursor) + whenever(contentResolver.query(uri, Projection, null, null, null)).thenReturn(cursor) val apnData = getApnDataFromUri(uri, context)