feat: copyable eid string #1

Closed
septs wants to merge 4 commits from copyable-eid into clipboard
11 changed files with 85 additions and 47 deletions

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ClipData
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -8,6 +9,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -16,6 +18,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.typeblog.lpac_jni.impl.DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 import net.typeblog.lpac_jni.impl.DEFAULT_PKID_GSMA_RSP2_ROOT_CI1
@ -32,6 +35,13 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
private var logicalSlotId: Int = -1 private var logicalSlotId: Int = -1
data class Item(
@StringRes
val titleResId: Int,
val content: String?,
val copyable: Boolean = false,
)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -41,19 +51,21 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
swipeRefresh = requireViewById(R.id.swipe_refresh) swipeRefresh = requireViewById(R.id.swipe_refresh)
infoList = requireViewById(R.id.recycler_view) infoList = requireViewById<RecyclerView>(R.id.recycler_view).also {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
infoList.layoutManager = it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) it.adapter = EuiccInfoAdapter()
infoList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) }
infoList.adapter = EuiccInfoAdapter()
logicalSlotId = intent.getIntExtra("logicalSlotId", 0) logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
title = getString( val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
R.string.euicc_info_activity_title, getString(R.string.usb)
} else {
getString(R.string.channel_name_format, logicalSlotId) getString(R.string.channel_name_format, logicalSlotId)
) }
title = getString(R.string.euicc_info_activity_title, channelTitle)
swipeRefresh.setOnRefreshListener { refresh() } swipeRefresh.setOnRefreshListener { refresh() }
@ -78,33 +90,31 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
lifecycleScope.launch { lifecycleScope.launch {
(infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems =
euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildPairs).map { euiccChannelManager.withEuiccChannel(logicalSlotId, ::buildEuiccInfoItems)
Pair(getString(it.first), it.second ?: getString(R.string.unknown))
}
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false
} }
} }
private fun buildPairs(channel: EuiccChannel) = buildList { private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
add(Pair(R.string.euicc_info_access_mode, channel.type)) add(Item(R.string.euicc_info_access_mode, channel.type))
add( add(
Pair( Item(
R.string.euicc_info_removable, R.string.euicc_info_removable,
formatByBoolean(channel.port.card.isRemovable, YES_NO) formatByBoolean(channel.port.card.isRemovable, YES_NO)
) )
) )
add(Pair(R.string.euicc_info_eid, channel.lpa.eID)) add(Item(R.string.euicc_info_eid, channel.lpa.eID, copyable = true))
channel.lpa.euiccInfo2.let { info -> channel.lpa.euiccInfo2.let { info ->
add(Pair(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion)) add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
add(Pair(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion)) add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
add(Pair(R.string.euicc_info_pp_version, info?.ppVersion)) add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
add(Pair(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber)) add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
add(Pair(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace))) add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
} }
channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers -> channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
add( add(
Pair( Item(
R.string.euicc_info_gsma_prod, R.string.euicc_info_gsma_prod,
formatByBoolean( formatByBoolean(
signers.contains(DEFAULT_PKID_GSMA_RSP2_ROOT_CI1), signers.contains(DEFAULT_PKID_GSMA_RSP2_ROOT_CI1),
@ -113,7 +123,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
) )
) )
add( add(
Pair( Item(
R.string.euicc_info_gsma_test, R.string.euicc_info_gsma_test,
formatByBoolean(PKID_GSMA_TEST_CI.any(signers::contains), SUPPORTED_UNSUPPORTED) formatByBoolean(PKID_GSMA_TEST_CI.any(signers::contains), SUPPORTED_UNSUPPORTED)
) )
@ -131,17 +141,28 @@ class EuiccInfoActivity : BaseEuiccAccessActivity() {
) )
inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) { inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
private val context = root.context
private val title: TextView = root.requireViewById(R.id.euicc_info_title) private val title: TextView = root.requireViewById(R.id.euicc_info_title)
private val content: TextView = root.requireViewById(R.id.euicc_info_content) private val content: TextView = root.requireViewById(R.id.euicc_info_content)
private var copyable = false
fun bind(item: Pair<String, String>) { init {
title.text = item.first root.setOnClickListener {
content.text = item.second if (copyable) context.setClipboard(title.text.toString()) {
ClipData.newPlainText(title.text, content.text.toString())
}
}
}
fun bind(item: Item) {
title.setText(item.titleResId)
content.text = item.content ?: getString(R.string.unknown)
copyable = item.copyable
} }
} }
inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() { inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
var euiccInfoItems: List<Pair<String, String>> = listOf() var euiccInfoItems: List<Item> = listOf()
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
set(newVal) { set(newVal) {
field = newVal field = newVal

View file

@ -2,7 +2,6 @@ package im.angry.openeuicc.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
@ -346,10 +345,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
} }
iccid.setOnLongClickListener { iccid.setOnLongClickListener {
requireContext().getSystemService(ClipboardManager::class.java)!! requireContext().setClipboard("ICCID") {
.setPrimaryClip(ClipData.newPlainText("iccid", iccid.text)) ClipData.newPlainText("iccid", iccid.text)
Toast.makeText(requireContext(), R.string.toast_iccid_copied, Toast.LENGTH_SHORT) }
.show()
true true
} }

View file

@ -1,5 +1,6 @@
package im.angry.openeuicc.ui.wizard package im.angry.openeuicc.ui.wizard
import android.app.AlertDialog
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -104,7 +105,16 @@ class DownloadWizardMethodSelectFragment : DownloadWizardActivity.DownloadWizard
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") return if (components.size < 3 || components[0] != "LPA:1") {
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.profile_download_incorrect_lpa_string)
setMessage(R.string.profile_download_incorrect_lpa_string_message)
setCancelable(true)
setNegativeButton(android.R.string.cancel, null)
show()
}
return
}
state.smdp = components[1] state.smdp = components[1]
state.matchingId = components[2] state.matchingId = components[2]
gotoNextFragment(DownloadWizardDetailsFragment()) gotoNextFragment(DownloadWizardDetailsFragment())

