Compare commits

..

No commits in common. "8d76f0eeaccd599857241852199547573e9e87d7" and "991cd2a156b8ad945698c2f1e3e6ae46ec6f7cb9" have entirely different histories.

58 changed files with 216 additions and 988 deletions

View file

@ -5,9 +5,6 @@
<SelectionState runConfigName="app-unpriv"> <SelectionState runConfigName="app-unpriv">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>

View file

@ -5,7 +5,7 @@ plugins {
android { android {
namespace = "im.angry.openeuicc.common" namespace = "im.angry.openeuicc.common"
compileSdk = 35 compileSdk = 34
defaultConfig { defaultConfig {
minSdk = 28 minSdk = 28

View file

@ -3,14 +3,10 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
package="im.angry.openeuicc.common"> package="im.angry.openeuicc.common">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application>
android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<activity <activity
android:name="im.angry.openeuicc.ui.SettingsActivity" android:name="im.angry.openeuicc.ui.SettingsActivity"
android:label="@string/pref_settings" /> android:label="@string/pref_settings" />
@ -35,7 +31,6 @@
<service <service
android:name="im.angry.openeuicc.service.EuiccChannelManagerService" android:name="im.angry.openeuicc.service.EuiccChannelManagerService"
android:foregroundServiceType="shortService"
android:exported="false" /> android:exported="false" />
</application> </application>
</manifest> </manifest>

View file

@ -33,18 +33,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}") Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
try { try {
return EuiccChannel( return EuiccChannel(port, OmapiApduInterface(seService!!, port))
port,
OmapiApduInterface(
seService!!,
port,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow
).also {
Log.i(DefaultEuiccChannelManager.TAG, "Is OMAPI channel, setting MSS to 60")
it.lpa.setEs10xMss(60)
}
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
// Failed // Failed
Log.w( Log.w(
@ -63,13 +52,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
if (!conn.claimInterface(usbInterface, true)) return null if (!conn.claimInterface(usbInterface, true)) return null
return EuiccChannel( return EuiccChannel(
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)), FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
UsbApduInterface( UsbApduInterface(conn, bulkIn, bulkOut)
conn,
bulkIn,
bulkOut,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow
) )
} }

View file

@ -173,7 +173,7 @@ open class DefaultEuiccChannelManager(
try { try {
// tryOpenEuiccChannel() will automatically dispose of invalid channels // tryOpenEuiccChannel() will automatically dispose of invalid channels
// and recreate when needed // and recreate when needed
val channel = findEuiccChannelByPort(physicalSlotId, portId)!! val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId)!!
check(channel.valid) { "Invalid channel" } check(channel.valid) { "Invalid channel" }
break break
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -1,7 +1,6 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
@ -10,14 +9,12 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
class EuiccChannel( class EuiccChannel(
val port: UiccPortInfoCompat, val port: UiccPortInfoCompat,
apduInterface: ApduInterface, apduInterface: ApduInterface,
verboseLoggingFlow: Flow<Boolean>
) { ) {
val slotId = port.card.physicalSlotIndex // PHYSICAL slot val slotId = port.card.physicalSlotIndex // PHYSICAL slot
val logicalSlotId = port.logicalSlotIndex val logicalSlotId = port.logicalSlotIndex
val portId = port.portIndex val portId = port.portIndex
val lpa: LocalProfileAssistant = val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl())
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow))
val valid: Boolean val valid: Boolean
get() = lpa.valid get() = lpa.valid

View file

@ -5,16 +5,11 @@ import android.se.omapi.SEService
import android.se.omapi.Session import android.se.omapi.Session
import android.util.Log import android.util.Log
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
class OmapiApduInterface( class OmapiApduInterface(
private val service: SEService, private val service: SEService,
private val port: UiccPortInfoCompat, private val port: UiccPortInfoCompat
private val verboseLoggingFlow: Flow<Boolean>
): ApduInterface { ): ApduInterface {
companion object { companion object {
const val TAG = "OmapiApduInterface" const val TAG = "OmapiApduInterface"
@ -54,30 +49,17 @@ class OmapiApduInterface(
"Unknown channel" "Unknown channel"
} }
if (runBlocking { verboseLoggingFlow.first() }) { Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
}
try { try {
for (i in 0..10) { return lastChannel.transmit(tx).also {
val res = lastChannel.transmit(tx) Log.d(TAG, "OMAPI APDU response: ${it.encodeHex()}")
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
}
if (res.size == 2 && res[0] == 0x66.toByte() && res[1] == 0x01.toByte()) {
Log.d(TAG, "Received checksum error 0x6601, retrying (count = $i)")
continue
}
return res
} }
throw RuntimeException("Retransmit attempts exhausted; this was likely caused by checksum errors")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "OMAPI APDU exception") Log.e(TAG, "OMAPI APDU exception")
e.printStackTrace() e.printStackTrace()
throw e throw e
} }
} }
} }

View file

@ -4,14 +4,12 @@ import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint import android.hardware.usb.UsbEndpoint
import android.util.Log import android.util.Log
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
class UsbApduInterface( class UsbApduInterface(
private val conn: UsbDeviceConnection, private val conn: UsbDeviceConnection,
private val bulkIn: UsbEndpoint, private val bulkIn: UsbEndpoint,
private val bulkOut: UsbEndpoint, private val bulkOut: UsbEndpoint
private val verboseLoggingFlow: Flow<Boolean>
): ApduInterface { ): ApduInterface {
companion object { companion object {
private const val TAG = "UsbApduInterface" private const val TAG = "UsbApduInterface"
@ -29,7 +27,7 @@ class UsbApduInterface(
throw IllegalArgumentException("Unsupported card reader; T=0 support is required") throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
} }
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow) transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription)
try { try {
transceiver.iccPowerOn() transceiver.iccPowerOn()

View file

@ -5,9 +5,6 @@ import android.hardware.usb.UsbEndpoint
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@ -21,8 +18,7 @@ class UsbCcidTransceiver(
private val usbConnection: UsbDeviceConnection, private val usbConnection: UsbDeviceConnection,
private val usbBulkIn: UsbEndpoint, private val usbBulkIn: UsbEndpoint,
private val usbBulkOut: UsbEndpoint, private val usbBulkOut: UsbEndpoint,
private val usbCcidDescription: UsbCcidDescription, private val usbCcidDescription: UsbCcidDescription
private val verboseLoggingFlow: Flow<Boolean>
) { ) {
companion object { companion object {
private const val TAG = "UsbCcidTransceiver" private const val TAG = "UsbCcidTransceiver"
@ -182,9 +178,7 @@ class UsbCcidTransceiver(
readBytes = usbConnection.bulkTransfer( readBytes = usbConnection.bulkTransfer(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
) )
if (runBlocking { verboseLoggingFlow.first() }) { Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
}
} while (readBytes <= 0 && attempts-- > 0) } while (readBytes <= 0 && attempts-- > 0)
if (readBytes < CCID_HEADER_LENGTH) { if (readBytes < CCID_HEADER_LENGTH) {
throw UsbTransportException("USB-CCID error - failed to receive CCID header") throw UsbTransportException("USB-CCID error - failed to receive CCID header")

View file

@ -1,36 +1,11 @@
package im.angry.openeuicc.service package im.angry.openeuicc.service
import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.os.Binder import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.yield
import net.typeblog.lpac_jni.ProfileDownloadCallback
/** /**
* An Android Service wrapper for EuiccChannelManager. * An Android Service wrapper for EuiccChannelManager.
@ -42,22 +17,8 @@ import net.typeblog.lpac_jni.ProfileDownloadCallback
* instance of EuiccChannelManager. UI components can keep being bound to this service for * instance of EuiccChannelManager. UI components can keep being bound to this service for
* their entire lifecycles, since the whole purpose of them is to expose the current state * their entire lifecycles, since the whole purpose of them is to expose the current state
* to the user. * to the user.
*
* Additionally, this service is also responsible for long-running "foreground" tasks that
* are not suitable to be managed by UI components. This includes profile downloading, etc.
* When a UI component needs to run one of these tasks, they have to bind to this service
* and call one of the `launch*` methods, which will run the task inside this service's
* lifecycle context and return a Flow instance for the UI component to subscribe to its
* progress.
*/ */
class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker { class EuiccChannelManagerService : Service(), OpenEuiccContextMarker {
companion object {
private const val TAG = "EuiccChannelManagerService"
private const val CHANNEL_ID = "tasks"
private const val FOREGROUND_ID = 1000
private const val TASK_FAILURE_ID = 1001
}
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
val service = this@EuiccChannelManagerService val service = this@EuiccChannelManagerService
} }
@ -67,341 +28,14 @@ class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
} }
val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
/** override fun onBind(intent: Intent?): IBinder = LocalBinder()
* The state of a "foreground" task (named so due to the need to startForeground())
*/
sealed interface ForegroundTaskState {
data object Idle : ForegroundTaskState
data class InProgress(val progress: Int) : ForegroundTaskState
data class Done(val error: Throwable?) : ForegroundTaskState
}
/**
* This flow emits whenever the service has had a start command, from startService()
* The service self-starts when foreground is required, because other components
* only bind to this service and do not start it per-se.
*/
private val foregroundStarted: MutableSharedFlow<Unit> = MutableSharedFlow()
/**
* This flow is used to emit progress updates when a foreground task is running.
*/
private val foregroundTaskState: MutableStateFlow<ForegroundTaskState> =
MutableStateFlow(ForegroundTaskState.Idle)
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return LocalBinder()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// This is the whole reason of the existence of this service:
// we can clean up opened channels when no one is using them
if (euiccChannelManagerDelegate.isInitialized()) { if (euiccChannelManagerDelegate.isInitialized()) {
euiccChannelManager.invalidate() euiccChannelManager.invalidate()
} }
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId).also {
lifecycleScope.launch {
foregroundStarted.emit(Unit)
}
}
}
private fun ensureForegroundTaskNotificationChannel() {
val nm = NotificationManagerCompat.from(this)
if (nm.getNotificationChannelCompat(CHANNEL_ID) == null) {
val channel =
NotificationChannelCompat.Builder(
CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_LOW
)
.setName(getString(R.string.task_notification))
.setVibrationEnabled(false)
.build()
nm.createNotificationChannel(channel)
}
}
private suspend fun updateForegroundNotification(title: String, iconRes: Int) {
ensureForegroundTaskNotificationChannel()
val nm = NotificationManagerCompat.from(this)
val state = foregroundTaskState.value
if (state is ForegroundTaskState.InProgress) {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setProgress(100, state.progress, state.progress == 0)
.setSmallIcon(iconRes)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setOnlyAlertOnce(true)
.build()
if (state.progress == 0) {
startForeground(FOREGROUND_ID, notification)
} else if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
nm.notify(FOREGROUND_ID, notification)
}
// Yield out so that the main looper can handle the notification event
// Without this yield, the notification sent above will not be shown in time.
yield()
} else {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private fun postForegroundTaskFailureNotification(title: String) {
if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setSmallIcon(R.drawable.ic_x_black)
.build()
NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification)
}
/**
* Launch a potentially blocking foreground task in this service's lifecycle context.
* This function does not block, but returns a Flow that emits ForegroundTaskState
* updates associated with this task. The last update the returned flow will emit is
* always ForegroundTaskState.Done. The returned flow MUST be started in order for the
* foreground task to run.
*
* The task closure is expected to update foregroundTaskState whenever appropriate.
* If a foreground task is already running, this function returns null.
*
* To wait for foreground tasks to be available, use waitForForegroundTask().
*
* The function will set the state back to Idle once it sees ForegroundTaskState.Done.
*/
private fun launchForegroundTask(
title: String,
failureTitle: String,
iconRes: Int,
task: suspend EuiccChannelManagerService.() -> Unit
): Flow<ForegroundTaskState>? {
// Atomically set the state to InProgress. If this returns true, we are
// the only task currently in progress.
if (!foregroundTaskState.compareAndSet(
ForegroundTaskState.Idle,
ForegroundTaskState.InProgress(0)
)
) {
return null
}
lifecycleScope.launch(Dispatchers.Main) {
// Wait until our self-start command has succeeded.
// We can only call startForeground() after that
val res = withTimeoutOrNull(30 * 1000) {
foregroundStarted.first()
}
if (res == null) {
// The only case where the wait above could time out is if the subscriber
// to the flow is stuck. Or we failed to start foreground.
// In that case, we should just set our state back to Idle -- setting it
// to Done wouldn't help much because nothing is going to then set it Idle.
foregroundTaskState.value = ForegroundTaskState.Idle
return@launch
}
updateForegroundNotification(title, iconRes)
try {
withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete
this@EuiccChannelManagerService.task()
}
// This update will be sent by the subscriber (as shown below)
foregroundTaskState.value = ForegroundTaskState.Done(null)
} catch (t: Throwable) {
Log.e(TAG, "Foreground task encountered an error")
Log.e(TAG, Log.getStackTraceString(t))
foregroundTaskState.value = ForegroundTaskState.Done(t)
if (isActive) {
postForegroundTaskFailureNotification(failureTitle)
}
} finally {
if (isActive) {
stopSelf()
}
}
}
// We should be the only task running, so we can subscribe to foregroundTaskState
// until we encounter ForegroundTaskState.Done.
// Then, we complete the returned flow, but we also set the state back to Idle.
// The state update back to Idle won't show up in the returned stream, because
// it has been completed by that point.
return foregroundTaskState.transformWhile {
// Also update our notification when we see an update
// But ignore the first progress = 0 update -- that is the current value.
// we need that to be handled by the main coroutine after it finishes.
if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
withContext(Dispatchers.Main) {
updateForegroundNotification(title, iconRes)
}
}
emit(it)
it !is ForegroundTaskState.Done
}.onStart {
// When this Flow is started, we unblock the coroutine launched above by
// self-starting as a foreground service.
withContext(Dispatchers.Main) {
startForegroundService(
Intent(
this@EuiccChannelManagerService,
this@EuiccChannelManagerService::class.java
)
)
}
}.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle }
}
val isForegroundTaskRunning: Boolean
get() = foregroundTaskState.value != ForegroundTaskState.Idle
suspend fun waitForForegroundTask() {
foregroundTaskState.takeWhile { it != ForegroundTaskState.Idle }
.collect()
}
fun launchProfileDownloadTask(
slotId: Int,
portId: Int,
smdp: String,
matchingId: String?,
confirmationCode: String?,
imei: String?
): Flow<ForegroundTaskState>? =
launchForegroundTask(
getString(R.string.task_profile_download),
getString(R.string.task_profile_download_failure),
R.drawable.ic_task_sim_card_download
) {
euiccChannelManager.beginTrackedOperation(slotId, portId) {
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)
val res = channel!!.lpa.downloadProfile(
smdp,
matchingId,
imei,
confirmationCode,
object : ProfileDownloadCallback {
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
if (state.progress == 0) return
foregroundTaskState.value =
ForegroundTaskState.InProgress(state.progress)
}
})
if (!res) {
// TODO: Provide more details on the error
throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.")
}
preferenceRepository.notificationDownloadFlow.first()
}
}
fun launchProfileRenameTask(
slotId: Int,
portId: Int,
iccid: String,
name: String
): Flow<ForegroundTaskState>? =
launchForegroundTask(
getString(R.string.task_profile_rename),
getString(R.string.task_profile_rename_failure),
R.drawable.ic_task_rename
) {
val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname(
iccid,
name
)
if (!res) {
throw RuntimeException("Profile not renamed")
}
}
fun launchProfileDeleteTask(
slotId: Int,
portId: Int,
iccid: String
): Flow<ForegroundTaskState>? =
launchForegroundTask(
getString(R.string.task_profile_delete),
getString(R.string.task_profile_delete_failure),
R.drawable.ic_task_delete
) {
euiccChannelManager.beginTrackedOperation(slotId, portId) {
euiccChannelManager.findEuiccChannelByPort(
slotId,
portId
)!!.lpa.deleteProfile(iccid)
preferenceRepository.notificationDeleteFlow.first()
}
}
class SwitchingProfilesRefreshException : Exception()
fun launchProfileSwitchTask(
slotId: Int,
portId: Int,
iccid: String,
enable: Boolean, // Enable or disable the profile indicated in iccid
reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers
): Flow<ForegroundTaskState>? =
launchForegroundTask(
getString(R.string.task_profile_switch),
getString(R.string.task_profile_switch_failure),
R.drawable.ic_task_switch
) {
euiccChannelManager.beginTrackedOperation(slotId, portId) {
val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!
val (res, refreshed) =
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
// Sometimes, we *can* enable or disable the profile, but we cannot
// send the refresh command to the modem because the profile somehow
// makes the modem "busy". In this case, we can still switch by setting
// refresh to false, but then the switch cannot take effect until the
// user resets the modem manually by toggling airplane mode or rebooting.
Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
} else {
Pair(true, true)
}
if (!res) {
throw RuntimeException("Could not switch profile")
}
if (!refreshed) {
// We may have switched the profile, but we could not refresh. Tell the caller about this
throw SwitchingProfilesRefreshException()
}
if (reconnectTimeoutMillis > 0) {
// Add an unconditional delay first to account for any race condition between
// the card sending the refresh command and the modem actually refreshing
delay(reconnectTimeoutMillis / 10)
// This throws TimeoutCancellationException if timed out
euiccChannelManager.waitForReconnect(
slotId,
portId,
reconnectTimeoutMillis / 10 * 9
)
}
preferenceRepository.notificationSwitchFlow.first()
}
}
} }

