diff --git a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt index aab9e63..b715ca0 100644 --- a/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt +++ b/app-common/src/main/java/im/angry/openeuicc/core/LocalProfileAssistantWrapper.kt @@ -54,8 +54,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) : override fun euiccMemoryReset() = lpa.euiccMemoryReset() - override fun setNickname(iccid: String, nickname: String): Boolean = + override fun setNickname(iccid: String, nickname: String) { lpa.setNickname(iccid, nickname) + } override fun close() = lpa.close() diff --git a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt index c4d16df..a20c8db 100644 --- a/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt +++ b/app-common/src/main/java/im/angry/openeuicc/service/EuiccChannelManagerService.kt @@ -414,16 +414,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { getString(R.string.task_profile_rename_failure), R.drawable.ic_task_rename ) { - val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> + euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> channel.lpa.setNickname( iccid, name ) } - - if (!res) { - throw RuntimeException("Profile not renamed") - } } fun launchProfileDeleteTask( 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..e3f2d8d 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 @@ -14,6 +14,7 @@ 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 class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { companion object { @@ -81,13 +82,18 @@ 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 - } + private fun showErrorAndCancel(errorStrRes: Int) { + Toast.makeText( + requireContext(), + errorStrRes, + Toast.LENGTH_LONG + ).show() + renaming = false + progress.visibility = View.GONE + } + + private fun rename() { renaming = true progress.isIndeterminate = true progress.visibility = View.VISIBLE @@ -95,21 +101,37 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment lifecycleScope.launch { ensureEuiccChannelManager() euiccChannelManagerService.waitForForegroundTask() - euiccChannelManagerService.launchProfileRenameTask( + val res = euiccChannelManagerService.launchProfileRenameTask( slotId, portId, requireArguments().getString("iccid")!!, - name + profileRenameNewName.editText!!.text.toString().trim() ).waitDone() - if (parentFragment is EuiccProfilesChangedListener) { - (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() - } + when (res) { + is LocalProfileAssistant.ProfileNameTooLongException -> { + showErrorAndCancel(R.string.profile_rename_too_long) + } - try { - dismiss() - } catch (e: IllegalStateException) { - // Ignored + is LocalProfileAssistant.ProfileNameIsInvalidUTF8Exception -> { + showErrorAndCancel(R.string.profile_rename_encoding_error) + } + + is Throwable -> { + showErrorAndCancel(R.string.profile_rename_failure) + } + + else -> { + if (parentFragment is EuiccProfilesChangedListener) { + (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() + } + + try { + dismiss() + } catch (e: IllegalStateException) { + // Ignored + } + } } } } diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt index d329048..6203364 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardMethodSelectFragment.kt @@ -1,6 +1,7 @@ package im.angry.openeuicc.ui.wizard import android.app.AlertDialog +import android.content.ClipboardManager import android.graphics.BitmapFactory import android.os.Bundle import android.view.LayoutInflater @@ -8,6 +9,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration @@ -68,6 +70,9 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) { gallerySelectorLauncher.launch("image/*") }, + DownloadMethod(R.drawable.ic_paste_go, R.string.download_wizard_method_clipboard) { + handleLoadFromClipboard() + }, DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) { gotoNextFragment(DownloadWizardDetailsFragment()) } @@ -103,6 +108,22 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard return view } + private fun handleLoadFromClipboard() { + val clipboard = requireContext().getSystemService(ClipboardManager::class.java) + val text = clipboard.primaryClip?.getItemAt(0)?.text + + if (text == null) { + Toast.makeText( + requireContext(), + R.string.profile_download_no_lpa_string, + Toast.LENGTH_SHORT + ).show() + return + } + + processLpaString(text.toString()) + } + private fun processLpaString(s: String) { val components = s.split("$") if (components.size < 3 || components[0] != "LPA:1") { diff --git a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt index f16a086..3723aea 100644 --- a/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt +++ b/app-common/src/main/java/im/angry/openeuicc/ui/wizard/DownloadWizardSlotSelectFragment.kt @@ -57,13 +57,15 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt super.beforeNext() if (adapter.selected.freeSpace < LOW_NVRAM_THRESHOLD) { + val activity = requireActivity() + AlertDialog.Builder(requireContext()).apply { setTitle(R.string.profile_download_low_nvram_title) setMessage(R.string.profile_download_low_nvram_message) setCancelable(true) setPositiveButton(android.R.string.ok, null) setNegativeButton(android.R.string.cancel) { _, _ -> - requireActivity().finish() + activity.finish() } show() } diff --git a/app-common/src/main/res/drawable/ic_paste_go.xml b/app-common/src/main/res/drawable/ic_paste_go.xml new file mode 100644 index 0000000..7536fff --- /dev/null +++ b/app-common/src/main/res/drawable/ic_paste_go.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app-common/src/main/res/layout/fragment_slot_select.xml b/app-common/src/main/res/layout/fragment_slot_select.xml deleted file mode 100644 index b818b80..0000000 --- a/app-common/src/main/res/layout/fragment_slot_select.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app-common/src/main/res/menu/fragment_slot_select.xml b/app-common/src/main/res/menu/fragment_slot_select.xml deleted file mode 100644 index e129008..0000000 --- a/app-common/src/main/res/menu/fragment_slot_select.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app-common/src/main/res/values-ja/strings.xml b/app-common/src/main/res/values-ja/strings.xml index dacfd68..54d5890 100644 --- a/app-common/src/main/res/values-ja/strings.xml +++ b/app-common/src/main/res/values-ja/strings.xml @@ -16,10 +16,7 @@ eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。 操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。 新しい eSIM プロファイルに切り替えることができません。 - ニックネームは 64 文字以内にしてください ICCID をクリップボードにコピーしました - スロットを選択 - 選択 USB の権限を許可 USB スマートカードリーダーにアクセスするには許可が必要です。 USB スマートカードリーダー経由で eSIM に接続できません。 diff --git a/app-common/src/main/res/values-zh-rCN/strings.xml b/app-common/src/main/res/values-zh-rCN/strings.xml index b97969d..200931a 100644 --- a/app-common/src/main/res/values-zh-rCN/strings.xml +++ b/app-common/src/main/res/values-zh-rCN/strings.xml @@ -17,10 +17,7 @@ 等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机 操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。 无法切换到新的 eSIM 配置文件。 - 昵称不能超过 64 个字符 已复制 ICCID 到剪贴板 - 选择卡槽 - 选择 授予 USB 权限 需要获得访问 USB 智能卡读卡器的权限。 无法通过 USB 智能卡读卡器连接到 eSIM。 diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index bd3b3af..95ea261 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -28,14 +28,10 @@ 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. - Nickname cannot be longer than 64 characters Confirmation string mismatch ICCID copied to clipboard EID copied to clipboard - Select Slot - Select - Grant USB permission Permission is needed to access the USB smart card reader. Cannot connect to eSIM via a USB smart card reader. @@ -58,8 +54,9 @@ This download may fail This download may fail due to low remaining capacity. + No LPA string found in clipboard Incorrect LPA String - The LPA string could not be parsed + Could not parse QR code or clipboard content as an LPA string for downloading eSIMs. Download Wizard Back @@ -75,6 +72,7 @@ How would you like to download the eSIM profile? Scan a QR code with camera Load a QR code from gallery + Load from Clipboard Enter manually Input or confirm details for downloading your eSIM: Downloading your eSIM… @@ -98,6 +96,9 @@ Logs have been saved to the selected path. Would you like to share the log through another app? New nickname + Failed to encode nickname as UTF-8 + Nickname is too long + Unknown failure when renaming profile Are you sure you want to delete the profile %s? This operation is irreversible. Type \'%s\' here to confirm deletion 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..48ab1c5 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,10 @@ interface LocalProfileAssistant { val lastApduException: Exception?, ) : Exception("Failed to download profile") + class ProfileRenameException() : Exception("Failed to rename profile") + class ProfileNameTooLongException() : Exception("Profile name too long") + class ProfileNameIsInvalidUTF8Exception() : Exception("Profile name is invalid UTF-8") + val valid: Boolean val profiles: List val notifications: List @@ -40,9 +44,14 @@ interface LocalProfileAssistant { fun euiccMemoryReset() + /** + * Nickname must be valid UTF-8 and shorter than 64 chars. + * + * May throw one of: ProfileRenameException, ProfileNameTooLongException, ProfileNameIsInvalidUTF8Exception + */ 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/LpacJni.kt b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt index 8e3f53a..d50c1c1 100644 --- a/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt +++ b/libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/LpacJni.kt @@ -19,7 +19,7 @@ internal object LpacJni { external fun es10cEnableProfile(handle: Long, iccid: String, refresh: Boolean): Int external fun es10cDisableProfile(handle: Long, iccid: String, refresh: Boolean): Int external fun es10cDeleteProfile(handle: Long, iccid: String): Int - external fun es10cSetNickname(handle: Long, iccid: String, nick: String): Int + external fun es10cSetNickname(handle: Long, iccid: String, nickNullTerminated: ByteArray): Int // es10b external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null) 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..0330d82 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,23 @@ 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) { + val encoded = try { + Charsets.UTF_8.encode(nickname).array() + } catch (e: CharacterCodingException) { + throw LocalProfileAssistant.ProfileNameIsInvalidUTF8Exception() + } + + if (encoded.size >= 64) { + throw LocalProfileAssistant.ProfileNameTooLongException() + } + + val encodedNullTerminated = encoded + byteArrayOf(0) + + if (LpacJni.es10cSetNickname(contextHandle, iccid, encodedNullTerminated) != 0) { + throw LocalProfileAssistant.ProfileRenameException() + } + } override fun euiccMemoryReset() { LpacJni.es10cEuiccMemoryReset(contextHandle) diff --git a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c index 8f4752b..e438107 100644 --- a/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c +++ b/libs/lpac-jni/src/main/jni/lpac-jni/lpac-jni.c @@ -205,16 +205,16 @@ Java_net_typeblog_lpac_1jni_LpacJni_es10cDisableProfile(JNIEnv *env, jobject thi JNIEXPORT jint JNICALL Java_net_typeblog_lpac_1jni_LpacJni_es10cSetNickname(JNIEnv *env, jobject thiz, jlong handle, - jstring iccid, jstring nick) { + jstring iccid, jbyteArray nick) { struct euicc_ctx *ctx = (struct euicc_ctx *) handle; const char *_iccid = NULL; - const char *_nick = NULL; + jbyte *_nick = NULL; int ret; _iccid = (*env)->GetStringUTFChars(env, iccid, NULL); - _nick = (*env)->GetStringUTFChars(env, nick, NULL); - ret = es10c_set_nickname(ctx, _iccid, _nick); - (*env)->ReleaseStringUTFChars(env, nick, _nick); + _nick = (*env)->GetByteArrayElements(env, nick, NULL); + ret = es10c_set_nickname(ctx, _iccid, (const char *) _nick); + (*env)->ReleaseByteArrayElements(env, nick, _nick, JNI_ABORT); (*env)->ReleaseStringUTFChars(env, iccid, _iccid); return ret; }