diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt index 8582278..1926790 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/ProfileRenameFragment.kt @@ -1,38 +1,64 @@ package im.angry.openeuicc.ui import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText import android.widget.ProgressBar import android.widget.Toast import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener import androidx.lifecycle.lifecycleScope import com.google.android.material.textfield.TextInputLayout import im.angry.openeuicc.common.R import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.util.* import kotlinx.coroutines.launch +import net.typeblog.lpac_jni.LocalProfileAssistant.ProfileNicknameException as NicknameException +import net.typeblog.lpac_jni.LocalProfileAssistant.ProfileNicknameException.Kind as SetFailedKind class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { companion object { const val TAG = "ProfileRenameFragment" + const val FIELD_ICCID = "iccid" + const val FIELD_CURRENT_NAME = "currentName" fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment { val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) instance.requireArguments().apply { - putString("iccid", iccid) - putString("currentName", currentName) + putString(FIELD_ICCID, iccid) + putString(FIELD_CURRENT_NAME, currentName) } return instance } } private lateinit var toolbar: Toolbar - private lateinit var profileRenameNewName: TextInputLayout + private lateinit var editText: EditText private lateinit var progress: ProgressBar + private var toast: Toast? = null + set(toast) { + field?.cancel() + field = toast + field?.show() + } + + private val editedName: String + get() = editText.text.toString().trim() + + private val iccid by lazy { + requireArguments().getString(FIELD_ICCID)!! + } + + private val currentName by lazy { + requireArguments().getString(FIELD_CURRENT_NAME)!! + } + private var renaming = false override fun onCreateView( @@ -40,14 +66,12 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment container: ViewGroup?, savedInstanceState: Bundle? ): View { - val view = inflater.inflate(R.layout.fragment_profile_rename, container, false) - - toolbar = view.requireViewById(R.id.toolbar) - profileRenameNewName = view.requireViewById(R.id.profile_rename_new_name) - progress = view.requireViewById(R.id.progress) - + val view = inflater.inflate(R.layout.fragment_profile_rename, container, false).apply { + toolbar = requireViewById(R.id.toolbar) + editText = requireViewById(R.id.profile_rename_new_name).editText!! + progress = requireViewById(R.id.progress) + } toolbar.inflateMenu(R.menu.fragment_profile_rename) - return view } @@ -63,11 +87,16 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment true } } + editText.addTextChangedListener { + val isUnchanged = it.toString().trim() == currentName + dialog!!.setCancelable(isUnchanged) + dialog!!.setCanceledOnTouchOutside(isUnchanged) + } } override fun onStart() { super.onStart() - profileRenameNewName.editText!!.setText(requireArguments().getString("currentName")) + editText.setText(currentName) } override fun onResume() { @@ -75,6 +104,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment setWidthPercent(95) } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + toast = Toast.makeText( + requireContext(), + R.string.toast_profile_name_is_unchanged, + Toast.LENGTH_LONG + ) + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return super.onCreateDialog(savedInstanceState).also { it.setCanceledOnTouchOutside(false) @@ -82,25 +120,53 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment } private fun rename() { - val name = profileRenameNewName.editText!!.text.toString().trim() - if (name.length >= 64) { - Toast.makeText(context, R.string.toast_profile_name_too_long, Toast.LENGTH_LONG).show() - return + toast = when { + editedName.isEmpty() -> Toast.makeText( + requireContext(), + R.string.toast_profile_name_restore_defaults, + Toast.LENGTH_LONG + ) + + editedName == currentName -> Toast.makeText( + requireContext(), + R.string.toast_profile_name_is_unchanged, + Toast.LENGTH_LONG + ) + + else -> Toast.makeText( + requireContext(), + getString(R.string.toast_profile_name_changed, currentName, editedName), + Toast.LENGTH_LONG + ) } renaming = true progress.isIndeterminate = true - progress.visibility = View.VISIBLE + progress.isVisible = true lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - euiccChannelManagerService.launchProfileRenameTask( - slotId, - portId, - requireArguments().getString("iccid")!!, - name - ).waitDone() + try { + if (editedName != currentName) euiccChannelManagerService + .launchProfileRenameTask(slotId, portId, iccid, editedName) + .waitDone() + } catch (e: NicknameException) { + toast = when (e.kind) { + SetFailedKind.NicknameTooLong -> Toast.makeText( + requireContext(), + R.string.toast_profile_name_too_long, + Toast.LENGTH_LONG + ) + + SetFailedKind.InvalidUTF8Sequence -> Toast.makeText( + requireContext(), + R.string.toast_profile_name_encode_failed, + Toast.LENGTH_LONG + ) + } + return@launch + } if (parentFragment is EuiccProfilesChangedListener) { (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 61823f7..5c6c2ba 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -28,7 +28,11 @@ The operation was successful, but your phone\'s modem refused to refresh. You might need to toggle airplane mode or reboot in order to use the new profile. Cannot switch to new eSIM profile. + The profile name will be updated from \'%s\' to \'%s\' + Nickname has is unchanged Nickname cannot be longer than 64 characters + could not be encoded as UTF-8 + This profile name will be restored to default Confirmation string mismatch ICCID copied to clipboard diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt index 4ff65fa..42eca18 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LocalProfileAssistant.kt @@ -12,6 +12,14 @@ interface LocalProfileAssistant { val lastApduException: Exception?, ) : Exception("Failed to download profile") + data class ProfileNicknameException(val kind: Kind) : + Exception("Failed to set nickname profile") { + enum class Kind { + NicknameTooLong, + InvalidUTF8Sequence + } + } + val valid: Boolean val profiles: List val notifications: List @@ -40,9 +48,7 @@ interface LocalProfileAssistant { fun euiccMemoryReset() - fun setNickname( - iccid: String, nickname: String - ): Boolean + fun setNickname(iccid: String, nickname: String): Boolean fun close() } \ No newline at end of file diff --git a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt index b617f2b..419d0e1 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt @@ -239,8 +239,24 @@ class LocalProfileAssistantImpl( } == 0 @Synchronized - override fun setNickname(iccid: String, nickname: String): Boolean = - LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 + override fun setNickname(iccid: String, nickname: String): Boolean { + try { + // SGP.22 v2.2.2 (Page 205 of 268) + // https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2020/06/SGP.22-v2.2.2.pdf + // ASN.1 definition is `profileNickname [16] UTF8String (SIZE(0..64))` + val length = nickname.toByteArray(Charsets.UTF_8).size + if (length > 64) { + throw LocalProfileAssistant.ProfileNicknameException( + kind = LocalProfileAssistant.ProfileNicknameException.Kind.NicknameTooLong + ) + } + } catch (e: CharacterCodingException) { + throw LocalProfileAssistant.ProfileNicknameException( + kind = LocalProfileAssistant.ProfileNicknameException.Kind.InvalidUTF8Sequence + ) + } + return LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 + } override fun euiccMemoryReset() { LpacJni.es10cEuiccMemoryReset(contextHandle)