View file

@ -9,18 +9,14 @@ import android.os.IBinder
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.service.EuiccChannelManagerService
import kotlinx.coroutines.CompletableDeferred
abstract class BaseEuiccAccessActivity : AppCompatActivity() { abstract class BaseEuiccAccessActivity : AppCompatActivity() {
val euiccChannelManagerLoaded = CompletableDeferred<Unit>()
lateinit var euiccChannelManager: EuiccChannelManager lateinit var euiccChannelManager: EuiccChannelManager
lateinit var euiccChannelManagerService: EuiccChannelManagerService
private val euiccChannelManagerServiceConnection = object : ServiceConnection { private val euiccChannelManagerServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
euiccChannelManagerService = (service!! as EuiccChannelManagerService.LocalBinder).service euiccChannelManager =
euiccChannelManager = euiccChannelManagerService.euiccChannelManager (service!! as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager
euiccChannelManagerLoaded.complete(Unit)
onInit() onInit()
} }

View file

@ -19,9 +19,6 @@ import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -30,12 +27,12 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import net.typeblog.lpac_jni.LocalProfileInfo import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -79,21 +76,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
fab = view.requireViewById(R.id.fab) fab = view.requireViewById(R.id.fab)
profileList = view.requireViewById(R.id.profile_list) profileList = view.requireViewById(R.id.profile_list)
val origFabMarginRight = (fab.layoutParams as ViewGroup.MarginLayoutParams).rightMargin
val origFabMarginBottom = (fab.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
ViewCompat.setOnApplyWindowInsetsListener(fab) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = origFabMarginRight + bars.right
bottomMargin = origFabMarginBottom + bars.bottom
}
WindowInsetsCompat.CONSUMED
}
setupRootViewInsets(profileList)
return view return view
} }
@ -108,10 +90,7 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
ProfileDownloadFragment.newInstance(slotId, portId) ProfileDownloadFragment.newInstance(slotId, portId)
.show(childFragmentManager, ProfileDownloadFragment.TAG) .show(childFragmentManager, ProfileDownloadFragment.TAG)
} }
}
override fun onStart() {
super.onStart()
refresh() refresh()
} }
@ -154,9 +133,6 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) { if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) {
disableSafeguardFlow = disableSafeguardFlow =
preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope) preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope)
@ -176,67 +152,40 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
} }
} }
private suspend fun showSwitchFailureText() = withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.toast_profile_enable_failed,
Toast.LENGTH_LONG
).show()
}
private fun enableOrDisableProfile(iccid: String, enable: Boolean) { private fun enableOrDisableProfile(iccid: String, enable: Boolean) {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
fab.isEnabled = false fab.isEnabled = false
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() beginTrackedOperation {
euiccChannelManagerService.waitForForegroundTask() val (res, refreshed) =
if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
val res = euiccChannelManagerService.launchProfileSwitchTask( // Sometimes, we *can* enable or disable the profile, but we cannot
slotId, // send the refresh command to the modem because the profile somehow
portId, // makes the modem "busy". In this case, we can still switch by setting
iccid, // refresh to false, but then the switch cannot take effect until the
enable, // user resets the modem manually by toggling airplane mode or rebooting.
reconnectTimeoutMillis = if (isUsb) { Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
0 } else {
} else { Pair(true, true)
30 * 1000
}
)?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done
if (res == null) {
showSwitchFailureText()
return@launch
}
when (res.error) {
null -> {}
is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
// This is only really fatal for internal eSIMs
if (!isUsb) {
withContext(Dispatchers.Main) {
AlertDialog.Builder(requireContext()).apply {
setMessage(R.string.switch_did_not_refresh)
setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
}
setOnDismissListener { _ ->
requireActivity().finish()
}
show()
}
}
} }
if (!res) {
Log.d(TAG, "Failed to enable / disable profile $iccid")
withContext(Dispatchers.Main) {
Toast.makeText(
context,
R.string.toast_profile_enable_failed,
Toast.LENGTH_LONG
).show()
}
return@beginTrackedOperation false
} }
is TimeoutCancellationException -> { if (!refreshed && !isUsb) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// Prevent this Fragment from being used again
invalid = true
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
AlertDialog.Builder(requireContext()).apply { AlertDialog.Builder(requireContext()).apply {
setMessage(R.string.enable_disable_timeout) setMessage(R.string.switch_did_not_refresh)
setPositiveButton(android.R.string.ok) { dialog, _ -> setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
requireActivity().finish() requireActivity().finish()
@ -247,11 +196,39 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
show() show()
} }
} }
return@beginTrackedOperation true
} }
else -> showSwitchFailureText() if (!isUsb) {
} try {
euiccChannelManager.waitForReconnect(
slotId,
portId,
timeoutMillis = 30 * 1000
)
} catch (e: TimeoutCancellationException) {
withContext(Dispatchers.Main) {
// Prevent this Fragment from being used again
invalid = true
// Timed out waiting for SIM to come back online, we can no longer assume that the LPA is still valid
AlertDialog.Builder(requireContext()).apply {
setMessage(R.string.enable_disable_timeout)
setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
}
setOnDismissListener { _ ->
requireActivity().finish()
}
show()
}
}
return@beginTrackedOperation false
}
}
preferenceRepository.notificationSwitchFlow.first()
}
refresh() refresh()
fab.isEnabled = true fab.isEnabled = true
} }

