Duplicate apn entry

Test: Visual Test
Fix: 319194851
Change-Id: I491655bb80a17cc9fc99d47f1e1ac5824eb11921
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 402f526..47fcf4e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3299,6 +3299,8 @@
     <string name="menu_cancel">Cancel</string>
     <!-- APN error dialog title -->
     <string name="error_title"></string>
+    <!-- APN error dialog messages when the new apn is a duplicate: -->
+    <string name="error_duplicate_apn_entry">Duplicate apn entry.</string>
     <!-- APN error dialog messages: -->
     <string name="error_name_empty">The Name field can\u2019t be empty.</string>
     <!-- APN error dialog messages: -->
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 2600618..5c7d7a4 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -19,16 +19,21 @@
 import android.net.Uri
 import android.os.Bundle
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Done
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
@@ -39,6 +44,7 @@
 import com.android.settings.network.apn.ApnNetworkTypes.getNetworkTypeSelectedOptionsState
 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
@@ -98,25 +104,47 @@
         getNetworkTypeSelectedOptionsState(apnData.networkType)
     }
     val navController = LocalNavController.current
+    var valid: String?
     RegularScaffold(
         title = if (apnDataInit.newApn) stringResource(id = R.string.apn_add) else stringResource(id = R.string.apn_edit),
         actions = {
             if (!apnData.customizedConfig.readOnlyApn) {
                 IconButton(onClick = {
-                    if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
-                    val valid = validateAndSaveApnData(
+                    apnData = apnData.copy(
+                        networkType = ApnNetworkTypes.getNetworkType(
+                            networkTypeSelectedOptionsState
+                        )
+                    )
+                    valid = validateAndSaveApnData(
                         apnDataInit,
                         apnData,
                         context,
-                        uriInit,
-                        networkTypeSelectedOptionsState
+                        uriInit
                     )
-                    if (valid) navController.navigateBack()
+                    if (valid == null) navController.navigateBack()
+                    else if (!apnData.validEnabled) apnData = apnData.copy(validEnabled = true)
                 }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = null) }
             }
         },
     ) {
         Column {
+            if (apnData.validEnabled) {
+                apnData = apnData.copy(
+                    networkType = ApnNetworkTypes.getNetworkType(
+                        networkTypeSelectedOptionsState
+                    )
+                )
+                valid = validateApnData(uriInit, apnData, context)
+                valid?.let {
+                    Text(
+                        text = it,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(SettingsDimension.menuFieldPadding),
+                        color = MaterialTheme.colorScheme.primary
+                    )
+                }
+            }
             SettingsOutlinedTextField(
                 value = apnData.name,
                 label = stringResource(R.string.apn_name),
diff --git a/src/com/android/settings/network/apn/ApnRepository.kt b/src/com/android/settings/network/apn/ApnRepository.kt
index e0121b4..2f16e69 100644
--- a/src/com/android/settings/network/apn/ApnRepository.kt
+++ b/src/com/android/settings/network/apn/ApnRepository.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.net.Uri
 import android.provider.Telephony
+import android.telephony.TelephonyManager
 import android.util.Log
 import com.android.settings.R
 import com.android.settingslib.utils.ThreadUtils
@@ -150,7 +151,6 @@
 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)
@@ -167,7 +167,6 @@
 
 fun convertOptions2Protocol(protocolIndex: Int, context: Context): String {
     val apnProtocolValues = context.resources.getStringArray(R.array.apn_protocol_values).toList()
-
     return if (protocolIndex == -1) {
         ""
     } else {
@@ -179,7 +178,12 @@
     }
 }
 
-fun updateApnDataToDatabase(newApn: Boolean, values: ContentValues, context: Context, uriInit: Uri) {
+fun updateApnDataToDatabase(
+    newApn: Boolean,
+    values: ContentValues,
+    context: Context,
+    uriInit: Uri
+) {
     ThreadUtils.postOnBackgroundThread {
         if (newApn) {
             // Add a new apn to the database
@@ -194,4 +198,24 @@
             )
         }
     }
+}
+
+fun isItemExist(uri: Uri, apnData: ApnData, context: Context): String? {
+    val contentValueMap = apnData.getContentValueMap(context)
+    contentValueMap.remove(Telephony.Carriers.CARRIER_ENABLED)
+    val list = contentValueMap.entries.toList()
+    val selection = list.joinToString(" AND ") { "${it.key} = ?" }
+    val selectionArgs: Array<String> = list.map { it.value.toString() }.toTypedArray()
+    context.contentResolver.query(
+        uri,
+        sProjection,
+        selection /* selection */,
+        selectionArgs /* selectionArgs */,
+        null /* sortOrder */
+    )?.use { cursor ->
+        if (cursor.count > 0) {
+            return context.resources.getString(R.string.error_duplicate_apn_entry)
+        }
+    }
+    return null
 }
\ No newline at end of file
diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt
index e4cb603..6f39305 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -72,41 +72,38 @@
     val validEnabled: Boolean = false,
     val customizedConfig: CustomizedConfig = CustomizedConfig()
 ) {
+    fun getContentValueMap(context: Context): MutableMap<String, Any> {
+        val simCarrierId =
+            context.getSystemService(TelephonyManager::class.java)!!
+                .createForSubscriptionId(subId)
+                .getSimCarrierId()
+        return mutableMapOf(
+            Telephony.Carriers.NAME to name, Telephony.Carriers.APN to apn,
+            Telephony.Carriers.PROXY to proxy, Telephony.Carriers.PORT to port,
+            Telephony.Carriers.MMSPROXY to mmsProxy, Telephony.Carriers.MMSPORT to mmsPort,
+            Telephony.Carriers.USER to userName, Telephony.Carriers.SERVER to server,
+            Telephony.Carriers.PASSWORD to passWord, Telephony.Carriers.MMSC to mmsc,
+            Telephony.Carriers.AUTH_TYPE to authType,
+            Telephony.Carriers.PROTOCOL to convertOptions2Protocol(apnProtocol, context),
+            Telephony.Carriers.ROAMING_PROTOCOL to convertOptions2Protocol(apnRoaming, context),
+            Telephony.Carriers.TYPE to apnType,
+            Telephony.Carriers.NETWORK_TYPE_BITMASK to networkType,
+            Telephony.Carriers.CARRIER_ENABLED to apnEnable,
+            Telephony.Carriers.EDITED_STATUS to Telephony.Carriers.USER_EDITED,
+            Telephony.Carriers.CARRIER_ID to simCarrierId
+        )
+    }
+
     fun getContentValues(context: Context): ContentValues {
         val values = ContentValues()
-        values.put(Telephony.Carriers.NAME, name)
-        values.put(Telephony.Carriers.APN, apn)
-        values.put(Telephony.Carriers.PROXY, proxy)
-        values.put(Telephony.Carriers.PORT, port)
-        values.put(Telephony.Carriers.MMSPROXY, mmsProxy)
-        values.put(Telephony.Carriers.MMSPORT, mmsPort)
-        values.put(Telephony.Carriers.USER, userName)
-        values.put(Telephony.Carriers.SERVER, server)
-        values.put(Telephony.Carriers.PASSWORD, passWord)
-        values.put(Telephony.Carriers.MMSC, mmsc)
-        values.put(Telephony.Carriers.AUTH_TYPE, authType)
-        values.put(Telephony.Carriers.PROTOCOL, convertOptions2Protocol(apnProtocol, context))
-        values.put(
-            Telephony.Carriers.ROAMING_PROTOCOL,
-            convertOptions2Protocol(apnRoaming, context)
-        )
-        values.put(Telephony.Carriers.TYPE, apnType)
-        values.put(Telephony.Carriers.NETWORK_TYPE_BITMASK, networkType)
-        values.put(Telephony.Carriers.CARRIER_ENABLED, apnEnable)
-        values.put(Telephony.Carriers.EDITED_STATUS, Telephony.Carriers.USER_EDITED)
-        if (newApn) {
-            val simCarrierId =
-                context.getSystemService(TelephonyManager::class.java)!!
-                    .createForSubscriptionId(subId)
-                    .getSimCarrierId()
-            values.put(Telephony.Carriers.CARRIER_ID, simCarrierId)
-        }
+        val contentValueMap = getContentValueMap(context)
+        if (!newApn) contentValueMap.remove(Telephony.Carriers.CARRIER_ID)
+        contentValueMap.forEach { (key, value) -> values.putObject(key, value) }
         return values
     }
 }
 
 data class CustomizedConfig(
-    val newApn: Boolean = false,
     val readOnlyApn: Boolean = false,
     val isAddApnAllowed: Boolean = true,
     val readOnlyApnTypes: List<String> = emptyList(),
@@ -227,20 +224,14 @@
  */
 fun validateAndSaveApnData(
     apnDataInit: ApnData,
-    apnData: ApnData,
+    newApnData: ApnData,
     context: Context,
-    uriInit: Uri,
-    networkTypeSelectedOptionsState: SnapshotStateList<Int>
-): Boolean {
-    // Nothing to do if it's a read only APN
-    if (apnData.customizedConfig.readOnlyApn) {
-        return true
-    }
-    val errorMsg = validateApnData(apnData, context)
+    uriInit: Uri
+): String? {
+    val errorMsg = validateApnData(uriInit, newApnData, context)
     if (errorMsg != null) {
-        return false
+        return errorMsg
     }
-    val newApnData = apnData.copy(networkType = getNetworkType(networkTypeSelectedOptionsState))
     if (newApnData.newApn || (newApnData != apnDataInit)) {
         Log.d(TAG, "[validateAndSaveApnData] newApnData.networkType: ${newApnData.networkType}")
         updateApnDataToDatabase(
@@ -250,7 +241,7 @@
             uriInit
         )
     }
-    return true
+    return null
 }
 
 /**
@@ -258,7 +249,7 @@
  *
  * @return An error message if the apn data is invalid, otherwise return null.
  */
-fun validateApnData(apnData: ApnData, context: Context): String? {
+fun validateApnData(uri: Uri, apnData: ApnData, context: Context): String? {
     var errorMsg: String?
     val name = apnData.name
     val apn = apnData.apn
@@ -267,11 +258,14 @@
     } else if (apn == "") {
         context.resources.getString(R.string.error_apn_empty)
     } else {
-        validateMMSC(apnData.validEnabled, apnData.mmsc, context)
+        validateMMSC(true, apnData.mmsc, context)
+    }
+    if (errorMsg == null) {
+        errorMsg = isItemExist(uri, apnData, context)
     }
     if (errorMsg == null) {
         errorMsg = validateAPNType(
-            apnData.validEnabled,
+            true,
             apnData.apnType,
             apnData.customizedConfig.readOnlyApnTypes,
             context