Compare commits

..

10 commits

22 changed files with 365 additions and 269 deletions

View file

@ -0,0 +1,43 @@
package im.angry.openeuicc.core
import android.content.Context
import android.se.omapi.SEService
import android.util.Log
import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
private var seService: SEService? = null
private suspend fun ensureSEService() {
if (seService == null) {
seService = connectSEService(context)
}
}
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(DefaultEuiccChannelManager.TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
}
ensureSEService()
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try {
return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(
DefaultEuiccChannelManager.TAG,
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}."
)
}
return null
}
override fun cleanup() {
seService?.shutdown()
seService = null
}
}

View file

@ -0,0 +1,135 @@
package im.angry.openeuicc.core
import android.content.Context
import android.telephony.SubscriptionManager
import android.util.Log
import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
open class DefaultEuiccChannelManager(
protected val appContainer: AppContainer,
protected val context: Context
) : EuiccChannelManager {
companion object {
const val TAG = "EuiccChannelManager"
}
private val channels = mutableListOf<EuiccChannel>()
private val lock = Mutex()
protected val tm by lazy {
appContainer.telephonyManager
}
private val euiccChannelFactory by lazy {
appContainer.euiccChannelFactory
}
protected open val uiccCards: Collection<UiccCardInfoCompat>
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
val existing =
channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing
} else {
existing.close()
channels.remove(existing)
}
}
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// We can only open channels on ports that are actually enabled
return null
}
return euiccChannelFactory.tryOpenEuiccChannel(port)?.also {
channels.add(it)
}
}
}
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
null
}
}
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
runBlocking {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return@runBlocking null
}
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
}
override suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) {
for (uiccInfo in uiccCards) {
for (port in uiccInfo.ports) {
if (tryOpenEuiccChannel(port) != null) {
Log.d(
TAG,
"Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}"
)
}
}
}
}
}
override val knownChannels: List<EuiccChannel>
get() = channels.toList()
override fun invalidate() {
for (channel in channels) {
channel.close()
}
channels.clear()
euiccChannelFactory.cleanup()
}
}

View file

@ -0,0 +1,16 @@
package im.angry.openeuicc.core
import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just
// "dumb" dependency injection.
interface EuiccChannelFactory {
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel?
/**
* Release all resources used by this EuiccChannelFactory
* Note that the same instance may be reused; any resources allocated must be automatically
* re-acquired when this happens
*/
fun cleanup()
}

View file

