Compare commits

...

9 commits

Author SHA1 Message Date
24076e8fb4 ui: Remove old toast for profile name length
We'll add back i18n later.
2024-12-15 22:51:36 -05:00
f135a0da60 ui: Fixup rename error toasts 2024-12-15 22:49:02 -05:00
3b7bd8b31e fix: Validate nickname and convert to proper UTF-8 before passing to JNI
The JNI "modified" UTF-8 isn't what SGP.22 mandates. Let's encode
properly, validate the length, and pass the string as a C
null-terminated string directly over JNI.

This also introduces new exceptions that are exposed via UI as Toasts.
2024-12-15 16:02:27 -05:00
74e946cc8f i18n: Update message for LPA string parsing failure 2024-12-15 14:42:12 -05:00
3430406603 ui: wizard: Add toast for when clipboard is empty 2024-12-15 14:40:59 -05:00
24f04f54e4 Add icon for loading from clipboard 2024-12-15 14:37:30 -05:00
0fbda7dd78 feat: load lpa string from clipboard 2024-12-15 14:34:00 -05:00
905d0c897e ui: wizard: Save activity before showing nvram warning dialog 2024-12-15 13:19:23 -05:00
f395cee2e0 chore: cleanup unused resource from 343dfb43f8 (#119)
Reviewed-on: PeterCxy/OpenEUICC#119
Co-authored-by: septs <github@septs.pw>
Co-committed-by: septs <github@septs.pw>
2024-12-15 18:47:57 +01:00
15 changed files with 110 additions and 78 deletions

View file

@ -54,8 +54,9 @@ class LocalProfileAssistantWrapper(orig: LocalProfileAssistant) :
override fun euiccMemoryReset() = lpa.euiccMemoryReset() override fun euiccMemoryReset() = lpa.euiccMemoryReset()
override fun setNickname(iccid: String, nickname: String): Boolean = override fun setNickname(iccid: String, nickname: String) {
lpa.setNickname(iccid, nickname) lpa.setNickname(iccid, nickname)
}
override fun close() = lpa.close() override fun close() = lpa.close()

View file

@ -414,16 +414,12 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
getString(R.string.task_profile_rename_failure), getString(R.string.task_profile_rename_failure),
R.drawable.ic_task_rename R.drawable.ic_task_rename
) { ) {
val res = euiccChannelManager.withEuiccChannel(slotId, portId) { channel -> euiccChannelManager.withEuiccChannel(slotId, portId) { channel ->
channel.lpa.setNickname( channel.lpa.setNickname(
iccid, iccid,
name name
) )
} }
if (!res) {
throw RuntimeException("Profile not renamed")
}
} }
fun launchProfileDeleteTask( fun launchProfileDeleteTask(

View file

@ -14,6 +14,7 @@ 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
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
companion object { companion object {
@ -81,13 +82,18 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
} }
} }
private fun rename() { private fun showErrorAndCancel(errorStrRes: Int) {
val name = profileRenameNewName.editText!!.text.toString().trim() Toast.makeText(
if (name.length >= 64) { requireContext(),
Toast.makeText(context, R.string.toast_profile_name_too_long, Toast.LENGTH_LONG).show() errorStrRes,
return Toast.LENGTH_LONG
} ).show()
renaming = false
progress.visibility = View.GONE
}
private fun rename() {
renaming = true renaming = true
progress.isIndeterminate = true progress.isIndeterminate = true
progress.visibility = View.VISIBLE progress.visibility = View.VISIBLE
@ -95,21 +101,37 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask() euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchProfileRenameTask( val res = euiccChannelManagerService.launchProfileRenameTask(
slotId, slotId,
portId, portId,
requireArguments().getString("iccid")!!, requireArguments().getString("iccid")!!,
name profileRenameNewName.editText!!.text.toString().trim()
).waitDone() ).waitDone()
if (parentFragment is EuiccProfilesChangedListener) { when (res) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() is LocalProfileAssistant.ProfileNameTooLongException -> {
} showErrorAndCancel(R.string.profile_rename_too_long)
}
try { is LocalProfileAssistant.ProfileNameIsInvalidUTF8Exception -> {
dismiss() showErrorAndCancel(R.string.profile_rename_encoding_error)
} catch (e: IllegalStateException) { }
// Ignored
is Throwable -> {
showErrorAndCancel(R.string.profile_rename_failure)
}
else -> {
if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
try {
dismiss()
} catch (e: IllegalStateException) {
// Ignored
}
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.ui.wizard package im.angry.openeuicc.ui.wizard
import android.app.AlertDialog import android.app.AlertDialog
import android.content.ClipboardManager
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -8,6 +9,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration 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) { DownloadMethod(R.drawable.ic_gallery_black, R.string.download_wizard_method_gallery) {
gallerySelectorLauncher.launch("image/*") 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) { DownloadMethod(R.drawable.ic_edit, R.string.download_wizard_method_manual) {
gotoNextFragment(DownloadWizardDetailsFragment()) gotoNextFragment(DownloadWizardDetailsFragment())
} }
@ -103,6 +108,22 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
return view 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) { private fun processLpaString(s: String) {
val components = s.split("$") val components = s.split("$")
if (components.size < 3 || components[0] != "LPA:1") { if (components.size < 3 || components[0] != "LPA:1") {

View file

@ -57,13 +57,15 @@ class DownloadWizardSlotSelectFragment : DownloadWizardActivity.DownloadWizardSt
super.beforeNext() super.beforeNext()
if (adapter.selected.freeSpace < LOW_NVRAM_THRESHOLD) { if (adapter.selected.freeSpace < LOW_NVRAM_THRESHOLD) {
val activity = requireActivity()
AlertDialog.Builder(requireContext()).apply { AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.profile_download_low_nvram_title) setTitle(R.string.profile_download_low_nvram_title)
setMessage(R.string.profile_download_low_nvram_message) setMessage(R.string.profile_download_low_nvram_message)
setCancelable(true) setCancelable(true)
setPositiveButton(android.R.string.ok, null) setPositiveButton(android.R.string.ok, null)
setNegativeButton(android.R.string.cancel) { _, _ -> setNegativeButton(android.R.string.cancel) { _, _ ->
requireActivity().finish() activity.finish()
} }
show() show()
} }

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M5,5h2v3h10V5h2v6h2V5c0,-1.1 -0.9,-2 -2,-2h-4.18C14.4,1.84 13.3,1 12,1S9.6,1.84 9.18,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h5v-2H5V5zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,3 12,3z"/>
<path android:fillColor="@android:color/white" android:pathData="M18.01,13l-1.42,1.41l1.58,1.58l-6.17,0l0,2l6.17,0l-1.58,1.59l1.42,1.41l3.99,-4z"/>
</vector>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1"
app:navigationIcon="?homeAsUpIndicator" />
<Spinner
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="48dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/ok"
android:icon="@drawable/ic_check_black"
android:title="@string/slot_select_select"
app:showAsAction="ifRoom"/>
</menu>

View file

@ -16,10 +16,7 @@
<string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string> <string name="enable_disable_timeout">eSIM チップがプロファイルの切り替えの待機中にタイムアウトしました。これはデバイスのモデムファームウェアのバグの可能性があります。機内モードに切り替えるかアプリを再起動、デバイスを再起動してください。</string>
<string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string> <string name="switch_did_not_refresh">操作は成功しましたが、デバイスのモデムが更新を拒否しました。新しいプロファイルを使用するには機内モードに切り替えるか、再起動する必要があります。</string>
<string name="toast_profile_enable_failed">新しい eSIM プロファイルに切り替えることができません。</string> <string name="toast_profile_enable_failed">新しい eSIM プロファイルに切り替えることができません。</string>
<string name="toast_profile_name_too_long">ニックネームは 64 文字以内にしてください</string>
<string name="toast_iccid_copied">ICCID をクリップボードにコピーしました</string> <string name="toast_iccid_copied">ICCID をクリップボードにコピーしました</string>
<string name="slot_select">スロットを選択</string>
<string name="slot_select_select">選択</string>
<string name="usb_permission">USB の権限を許可</string> <string name="usb_permission">USB の権限を許可</string>
<string name="usb_permission_needed">USB スマートカードリーダーにアクセスするには許可が必要です。</string> <string name="usb_permission_needed">USB スマートカードリーダーにアクセスするには許可が必要です。</string>
<string name="usb_failed">USB スマートカードリーダー経由で eSIM に接続できません。</string> <string name="usb_failed">USB スマートカードリーダー経由で eSIM に接続できません。</string>

View file

@ -17,10 +17,7 @@
<string name="enable_disable_timeout">等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机</string> <string name="enable_disable_timeout">等待 eSIM 芯片切换配置文件时超时。这可能是您手机基带固件中的一个错误。请尝试切换飞行模式、重新启动应用程序或重新启动手机</string>
<string name="switch_did_not_refresh">操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。</string> <string name="switch_did_not_refresh">操作成功, 但是您手机的基带拒绝刷新。您可能需要切换飞行模式或重新启动,以便使用新的配置文件。</string>
<string name="toast_profile_enable_failed">无法切换到新的 eSIM 配置文件。</string> <string name="toast_profile_enable_failed">无法切换到新的 eSIM 配置文件。</string>
<string name="toast_profile_name_too_long">昵称不能超过 64 个字符</string>
<string name="toast_iccid_copied">已复制 ICCID 到剪贴板</string> <string name="toast_iccid_copied">已复制 ICCID 到剪贴板</string>
<string name="slot_select">选择卡槽</string>
<string name="slot_select_select">选择</string>
<string name="usb_permission">授予 USB 权限</string> <string name="usb_permission">授予 USB 权限</string>
<string name="usb_permission_needed">需要获得访问 USB 智能卡读卡器的权限。</string> <string name="usb_permission_needed">需要获得访问 USB 智能卡读卡器的权限。</string>
<string name="usb_failed">无法通过 USB 智能卡读卡器连接到 eSIM。</string> <string name="usb_failed">无法通过 USB 智能卡读卡器连接到 eSIM。</string>

View file

@ -28,14 +28,10 @@
<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_too_long">Nickname cannot be longer than 64 characters</string>
<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>
<string name="toast_eid_copied">EID copied to clipboard</string> <string name="toast_eid_copied">EID copied to clipboard</string>
<string name="slot_select">Select Slot</string>
<string name="slot_select_select">Select</string>
<string name="usb_permission">Grant USB permission</string> <string name="usb_permission">Grant USB permission</string>
<string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string> <string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string>
<string name="usb_failed">Cannot connect to eSIM via a USB smart card reader.</string> <string name="usb_failed">Cannot connect to eSIM via a USB smart card reader.</string>
@ -58,8 +54,9 @@
<string name="profile_download_low_nvram_title">This download may fail</string> <string name="profile_download_low_nvram_title">This download may fail</string>
<string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string> <string name="profile_download_low_nvram_message">This download may fail due to low remaining capacity.</string>
<string name="profile_download_no_lpa_string">No LPA string found in clipboard</string>
<string name="profile_download_incorrect_lpa_string">Incorrect LPA String</string> <string name="profile_download_incorrect_lpa_string">Incorrect LPA String</string>
<string name="profile_download_incorrect_lpa_string_message">The LPA string could not be parsed</string> <string name="profile_download_incorrect_lpa_string_message">Could not parse QR code or clipboard content as an LPA string for downloading eSIMs.</string>
<string name="download_wizard">Download Wizard</string> <string name="download_wizard">Download Wizard</string>
<string name="download_wizard_back">Back</string> <string name="download_wizard_back">Back</string>
@ -75,6 +72,7 @@
<string name="download_wizard_method_select">How would you like to download the eSIM profile?</string> <string name="download_wizard_method_select">How would you like to download the eSIM profile?</string>
<string name="download_wizard_method_qr_code">Scan a QR code with camera</string> <string name="download_wizard_method_qr_code">Scan a QR code with camera</string>
<string name="download_wizard_method_gallery">Load a QR code from gallery</string> <string name="download_wizard_method_gallery">Load a QR code from gallery</string>
<string name="download_wizard_method_clipboard">Load from Clipboard</string>
<string name="download_wizard_method_manual">Enter manually</string> <string name="download_wizard_method_manual">Enter manually</string>
<string name="download_wizard_details">Input or confirm details for downloading your eSIM:</string> <string name="download_wizard_details">Input or confirm details for downloading your eSIM:</string>
<string name="download_wizard_progress">Downloading your eSIM…</string> <string name="download_wizard_progress">Downloading your eSIM…</string>
@ -98,6 +96,9 @@
<string name="logs_saved_message">Logs have been saved to the selected path. Would you like to share the log through another app?</string> <string name="logs_saved_message">Logs have been saved to the selected path. Would you like to share the log through another app?</string>
<string name="profile_rename_new_name">New nickname</string> <string name="profile_rename_new_name">New nickname</string>
<string name="profile_rename_encoding_error">Failed to encode nickname as UTF-8</string>
<string name="profile_rename_too_long">Nickname is too long</string>
<string name="profile_rename_failure">Unknown failure when renaming profile</string>
<string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string> <string name="profile_delete_confirm">Are you sure you want to delete the profile %s? This operation is irreversible.</string>
<string name="profile_delete_confirm_input">Type \'%s\' here to confirm deletion</string> <string name="profile_delete_confirm_input">Type \'%s\' here to confirm deletion</string>

View file

@ -12,6 +12,10 @@ interface LocalProfileAssistant {
val lastApduException: Exception?, val lastApduException: Exception?,
) : Exception("Failed to download profile") ) : 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 valid: Boolean
val profiles: List<LocalProfileInfo> val profiles: List<LocalProfileInfo>
val notifications: List<LocalProfileNotification> val notifications: List<LocalProfileNotification>
@ -40,9 +44,14 @@ interface LocalProfileAssistant {
fun euiccMemoryReset() fun euiccMemoryReset()
/**
* Nickname must be valid UTF-8 and shorter than 64 chars.
*
* May throw one of: ProfileRenameException, ProfileNameTooLongException, ProfileNameIsInvalidUTF8Exception
*/
fun setNickname( fun setNickname(
iccid: String, nickname: String iccid: String, nickname: String
): Boolean )
fun close() fun close()
} }

View file

@ -19,7 +19,7 @@ internal object LpacJni {
external fun es10cEnableProfile(handle: Long, iccid: String, refresh: Boolean): Int external fun es10cEnableProfile(handle: Long, iccid: String, refresh: Boolean): Int
external fun es10cDisableProfile(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 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 // 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) external fun es10bListNotification(handle: Long): Long // A native pointer to a linked list. Handle with linked list-related methods below. May be 0 (null)

View file

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

View file

@ -205,16 +205,16 @@ Java_net_typeblog_lpac_1jni_LpacJni_es10cDisableProfile(JNIEnv *env, jobject thi
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_es10cSetNickname(JNIEnv *env, jobject thiz, jlong handle, 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; struct euicc_ctx *ctx = (struct euicc_ctx *) handle;
const char *_iccid = NULL; const char *_iccid = NULL;
const char *_nick = NULL; jbyte *_nick = NULL;
int ret; int ret;
_iccid = (*env)->GetStringUTFChars(env, iccid, NULL); _iccid = (*env)->GetStringUTFChars(env, iccid, NULL);
_nick = (*env)->GetStringUTFChars(env, nick, NULL); _nick = (*env)->GetByteArrayElements(env, nick, NULL);
ret = es10c_set_nickname(ctx, _iccid, _nick); ret = es10c_set_nickname(ctx, _iccid, (const char *) _nick);
(*env)->ReleaseStringUTFChars(env, nick, _nick); (*env)->ReleaseByteArrayElements(env, nick, _nick, JNI_ABORT);
(*env)->ReleaseStringUTFChars(env, iccid, _iccid); (*env)->ReleaseStringUTFChars(env, iccid, _iccid);
return ret; return ret;
} }