Add validateAndSaveApnData.

Fix: 304649927
Test: Visual Test
Change-Id: I900a096a6e27f1db66af204201d4ca2523537c0d
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index ad16ae3..0b0431d 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -19,6 +19,10 @@
 import android.net.Uri
 import android.os.Bundle
 import androidx.compose.foundation.layout.Column
+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.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
@@ -70,7 +74,7 @@
         val apnDataCur = remember {
             mutableStateOf(apnDataInit)
         }
-        ApnPage(apnDataCur)
+        ApnPage(apnDataInit, apnDataCur)
     }
 
     fun getRoute(
@@ -83,7 +87,7 @@
 }
 
 @Composable
-fun ApnPage(apnDataCur: MutableState<ApnData>) {
+fun ApnPage(apnDataInit: ApnData, apnDataCur: MutableState<ApnData>) {
     var apnData by apnDataCur
     val context = LocalContext.current
     val authTypeOptions = stringArrayResource(R.array.apn_auth_entries).toList()
@@ -93,6 +97,11 @@
     }
     RegularScaffold(
         title = stringResource(id = R.string.apn_edit),
+        actions = {
+            IconButton(onClick = {
+                validateAndSaveApnData(apnDataInit, apnData, context)
+            }) { Icon(imageVector = Icons.Outlined.Done, contentDescription = "Save APN") }
+        }
     ) {
         Column() {
             SettingsOutlinedTextField(
diff --git a/src/com/android/settings/network/apn/ApnRepository.kt b/src/com/android/settings/network/apn/ApnRepository.kt
index f758439..a518aa5 100644
--- a/src/com/android/settings/network/apn/ApnRepository.kt
+++ b/src/com/android/settings/network/apn/ApnRepository.kt
@@ -161,4 +161,4 @@
             ""
         }
     }
-}
+}
\ 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 c5ca45a..5904e98 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -82,6 +82,55 @@
 )
 
 /**
+ * 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.<br></br>
+ * APN_TYPE_ALL is a special type to indicate that this APN entry can
+ * service all data connections.
+ */
+const val APN_TYPE_ALL = "*"
+/** 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"
+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
+)
+
+/**
  * Initialize ApnData according to the arguments.
  * @param arguments The data passed in when the user calls PageProvider.
  * @param uriInit The decoded user incoming uri data in Page.
@@ -124,6 +173,108 @@
 }
 
 /**
+ * Validates the apn data and save it to the database if it's valid.
+ *
+ *
+ *
+ * A dialog with error message will be displayed if the APN data is invalid.
+ *
+ * @return true if there is no error
+ */
+fun validateAndSaveApnData(apnDataInit: ApnData, apnData: ApnData, context: Context): Boolean {
+    // Nothing to do if it's a read only APN
+    if (apnData.customizedConfig.readOnlyApn) {
+        return true
+    }
+    val errorMsg = validateApnData(apnData, context)
+    if (errorMsg != null) {
+        //TODO: showError(this)
+        return false
+    }
+    if (apnData.newApn || (apnData != apnDataInit)) {
+        Log.d(TAG, "validateAndSaveApnData: apnData ${apnData.name}")
+        // TODO: updateApnDataToDatabase
+    }
+    return true
+}
+
+/**
+ * Validates whether the apn data is valid.
+ *
+ * @return An error message if the apn data is invalid, otherwise return null.
+ */
+fun validateApnData(apnData: ApnData, context: Context): String? {
+    var errorMsg: String? = null
+    val name = apnData.name
+    val apn = apnData.apn
+    if (name == "") {
+        errorMsg = context.resources.getString(R.string.error_name_empty)
+    } else if (apn == "") {
+        errorMsg = context.resources.getString(R.string.error_apn_empty)
+    }
+    if (errorMsg == null) {
+        // if carrier does not allow editing certain apn types, make sure type does not include
+        // those
+        if (!ArrayUtils.isEmpty(apnData.customizedConfig.readOnlyApnTypes)
+            && apnTypesMatch(apnData.customizedConfig.readOnlyApnTypes, getUserEnteredApnType(apnData.apnType, apnData.customizedConfig.readOnlyApnTypes))
+        ) {
+            val stringBuilder = StringBuilder()
+            for (type in apnData.customizedConfig.readOnlyApnTypes) {
+                stringBuilder.append(type).append(", ")
+                Log.d(TAG, "validateApnData: appending type: $type")
+            }
+            // remove last ", "
+            if (stringBuilder.length >= 2) {
+                stringBuilder.delete(stringBuilder.length - 2, stringBuilder.length)
+            }
+            errorMsg = String.format(
+                context.resources.getString(R.string.error_adding_apn_type),
+                stringBuilder
+            )
+        }
+    }
+    return errorMsg
+}
+
+private fun getUserEnteredApnType(apnType: String, readOnlyApnTypes: List<String>): 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>): 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()
+}
+
+/**
  * Initialize CustomizedConfig information through subId.
  * @param subId subId information obtained from arguments.
  *
@@ -299,11 +450,11 @@
         return false
     }
     val apnList: List<*> = Arrays.asList(*apnTypes)
-    if (apnList.contains(ApnEditor.APN_TYPE_ALL)) {
+    if (apnList.contains(APN_TYPE_ALL)) {
         Log.d(TAG, "hasAllApns: true because apnList.contains(APN_TYPE_ALL)")
         return true
     }
-    for (apn in ApnEditor.APN_TYPES) {
+    for (apn in APN_TYPES) {
         if (!apnList.contains(apn)) {
             return false
         }
diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt
index 7cd0f5d..ddc8364 100644
--- a/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnEditPageProviderTest.kt
@@ -17,7 +17,6 @@
 package com.android.settings.network.apn
 
 import android.content.Context
-import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.test.assertIsDisplayed
@@ -58,15 +57,16 @@
         context.resources.getStringArray(R.array.apn_protocol_entries).toList()
     private val networkType = context.resources.getString(R.string.network_type)
     private val passwordTitle = context.resources.getString(R.string.apn_password)
+    private val apnInit = ApnData(
+        name = apnName,
+        mmsc = mmsc,
+        mmsProxy = mmsProxy,
+        apnType = apnType,
+        apnRoaming = apnProtocolOptions.indexOf(apnRoaming),
+        apnEnable = true
+    )
     private val apnData = mutableStateOf(
-        ApnData(
-            name = apnName,
-            mmsc = mmsc,
-            mmsProxy = mmsProxy,
-            apnType = apnType,
-            apnRoaming = apnProtocolOptions.indexOf(apnRoaming),
-            apnEnable = true
-        )
+        apnInit
     )
 
     @Test
@@ -77,7 +77,7 @@
     @Test
     fun title_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -87,7 +87,7 @@
     @Test
     fun name_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -97,7 +97,7 @@
     @Test
     fun mmsc_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -109,7 +109,7 @@
     @Test
     fun mms_proxy_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -121,7 +121,7 @@
     @Test
     fun apn_type_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -133,7 +133,7 @@
     @Test
     fun apn_roaming_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -145,7 +145,7 @@
     @Test
     fun carrier_enabled_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -157,7 +157,7 @@
     @Test
     fun carrier_enabled_isChecked() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -169,7 +169,7 @@
     @Test
     fun carrier_enabled_checkChanged() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -182,7 +182,7 @@
     @Test
     fun network_type_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }
@@ -193,12 +193,10 @@
 
     @Test
     fun network_type_changed() {
-        var apnDataa: MutableState<ApnData> = apnData
         composeTestRule.setContent {
-            apnDataa = remember {
+            ApnPage(apnInit, remember {
                 apnData
-            }
-            ApnPage(apnDataa)
+            })
         }
         composeTestRule.onRoot().onChild().onChildAt(0)
             .performScrollToNode(hasText(networkType, true))
@@ -211,12 +209,10 @@
 
     @Test
     fun network_type_changed_back2Default() {
-        var apnDataa: MutableState<ApnData> = apnData
         composeTestRule.setContent {
-            apnDataa = remember {
+            ApnPage(apnInit, remember {
                 apnData
-            }
-            ApnPage(apnDataa)
+            })
         }
         composeTestRule.onRoot().onChild().onChildAt(0)
             .performScrollToNode(hasText(networkType, true))
@@ -234,7 +230,7 @@
     @Test
     fun password_displayed() {
         composeTestRule.setContent {
-            ApnPage(remember {
+            ApnPage(apnInit, remember {
                 apnData
             })
         }