@ -1,174 +1,47 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.content.Context interface EuiccChannelManager {
import android.se.omapi.SEService val knownChannels: List<EuiccChannel>
import android.telephony.SubscriptionManager
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.lang.IllegalArgumentException
open class EuiccChannelManager(protected val context: Context) : IEuiccChannelManager { /**
companion object { * Scan all possible sources for EuiccChannels and have them cached for future use
const val TAG = "EuiccChannelManager" */
} suspend fun enumerateEuiccChannels()
private val channels = mutableListOf<EuiccChannel>() /**
* Returns the EuiccChannel corresponding to a **logical** slot
*/
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel?
private var seService: SEService? = null /**
* Returns the first EuiccChannel corresponding to a **physical** slot
* If the physical slot supports MEP and has multiple ports, it is undefined
* which of the two channels will be returned.
*/
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel?
private val lock = Mutex() /**
* Returns all EuiccChannels corresponding to a **physical** slot
* Multiple channels are possible in the case of MEP
*/
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>?
protected val tm by lazy { /**
(context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager * Returns the EuiccChannel corresponding to a **physical** slot and a port ID
} */
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
protected open val uiccCards: Collection<UiccCardInfoCompat> /**
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) } * Invalidate all EuiccChannels previously known by this Manager
*/
fun invalidate()
private suspend fun ensureSEService() { /**
if (seService == null) { * If possible, trigger the system to update the cached list of profiles
seService = connectSEService(context) * This is only expected to be implemented when the application is privileged
} * TODO: Remove this from the common interface
} */
fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
protected open fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? { // no-op by default
// No-op when unprivileged
return null
}
protected fun tryOpenEuiccChannelUnprivileged(port: UiccPortInfoCompat): EuiccChannel? {
if (port.portIndex != 0) {
Log.w(TAG, "OMAPI channel attempted on non-zero portId, this may or may not work.")
}
Log.i(TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try {
return OmapiChannel(seService!!, port)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(
TAG,
"OMAPI APDU interface unavailable for physical slot ${port.card.physicalSlotIndex}."
)
}
return null
}
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock {
ensureSEService()
val existing =
channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) {
if (existing.valid && port.logicalSlotIndex == existing.logicalSlotId) {
return existing
} else {
existing.close()
channels.remove(existing)
}
}
if (port.logicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// We can only open channels on ports that are actually enabled
return null
}
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port)
if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(port)
}
if (euiccChannel != null) {
channels.add(euiccChannel)
}
return euiccChannel
}
}
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) {
return@withContext tryOpenEuiccChannel(port)
}
}
}
null
}
}
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) {
tryOpenEuiccChannel(port)?.let { return@withContext it }
}
}
null
}
}
override fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>? =
runBlocking {
for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue
return@runBlocking card.ports.mapNotNull { tryOpenEuiccChannel(it) }
.ifEmpty { null }
}
return@runBlocking null
}
override fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel? =
runBlocking {
withContext(Dispatchers.IO) {
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
}
}
}
override suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) {
ensureSEService()
for (uiccInfo in uiccCards) {
for (port in uiccInfo.ports) {
if (tryOpenEuiccChannel(port) != null) {
Log.d(
TAG,
"Found eUICC on slot ${uiccInfo.physicalSlotIndex} port ${port.portIndex}"
)
}
}
}
}
}
override val knownChannels: List<EuiccChannel>
get() = channels.toList()
override fun invalidate() {
for (channel in channels) {
channel.close()
}
channels.clear()
seService?.shutdown()
seService = null
} }
} }

View file

@ -1,47 +0,0 @@
package im.angry.openeuicc.core
interface IEuiccChannelManager {
val knownChannels: List<EuiccChannel>
/**
* Scan all possible sources for EuiccChannels and have them cached for future use
*/
suspend fun enumerateEuiccChannels()
/**
* Returns the EuiccChannel corresponding to a **logical** slot
*/
fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel?
/**
* Returns the first EuiccChannel corresponding to a **physical** slot
* If the physical slot supports MEP and has multiple ports, it is undefined
* which of the two channels will be returned.
*/
fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel?
/**
* Returns all EuiccChannels corresponding to a **physical** slot
* Multiple channels are possible in the case of MEP
*/
fun findAllEuiccChannelsByPhysicalSlotBlocking(physicalSlotId: Int): List<EuiccChannel>?
/**
* Returns the EuiccChannel corresponding to a **physical** slot and a port ID
*/
fun findEuiccChannelByPortBlocking(physicalSlotId: Int, portId: Int): EuiccChannel?
/**
* Invalidate all EuiccChannels previously known by this Manager
*/
fun invalidate()
/**
* If possible, trigger the system to update the cached list of profiles
* This is only expected to be implemented when the application is privileged
* TODO: Remove this from the common interface
*/
fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
// no-op by default
}
}

View file

@ -2,12 +2,15 @@ package im.angry.openeuicc.di
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import im.angry.openeuicc.core.IEuiccChannelManager import im.angry.openeuicc.core.EuiccChannelFactory
import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
interface AppContainer { interface AppContainer {
val telephonyManager: TelephonyManager val telephonyManager: TelephonyManager
val euiccChannelManager: IEuiccChannelManager val euiccChannelManager: EuiccChannelManager
val subscriptionManager: SubscriptionManager val subscriptionManager: SubscriptionManager
val preferenceRepository: PreferenceRepository val preferenceRepository: PreferenceRepository
val uiComponentFactory: UiComponentFactory
val euiccChannelFactory: EuiccChannelFactory
} }

View file