View file

@ -7,7 +7,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -38,19 +37,15 @@ class LogsActivity : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_logs) setContentView(R.layout.activity_logs)
setSupportActionBar(requireViewById(R.id.toolbar)) setSupportActionBar(requireViewById(R.id.toolbar))
setupToolbarInsets()
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
swipeRefresh = requireViewById(R.id.swipe_refresh) swipeRefresh = requireViewById(R.id.swipe_refresh)
scrollView = requireViewById(R.id.scroll_view) scrollView = requireViewById(R.id.scroll_view)
logText = requireViewById(R.id.log_text) logText = requireViewById(R.id.log_text)
setupRootViewInsets(scrollView)
swipeRefresh.setOnRefreshListener { swipeRefresh.setOnRefreshListener {
lifecycleScope.launch { lifecycleScope.launch {
reload() reload()
@ -71,10 +66,6 @@ class LogsActivity : AppCompatActivity() {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
finish()
true
}
R.id.save -> { R.id.save -> {
saveLogs.launch(getString(R.string.logs_filename_template, saveLogs.launch(getString(R.string.logs_filename_template,
SimpleDateFormat.getDateTimeInstance().format(Date()) SimpleDateFormat.getDateTimeInstance().format(Date())

View file

@ -5,9 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.util.Log import android.util.Log
@ -15,7 +13,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.activity.enableEdgeToEdge
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
@ -25,7 +22,6 @@ import com.google.android.material.tabs.TabLayoutMediator
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -33,8 +29,6 @@ import kotlinx.coroutines.withContext
open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
companion object { companion object {
const val TAG = "MainActivity" const val TAG = "MainActivity"
const val PERMISSION_REQUEST_CODE = 1000
} }
private lateinit var loadingProgress: ProgressBar private lateinit var loadingProgress: ProgressBar
@ -70,11 +64,9 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
@SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag") @SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
setSupportActionBar(requireViewById(R.id.toolbar)) setSupportActionBar(requireViewById(R.id.toolbar))
setupToolbarInsets()
loadingProgress = requireViewById(R.id.loading) loadingProgress = requireViewById(R.id.loading)
tabs = requireViewById(R.id.main_tabs) tabs = requireViewById(R.id.main_tabs)
viewPager = requireViewById(R.id.view_pager) viewPager = requireViewById(R.id.view_pager)
@ -121,29 +113,16 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
} }
private fun ensureNotificationPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
PERMISSION_REQUEST_CODE
)
}
}
private suspend fun init(fromUsbEvent: Boolean = false) { private suspend fun init(fromUsbEvent: Boolean = false) {
refreshing = true // We don't check this here -- the check happens in refresh() refreshing = true // We don't check this here -- the check happens in refresh()
loadingProgress.visibility = View.VISIBLE loadingProgress.visibility = View.VISIBLE
viewPager.visibility = View.GONE viewPager.visibility = View.GONE
tabs.visibility = View.GONE tabs.visibility = View.GONE
// Prevent concurrent access with any running foreground task
euiccChannelManagerService.waitForForegroundTask()
val knownChannels = withContext(Dispatchers.IO) { val knownChannels = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateEuiccChannels().onEach { euiccChannelManager.enumerateEuiccChannels().onEach {
Log.d(TAG, "slot ${it.slotId} port ${it.portId}") Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
if (preferenceRepository.verboseLoggingFlow.first()) { Log.d(TAG, it.lpa.eID)
Log.d(TAG, it.lpa.eID)
}
// Request the system to refresh the list of profiles every time we start // Request the system to refresh the list of profiles every time we start
// Note that this is currently supposed to be no-op when unprivileged, // Note that this is currently supposed to be no-op when unprivileged,
// but it could change in the future // but it could change in the future
@ -191,10 +170,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
viewPager.currentItem = 0 viewPager.currentItem = 0
} }
if (pages.size > 0) {
ensureNotificationPermissions()
}
refreshing = false refreshing = false
} }
} }

View file

@ -11,7 +11,6 @@ import android.view.MenuItem.OnMenuItemClickListener
import android.view.View 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.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -36,34 +35,31 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
private lateinit var euiccChannel: EuiccChannel private lateinit var euiccChannel: EuiccChannel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_notifications) setContentView(R.layout.activity_notifications)
setSupportActionBar(requireViewById(R.id.toolbar)) setSupportActionBar(requireViewById(R.id.toolbar))
setupToolbarInsets()
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
override fun onInit() {
euiccChannel = euiccChannelManager
.findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!!
swipeRefresh = requireViewById(R.id.swipe_refresh) swipeRefresh = requireViewById(R.id.swipe_refresh)
notificationList = requireViewById(R.id.recycler_view) notificationList = requireViewById(R.id.recycler_view)
setupRootViewInsets(notificationList)
}
override fun onInit() {
notificationList.layoutManager = notificationList.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
notificationList.adapter = notificationAdapter notificationList.adapter = notificationAdapter
registerForContextMenu(notificationList) registerForContextMenu(notificationList)
val logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
// This is slightly different from the MainActivity logic // This is slightly different from the MainActivity logic
// due to the length (we don't want to display the full USB product name) // due to the length (we don't want to display the full USB product name)
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) { val channelTitle = if (euiccChannel.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
getString(R.string.usb) getString(R.string.usb)
} else { } else {
getString(R.string.channel_name_format, logicalSlotId) getString(R.string.channel_name_format, euiccChannel.logicalSlotId)
} }
title = getString(R.string.profile_notifications_detailed_format, channelTitle) title = getString(R.string.profile_notifications_detailed_format, channelTitle)
@ -104,18 +100,6 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
swipeRefresh.isRefreshing = true swipeRefresh.isRefreshing = true
lifecycleScope.launch { lifecycleScope.launch {
if (!this@NotificationsActivity::euiccChannel.isInitialized) {
withContext(Dispatchers.IO) {
euiccChannelManagerLoaded.await()
euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking(
intent.getIntExtra(
"logicalSlotId",
0
)
)!!
}
}
task() task()
swipeRefresh.isRefreshing = false swipeRefresh.isRefreshing = false

View file

@ -3,15 +3,16 @@ package im.angry.openeuicc.ui
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.util.Log
import android.widget.EditText import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.lang.Exception
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker { class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
companion object { companion object {
@ -66,27 +67,23 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
requireParentFragment().lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager() try {
euiccChannelManagerService.waitForForegroundTask() doDelete()
} catch (e: Exception) {
euiccChannelManagerService.launchProfileDeleteTask( Log.d(ProfileDownloadFragment.TAG, "Error deleting profile")
slotId, Log.d(ProfileDownloadFragment.TAG, Log.getStackTraceString(e))
portId, } finally {
requireArguments().getString("iccid")!!
)!!.onStart {
if (parentFragment is EuiccProfilesChangedListener) { if (parentFragment is EuiccProfilesChangedListener) {
// Trigger a refresh in the parent fragment -- it should wait until
// any foreground task is completed before actually doing a refresh
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged() (parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
} }
dismiss()
try { }
dismiss()
} catch (e: IllegalStateException) {
// Ignored
}
}.collect()
} }
} }
private suspend fun doDelete() = beginTrackedOperation {
channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
preferenceRepository.notificationDeleteFlow.first()
}
} }

View file

@ -18,13 +18,12 @@ import com.google.android.material.textfield.TextInputLayout
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.ProfileDownloadCallback
import kotlin.Exception import kotlin.Exception
class ProfileDownloadFragment : BaseMaterialDialogFragment(), class ProfileDownloadFragment : BaseMaterialDialogFragment(),
@ -56,6 +55,7 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result -> private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
result.contents?.let { content -> result.contents?.let { content ->
Log.d(TAG, content)
onScanResult(content) onScanResult(content)
} }
} }
@ -149,22 +149,15 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable(
lifecycleScope.launch(Dispatchers.IO) { try {
ensureEuiccChannelManager()
if (euiccChannelManagerService.isForegroundTaskRunning) {
withContext(Dispatchers.Main) {
dismiss()
}
return@launch
}
val imei = try {
telephonyManager.getImei(channel.logicalSlotId) ?: "" telephonyManager.getImei(channel.logicalSlotId) ?: ""
} catch (e: Exception) { } catch (e: Exception) {
"" ""
} }
)
lifecycleScope.launch(Dispatchers.IO) {
// Fetch remaining NVRAM // Fetch remaining NVRAM
val str = channel.lpa.euiccInfo2?.freeNvram?.also { val str = channel.lpa.euiccInfo2?.freeNvram?.also {
freeNvram = it freeNvram = it
@ -173,8 +166,6 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space, profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space,
str ?: getText(R.string.unknown)) str ?: getText(R.string.unknown))
profileDownloadIMEI.editText!!.text =
Editable.Factory.getInstance().newEditable(imei)
} }
} }
} }
@ -213,28 +204,17 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
progress.visibility = View.VISIBLE progress.visibility = View.VISIBLE
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
val res = doDownloadProfile(server, code, confirmationCode, imei)
if (res == null || res.error != null) {
Log.d(TAG, "Error downloading profile")
if (res?.error != null) {
Log.d(TAG, Log.getStackTraceString(res.error))
}
Toast.makeText(requireContext(), R.string.profile_download_failed, Toast.LENGTH_LONG).show()
}
if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
try { try {
doDownloadProfile(server, code, confirmationCode, imei)
} catch (e: Exception) {
Log.d(TAG, "Error downloading profile")
Log.d(TAG, Log.getStackTraceString(e))
Toast.makeText(context, R.string.profile_download_failed, Toast.LENGTH_LONG).show()
} finally {
if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
dismiss() dismiss()
} catch (e: IllegalStateException) {
// Ignored
} }
} }
} }
@ -244,26 +224,30 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
code: String?, code: String?,
confirmationCode: String?, confirmationCode: String?,
imei: String? imei: String?
) = withContext(Dispatchers.Main) { ) = beginTrackedOperation {
// The service is responsible for launching the actual blocking part on the IO context val res = channel.lpa.downloadProfile(
val res = euiccChannelManagerService.launchProfileDownloadTask(
slotId,
portId,
server, server,
code, code,
imei,
confirmationCode, confirmationCode,
imei object : ProfileDownloadCallback {
)!!.onEach { override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) { lifecycleScope.launch(Dispatchers.Main) {
progress.progress = it.progress progress.isIndeterminate = false
progress.isIndeterminate = it.progress == 0 progress.progress = state.progress
} else { }
progress.progress = 100 }
progress.isIndeterminate = false })
}
}.last()
res as? EuiccChannelManagerService.ForegroundTaskState.Done if (!res) {
// TODO: Provide more details on the error
throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.")
}
// If we get here, we are successful
// This function is wrapped in beginTrackedOperation, so by returning the settings value,
// We only send notifications if the user allowed us to
preferenceRepository.notificationDownloadFlow.first()
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {

View file

@ -2,6 +2,7 @@ package im.angry.openeuicc.ui
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -12,8 +13,11 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.Exception
import java.lang.RuntimeException
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker { class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
companion object { companion object {
@ -93,24 +97,23 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
progress.visibility = View.VISIBLE progress.visibility = View.VISIBLE
lifecycleScope.launch { lifecycleScope.launch {
ensureEuiccChannelManager()
euiccChannelManagerService.waitForForegroundTask()
euiccChannelManagerService.launchProfileRenameTask(
slotId,
portId,
requireArguments().getString("iccid")!!,
name
)?.collect()
if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
try { try {
doRename(name)
} catch (e: Exception) {
Log.d(TAG, "Failed to rename profile")
Log.d(TAG, Log.getStackTraceString(e))
} finally {
if (parentFragment is EuiccProfilesChangedListener) {
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
}
dismiss() dismiss()
} catch (e: IllegalStateException) {
// Ignored
} }
} }
} }
private suspend fun doRename(name: String) = withContext(Dispatchers.IO) {
if (!channel.lpa.setNickname(requireArguments().getString("iccid")!!, name)) {
throw RuntimeException("Profile nickname not changed")
}
}
} }

