OpenEuiccService.kt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package im.angry.openeuicc.service
  2. import android.content.Context
  3. import android.content.Intent
  4. import android.os.Build
  5. import android.service.euicc.*
  6. import android.telephony.UiccSlotMapping
  7. import android.telephony.euicc.DownloadableSubscription
  8. import android.telephony.euicc.EuiccInfo
  9. import android.util.Log
  10. import net.typeblog.lpac_jni.LocalProfileInfo
  11. import im.angry.openeuicc.core.EuiccChannel
  12. import im.angry.openeuicc.core.EuiccChannelManager
  13. import im.angry.openeuicc.util.*
  14. import kotlinx.coroutines.runBlocking
  15. import java.lang.IllegalStateException
  16. class OpenEuiccService : EuiccService(), OpenEuiccContextMarker {
  17. companion object {
  18. const val TAG = "OpenEuiccService"
  19. }
  20. private val hasInternalEuicc by lazy {
  21. telephonyManager.uiccCardsInfoCompat.any { it.isEuicc && !it.isRemovable }
  22. }
  23. // TODO: Should this be configurable?
  24. private fun shouldIgnoreSlot(physicalSlotId: Int) =
  25. if (hasInternalEuicc) {
  26. // For devices with an internal eUICC slot, ignore any removable UICC
  27. telephonyManager.uiccCardsInfoCompat.find { it.physicalSlotIndex == physicalSlotId }!!.isRemovable
  28. } else {
  29. // Otherwise, we can report at least one removable eUICC to the system without confusing
  30. // it too much.
  31. telephonyManager.uiccCardsInfoCompat.firstOrNull { it.isEuicc }?.physicalSlotIndex == physicalSlotId
  32. }
  33. private data class EuiccChannelManagerContext(
  34. val euiccChannelManager: EuiccChannelManager
  35. ) {
  36. fun findChannel(physicalSlotId: Int): EuiccChannel? =
  37. euiccChannelManager.findEuiccChannelByPhysicalSlotBlocking(physicalSlotId)
  38. fun findChannel(slotId: Int, portId: Int): EuiccChannel? =
  39. euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)
  40. fun findAllChannels(physicalSlotId: Int): List<EuiccChannel>? =
  41. euiccChannelManager.findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId)
  42. }
  43. /**
  44. * Bind to EuiccChannelManagerService, run the callback with a EuiccChannelManager instance,
  45. * and then unbind after the callback is finished. All methods in this class that require access
  46. * to a EuiccChannelManager should be wrapped inside this call.
  47. *
  48. * This ensures that we only spawn and connect to APDU channels when we absolutely need to,
  49. * instead of keeping them open unnecessarily in the background at all times.
  50. */
  51. private inline fun <T> withEuiccChannelManager(fn: EuiccChannelManagerContext.() -> T): T {
  52. val (binder, unbind) = runBlocking {
  53. bindServiceSuspended(
  54. Intent(
  55. this@OpenEuiccService,
  56. EuiccChannelManagerService::class.java
  57. ), Context.BIND_AUTO_CREATE
  58. )
  59. }
  60. if (binder == null) {
  61. throw RuntimeException("Unable to bind to EuiccChannelManagerService; aborting")
  62. }
  63. val ret =
  64. EuiccChannelManagerContext((binder as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager).fn()
  65. unbind()
  66. return ret
  67. }
  68. override fun onGetEid(slotId: Int): String? = withEuiccChannelManager {
  69. findChannel(slotId)?.lpa?.eID
  70. }
  71. // When two eSIM cards are present on one device, the Android settings UI
  72. // gets confused and sets the incorrect slotId for profiles from one of
  73. // the cards. This function helps Detect this case and abort early.
  74. private fun EuiccChannel.profileExists(iccid: String?) =
  75. lpa.profiles.any { it.iccid == iccid }
  76. private fun ensurePortIsMapped(slotId: Int, portId: Int) {
  77. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
  78. return
  79. }
  80. val mappings = telephonyManager.simSlotMapping.toMutableList()
  81. mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId }?.let {
  82. throw IllegalStateException("Slot $slotId port $portId has already been mapped")
  83. }
  84. val idx = mappings.indexOfFirst { it.physicalSlotIndex != slotId || it.portIndex != portId }
  85. if (idx >= 0) {
  86. mappings[idx] = UiccSlotMapping(portId, slotId, mappings[idx].logicalSlotIndex)
  87. }
  88. mappings.firstOrNull { it.physicalSlotIndex == slotId && it.portIndex == portId } ?: run {
  89. throw IllegalStateException("Cannot map slot $slotId port $portId")
  90. }
  91. try {
  92. telephonyManager.simSlotMapping = mappings
  93. return
  94. } catch (_: Exception) {
  95. }
  96. // Sometimes hardware supports one ordering but not the reverse
  97. telephonyManager.simSlotMapping = mappings.reversed()
  98. }
  99. private fun <T> retryWithTimeout(timeoutMillis: Int, backoff: Int = 1000, f: () -> T?): T? {
  100. val startTimeMillis = System.currentTimeMillis()
  101. do {
  102. try {
  103. f()?.let { return@retryWithTimeout it }
  104. } catch (_: Exception) {
  105. // Ignore
  106. } finally {
  107. Thread.sleep(backoff.toLong())
  108. }
  109. } while (System.currentTimeMillis() - startTimeMillis < timeoutMillis)
  110. return null
  111. }
  112. override fun onGetOtaStatus(slotId: Int): Int {
  113. // Not implemented
  114. return 5 // EUICC_OTA_STATUS_UNAVAILABLE
  115. }
  116. override fun onStartOtaIfNecessary(
  117. slotId: Int,
  118. statusChangedCallback: OtaStatusChangedCallback?
  119. ) {
  120. // Not implemented
  121. }
  122. override fun onGetDownloadableSubscriptionMetadata(
  123. slotId: Int,
  124. subscription: DownloadableSubscription?,
  125. forceDeactivateSim: Boolean
  126. ): GetDownloadableSubscriptionMetadataResult {
  127. // Stub: return as-is and do not fetch anything
  128. // This is incompatible with carrier eSIM apps; should we make it compatible?
  129. return GetDownloadableSubscriptionMetadataResult(RESULT_OK, subscription)
  130. }
  131. override fun onGetDefaultDownloadableSubscriptionList(
  132. slotId: Int,
  133. forceDeactivateSim: Boolean
  134. ): GetDefaultDownloadableSubscriptionListResult {
  135. // Stub: we do not implement this (as this would require phoning in a central GSMA server)
  136. return GetDefaultDownloadableSubscriptionListResult(RESULT_OK, arrayOf())
  137. }
  138. override fun onGetEuiccProfileInfoList(slotId: Int): GetEuiccProfileInfoListResult = withEuiccChannelManager {
  139. Log.i(TAG, "onGetEuiccProfileInfoList slotId=$slotId")
  140. if (shouldIgnoreSlot(slotId)) {
  141. Log.i(TAG, "ignoring slot $slotId")
  142. return GetEuiccProfileInfoListResult(RESULT_FIRST_USER, arrayOf(), true)
  143. }
  144. // TODO: Temporarily enable the slot to access its profiles if it is currently unmapped
  145. val channel = findChannel(slotId) ?: return GetEuiccProfileInfoListResult(
  146. RESULT_FIRST_USER,
  147. arrayOf(),
  148. true
  149. )
  150. val profiles = channel.lpa.profiles.operational.map {
  151. EuiccProfileInfo.Builder(it.iccid).apply {
  152. setProfileName(it.name)
  153. setNickname(it.displayName)
  154. setServiceProviderName(it.providerName)
  155. setState(
  156. when (it.state) {
  157. LocalProfileInfo.State.Enabled -> EuiccProfileInfo.PROFILE_STATE_ENABLED
  158. LocalProfileInfo.State.Disabled -> EuiccProfileInfo.PROFILE_STATE_DISABLED
  159. }
  160. )
  161. setProfileClass(
  162. when (it.profileClass) {
  163. LocalProfileInfo.Clazz.Testing -> EuiccProfileInfo.PROFILE_CLASS_TESTING
  164. LocalProfileInfo.Clazz.Provisioning -> EuiccProfileInfo.PROFILE_CLASS_PROVISIONING
  165. LocalProfileInfo.Clazz.Operational -> EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL
  166. }
  167. )
  168. }.build()
  169. }
  170. return GetEuiccProfileInfoListResult(RESULT_OK, profiles.toTypedArray(), channel.removable)
  171. }
  172. override fun onGetEuiccInfo(slotId: Int): EuiccInfo {
  173. return EuiccInfo("Unknown") // TODO: Can we actually implement this?
  174. }
  175. override fun onDeleteSubscription(slotId: Int, iccid: String): Int = withEuiccChannelManager {
  176. Log.i(TAG, "onDeleteSubscription slotId=$slotId iccid=$iccid")
  177. if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
  178. try {
  179. val channels = findAllChannels(slotId) ?: return RESULT_FIRST_USER
  180. if (!channels[0].profileExists(iccid)) {
  181. return RESULT_FIRST_USER
  182. }
  183. // If the profile is enabled by ANY channel (port), we cannot delete it
  184. channels.forEach { channel ->
  185. val profile = channel.lpa.profiles.find {
  186. it.iccid == iccid
  187. } ?: return RESULT_FIRST_USER
  188. if (profile.state == LocalProfileInfo.State.Enabled) {
  189. // Must disable the profile first
  190. return RESULT_FIRST_USER
  191. }
  192. }
  193. return if (channels[0].lpa.deleteProfile(iccid)) {
  194. RESULT_OK
  195. } else {
  196. RESULT_FIRST_USER
  197. }
  198. } catch (e: Exception) {
  199. return RESULT_FIRST_USER
  200. }
  201. }
  202. @Deprecated("Deprecated in Java")
  203. override fun onSwitchToSubscription(
  204. slotId: Int,
  205. iccid: String?,
  206. forceDeactivateSim: Boolean
  207. ): Int =
  208. // -1 = any port
  209. onSwitchToSubscriptionWithPort(slotId, -1, iccid, forceDeactivateSim)
  210. override fun onSwitchToSubscriptionWithPort(
  211. slotId: Int,
  212. portIndex: Int,
  213. iccid: String?,
  214. forceDeactivateSim: Boolean
  215. ): Int = withEuiccChannelManager {
  216. Log.i(TAG,"onSwitchToSubscriptionWithPort slotId=$slotId portIndex=$portIndex iccid=$iccid forceDeactivateSim=$forceDeactivateSim")
  217. if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
  218. try {
  219. // retryWithTimeout is needed here because this function may be called just after
  220. // AOSP has switched slot mappings, in which case the slots may not be ready yet.
  221. val channel = if (portIndex == -1) {
  222. retryWithTimeout(5000) { findChannel(slotId) }
  223. } else {
  224. retryWithTimeout(5000) { findChannel(slotId, portIndex) }
  225. } ?: run {
  226. if (!forceDeactivateSim) {
  227. // The user must select which SIM to deactivate
  228. return@onSwitchToSubscriptionWithPort RESULT_MUST_DEACTIVATE_SIM
  229. } else {
  230. try {
  231. // If we are allowed to deactivate any SIM we like, try mapping the indicated port first
  232. ensurePortIsMapped(slotId, portIndex)
  233. retryWithTimeout(5000) { findChannel(slotId, portIndex) }
  234. } catch (e: Exception) {
  235. // We cannot map the port (or it is already mapped)
  236. // but we can also use any port available on the card
  237. retryWithTimeout(5000) { findChannel(slotId) }
  238. } ?: return@onSwitchToSubscriptionWithPort RESULT_FIRST_USER
  239. }
  240. }
  241. if (iccid != null && !channel.profileExists(iccid)) {
  242. Log.i(TAG, "onSwitchToSubscriptionWithPort iccid=$iccid not found")
  243. return RESULT_FIRST_USER
  244. }
  245. // Disable any active profile first if present
  246. if (!channel.lpa.disableActiveProfile(false)) {
  247. return RESULT_FIRST_USER
  248. }
  249. if (iccid != null) {
  250. if (!channel.lpa.enableProfile(iccid)) {
  251. return RESULT_FIRST_USER
  252. }
  253. }
  254. return RESULT_OK
  255. } catch (e: Exception) {
  256. return RESULT_FIRST_USER
  257. } finally {
  258. euiccChannelManager.invalidate()
  259. }
  260. }
  261. override fun onUpdateSubscriptionNickname(slotId: Int, iccid: String, nickname: String?): Int =
  262. withEuiccChannelManager {
  263. Log.i(
  264. TAG,
  265. "onUpdateSubscriptionNickname slotId=$slotId iccid=$iccid nickname=$nickname"
  266. )
  267. if (shouldIgnoreSlot(slotId)) return RESULT_FIRST_USER
  268. val channel = findChannel(slotId) ?: return RESULT_FIRST_USER
  269. if (!channel.profileExists(iccid)) {
  270. return RESULT_FIRST_USER
  271. }
  272. val success = channel.lpa
  273. .setNickname(iccid, nickname!!)
  274. appContainer.subscriptionManager.tryRefreshCachedEuiccInfo(channel.cardId)
  275. return if (success) {
  276. RESULT_OK
  277. } else {
  278. RESULT_FIRST_USER
  279. }
  280. }
  281. @Deprecated("Deprecated in Java")
  282. override fun onEraseSubscriptions(slotId: Int): Int {
  283. // No-op
  284. return RESULT_FIRST_USER
  285. }
  286. override fun onRetainSubscriptionsForFactoryReset(slotId: Int): Int {
  287. // No-op -- we do not care
  288. return RESULT_FIRST_USER
  289. }
  290. }