Compare commits

...

2 commits

Author SHA1 Message Date
6c2b1675bd fixup: Infinite loop in PreferenceUtils after adopting DI
All checks were successful
/ build-debug (push) Successful in 3m48s
2024-03-04 19:35:17 -05:00
1a69c5294b refactor: Use DI techniques for EuiccChannel's as well 2024-03-04 19:30:04 -05:00
9 changed files with 134 additions and 81 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

@ -1,69 +1,41 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.content.Context import android.content.Context
import android.se.omapi.SEService
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.util.Log import android.util.Log
import im.angry.openeuicc.OpenEuiccApplication import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.IllegalArgumentException
open class DefaultEuiccChannelManager(protected val context: Context) : EuiccChannelManager { open class DefaultEuiccChannelManager(
protected val appContainer: AppContainer,
protected val context: Context
) : EuiccChannelManager {
companion object { companion object {
const val TAG = "EuiccChannelManager" const val TAG = "EuiccChannelManager"
} }
private val channels = mutableListOf<EuiccChannel>() private val channels = mutableListOf<EuiccChannel>()
private var seService: SEService? = null
private val lock = Mutex() private val lock = Mutex()
protected val tm by lazy { protected val tm by lazy {
(context.applicationContext as OpenEuiccApplication).appContainer.telephonyManager appContainer.telephonyManager
}
private val euiccChannelFactory by lazy {
appContainer.euiccChannelFactory
} }
protected open val uiccCards: Collection<UiccCardInfoCompat> protected open val uiccCards: Collection<UiccCardInfoCompat>
get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) } get() = (0..<tm.activeModemCountCompat).map { FakeUiccCardInfoCompat(it) }
private suspend fun ensureSEService() {
if (seService == null) {
seService = connectSEService(context)
}
}
protected open fun tryOpenEuiccChannelPrivileged(port: UiccPortInfoCompat): EuiccChannel? {
// 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? { private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock { lock.withLock {
ensureSEService()
val existing = val existing =
channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } channels.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) { if (existing != null) {
@ -80,17 +52,9 @@ open class DefaultEuiccChannelManager(protected val context: Context) : EuiccCha
return null return null
} }
var euiccChannel: EuiccChannel? = tryOpenEuiccChannelPrivileged(port) return euiccChannelFactory.tryOpenEuiccChannel(port)?.also {
channels.add(it)
if (euiccChannel == null) {
euiccChannel = tryOpenEuiccChannelUnprivileged(port)
} }
if (euiccChannel != null) {
channels.add(euiccChannel)
}
return euiccChannel
} }
} }
@ -144,8 +108,6 @@ open class DefaultEuiccChannelManager(protected val context: Context) : EuiccCha
override suspend fun enumerateEuiccChannels() { override suspend fun enumerateEuiccChannels() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
ensureSEService()
for (uiccInfo in uiccCards) { for (uiccInfo in uiccCards) {
for (port in uiccInfo.ports) { for (port in uiccInfo.ports) {
if (tryOpenEuiccChannel(port) != null) { if (tryOpenEuiccChannel(port) != null) {
@ -168,7 +130,6 @@ open class DefaultEuiccChannelManager(protected val context: Context) : EuiccCha
} }
channels.clear() channels.clear()
seService?.shutdown() euiccChannelFactory.cleanup()
seService = null
} }
} }

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

@ -2,6 +2,7 @@ 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.EuiccChannelFactory
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
@ -11,4 +12,5 @@ interface AppContainer {
val subscriptionManager: SubscriptionManager val subscriptionManager: SubscriptionManager
val preferenceRepository: PreferenceRepository val preferenceRepository: PreferenceRepository
val uiComponentFactory: UiComponentFactory val uiComponentFactory: UiComponentFactory
val euiccChannelFactory: EuiccChannelFactory
} }

View file

@ -3,6 +3,7 @@ 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.DefaultEuiccChannelManager
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
@ -13,7 +14,7 @@ open class DefaultAppContainer(context: Context) : AppContainer {
} }
override val euiccChannelManager: EuiccChannelManager by lazy { override val euiccChannelManager: EuiccChannelManager by lazy {
DefaultEuiccChannelManager(context) DefaultEuiccChannelManager(this, context)
} }
override val subscriptionManager by lazy { override val subscriptionManager by lazy {
@ -27,4 +28,8 @@ open class DefaultAppContainer(context: Context) : AppContainer {
override val uiComponentFactory by lazy { override val uiComponentFactory by lazy {
DefaultUiComponentFactory() DefaultUiComponentFactory()
} }
override val euiccChannelFactory by lazy {
DefaultEuiccChannelFactory(context)
}
} }

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

@ -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): DefaultEuiccChannelManager(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): DefaultEuiccChannelManage
} }
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

@ -2,14 +2,19 @@ package im.angry.openeuicc.di
import android.content.Context import android.content.Context
import im.angry.openeuicc.core.EuiccChannelManager 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: EuiccChannelManager by lazy { override val euiccChannelManager: EuiccChannelManager by lazy {
PrivilegedEuiccChannelManager(context) PrivilegedEuiccChannelManager(this, context)
} }
override val uiComponentFactory by lazy { override val uiComponentFactory by lazy {
PrivilegedUiComponentFactory() PrivilegedUiComponentFactory()
} }
override val euiccChannelFactory by lazy {
PrivilegedEuiccChannelFactory(context)
}
} }