View file

@ -0,0 +1,14 @@
package im.angry.openeuicc.util
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.widget.Toast
import im.angry.openeuicc.common.R
fun Context.setClipboard(label: String, callback: () -> ClipData) {
getSystemService(ClipboardManager::class.java)!!
.setPrimaryClip(callback())
Toast.makeText(this, getString(R.string.toast_copied, label), Toast.LENGTH_SHORT)
.show()
}

View file

@ -17,7 +17,7 @@
<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_profile_name_too_long">ニックネームは 64 文字以内にしてください</string>
<string name="toast_iccid_copied">ICCID をクリップボードにコピーしました</string> <string name="toast_copied">%s をクリップボードにコピーしました</string>
<string name="slot_select">スロットを選択</string> <string name="slot_select">スロットを選択</string>
<string name="slot_select_select">選択</string> <string name="slot_select_select">選択</string>
<string name="usb_permission">USB の権限を許可</string> <string name="usb_permission">USB の権限を許可</string>

View file

@ -18,7 +18,7 @@
<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_profile_name_too_long">昵称不能超过 64 个字符</string>
<string name="toast_iccid_copied">已复制 ICCID 到剪贴板</string> <string name="toast_copied">已复制 %s 到剪贴板</string>
<string name="slot_select">选择卡槽</string> <string name="slot_select">选择卡槽</string>
<string name="slot_select_select">选择</string> <string name="slot_select_select">选择</string>
<string name="usb_permission">授予 USB 权限</string> <string name="usb_permission">授予 USB 权限</string>

View file

@ -29,7 +29,7 @@
<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_name_too_long">Nickname cannot be longer than 64 characters</string>
<string name="toast_iccid_copied">ICCID copied to clipboard</string> <string name="toast_copied">%s copied to clipboard</string>
<string name="slot_select">Select Slot</string> <string name="slot_select">Select Slot</string>
<string name="slot_select_select">Select</string> <string name="slot_select_select">Select</string>
@ -56,6 +56,8 @@
<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_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="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>

View file

