EuiccInfoActivity.kt 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package im.angry.openeuicc.ui
  2. import android.annotation.SuppressLint
  3. import android.content.ClipData
  4. import android.content.ClipboardManager
  5. import android.os.Build
  6. import android.os.Bundle
  7. import android.view.LayoutInflater
  8. import android.view.MenuItem
  9. import android.view.View
  10. import android.view.ViewGroup
  11. import android.widget.TextView
  12. import android.widget.Toast
  13. import androidx.activity.enableEdgeToEdge
  14. import androidx.annotation.StringRes
  15. import androidx.lifecycle.lifecycleScope
  16. import androidx.recyclerview.widget.DividerItemDecoration
  17. import androidx.recyclerview.widget.LinearLayoutManager
  18. import androidx.recyclerview.widget.RecyclerView
  19. import androidx.recyclerview.widget.RecyclerView.ViewHolder
  20. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
  21. import im.angry.openeuicc.common.R
  22. import im.angry.openeuicc.core.EuiccChannel
  23. import im.angry.openeuicc.core.EuiccChannelManager
  24. import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID
  25. import im.angry.openeuicc.util.OpenEuiccContextMarker
  26. import im.angry.openeuicc.util.activityToolbarInsetHandler
  27. import im.angry.openeuicc.util.decodeHex
  28. import im.angry.openeuicc.util.encodeHex
  29. import im.angry.openeuicc.util.formatFreeSpace
  30. import im.angry.openeuicc.util.mainViewPaddingInsetHandler
  31. import im.angry.openeuicc.util.setupRootViewSystemBarInsets
  32. import im.angry.openeuicc.util.tryParseEuiccVendorInfo
  33. import kotlinx.coroutines.Dispatchers
  34. import kotlinx.coroutines.launch
  35. import kotlinx.coroutines.withContext
  36. import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
  37. import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
  38. // https://euicc-manual.osmocom.org/docs/pki/eum/accredited.json
  39. // ref: <https://regex101.com/r/5FFz8u>
  40. private val RE_SAS = Regex(
  41. """^[A-Z]{2}-[A-Z]{2}(?:-UP)?-\d{4}T?(?:-\d+)?T?$""",
  42. setOf(RegexOption.IGNORE_CASE),
  43. )
  44. class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
  45. companion object {
  46. private val YES_NO = Pair(R.string.euicc_info_yes, R.string.euicc_info_no)
  47. }
  48. private lateinit var swipeRefresh: SwipeRefreshLayout
  49. private lateinit var infoList: RecyclerView
  50. private var logicalSlotId: Int = -1
  51. private var seId: EuiccChannel.SecureElementId = EuiccChannel.SecureElementId.DEFAULT
  52. data class Item(
  53. @get:StringRes
  54. val titleResId: Int,
  55. val content: String?,
  56. val copiedToastResId: Int? = null,
  57. )
  58. override fun onCreate(savedInstanceState: Bundle?) {
  59. enableEdgeToEdge()
  60. super.onCreate(savedInstanceState)
  61. setContentView(R.layout.activity_euicc_info)
  62. setSupportActionBar(requireViewById(R.id.toolbar))
  63. supportActionBar!!.setDisplayHomeAsUpEnabled(true)
  64. swipeRefresh = requireViewById(R.id.swipe_refresh)
  65. infoList = requireViewById<RecyclerView>(R.id.recycler_view).also {
  66. it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
  67. it.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
  68. it.adapter = EuiccInfoAdapter()
  69. }
  70. logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
  71. seId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
  72. intent.getParcelableExtra("seId", EuiccChannel.SecureElementId::class.java)
  73. } else {
  74. @Suppress("DEPRECATION")
  75. intent.getParcelableExtra("seId")
  76. } ?: EuiccChannel.SecureElementId.DEFAULT
  77. val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
  78. getString(R.string.channel_type_usb)
  79. } else {
  80. appContainer.customizableTextProvider.formatNonUsbChannelName(logicalSlotId)
  81. }
  82. title = getString(R.string.euicc_info_activity_title, channelTitle)
  83. swipeRefresh.setOnRefreshListener { refresh() }
  84. setupRootViewSystemBarInsets(
  85. window.decorView.rootView, arrayOf(
  86. this::activityToolbarInsetHandler,
  87. mainViewPaddingInsetHandler(infoList)
  88. )
  89. )
  90. }
  91. override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
  92. android.R.id.home -> {
  93. finish()
  94. true
  95. }
  96. else -> super.onOptionsItemSelected(item)
  97. }
  98. override fun onInit() {
  99. refresh()
  100. }
  101. private fun refresh() {
  102. swipeRefresh.isRefreshing = true
  103. lifecycleScope.launch {
  104. euiccChannelManager.withEuiccChannel(logicalSlotId, seId) { channel ->
  105. // When the chip multi-SE, we need to include seId in the title (because we don't have access
  106. // to hasMultipleSE in the onCreate() function, we need to do it here).
  107. // TODO: Move channel formatting to somewhere centralized and remove this hack. (And also, of course, add support for USB)
  108. if (channel.hasMultipleSE && logicalSlotId != EuiccChannelManager.USB_CHANNEL_ID) {
  109. withContext(Dispatchers.Main) {
  110. title =
  111. appContainer.customizableTextProvider.formatNonUsbChannelNameWithSeId(logicalSlotId, seId)
  112. }
  113. }
  114. val items = buildEuiccInfoItems(channel)
  115. withContext(Dispatchers.Main) {
  116. (infoList.adapter!! as EuiccInfoAdapter).euiccInfoItems = items
  117. }
  118. }
  119. swipeRefresh.isRefreshing = false
  120. }
  121. }
  122. private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
  123. add(Item(R.string.euicc_info_access_mode, channel.type))
  124. add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO)))
  125. add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
  126. if (!channel.isdrAid.contentEquals(EUICC_DEFAULT_ISDR_AID.decodeHex())) {
  127. // Only show if it's not the default ISD-R AID
  128. add(Item(R.string.euicc_info_isdr_aid, channel.isdrAid.encodeHex()))
  129. }
  130. channel.tryParseEuiccVendorInfo()?.let { vendorInfo ->
  131. // @formatter:off
  132. vendorInfo.skuName?.let { add(Item(R.string.euicc_info_sku, it)) }
  133. vendorInfo.serialNumber?.let { add(Item(R.string.euicc_info_sn, it, copiedToastResId = R.string.toast_sn_copied)) }
  134. vendorInfo.firmwareVersion?.let { add(Item(R.string.euicc_info_fw_ver, it)) }
  135. // @formatter:on
  136. }
  137. channel.lpa.euiccInfo2?.let { info ->
  138. add(Item(R.string.euicc_info_sgp22_version, info.sgp22Version.toString()))
  139. info.sasAccreditationNumber.trim().takeIf(RE_SAS::matches)
  140. ?.let { add(Item(R.string.euicc_info_sas_accreditation_number, it.uppercase())) }
  141. val nvramText = buildString {
  142. append(formatFreeSpace(info.freeNvram))
  143. append(' ')
  144. append(getString(R.string.euicc_info_free_nvram_hint))
  145. }
  146. add(Item(R.string.euicc_info_free_nvram, nvramText))
  147. }
  148. channel.lpa.euiccInfo2?.euiccCiPKIdListForSigning.orEmpty().let { signers ->
  149. // SGP.28 v1.0, eSIM CI Registration Criteria (Page 5 of 9, 2019-10-24)
  150. // https://www.gsma.com/newsroom/wp-content/uploads/SGP.28-v1.0.pdf#page=5
  151. // FS.27 v2.0, Security Guidelines for UICC Profiles (Page 25 of 27, 2024-01-30)
  152. // https://www.gsma.com/solutions-and-impact/technologies/security/wp-content/uploads/2024/01/FS.27-Security-Guidelines-for-UICC-Credentials-v2.0-FINAL-23-July.pdf#page=25
  153. val resId = when {
  154. signers.isEmpty() -> R.string.euicc_info_unknown // the case is not mp, but it's is not common
  155. PKID_GSMA_LIVE_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_live
  156. PKID_GSMA_TEST_CI.any(signers::contains) -> R.string.euicc_info_ci_gsma_test
  157. else -> R.string.euicc_info_ci_unknown
  158. }
  159. add(Item(R.string.euicc_info_ci_type, getString(resId)))
  160. }
  161. val atr = channel.atr?.encodeHex() ?: getString(R.string.euicc_info_unavailable)
  162. add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
  163. }
  164. @Suppress("SameParameterValue")
  165. private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
  166. getString(if (b) res.first else res.second)
  167. inner class EuiccInfoViewHolder(root: View) : ViewHolder(root) {
  168. private val title: TextView = root.requireViewById(R.id.euicc_info_title)
  169. private val content: TextView = root.requireViewById(R.id.euicc_info_content)
  170. private var copiedToastResId: Int? = null
  171. init {
  172. root.setOnClickListener {
  173. if (copiedToastResId != null) {
  174. val label = title.text.toString()
  175. getSystemService(ClipboardManager::class.java)!!
  176. .setPrimaryClip(ClipData.newPlainText(label, content.text))
  177. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
  178. Toast.makeText(
  179. this@EuiccInfoActivity,
  180. copiedToastResId!!,
  181. Toast.LENGTH_SHORT
  182. ).show()
  183. }
  184. }
  185. }
  186. }
  187. fun bind(item: Item) {
  188. copiedToastResId = item.copiedToastResId
  189. title.setText(item.titleResId)
  190. content.text = item.content ?: getString(R.string.euicc_info_unknown)
  191. }
  192. }
  193. inner class EuiccInfoAdapter : RecyclerView.Adapter<EuiccInfoViewHolder>() {
  194. var euiccInfoItems: List<Item> = listOf()
  195. @SuppressLint("NotifyDataSetChanged")
  196. set(newVal) {
  197. field = newVal
  198. notifyDataSetChanged()
  199. }
  200. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EuiccInfoViewHolder {
  201. val root = LayoutInflater.from(parent.context)
  202. .inflate(R.layout.euicc_info_item, parent, false)
  203. return EuiccInfoViewHolder(root)
  204. }
  205. override fun getItemCount(): Int = euiccInfoItems.size
  206. override fun onBindViewHolder(holder: EuiccInfoViewHolder, position: Int) {
  207. holder.bind(euiccInfoItems[position])
  208. }
  209. }
  210. }