View file

@ -2,18 +2,14 @@ package im.angry.openeuicc.ui
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.*
class SettingsActivity: AppCompatActivity() { class SettingsActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
setSupportActionBar(requireViewById(R.id.toolbar)) setSupportActionBar(requireViewById(R.id.toolbar))
setupToolbarInsets()
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.settings_container, SettingsFragment()) .replace(R.id.settings_container, SettingsFragment())

View file

@ -3,9 +3,6 @@ package im.angry.openeuicc.ui
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
@ -47,14 +44,6 @@ class SettingsFragment: PreferenceFragmentCompat() {
findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim") findPreference<CheckBoxPreference>("pref_advanced_disable_safeguard_removable_esim")
?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM) ?.bindBooleanFlow(preferenceRepository.disableSafeguardFlow, PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM)
findPreference<CheckBoxPreference>("pref_advanced_verbose_logging")
?.bindBooleanFlow(preferenceRepository.verboseLoggingFlow, PreferenceKeys.VERBOSE_LOGGING)
}
override fun onStart() {
super.onStart()
setupRootViewInsets(requireView().requireViewById(androidx.preference.R.id.recycler_view))
} }
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) { private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {

View file

@ -4,8 +4,9 @@ import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.angry.openeuicc.core.EuiccChannel import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.service.EuiccChannelManagerService
import im.angry.openeuicc.ui.BaseEuiccAccessActivity import im.angry.openeuicc.ui.BaseEuiccAccessActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface EuiccChannelFragmentMarker: OpenEuiccContextMarker interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
@ -34,15 +35,18 @@ val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManager
val <T> T.euiccChannelManagerService: EuiccChannelManagerService where T: Fragment, T: EuiccChannelFragmentMarker
get() = (requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerService
val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker val <T> T.channel: EuiccChannel where T: Fragment, T: EuiccChannelFragmentMarker
get() = get() =
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!! euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
interface EuiccProfilesChangedListener { interface EuiccProfilesChangedListener {
fun onEuiccProfilesChanged() fun onEuiccProfilesChanged()
} }
suspend fun <T> T.beginTrackedOperation(op: suspend () -> Boolean) where T: Fragment, T: EuiccChannelFragmentMarker {
withContext(Dispatchers.IO) {
euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) {
op()
}
}
}

View file

@ -73,42 +73,6 @@ fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean
* should be the concern of op() itself, and this function assumes that when * should be the concern of op() itself, and this function assumes that when
* op() returns, the slotId and portId will correspond to a valid channel again. * op() returns, the slotId and portId will correspond to a valid channel again.
*/ */
suspend inline fun EuiccChannelManager.beginTrackedOperation(
slotId: Int,
portId: Int,
op: () -> Boolean
) {
val latestSeq =
findEuiccChannelByPort(slotId, portId)!!.lpa.notifications.firstOrNull()?.seqNumber
?: 0
Log.d(TAG, "Latest notification is $latestSeq before operation")
if (op()) {
Log.d(TAG, "Operation has requested notification handling")
try {
// Note that the exact instance of "channel" might have changed here if reconnected;
// so we MUST use the automatic getter for "channel"
findEuiccChannelByPort(
slotId,
portId
)?.lpa?.notifications?.filter { it.seqNumber > latestSeq }?.forEach {
Log.d(TAG, "Handling notification $it")
findEuiccChannelByPort(
slotId,
portId
)?.lpa?.handleNotification(it.seqNumber)
}
} catch (e: Exception) {
// Ignore any error during notification handling
e.printStackTrace()
}
}
Log.d(TAG, "Operation complete")
}
/**
* Same as beginTrackedOperation but uses blocking primitives.
* TODO: This function needs to be phased out of use.
*/
inline fun EuiccChannelManager.beginTrackedOperationBlocking( inline fun EuiccChannelManager.beginTrackedOperationBlocking(
slotId: Int, slotId: Int,
portId: Int, portId: Int,

View file

@ -24,7 +24,6 @@ object PreferenceKeys {
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete") val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch") val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim") val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
} }
class PreferenceRepository(context: Context) { class PreferenceRepository(context: Context) {
@ -45,9 +44,6 @@ class PreferenceRepository(context: Context) {
val disableSafeguardFlow: Flow<Boolean> = val disableSafeguardFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM] ?: false } dataStore.data.map { it[PreferenceKeys.DISABLE_SAFEGUARD_REMOVABLE_ESIM] ?: false }
val verboseLoggingFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.VERBOSE_LOGGING] ?: false }
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) { suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
dataStore.edit { dataStore.edit {
it[key] = value it[key] = value

View file

@ -2,16 +2,8 @@ package im.angry.openeuicc.util
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import im.angry.openeuicc.common.R
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height> // Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
/** /**
@ -34,39 +26,3 @@ fun DialogFragment.setWidthPercent(percentage: Int) {
fun DialogFragment.setFullScreen() { fun DialogFragment.setFullScreen() {
dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
} }
fun AppCompatActivity.setupToolbarInsets() {
val spacer = requireViewById<View>(R.id.toolbar_spacer)
ViewCompat.setOnApplyWindowInsetsListener(requireViewById(R.id.toolbar)) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = bars.top
}
v.updatePadding(bars.left, v.paddingTop, bars.right, v.paddingBottom)
spacer.updateLayoutParams {
height = v.top
}
WindowInsetsCompat.CONSUMED
}
}
fun setupRootViewInsets(view: ViewGroup) {
// Disable clipToPadding to make sure content actually display
view.clipToPadding = false
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(bars.left, v.paddingTop, bars.right, bars.bottom)
WindowInsetsCompat.CONSUMED
}
}

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18,2h-8L4,8v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V4C20,2.9 19.1,2 18,2zM12,17l-4,-4h3V9.02L13,9v4h3L12,17z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View file

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -5,7 +5,13 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<include layout="@layout/toolbar_activity" /> <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh" android:id="@+id/swipe_refresh"

View file

@ -5,7 +5,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include layout="@layout/toolbar_activity" /> <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/main_tabs" android:id="@+id/main_tabs"

View file

@ -4,7 +4,13 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<include layout="@layout/toolbar_activity" /> <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh" android:id="@+id/swipe_refresh"

View file

@ -4,7 +4,13 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<include layout="@layout/toolbar_activity" /> <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
<FrameLayout <FrameLayout
android:id="@+id/settings_container" android:id="@+id/settings_container"

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:id="@+id/toolbar_spacer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="?attr/colorSurfaceVariant"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toTopOf="@id/toolbar" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
</merge>

View file

@ -33,16 +33,6 @@
<string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string> <string name="usb_permission_needed">Permission is needed to access the USB smart card reader.</string>
<string name="usb_failed">Cannot connect to eSIM via a USB smart card reader.</string> <string name="usb_failed">Cannot connect to eSIM via a USB smart card reader.</string>
<string name="task_notification">Long-running Tasks</string>
<string name="task_profile_download">Downloading eSIM profile</string>
<string name="task_profile_download_failure">Failed to download eSIM profile</string>
<string name="task_profile_rename">Renaming eSIM profile</string>
<string name="task_profile_rename_failure">Failed to rename eSIM profile</string>
<string name="task_profile_delete">Deleting eSIM profile</string>
<string name="task_profile_delete_failure">Failed to delete eSIM profile</string>
<string name="task_profile_switch">Switching eSIM profile</string>
<string name="task_profile_switch_failure">Failed to switch eSIM profile</string>
<string name="profile_download">New eSIM</string> <string name="profile_download">New eSIM</string>
<string name="profile_download_server">Server (RSP / SM-DP+)</string> <string name="profile_download_server">Server (RSP / SM-DP+)</string>
<string name="profile_download_code">Activation Code</string> <string name="profile_download_code">Activation Code</string>
@ -84,10 +74,8 @@
<string name="pref_notifications_switch">Switching</string> <string name="pref_notifications_switch">Switching</string>
<string name="pref_notifications_switch_desc">Send notifications for <i>switching</i> profiles\nNote that this type of notification is unreliable.</string> <string name="pref_notifications_switch_desc">Send notifications for <i>switching</i> profiles\nNote that this type of notification is unreliable.</string>
<string name="pref_advanced">Advanced</string> <string name="pref_advanced">Advanced</string>
<string name="pref_advanced_disable_safeguard_removable_esim">Allow Disabling / Deleting Active Profile</string> <string name="pref_advanced_disable_safeguard_removable_esim">Disable Safeguards for Removable eSIMs</string>
<string name="pref_advanced_disable_safeguard_removable_esim_desc">By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may <i>sometimes</i> render it inaccessible.\nCheck this box to <i>remove</i> this safeguard.</string> <string name="pref_advanced_disable_safeguard_removable_esim_desc">By default, this app prevents you from disabling the active profile on a removable eSIM inserted in the device, because doing so may <i>sometimes</i> render it inaccessible.\nCheck this box to <i>remove</i> this safeguard.</string>
<string name="pref_advanced_verbose_logging">Verbose Logging</string>
<string name="pref_advanced_verbose_logging_desc">Enable verbose logs, which may contain sensitive information. Only share your logs with someone you trust after turning this on.</string>
<string name="pref_advanced_logs">Logs</string> <string name="pref_advanced_logs">Logs</string>
<string name="pref_advanced_logs_desc">View recent debug logs of the application</string> <string name="pref_advanced_logs_desc">View recent debug logs of the application</string>
<string name="pref_info">Info</string> <string name="pref_info">Info</string>

View file

@ -30,12 +30,6 @@
app:title="@string/pref_advanced_disable_safeguard_removable_esim" app:title="@string/pref_advanced_disable_safeguard_removable_esim"
app:summary="@string/pref_advanced_disable_safeguard_removable_esim_desc" /> app:summary="@string/pref_advanced_disable_safeguard_removable_esim_desc" />
<CheckBoxPreference
app:key="pref_advanced_verbose_logging"
app:iconSpaceReserved="false"
app:title="@string/pref_advanced_verbose_logging"
app:summary="@string/pref_advanced_verbose_logging_desc" />
<Preference <Preference
app:key="pref_advanced_logs" app:key="pref_advanced_logs"
app:iconSpaceReserved="false" app:iconSpaceReserved="false"

View file

@ -8,7 +8,6 @@ java_defaults {
"androidx-constraintlayout_constraintlayout", "androidx-constraintlayout_constraintlayout",
"androidx.preference_preference", "androidx.preference_preference",
"androidx.lifecycle_lifecycle-runtime-ktx", "androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.lifecycle_lifecycle-service",
"androidx.swiperefreshlayout_swiperefreshlayout", "androidx.swiperefreshlayout_swiperefreshlayout",
"androidx.cardview_cardview", "androidx.cardview_cardview",
"androidx.viewpager2_viewpager2", "androidx.viewpager2_viewpager2",

View file

@ -48,7 +48,6 @@ dependencies {
//noinspection KtxExtensionAvailable //noinspection KtxExtensionAvailable
api("androidx.preference:preference:1.2.1") api("androidx.preference:preference:1.2.1")
api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
api("androidx.lifecycle:lifecycle-service:2.6.2")
api("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") api("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
api("androidx.cardview:cardview:1.0.0") api("androidx.cardview:cardview:1.0.0")
api("androidx.viewpager2:viewpager2:1.1.0") api("androidx.viewpager2:viewpager2:1.1.0")

View file

@ -17,13 +17,13 @@ apply {
android { android {
namespace = "im.angry.easyeuicc" namespace = "im.angry.easyeuicc"
compileSdk = 35 compileSdk = 34
ndkVersion = "26.1.10909125" ndkVersion = "26.1.10909125"
defaultConfig { defaultConfig {
applicationId = "im.angry.easyeuicc" applicationId = "im.angry.easyeuicc"
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 34
} }
buildTypes { buildTypes {

View file

@ -6,7 +6,6 @@ import android.view.MenuItem
import android.view.View 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.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children import androidx.core.view.children
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -23,11 +22,9 @@ class CompatibilityCheckActivity: AppCompatActivity() {
private val adapter = CompatibilityChecksAdapter() private val adapter = CompatibilityChecksAdapter()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_compatibility_check) setContentView(R.layout.activity_compatibility_check)
setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar)) setSupportActionBar(requireViewById(R.id.toolbar))
setupToolbarInsets()
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
compatibilityCheckList = requireViewById(R.id.recycler_view) compatibilityCheckList = requireViewById(R.id.recycler_view)
@ -35,8 +32,6 @@ class CompatibilityCheckActivity: AppCompatActivity() {
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
compatibilityCheckList.adapter = adapter compatibilityCheckList.adapter = adapter
setupRootViewInsets(compatibilityCheckList)
} }
override fun onStart() { override fun onStart() {

View file

@ -4,7 +4,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include layout="@layout/toolbar_activity" /> <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintWidth_percent="1" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -12,12 +12,12 @@ apply {
android { android {
namespace = "im.angry.openeuicc" namespace = "im.angry.openeuicc"
compileSdk = 35 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "im.angry.openeuicc" applicationId = "im.angry.openeuicc"
minSdk = 30 minSdk = 30
targetSdk = 35 targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View file

@ -26,15 +26,7 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}" "Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
) )
try { try {
return EuiccChannel( return EuiccChannel(port, TelephonyManagerApduInterface(port, tm))
port,
TelephonyManagerApduInterface(
port,
tm,
context.preferenceRepository.verboseLoggingFlow
),
context.preferenceRepository.verboseLoggingFlow
)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
// Failed // Failed
Log.w( Log.w(

View file

@ -2,22 +2,13 @@ package im.angry.openeuicc.core
import android.telephony.IccOpenLogicalChannelResponse import android.telephony.IccOpenLogicalChannelResponse
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.util.Log
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
class TelephonyManagerApduInterface( class TelephonyManagerApduInterface(
private val port: UiccPortInfoCompat, private val port: UiccPortInfoCompat,
private val tm: TelephonyManager, private val tm: TelephonyManager
private val verboseLoggingFlow: Flow<Boolean>
): ApduInterface { ): ApduInterface {
companion object {
const val TAG = "TelephonyManagerApduInterface"
}
private var lastChannel: Int = -1 private var lastChannel: Int = -1
override val valid: Boolean override val valid: Boolean
@ -54,10 +45,6 @@ class TelephonyManagerApduInterface(
override fun transmit(tx: ByteArray): ByteArray { override fun transmit(tx: ByteArray): ByteArray {
check(lastChannel != -1) { "Uninitialized" } check(lastChannel != -1) { "Uninitialized" }
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
}
val cla = tx[0].toUByte().toInt() val cla = tx[0].toUByte().toInt()
val instruction = tx[1].toUByte().toInt() val instruction = tx[1].toUByte().toInt()
val p1 = tx[2].toUByte().toInt() val p1 = tx[2].toUByte().toInt()
@ -66,17 +53,7 @@ class TelephonyManagerApduInterface(
val p4 = tx.drop(5).toByteArray().encodeHex() val p4 = tx.drop(5).toByteArray().encodeHex()
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel, return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
cla, cla, instruction, p1, p2, p3, p4)?.decodeHex() ?: byteArrayOf()
instruction,
p1,
p2,
p3,
p4
).also {
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "TelephonyManager APDU response: $it")
}
}?.decodeHex() ?: byteArrayOf()
} }
} }

View file

@ -2,28 +2,14 @@ package im.angry.openeuicc.ui
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import im.angry.openeuicc.R import im.angry.openeuicc.R
class LuiActivity : AppCompatActivity() { class LuiActivity : AppCompatActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
enableEdgeToEdge()
setContentView(R.layout.activity_lui) setContentView(R.layout.activity_lui)
ViewCompat.setOnApplyWindowInsetsListener(requireViewById(R.id.lui_container)) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(bars.left, bars.top, bars.right, bars.bottom)
WindowInsetsCompat.CONSUMED
}
requireViewById<View>(R.id.lui_skip).setOnClickListener { finish() } requireViewById<View>(R.id.lui_skip).setOnClickListener { finish() }
// TODO: Deactivate LuiActivity if there is no eSIM found. // TODO: Deactivate LuiActivity if there is no eSIM found.
// TODO: Support pre-filled download info (from carrier apps); UX // TODO: Support pre-filled download info (from carrier apps); UX

View file

@ -2,7 +2,6 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/lui_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurfaceVariant"> android:background="?attr/colorSurfaceVariant">

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -5,7 +5,7 @@ plugins {
android { android {
namespace = "net.typeblog.lpac_jni" namespace = "net.typeblog.lpac_jni"
compileSdk = 35 compileSdk = 34
ndkVersion = "26.1.10909125" ndkVersion = "26.1.10909125"
defaultConfig { defaultConfig {

View file

@ -8,13 +8,6 @@ interface LocalProfileAssistant {
// Extended EuiccInfo for use with LUIs, containing information such as firmware version // Extended EuiccInfo for use with LUIs, containing information such as firmware version
val euiccInfo2: EuiccInfo2? val euiccInfo2: EuiccInfo2?
/**
* Set the max segment size (mss) for all es10x commands. This can help with removable
* eUICCs that may run at a baud rate too fast for the modem.
* By default, this is set to 60 by libeuicc.
*/
fun setEs10xMss(mss: Byte)
// All blocking functions in this class assume that they are executed on non-Main threads // All blocking functions in this class assume that they are executed on non-Main threads
// The IO context in Kotlin's coroutine library is recommended. // The IO context in Kotlin's coroutine library is recommended.
fun enableProfile(iccid: String, refresh: Boolean = true): Boolean fun enableProfile(iccid: String, refresh: Boolean = true): Boolean

View file

@ -9,7 +9,6 @@ internal object LpacJni {
external fun destroyContext(handle: Long) external fun destroyContext(handle: Long)
external fun euiccInit(handle: Long): Int external fun euiccInit(handle: Long): Int
external fun euiccSetMss(handle: Long, mss: Byte)
external fun euiccFini(handle: Long) external fun euiccFini(handle: Long)
// es10c // es10c

View file

@ -1,9 +1,6 @@
package net.typeblog.lpac_jni.impl package net.typeblog.lpac_jni.impl
import android.util.Log import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.HttpInterface import net.typeblog.lpac_jni.HttpInterface
import java.net.URL import java.net.URL
import java.security.SecureRandom import java.security.SecureRandom
@ -12,7 +9,7 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory import javax.net.ssl.TrustManagerFactory
class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : HttpInterface { class HttpInterfaceImpl: HttpInterface {
companion object { companion object {
private const val TAG = "HttpInterfaceImpl" private const val TAG = "HttpInterfaceImpl"
} }
@ -26,10 +23,6 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : HttpInt
): HttpInterface.HttpResponse { ): HttpInterface.HttpResponse {
Log.d(TAG, "transmit(url = $url)") Log.d(TAG, "transmit(url = $url)")
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "HTTP tx = ${tx.decodeToString(throwOnInvalidSequence = false)}")
}
val parsedUrl = URL(url) val parsedUrl = URL(url)
if (parsedUrl.protocol != "https") { if (parsedUrl.protocol != "https") {
throw IllegalArgumentException("SM-DP+ servers must use the HTTPS protocol") throw IllegalArgumentException("SM-DP+ servers must use the HTTPS protocol")
@ -41,12 +34,6 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : HttpInt
val conn = parsedUrl.openConnection() as HttpsURLConnection val conn = parsedUrl.openConnection() as HttpsURLConnection
conn.connectTimeout = 2000 conn.connectTimeout = 2000
if (url.contains("handleNotification")) {
conn.connectTimeout = 1000
conn.readTimeout = 1000
}
conn.sslSocketFactory = sslContext.socketFactory conn.sslSocketFactory = sslContext.socketFactory
conn.requestMethod = "POST" conn.requestMethod = "POST"
conn.doInput = true conn.doInput = true
@ -63,16 +50,7 @@ class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : HttpInt
Log.d(TAG, "transmit responseCode = ${conn.responseCode}") Log.d(TAG, "transmit responseCode = ${conn.responseCode}")
val bytes = conn.inputStream.readBytes().also { return HttpInterface.HttpResponse(conn.responseCode, conn.inputStream.readBytes())
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(
TAG,
"HTTP response body = ${it.decodeToString(throwOnInvalidSequence = false)}"
)
}
}
return HttpInterface.HttpResponse(conn.responseCode, bytes)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw e throw e

View file

@ -30,10 +30,6 @@ class LocalProfileAssistantImpl(
httpInterface.usePublicKeyIds(pkids) httpInterface.usePublicKeyIds(pkids)
} }
override fun setEs10xMss(mss: Byte) {
LpacJni.euiccSetMss(contextHandle, mss)
}
override val valid: Boolean override val valid: Boolean
get() = !finalized && apduInterface.valid && try { get() = !finalized && apduInterface.valid && try {
// If we can read both eID and euiccInfo2 properly, we are likely looking at // If we can read both eID and euiccInfo2 properly, we are likely looking at
@ -46,7 +42,6 @@ class LocalProfileAssistantImpl(
} }
override val profiles: List<LocalProfileInfo> override val profiles: List<LocalProfileInfo>
@Synchronized
get() { get() {
val head = LpacJni.es10cGetProfilesInfo(contextHandle) val head = LpacJni.es10cGetProfilesInfo(contextHandle)
var curr = head var curr = head
@ -71,7 +66,6 @@ class LocalProfileAssistantImpl(
} }
override val notifications: List<LocalProfileNotification> override val notifications: List<LocalProfileNotification>
@Synchronized
get() { get() {
val head = LpacJni.es10bListNotification(contextHandle) val head = LpacJni.es10bListNotification(contextHandle)
var curr = head var curr = head
@ -90,11 +84,9 @@ class LocalProfileAssistantImpl(
} }
override val eID: String override val eID: String
@Synchronized
get() = LpacJni.es10cGetEid(contextHandle)!! get() = LpacJni.es10cGetEid(contextHandle)!!
override val euiccInfo2: EuiccInfo2? override val euiccInfo2: EuiccInfo2?
@Synchronized
get() { get() {
val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle) val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
if (cInfo == 0L) return null if (cInfo == 0L) return null
@ -130,15 +122,12 @@ class LocalProfileAssistantImpl(
return ret return ret
} }
@Synchronized
override fun enableProfile(iccid: String, refresh: Boolean): Boolean = override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0 LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0
@Synchronized
override fun disableProfile(iccid: String, refresh: Boolean): Boolean = override fun disableProfile(iccid: String, refresh: Boolean): Boolean =
LpacJni.es10cDisableProfile(contextHandle, iccid, refresh) == 0 LpacJni.es10cDisableProfile(contextHandle, iccid, refresh) == 0
@Synchronized
override fun deleteProfile(iccid: String): Boolean = override fun deleteProfile(iccid: String): Boolean =
LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0 LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
@ -155,7 +144,6 @@ class LocalProfileAssistantImpl(
) == 0 ) == 0
} }
@Synchronized
override fun deleteNotification(seqNumber: Long): Boolean = override fun deleteNotification(seqNumber: Long): Boolean =
LpacJni.es10bDeleteNotification(contextHandle, seqNumber) == 0 LpacJni.es10bDeleteNotification(contextHandle, seqNumber) == 0
@ -165,7 +153,6 @@ class LocalProfileAssistantImpl(
Log.d(TAG, "handleNotification $seqNumber = $it") Log.d(TAG, "handleNotification $seqNumber = $it")
} == 0 } == 0
@Synchronized
override fun setNickname(iccid: String, nickname: String): Boolean = override fun setNickname(iccid: String, nickname: String): Boolean =
LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0 LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0

@ -1 +1 @@
Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef Subproject commit 0011ea6cc4c045c84f7aac839c1cce7804422355

View file

@ -75,13 +75,6 @@ Java_net_typeblog_lpac_1jni_LpacJni_euiccFini(JNIEnv *env, jobject thiz, jlong h
euicc_fini(ctx); euicc_fini(ctx);
} }
JNIEXPORT void JNICALL
Java_net_typeblog_lpac_1jni_LpacJni_euiccSetMss(JNIEnv *env, jobject thiz, jlong handle,
jbyte mss) {
struct euicc_ctx *ctx = (struct euicc_ctx *) handle;
ctx->es10x_mss = (uint8_t) mss;
}
jstring toJString(JNIEnv *env, const char *pat) { jstring toJString(JNIEnv *env, const char *pat) {
jbyteArray bytes = NULL; jbyteArray bytes = NULL;
jstring encoding = NULL; jstring encoding = NULL;

View file

@ -5,7 +5,5 @@
<permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" /> <permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" />
<permission name="android.permission.MODIFY_PHONE_STATE" /> <permission name="android.permission.MODIFY_PHONE_STATE" />
<permission name="android.permission.SECURE_ELEMENT_PRIVILEGED_OPERATION" /> <permission name="android.permission.SECURE_ELEMENT_PRIVILEGED_OPERATION" />
<permission name="android.permission.FOREGROUND_SERVICE" />
<permission name="android.permission.POST_NOTIFICATIONS" />
</privapp-permissions> </privapp-permissions>
</permissions> </permissions>