@ -3,8 +3,9 @@ package im.angry.openeuicc.di
import android.content.Context import android.content.Context
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import im.angry.openeuicc.core.DefaultEuiccChannelFactory
import im.angry.openeuicc.core.DefaultEuiccChannelManager
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.core.IEuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
open class DefaultAppContainer(context: Context) : AppContainer { open class DefaultAppContainer(context: Context) : AppContainer {
@ -12,8 +13,8 @@ open class DefaultAppContainer(context: Context) : AppContainer {
context.getSystemService(TelephonyManager::class.java)!! context.getSystemService(TelephonyManager::class.java)!!
} }
override val euiccChannelManager: IEuiccChannelManager by lazy { override val euiccChannelManager: EuiccChannelManager by lazy {
EuiccChannelManager(context) DefaultEuiccChannelManager(this, context)
} }
override val subscriptionManager by lazy { override val subscriptionManager by lazy {
@ -23,4 +24,12 @@ open class DefaultAppContainer(context: Context) : AppContainer {
override val preferenceRepository by lazy { override val preferenceRepository by lazy {
PreferenceRepository(context) PreferenceRepository(context)
} }
override val uiComponentFactory by lazy {
DefaultUiComponentFactory()
}
override val euiccChannelFactory by lazy {
DefaultEuiccChannelFactory(context)
}
} }

View file

@ -0,0 +1,9 @@
package im.angry.openeuicc.di
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
open class DefaultUiComponentFactory : UiComponentFactory {
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
}

View file

@ -0,0 +1,8 @@
package im.angry.openeuicc.di
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
interface UiComponentFactory {
fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment
}

View file

@ -23,13 +23,15 @@ class LogsActivity : AppCompatActivity() {
private lateinit var swipeRefresh: SwipeRefreshLayout private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var scrollView: ScrollView private lateinit var scrollView: ScrollView
private lateinit var logText: TextView private lateinit var logText: TextView
private lateinit var logStr: String
private val saveLogs = private val saveLogs =
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
if (uri == null) return@registerForActivityResult if (uri == null) return@registerForActivityResult
if (!this::logStr.isInitialized) return@registerForActivityResult
contentResolver.openFileDescriptor(uri, "w")?.use { contentResolver.openFileDescriptor(uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use { os -> FileOutputStream(it.fileDescriptor).use { os ->
os.write(logText.text.toString().encodeToByteArray()) os.write(logStr.encodeToByteArray())
} }
} }
} }
@ -76,7 +78,12 @@ class LogsActivity : AppCompatActivity() {
private suspend fun reload() = withContext(Dispatchers.Main) { private suspend fun reload() = withContext(Dispatchers.Main) {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
logText.text = intent.extras?.getString("log") ?: readSelfLog() logStr = intent.extras?.getString("log") ?: readSelfLog()
logText.text = withContext(Dispatchers.IO) {
// Limit the UI to display only 256 lines
logStr.lines().takeLast(256).joinToString("\n")
}
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false

View file

@ -88,10 +88,6 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
protected open fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
EuiccManagementFragment.newInstance(channel.slotId, channel.portId)
private suspend fun init() { private suspend fun init() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
euiccChannelManager.enumerateEuiccChannels() euiccChannelManager.enumerateEuiccChannels()
@ -108,7 +104,7 @@ open class MainActivity : AppCompatActivity(), OpenEuiccContextMarker {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> euiccChannelManager.knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId)) spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
fragments.add(createEuiccManagementFragment(channel)) fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel))
} }
if (fragments.isNotEmpty()) { if (fragments.isNotEmpty()) {

View file

@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs")
val Context.preferenceRepository: PreferenceRepository val Context.preferenceRepository: PreferenceRepository
get() = (applicationContext as OpenEuiccApplication).preferenceRepository get() = (applicationContext as OpenEuiccApplication).appContainer.preferenceRepository
val Fragment.preferenceRepository: PreferenceRepository val Fragment.preferenceRepository: PreferenceRepository
get() = requireContext().preferenceRepository get() = requireContext().preferenceRepository

View file

@ -7,7 +7,7 @@ import android.telephony.TelephonyManager
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.IEuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.di.AppContainer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -52,7 +52,7 @@ interface OpenEuiccContextMarker {
val appContainer: AppContainer val appContainer: AppContainer
get() = openEuiccApplication.appContainer get() = openEuiccApplication.appContainer
val euiccChannelManager: IEuiccChannelManager val euiccChannelManager: EuiccChannelManager
get() = appContainer.euiccChannelManager get() = appContainer.euiccChannelManager
val telephonyManager: TelephonyManager val telephonyManager: TelephonyManager

View file

@ -47,11 +47,13 @@ abstract class CompatibilityCheck(context: Context) {
abstract val title: String abstract val title: String
protected abstract val defaultDescription: String protected abstract val defaultDescription: String
protected lateinit var successDescription: String
protected lateinit var failureDescription: String protected lateinit var failureDescription: String
val description: String val description: String
get() = when { get() = when {
(state == State.FAILURE || state == State.FAILURE_UNKNOWN) && this::failureDescription.isInitialized -> failureDescription (state == State.FAILURE || state == State.FAILURE_UNKNOWN) && this::failureDescription.isInitialized -> failureDescription
state == State.SUCCESS && this::successDescription.isInitialized -> successDescription
else -> defaultDescription else -> defaultDescription
} }
@ -109,11 +111,11 @@ internal class OmapiConnCheck(private val context: Context): CompatibilityCheck(
val simReaders = seService.readers.filter { it.isSIM } val simReaders = seService.readers.filter { it.isSIM }
if (simReaders.isEmpty()) { if (simReaders.isEmpty()) {
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail) failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail)
return State.FAILURE return State.FAILURE_UNKNOWN
} else if (simReaders.size < tm.activeModemCountCompat) { } else if (simReaders.size < tm.activeModemCountCompat) {
failureDescription = context.getString(R.string.compatibility_check_omapi_connectivity_fail_sim_number, successDescription = context.getString(R.string.compatibility_check_omapi_connectivity_partial_success_sim_number,
simReaders.map { it.slotIndex }.joinToString(", ")) simReaders.map { it.slotIndex }.joinToString(", "))
return State.FAILURE return State.SUCCESS
} }
return State.SUCCESS return State.SUCCESS
@ -132,7 +134,13 @@ internal class IsdrChannelAccessCheck(private val context: Context): Compatibili
override suspend fun doCheck(): State { override suspend fun doCheck(): State {
val seService = connectSEService(context) val seService = connectSEService(context)
val (validSlotIds, result) = seService.readers.filter { it.isSIM }.map { val readers = seService.readers.filter { it.isSIM }
if (readers.isEmpty()) {
failureDescription = context.getString(R.string.compatibility_check_isdr_channel_desc_unknown)
return State.FAILURE_UNKNOWN
}
val (validSlotIds, result) = readers.map {
try { try {
it.openSession().openLogicalChannel(ISDR_AID)?.close() it.openSession().openLogicalChannel(ISDR_AID)?.close()
Pair(it.slotIndex, State.SUCCESS) Pair(it.slotIndex, State.SUCCESS)

View file

@ -6,11 +6,11 @@
<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>
<string name="compatibility_check_system_features_no_telephony">Your device has no telephony features.</string> <string name="compatibility_check_system_features_no_telephony">Your device has no telephony features.</string>
<string name="compatibility_check_system_features_no_omapi">Your device has no support for accessing SIM cards via OMAPI.</string> <string name="compatibility_check_system_features_no_omapi">Your device has no support for accessing SIM cards via OMAPI. If you are using a custom ROM, consider contacting the developer to determine whether it is due to hardware or a missing feature declaration in the OS.</string>
<string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string> <string name="compatibility_check_omapi_connectivity">OMAPI Connectivity</string>
<string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string> <string name="compatibility_check_omapi_connectivity_desc">Does your device allow access to Secure Elements on SIM cards via OMAPI?</string>
<string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI.</string> <string name="compatibility_check_omapi_connectivity_fail">Unable to detect Secure Element readers for SIM cards via OMAPI. If you have not inserted a SIM in this device, try inserting one and retry this check.</string>
<string name="compatibility_check_omapi_connectivity_fail_sim_number">Only the following SIM slots are accessible via OMAPI: %s.</string> <string name="compatibility_check_omapi_connectivity_partial_success_sim_number">Successfully detected Secure Element access, but only for the following SIM slots: %s.</string>
<string name="compatibility_check_isdr_channel">ISD-R Channel Access</string> <string name="compatibility_check_isdr_channel">ISD-R Channel Access</string>
<string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string> <string name="compatibility_check_isdr_channel_desc">Does your device support opening an ISD-R (management) channel to eSIMs via OMAPI?</string>
<string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string> <string name="compatibility_check_isdr_channel_desc_unknown">Cannot determine whether ISD-R access through OMAPI is supported. You might want to retry with SIM cards inserted (any SIM card will do) if not already.</string>

View file

@ -0,0 +1,43 @@
package im.angry.openeuicc.core
import android.content.Context
import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFactory(context) {
private val tm by lazy {
(context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager
}
@Suppress("NAME_SHADOWING")
override suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
val port = port as RealUiccPortInfoCompat
if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannel(port)?.let { return it }
}
if (port.card.isEuicc) {
Log.i(
DefaultEuiccChannelManager.TAG,
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
)
try {
return TelephonyManagerChannel(
port, tm
)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(
DefaultEuiccChannelManager.TAG,
"TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back"
)
}
}
return super.tryOpenEuiccChannel(port)
}
}

View file

@ -1,37 +1,15 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.content.Context import android.content.Context
import android.util.Log import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.OpenEuiccApplication
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import java.lang.Exception import java.lang.Exception
import java.lang.IllegalArgumentException
class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(context) { class PrivilegedEuiccChannelManager(appContainer: AppContainer, context: Context) :
DefaultEuiccChannelManager(appContainer, context) {
override val uiccCards: Collection<UiccCardInfoCompat> override val uiccCards: Collection<UiccCardInfoCompat>
get() = tm.uiccCardsInfoCompat get() = tm.uiccCardsInfoCompat
@Suppress("NAME_SHADOWING")
override fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
val port = port as RealUiccPortInfoCompat
if (port.card.isRemovable) {
// Attempt unprivileged (OMAPI) before TelephonyManager
// but still try TelephonyManager in case OMAPI is broken
super.tryOpenEuiccChannelUnprivileged(port)?.let { return it }
}
if (port.card.isEuicc) {
Log.i(TAG, "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
try {
return TelephonyManagerChannel(port, tm)
} catch (e: IllegalArgumentException) {
// Failed
Log.w(TAG, "TelephonyManager APDU interface unavailable for slot ${port.card.physicalSlotIndex} port ${port.portIndex}, falling back")
}
}
return null
}
// Clean up channels left open in TelephonyManager // Clean up channels left open in TelephonyManager
// due to a (potentially) forced restart // due to a (potentially) forced restart
// This should be called every time the application is restarted // This should be called every time the application is restarted
@ -48,7 +26,7 @@ class PrivilegedEuiccChannelManager(context: Context): EuiccChannelManager(conte
} }
override fun notifyEuiccProfilesChanged(logicalSlotId: Int) { override fun notifyEuiccProfilesChanged(logicalSlotId: Int) {
(context.applicationContext as OpenEuiccApplication).appContainer.subscriptionManager.apply { appContainer.subscriptionManager.apply {
findEuiccChannelBySlotBlocking(logicalSlotId)?.let { findEuiccChannelBySlotBlocking(logicalSlotId)?.let {
tryRefreshCachedEuiccInfo(it.cardId) tryRefreshCachedEuiccInfo(it.cardId)
} }

View file

@ -1,11 +1,20 @@
package im.angry.openeuicc.di package im.angry.openeuicc.di
import android.content.Context import android.content.Context
import im.angry.openeuicc.core.IEuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.core.PrivilegedEuiccChannelFactory
import im.angry.openeuicc.core.PrivilegedEuiccChannelManager import im.angry.openeuicc.core.PrivilegedEuiccChannelManager
class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) { class PrivilegedAppContainer(context: Context) : DefaultAppContainer(context) {
override val euiccChannelManager: IEuiccChannelManager by lazy { override val euiccChannelManager: EuiccChannelManager by lazy {
PrivilegedEuiccChannelManager(context) PrivilegedEuiccChannelManager(this, context)
}
override val uiComponentFactory by lazy {
PrivilegedUiComponentFactory()
}
override val euiccChannelFactory by lazy {
PrivilegedEuiccChannelFactory(context)
} }
} }

View file

@ -0,0 +1,10 @@
package im.angry.openeuicc.di
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.ui.EuiccManagementFragment
import im.angry.openeuicc.ui.PrivilegedEuiccManagementFragment
class PrivilegedUiComponentFactory : DefaultUiComponentFactory() {
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
}

View file

@ -4,7 +4,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import im.angry.openeuicc.R import im.angry.openeuicc.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
class PrivilegedMainActivity : MainActivity() { class PrivilegedMainActivity : MainActivity() {
@ -37,7 +36,4 @@ class PrivilegedMainActivity : MainActivity() {
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun createEuiccManagementFragment(channel: EuiccChannel): EuiccManagementFragment =
PrivilegedEuiccManagementFragment.newInstance(channel.slotId, channel.portId)
} }

View file

@ -4,7 +4,7 @@ import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.telephony.UiccSlotMapping import android.telephony.UiccSlotMapping
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.IEuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.lang.Exception import java.lang.Exception
@ -14,7 +14,7 @@ val TelephonyManager.supportsDSDS: Boolean
val TelephonyManager.dsdsEnabled: Boolean val TelephonyManager.dsdsEnabled: Boolean
get() = activeModemCount >= 2 get() = activeModemCount >= 2
fun TelephonyManager.setDsdsEnabled(euiccManager: IEuiccChannelManager, enabled: Boolean) { fun TelephonyManager.setDsdsEnabled(euiccManager: EuiccChannelManager, enabled: Boolean) {
runBlocking { runBlocking {
euiccManager.enumerateEuiccChannels() euiccManager.enumerateEuiccChannels()
} }
@ -32,7 +32,7 @@ fun TelephonyManager.setDsdsEnabled(euiccManager: IEuiccChannelManager, enabled:
// Disable eSIM profiles before switching the slot mapping // Disable eSIM profiles before switching the slot mapping
// This ensures that unmapped eSIM ports never have "ghost" profiles enabled // This ensures that unmapped eSIM ports never have "ghost" profiles enabled
fun TelephonyManager.updateSimSlotMapping( fun TelephonyManager.updateSimSlotMapping(
euiccManager: IEuiccChannelManager, newMapping: Collection<UiccSlotMapping>, euiccManager: EuiccChannelManager, newMapping: Collection<UiccSlotMapping>,
currentMapping: Collection<UiccSlotMapping> = simSlotMapping currentMapping: Collection<UiccSlotMapping> = simSlotMapping
) { ) {
val unmapped = currentMapping.filterNot { mapping -> val unmapped = currentMapping.filterNot { mapping ->

View file

@ -8,7 +8,7 @@ import java.security.cert.CertificateFactory
const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795bebfb" const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795bebfb"
private fun getCertificate(keyId: String): Certificate? = private fun getCertificate(keyId: String): Certificate? =
KNOWN_CI_CERTS[keyId]?.toByteArray().let { cert -> KNOWN_CI_CERTS[keyId]?.toByteArray()?.let { cert ->
ByteArrayInputStream(cert).use { stream -> ByteArrayInputStream(cert).use { stream ->
val cf = CertificateFactory.getInstance("X.509") val cf = CertificateFactory.getInstance("X.509")
cf.generateCertificate(stream) cf.generateCertificate(stream)