feat: profile rename more toast hints #111

Closed
septs wants to merge 15 commits from septs:profile-rename-strong-checking into master
4 changed files with 119 additions and 27 deletions

View file

@ -1,38 +1,64 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone import im.angry.openeuicc.service.EuiccChannelManagerService.Companion.waitDone
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch 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 { class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
companion object { companion object {
const val TAG = "ProfileRenameFragment" 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 { fun newInstance(slotId: Int, portId: Int, iccid: String, currentName: String): ProfileRenameFragment {
val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId) val instance = newInstanceEuicc(ProfileRenameFragment::class.java, slotId, portId)
instance.requireArguments().apply { instance.requireArguments().apply {
putString("iccid", iccid) putString(FIELD_ICCID, iccid)
putString("currentName", currentName) putString(FIELD_CURRENT_NAME, currentName)
} }
return instance return instance
} }
} }
private lateinit var toolbar: Toolbar private lateinit var toolbar: Toolbar
private lateinit var profileRenameNewName: TextInputLayout private lateinit var editText: EditText
private lateinit var progress: ProgressBar 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 private var renaming = false
override fun onCreateView( override fun onCreateView(
@ -40,14 +66,12 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val view = inflater.inflate(R.layout.fragment_profile_rename, container, false) val view = inflater.inflate(R.layout.fragment_profile_rename, container, false).apply {
toolbar = requireViewById(R.id.toolbar)
toolbar = view.requireViewById(R.id.toolbar) editText = requireViewById<TextInputLayout>(R.id.profile_rename_new_name).editText!!
profileRenameNewName = view.requireViewById(R.id.profile_rename_new_name) progress = requireViewById(R.id.progress)
progress = view.requireViewById(R.id.progress) }
toolbar.inflateMenu(R.menu.fragment_profile_rename) toolbar.inflateMenu(R.menu.fragment_profile_rename)
return view return view
} }
@ -63,11 +87,16 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
true true
} }
} }
editText.addTextChangedListener {
val isUnchanged = it.toString().trim() == currentName
dialog!!.setCancelable(isUnchanged)
dialog!!.setCanceledOnTouchOutside(isUnchanged)
}
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
profileRenameNewName.editText!!.setText(requireArguments().getString("currentName")) editText.setText(currentName)
} }
override fun onResume() { override fun onResume() {
@ -75,6 +104,15 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
setWidthPercent(95) 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 { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also { return super.onCreateDialog(savedInstanceState).also {
it.setCanceledOnTouchOutside(false) it.setCanceledOnTouchOutside(false)
@ -82,25 +120,53 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
} }
private fun rename() { private fun rename() {
val name = profileRenameNewName.editText!!.text.toString().trim() toast = when {
if (name.length >= 64) { editedName.isEmpty() -> Toast.makeText(
Toast.makeText(context, R.string.toast_profile_name_too_long, Toast.LENGTH_LONG).show() requireContext(),
return 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 renaming = true
progress.isIndeterminate = true progress.isIndeterminate = true
progress.visibility = View.VISIBLE progress.isVisible = true
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchProfileRenameTask( try {
slotId, if (editedName != currentName) euiccChannelManagerService
portId, .launchProfileRenameTask(slotId, portId, iccid, editedName)
requireArguments().getString("iccid")!!, .waitDone()
name } catch (e: NicknameException) {
).waitDone() 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) { if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()

View file

@ -28,7 +28,11 @@
<string name="switch_did_not_refresh">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.</string> <string name="switch_did_not_refresh">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.</string>
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string> <string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
<string name="toast_profile_name_changed">The profile name will be updated from \'%s\' to \'%s\'</string>
<string name="toast_profile_name_is_unchanged">Nickname has is unchanged</string>

is unchanged

is unchanged
<string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string> <string name="toast_profile_name_too_long">Nickname cannot be longer than 64 characters</string>
<string name="toast_profile_name_encode_failed">could not be encoded as UTF-8</string>

could not be encoded as UTF-8

could not be encoded as UTF-8
<string name="toast_profile_name_restore_defaults">This profile name will be restored to default</string>

delete "the"

delete "the"
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string> <string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
<string name="toast_iccid_copied">ICCID copied to clipboard</string> <string name="toast_iccid_copied">ICCID copied to clipboard</string>

View file

@ -12,6 +12,14 @@ interface LocalProfileAssistant {
val lastApduException: Exception?, val lastApduException: Exception?,
) : Exception("Failed to download profile") ) : 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 valid: Boolean
val profiles: List<LocalProfileInfo> val profiles: List<LocalProfileInfo>
val notifications: List<LocalProfileNotification> val notifications: List<LocalProfileNotification>
@ -40,9 +48,7 @@ interface LocalProfileAssistant {
fun euiccMemoryReset() fun euiccMemoryReset()
fun setNickname( fun setNickname(iccid: String, nickname: String): Boolean
iccid: String, nickname: String
): Boolean
fun close() fun close()
} }

View file

@ -239,8 +239,24 @@ class LocalProfileAssistantImpl(
} == 0 } == 0
@Synchronized @Synchronized
override fun setNickname(iccid: String, nickname: String): Boolean = override fun setNickname(iccid: String, nickname: String): Boolean {
LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 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() { override fun euiccMemoryReset() {
LpacJni.es10cEuiccMemoryReset(contextHandle) LpacJni.es10cEuiccMemoryReset(contextHandle)