@ -1,13 +1,12 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.preference.Preference import androidx.preference.Preference
import im.angry.easyeuicc.R import im.angry.easyeuicc.R
import im.angry.openeuicc.util.encodeHex import im.angry.openeuicc.util.encodeHex
import im.angry.openeuicc.util.setClipboard
import java.security.MessageDigest import java.security.MessageDigest
class UnprivilegedSettingsFragment : SettingsFragment() { class UnprivilegedSettingsFragment : SettingsFragment() {
@ -33,10 +32,9 @@ class UnprivilegedSettingsFragment : SettingsFragment() {
findPreference<Preference>("pref_info_ara_m")?.apply { findPreference<Preference>("pref_info_ara_m")?.apply {
summary = firstSigner.encodeHex() summary = firstSigner.encodeHex()
setOnPreferenceClickListener { setOnPreferenceClickListener {
requireContext().getSystemService(ClipboardManager::class.java)!! requireContext().setClipboard("ARA-M SHA-1") {
.setPrimaryClip(ClipData.newPlainText("ara-m", summary)) ClipData.newPlainText("ara-m", summary)
Toast.makeText(requireContext(), R.string.toast_ara_m_copied, Toast.LENGTH_SHORT) }
.show()
true true
} }
} }

View file

@ -30,5 +30,4 @@
<string name="compatibility_check_verdict_unknown_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string> <string name="compatibility_check_verdict_unknown_likely_fail">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかは判断できません。デバイスが OMAPI のサポートを宣言していないため、このデバイス上で取り外し可能な eSIM を管理することはサポートされていない可能性があります。\n%s</string>
<string name="compatibility_check_verdict_unknown">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s</string> <string name="compatibility_check_verdict_unknown">挿入された取り外し可能な eSIM がデバイス上で管理できるかどうかを確認できません。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string> <string name="compatibility_check_verdict_fail_shared">ただし、eSIM プロファイルがすでに読み込まれている場合、有効化されたプロファイル自体は引き続き機能します。また、プロファイルが管理できない場合は、このデバイスで USB カードリーダーを介してプロファイルを管理できる可能性があります。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 をクリップボードにコピーしました</string>
</resources> </resources>

View file

@ -28,5 +28,4 @@
<string name="compatibility_check_verdict_unknown_likely_fail">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。由于您的设备未声明支持OMAPI因此更有可能不支持在此设备上管理可插拔 eSIM。\n%s</string> <string name="compatibility_check_verdict_unknown_likely_fail">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。由于您的设备未声明支持OMAPI因此更有可能不支持在此设备上管理可插拔 eSIM。\n%s</string>
<string name="compatibility_check_verdict_unknown">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。\n%s</string> <string name="compatibility_check_verdict_unknown">我们无法确定是否可以在您的设备上管理可插拔 eSIM 卡。\n%s</string>
<string name="compatibility_check_verdict_fail_shared">然而已经加载了eSIM配置文件的可插拔 eSIM 卡仍然可以工作; 即使无法在装置上直接管理可插拔 eSIM 卡中的配置文件,您仍然可以使用 USB 卡读卡器来管理配置文件。</string> <string name="compatibility_check_verdict_fail_shared">然而已经加载了eSIM配置文件的可插拔 eSIM 卡仍然可以工作; 即使无法在装置上直接管理可插拔 eSIM 卡中的配置文件,您仍然可以使用 USB 卡读卡器来管理配置文件。</string>
<string name="toast_ara_m_copied">ARA-M SHA-1 已拷贝到剪贴板</string>
</resources> </resources>

View file

@ -7,9 +7,6 @@
<!-- Settings --> <!-- Settings -->
<string name="pref_developer_ara_m" translatable="false">ARA-M SHA-1</string> <string name="pref_developer_ara_m" translatable="false">ARA-M SHA-1</string>
<!-- Toast -->
<string name="toast_ara_m_copied">ARA-M SHA-1 copied to clipboard</string>
<!-- Compatibility Check Descriptions --> <!-- Compatibility Check Descriptions -->
<string name="compatibility_check_system_features">System Features</string> <string name="compatibility_check_system_features">System Features</string>
<string name="compatibility_check_system_features_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string> <string name="compatibility_check_system_features_desc">Whether your device has all the required features for managing removable eUICC cards. For example, basic telephony and OMAPI support.</string>