forked from PeterCxy/OpenEUICC
Compare commits
43 commits
991cd2a156
...
8d76f0eeac
Author | SHA1 | Date | |
---|---|---|---|
8d76f0eeac | |||
d26a8ddc78 | |||
aac457f4b5 | |||
2337ad035d | |||
7197501cca | |||
4709b6994f | |||
349c8179b0 | |||
16b6aceedf | |||
eab96dde05 | |||
84dd16c169 | |||
d3a04b94a9 | |||
19dc215b3f | |||
ddc421dae7 | |||
69e63b0a8b | |||
290bdca75a | |||
5c8bbeb217 | |||
ff266a4a9b | |||
6b71a746a4 | |||
165f685abb | |||
42942c2816 | |||
54b4f61fd7 | |||
7661b4b84f | |||
479e0ff34a | |||
79f43e2fda | |||
8573834a03 | |||
2721f91277 | |||
653123939c | |||
48b5f8ce06 | |||
31c06470c6 | |||
cf5704be42 | |||
f71da0e4ff | |||
fe1319537a | |||
8de0d86895 | |||
64a350d271 | |||
9a77824f79 | |||
3add3ffa90 | |||
324dcdc563 | |||
b94eedac0a | |||
a6777d1d17 | |||
dc70f7ca46 | |||
77d95e4d02 | |||
4a32f53c06 | |||
97bc0a0827 |
58 changed files with 985 additions and 213 deletions
3
.idea/deploymentTargetSelector.xml
generated
3
.idea/deploymentTargetSelector.xml
generated
|
@ -5,6 +5,9 @@
|
|||
<SelectionState runConfigName="app-unpriv">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "im.angry.openeuicc.common"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application>
|
||||
<application
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name="im.angry.openeuicc.ui.SettingsActivity"
|
||||
android:label="@string/pref_settings" />
|
||||
|
@ -31,6 +35,7 @@
|
|||
|
||||
<service
|
||||
android:name="im.angry.openeuicc.service.EuiccChannelManagerService"
|
||||
android:foregroundServiceType="shortService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -33,7 +33,18 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
|
||||
Log.i(DefaultEuiccChannelManager.TAG, "Trying OMAPI for physical slot ${port.card.physicalSlotIndex}")
|
||||
try {
|
||||
return EuiccChannel(port, OmapiApduInterface(seService!!, port))
|
||||
return EuiccChannel(
|
||||
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) {
|
||||
// Failed
|
||||
Log.w(
|
||||
|
@ -52,7 +63,13 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
if (!conn.claimInterface(usbInterface, true)) return null
|
||||
return EuiccChannel(
|
||||
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
|
||||
UsbApduInterface(conn, bulkIn, bulkOut)
|
||||
UsbApduInterface(
|
||||
conn,
|
||||
bulkIn,
|
||||
bulkOut,
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ open class DefaultEuiccChannelManager(
|
|||
try {
|
||||
// tryOpenEuiccChannel() will automatically dispose of invalid channels
|
||||
// and recreate when needed
|
||||
val channel = findEuiccChannelByPortBlocking(physicalSlotId, portId)!!
|
||||
val channel = findEuiccChannelByPort(physicalSlotId, portId)!!
|
||||
check(channel.valid) { "Invalid channel" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
import net.typeblog.lpac_jni.impl.HttpInterfaceImpl
|
||||
|
@ -9,12 +10,14 @@ import net.typeblog.lpac_jni.impl.LocalProfileAssistantImpl
|
|||
class EuiccChannel(
|
||||
val port: UiccPortInfoCompat,
|
||||
apduInterface: ApduInterface,
|
||||
verboseLoggingFlow: Flow<Boolean>
|
||||
) {
|
||||
val slotId = port.card.physicalSlotIndex // PHYSICAL slot
|
||||
val logicalSlotId = port.logicalSlotIndex
|
||||
val portId = port.portIndex
|
||||
|
||||
val lpa: LocalProfileAssistant = LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl())
|
||||
val lpa: LocalProfileAssistant =
|
||||
LocalProfileAssistantImpl(apduInterface, HttpInterfaceImpl(verboseLoggingFlow))
|
||||
|
||||
val valid: Boolean
|
||||
get() = lpa.valid
|
||||
|
|
|
@ -5,11 +5,16 @@ import android.se.omapi.SEService
|
|||
import android.se.omapi.Session
|
||||
import android.util.Log
|
||||
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
|
||||
|
||||
class OmapiApduInterface(
|
||||
private val service: SEService,
|
||||
private val port: UiccPortInfoCompat
|
||||
private val port: UiccPortInfoCompat,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
): ApduInterface {
|
||||
companion object {
|
||||
const val TAG = "OmapiApduInterface"
|
||||
|
@ -49,17 +54,30 @@ class OmapiApduInterface(
|
|||
"Unknown channel"
|
||||
}
|
||||
|
||||
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
|
||||
}
|
||||
|
||||
try {
|
||||
return lastChannel.transmit(tx).also {
|
||||
Log.d(TAG, "OMAPI APDU response: ${it.encodeHex()}")
|
||||
for (i in 0..10) {
|
||||
val res = lastChannel.transmit(tx)
|
||||
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) {
|
||||
Log.e(TAG, "OMAPI APDU exception")
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,12 +4,14 @@ import android.hardware.usb.UsbDeviceConnection
|
|||
import android.hardware.usb.UsbEndpoint
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
|
||||
class UsbApduInterface(
|
||||
private val conn: UsbDeviceConnection,
|
||||
private val bulkIn: UsbEndpoint,
|
||||
private val bulkOut: UsbEndpoint
|
||||
private val bulkOut: UsbEndpoint,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
): ApduInterface {
|
||||
companion object {
|
||||
private const val TAG = "UsbApduInterface"
|
||||
|
@ -27,7 +29,7 @@ class UsbApduInterface(
|
|||
throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
|
||||
}
|
||||
|
||||
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription)
|
||||
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription, verboseLoggingFlow)
|
||||
|
||||
try {
|
||||
transceiver.iccPowerOn()
|
||||
|
|
|
@ -5,6 +5,9 @@ import android.hardware.usb.UsbEndpoint
|
|||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
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.ByteOrder
|
||||
|
||||
|
@ -18,7 +21,8 @@ class UsbCcidTransceiver(
|
|||
private val usbConnection: UsbDeviceConnection,
|
||||
private val usbBulkIn: UsbEndpoint,
|
||||
private val usbBulkOut: UsbEndpoint,
|
||||
private val usbCcidDescription: UsbCcidDescription
|
||||
private val usbCcidDescription: UsbCcidDescription,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "UsbCcidTransceiver"
|
||||
|
@ -178,7 +182,9 @@ class UsbCcidTransceiver(
|
|||
readBytes = usbConnection.bulkTransfer(
|
||||
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
|
||||
)
|
||||
Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
|
||||
}
|
||||
} while (readBytes <= 0 && attempts-- > 0)
|
||||
if (readBytes < CCID_HEADER_LENGTH) {
|
||||
throw UsbTransportException("USB-CCID error - failed to receive CCID header")
|
||||
|
|
|
@ -1,11 +1,36 @@
|
|||
package im.angry.openeuicc.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Binder
|
||||
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.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.
|
||||
|
@ -17,8 +42,22 @@ import im.angry.openeuicc.util.*
|
|||
* 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
|
||||
* 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 : Service(), OpenEuiccContextMarker {
|
||||
class EuiccChannelManagerService : LifecycleService(), 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() {
|
||||
val service = this@EuiccChannelManagerService
|
||||
}
|
||||
|
@ -28,14 +67,341 @@ class EuiccChannelManagerService : Service(), OpenEuiccContextMarker {
|
|||
}
|
||||
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() {
|
||||
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()) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,14 +9,18 @@ import android.os.IBinder
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
abstract class BaseEuiccAccessActivity : AppCompatActivity() {
|
||||
val euiccChannelManagerLoaded = CompletableDeferred<Unit>()
|
||||
lateinit var euiccChannelManager: EuiccChannelManager
|
||||
lateinit var euiccChannelManagerService: EuiccChannelManagerService
|
||||
|
||||
private val euiccChannelManagerServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
euiccChannelManager =
|
||||
(service!! as EuiccChannelManagerService.LocalBinder).service.euiccChannelManager
|
||||
euiccChannelManagerService = (service!! as EuiccChannelManagerService.LocalBinder).service
|
||||
euiccChannelManager = euiccChannelManagerService.euiccChannelManager
|
||||
euiccChannelManagerLoaded.complete(Unit)
|
||||
onInit()
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ import android.widget.PopupMenu
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
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.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -27,12 +30,12 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -76,6 +79,21 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
fab = view.requireViewById(R.id.fab)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -90,7 +108,10 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
ProfileDownloadFragment.newInstance(slotId, portId)
|
||||
.show(childFragmentManager, ProfileDownloadFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
@ -133,6 +154,9 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
if (!this@EuiccManagementFragment::disableSafeguardFlow.isInitialized) {
|
||||
disableSafeguardFlow =
|
||||
preferenceRepository.disableSafeguardFlow.stateIn(lifecycleScope)
|
||||
|
@ -152,67 +176,47 @@ 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) {
|
||||
swipeRefresh.isRefreshing = true
|
||||
fab.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
beginTrackedOperation {
|
||||
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)
|
||||
}
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
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
|
||||
val res = euiccChannelManagerService.launchProfileSwitchTask(
|
||||
slotId,
|
||||
portId,
|
||||
iccid,
|
||||
enable,
|
||||
reconnectTimeoutMillis = if (isUsb) {
|
||||
0
|
||||
} else {
|
||||
30 * 1000
|
||||
}
|
||||
)?.last() as? EuiccChannelManagerService.ForegroundTaskState.Done
|
||||
|
||||
if (!refreshed && !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()
|
||||
}
|
||||
}
|
||||
return@beginTrackedOperation true
|
||||
}
|
||||
if (res == null) {
|
||||
showSwitchFailureText()
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!isUsb) {
|
||||
try {
|
||||
euiccChannelManager.waitForReconnect(
|
||||
slotId,
|
||||
portId,
|
||||
timeoutMillis = 30 * 1000
|
||||
)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
when (res.error) {
|
||||
null -> {}
|
||||
is EuiccChannelManagerService.SwitchingProfilesRefreshException -> {
|
||||
// This is only really fatal for internal eSIMs
|
||||
if (!isUsb) {
|
||||
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)
|
||||
setMessage(R.string.switch_did_not_refresh)
|
||||
setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
|
@ -223,12 +227,31 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
|
|||
show()
|
||||
}
|
||||
}
|
||||
return@beginTrackedOperation false
|
||||
}
|
||||
}
|
||||
|
||||
preferenceRepository.notificationSwitchFlow.first()
|
||||
is 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> showSwitchFailureText()
|
||||
}
|
||||
|
||||
refresh()
|
||||
fab.isEnabled = true
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -37,15 +38,19 @@ class LogsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_logs)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||
scrollView = requireViewById(R.id.scroll_view)
|
||||
logText = requireViewById(R.id.log_text)
|
||||
|
||||
setupRootViewInsets(scrollView)
|
||||
|
||||
swipeRefresh.setOnRefreshListener {
|
||||
lifecycleScope.launch {
|
||||
reload()
|
||||
|
@ -66,6 +71,10 @@ class LogsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
R.id.save -> {
|
||||
saveLogs.launch(getString(R.string.logs_filename_template,
|
||||
SimpleDateFormat.getDateTimeInstance().format(Date())
|
||||
|
|
|
@ -5,7 +5,9 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
|
@ -13,6 +15,7 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
@ -22,6 +25,7 @@ import com.google.android.material.tabs.TabLayoutMediator
|
|||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -29,6 +33,8 @@ import kotlinx.coroutines.withContext
|
|||
open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||
companion object {
|
||||
const val TAG = "MainActivity"
|
||||
|
||||
const val PERMISSION_REQUEST_CODE = 1000
|
||||
}
|
||||
|
||||
private lateinit var loadingProgress: ProgressBar
|
||||
|
@ -64,9 +70,11 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
|
||||
@SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
loadingProgress = requireViewById(R.id.loading)
|
||||
tabs = requireViewById(R.id.main_tabs)
|
||||
viewPager = requireViewById(R.id.view_pager)
|
||||
|
@ -113,16 +121,29 @@ 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) {
|
||||
refreshing = true // We don't check this here -- the check happens in refresh()
|
||||
loadingProgress.visibility = View.VISIBLE
|
||||
viewPager.visibility = View.GONE
|
||||
tabs.visibility = View.GONE
|
||||
// Prevent concurrent access with any running foreground task
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
val knownChannels = withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.enumerateEuiccChannels().onEach {
|
||||
Log.d(TAG, "slot ${it.slotId} port ${it.portId}")
|
||||
Log.d(TAG, it.lpa.eID)
|
||||
if (preferenceRepository.verboseLoggingFlow.first()) {
|
||||
Log.d(TAG, it.lpa.eID)
|
||||
}
|
||||
// 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,
|
||||
// but it could change in the future
|
||||
|
@ -170,6 +191,10 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
viewPager.currentItem = 0
|
||||
}
|
||||
|
||||
if (pages.size > 0) {
|
||||
ensureNotificationPermissions()
|
||||
}
|
||||
|
||||
refreshing = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.view.MenuItem.OnMenuItemClickListener
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.forEach
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -35,31 +36,34 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
private lateinit var euiccChannel: EuiccChannel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_notifications)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onInit() {
|
||||
euiccChannel = euiccChannelManager
|
||||
.findEuiccChannelBySlotBlocking(intent.getIntExtra("logicalSlotId", 0))!!
|
||||
|
||||
swipeRefresh = requireViewById(R.id.swipe_refresh)
|
||||
notificationList = requireViewById(R.id.recycler_view)
|
||||
|
||||
setupRootViewInsets(notificationList)
|
||||
}
|
||||
|
||||
override fun onInit() {
|
||||
notificationList.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
notificationList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
notificationList.adapter = notificationAdapter
|
||||
registerForContextMenu(notificationList)
|
||||
|
||||
val logicalSlotId = intent.getIntExtra("logicalSlotId", 0)
|
||||
|
||||
// This is slightly different from the MainActivity logic
|
||||
// due to the length (we don't want to display the full USB product name)
|
||||
val channelTitle = if (euiccChannel.logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
val channelTitle = if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
|
||||
getString(R.string.usb)
|
||||
} else {
|
||||
getString(R.string.channel_name_format, euiccChannel.logicalSlotId)
|
||||
getString(R.string.channel_name_format, logicalSlotId)
|
||||
}
|
||||
|
||||
title = getString(R.string.profile_notifications_detailed_format, channelTitle)
|
||||
|
@ -100,6 +104,18 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
swipeRefresh.isRefreshing = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
if (!this@NotificationsActivity::euiccChannel.isInitialized) {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannelManagerLoaded.await()
|
||||
euiccChannel = euiccChannelManager.findEuiccChannelBySlotBlocking(
|
||||
intent.getIntExtra(
|
||||
"logicalSlotId",
|
||||
0
|
||||
)
|
||||
)!!
|
||||
}
|
||||
}
|
||||
|
||||
task()
|
||||
|
||||
swipeRefresh.isRefreshing = false
|
||||
|
|
|
@ -3,16 +3,15 @@ package im.angry.openeuicc.ui
|
|||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.Exception
|
||||
|
||||
class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
||||
companion object {
|
||||
|
@ -67,23 +66,27 @@ class ProfileDeleteFragment : DialogFragment(), EuiccChannelFragmentMarker {
|
|||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
doDelete()
|
||||
} catch (e: Exception) {
|
||||
Log.d(ProfileDownloadFragment.TAG, "Error deleting profile")
|
||||
Log.d(ProfileDownloadFragment.TAG, Log.getStackTraceString(e))
|
||||
} finally {
|
||||
requireParentFragment().lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
|
||||
euiccChannelManagerService.launchProfileDeleteTask(
|
||||
slotId,
|
||||
portId,
|
||||
requireArguments().getString("iccid")!!
|
||||
)!!.onStart {
|
||||
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()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
try {
|
||||
dismiss()
|
||||
} catch (e: IllegalStateException) {
|
||||
// Ignored
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doDelete() = beginTrackedOperation {
|
||||
channel.lpa.deleteProfile(requireArguments().getString("iccid")!!)
|
||||
preferenceRepository.notificationDeleteFlow.first()
|
||||
}
|
||||
}
|
|
@ -18,12 +18,13 @@ import com.google.android.material.textfield.TextInputLayout
|
|||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
import kotlin.Exception
|
||||
|
||||
class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
||||
|
@ -55,7 +56,6 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
|
||||
private val barcodeScannerLauncher = registerForActivityResult(ScanContract()) { result ->
|
||||
result.contents?.let { content ->
|
||||
Log.d(TAG, content)
|
||||
onScanResult(content)
|
||||
}
|
||||
}
|
||||
|
@ -149,15 +149,22 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
@SuppressLint("MissingPermission")
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
profileDownloadIMEI.editText!!.text = Editable.Factory.getInstance().newEditable(
|
||||
try {
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
ensureEuiccChannelManager()
|
||||
if (euiccChannelManagerService.isForegroundTaskRunning) {
|
||||
withContext(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val imei = try {
|
||||
telephonyManager.getImei(channel.logicalSlotId) ?: ""
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
// Fetch remaining NVRAM
|
||||
val str = channel.lpa.euiccInfo2?.freeNvram?.also {
|
||||
freeNvram = it
|
||||
|
@ -166,6 +173,8 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
withContext(Dispatchers.Main) {
|
||||
profileDownloadFreeSpace.text = getString(R.string.profile_download_free_space,
|
||||
str ?: getText(R.string.unknown))
|
||||
profileDownloadIMEI.editText!!.text =
|
||||
Editable.Factory.getInstance().newEditable(imei)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,17 +213,28 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
progress.visibility = View.VISIBLE
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
doDownloadProfile(server, code, confirmationCode, imei)
|
||||
} catch (e: Exception) {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
val res = doDownloadProfile(server, code, confirmationCode, imei)
|
||||
|
||||
if (res == null || res.error != null) {
|
||||
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()
|
||||
|
||||
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 {
|
||||
dismiss()
|
||||
} catch (e: IllegalStateException) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,30 +244,26 @@ class ProfileDownloadFragment : BaseMaterialDialogFragment(),
|
|||
code: String?,
|
||||
confirmationCode: String?,
|
||||
imei: String?
|
||||
) = beginTrackedOperation {
|
||||
val res = channel.lpa.downloadProfile(
|
||||
) = withContext(Dispatchers.Main) {
|
||||
// The service is responsible for launching the actual blocking part on the IO context
|
||||
val res = euiccChannelManagerService.launchProfileDownloadTask(
|
||||
slotId,
|
||||
portId,
|
||||
server,
|
||||
code,
|
||||
imei,
|
||||
confirmationCode,
|
||||
object : ProfileDownloadCallback {
|
||||
override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
progress.isIndeterminate = false
|
||||
progress.progress = state.progress
|
||||
}
|
||||
}
|
||||
})
|
||||
imei
|
||||
)!!.onEach {
|
||||
if (it is EuiccChannelManagerService.ForegroundTaskState.InProgress) {
|
||||
progress.progress = it.progress
|
||||
progress.isIndeterminate = it.progress == 0
|
||||
} else {
|
||||
progress.progress = 100
|
||||
progress.isIndeterminate = false
|
||||
}
|
||||
}.last()
|
||||
|
||||
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()
|
||||
res as? EuiccChannelManagerService.ForegroundTaskState.Done
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package im.angry.openeuicc.ui
|
|||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -13,11 +12,8 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.google.android.material.textfield.TextInputLayout
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.Exception
|
||||
import java.lang.RuntimeException
|
||||
|
||||
class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragmentMarker {
|
||||
companion object {
|
||||
|
@ -97,23 +93,24 @@ class ProfileRenameFragment : BaseMaterialDialogFragment(), EuiccChannelFragment
|
|||
progress.visibility = View.VISIBLE
|
||||
|
||||
lifecycleScope.launch {
|
||||
ensureEuiccChannelManager()
|
||||
euiccChannelManagerService.waitForForegroundTask()
|
||||
euiccChannelManagerService.launchProfileRenameTask(
|
||||
slotId,
|
||||
portId,
|
||||
requireArguments().getString("iccid")!!,
|
||||
name
|
||||
)?.collect()
|
||||
|
||||
if (parentFragment is EuiccProfilesChangedListener) {
|
||||
(parentFragment as EuiccProfilesChangedListener).onEuiccProfilesChanged()
|
||||
}
|
||||
|
||||
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()
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,14 +2,18 @@ package im.angry.openeuicc.ui
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.util.*
|
||||
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings_container, SettingsFragment())
|
||||
|
|
|
@ -3,6 +3,9 @@ package im.angry.openeuicc.ui
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.CheckBoxPreference
|
||||
|
@ -44,6 +47,14 @@ class SettingsFragment: PreferenceFragmentCompat() {
|
|||
|
||||
findPreference<CheckBoxPreference>("pref_advanced_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>) {
|
||||
|
|
|
@ -4,9 +4,8 @@ import android.os.Bundle
|
|||
import androidx.fragment.app.Fragment
|
||||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.service.EuiccChannelManagerService
|
||||
import im.angry.openeuicc.ui.BaseEuiccAccessActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface EuiccChannelFragmentMarker: OpenEuiccContextMarker
|
||||
|
||||
|
@ -35,18 +34,15 @@ val <T> T.isUsb: Boolean where T: Fragment, T: EuiccChannelFragmentMarker
|
|||
|
||||
val <T> T.euiccChannelManager: EuiccChannelManager where T: Fragment, T: EuiccChannelFragmentMarker
|
||||
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
|
||||
get() =
|
||||
euiccChannelManager.findEuiccChannelByPortBlocking(slotId, portId)!!
|
||||
|
||||
suspend fun <T> T.ensureEuiccChannelManager() where T: Fragment, T: EuiccChannelFragmentMarker =
|
||||
(requireActivity() as BaseEuiccAccessActivity).euiccChannelManagerLoaded.await()
|
||||
|
||||
interface EuiccProfilesChangedListener {
|
||||
fun onEuiccProfilesChanged()
|
||||
}
|
||||
|
||||
suspend fun <T> T.beginTrackedOperation(op: suspend () -> Boolean) where T: Fragment, T: EuiccChannelFragmentMarker {
|
||||
withContext(Dispatchers.IO) {
|
||||
euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) {
|
||||
op()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -73,6 +73,42 @@ fun LocalProfileAssistant.disableActiveProfileWithUndo(refreshOnDisable: Boolean
|
|||
* 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.
|
||||
*/
|
||||
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(
|
||||
slotId: Int,
|
||||
portId: Int,
|
||||
|
|
|
@ -24,6 +24,7 @@ object PreferenceKeys {
|
|||
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
|
||||
val NOTIFICATION_SWITCH = booleanPreferencesKey("notification_switch")
|
||||
val DISABLE_SAFEGUARD_REMOVABLE_ESIM = booleanPreferencesKey("disable_safeguard_removable_esim")
|
||||
val VERBOSE_LOGGING = booleanPreferencesKey("verbose_logging")
|
||||
}
|
||||
|
||||
class PreferenceRepository(context: Context) {
|
||||
|
@ -44,6 +45,9 @@ class PreferenceRepository(context: Context) {
|
|||
val disableSafeguardFlow: Flow<Boolean> =
|
||||
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) {
|
||||
dataStore.edit {
|
||||
it[key] = value
|
||||
|
|
|
@ -2,8 +2,16 @@ package im.angry.openeuicc.util
|
|||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
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 im.angry.openeuicc.common.R
|
||||
|
||||
// Source: <https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height>
|
||||
/**
|
||||
|
@ -26,3 +34,39 @@ fun DialogFragment.setWidthPercent(percentage: Int) {
|
|||
fun DialogFragment.setFullScreen() {
|
||||
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
|
||||
}
|
||||
}
|
5
app-common/src/main/res/drawable/ic_task_delete.xml
Normal file
5
app-common/src/main/res/drawable/ic_task_delete.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<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>
|
5
app-common/src/main/res/drawable/ic_task_rename.xml
Normal file
5
app-common/src/main/res/drawable/ic_task_rename.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<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>
|
|
@ -0,0 +1,5 @@
|
|||
<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>
|
5
app-common/src/main/res/drawable/ic_task_switch.xml
Normal file
5
app-common/src/main/res/drawable/ic_task_switch.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<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>
|
5
app-common/src/main/res/drawable/ic_x_black.xml
Normal file
5
app-common/src/main/res/drawable/ic_x_black.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<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>
|
|
@ -5,13 +5,7 @@
|
|||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<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" />
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
|
|
|
@ -5,13 +5,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<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" />
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/main_tabs"
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<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" />
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<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" />
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings_container"
|
||||
|
|
22
app-common/src/main/res/layout/toolbar_activity.xml
Normal file
22
app-common/src/main/res/layout/toolbar_activity.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?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>
|
|
@ -33,6 +33,16 @@
|
|||
<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="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_server">Server (RSP / SM-DP+)</string>
|
||||
<string name="profile_download_code">Activation Code</string>
|
||||
|
@ -74,8 +84,10 @@
|
|||
<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_advanced">Advanced</string>
|
||||
<string name="pref_advanced_disable_safeguard_removable_esim">Disable Safeguards for Removable eSIMs</string>
|
||||
<string name="pref_advanced_disable_safeguard_removable_esim">Allow Disabling / Deleting Active Profile</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_desc">View recent debug logs of the application</string>
|
||||
<string name="pref_info">Info</string>
|
||||
|
|
|
@ -30,6 +30,12 @@
|
|||
app:title="@string/pref_advanced_disable_safeguard_removable_esim"
|
||||
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
|
||||
app:key="pref_advanced_logs"
|
||||
app:iconSpaceReserved="false"
|
||||
|
|
|
@ -8,6 +8,7 @@ java_defaults {
|
|||
"androidx-constraintlayout_constraintlayout",
|
||||
"androidx.preference_preference",
|
||||
"androidx.lifecycle_lifecycle-runtime-ktx",
|
||||
"androidx.lifecycle_lifecycle-service",
|
||||
"androidx.swiperefreshlayout_swiperefreshlayout",
|
||||
"androidx.cardview_cardview",
|
||||
"androidx.viewpager2_viewpager2",
|
||||
|
|
|
@ -48,6 +48,7 @@ dependencies {
|
|||
//noinspection KtxExtensionAvailable
|
||||
api("androidx.preference:preference:1.2.1")
|
||||
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.cardview:cardview:1.0.0")
|
||||
api("androidx.viewpager2:viewpager2:1.1.0")
|
||||
|
|
|
@ -17,13 +17,13 @@ apply {
|
|||
|
||||
android {
|
||||
namespace = "im.angry.easyeuicc"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
ndkVersion = "26.1.10909125"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "im.angry.easyeuicc"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.children
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -22,9 +23,11 @@ class CompatibilityCheckActivity: AppCompatActivity() {
|
|||
private val adapter = CompatibilityChecksAdapter()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_compatibility_check)
|
||||
setSupportActionBar(requireViewById(R.id.toolbar))
|
||||
setSupportActionBar(requireViewById(im.angry.openeuicc.common.R.id.toolbar))
|
||||
setupToolbarInsets()
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
compatibilityCheckList = requireViewById(R.id.recycler_view)
|
||||
|
@ -32,6 +35,8 @@ class CompatibilityCheckActivity: AppCompatActivity() {
|
|||
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||||
compatibilityCheckList.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
compatibilityCheckList.adapter = adapter
|
||||
|
||||
setupRootViewInsets(compatibilityCheckList)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<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" />
|
||||
<include layout="@layout/toolbar_activity" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?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>
|
|
@ -0,0 +1,6 @@
|
|||
<?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>
|
|
@ -12,12 +12,12 @@ apply {
|
|||
|
||||
android {
|
||||
namespace = "im.angry.openeuicc"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "im.angry.openeuicc"
|
||||
minSdk = 30
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
|
@ -26,7 +26,15 @@ class PrivilegedEuiccChannelFactory(context: Context) : DefaultEuiccChannelFacto
|
|||
"Trying TelephonyManager for slot ${port.card.physicalSlotIndex} port ${port.portIndex}"
|
||||
)
|
||||
try {
|
||||
return EuiccChannel(port, TelephonyManagerApduInterface(port, tm))
|
||||
return EuiccChannel(
|
||||
port,
|
||||
TelephonyManagerApduInterface(
|
||||
port,
|
||||
tm,
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
),
|
||||
context.preferenceRepository.verboseLoggingFlow
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Failed
|
||||
Log.w(
|
||||
|
|
|
@ -2,13 +2,22 @@ package im.angry.openeuicc.core
|
|||
|
||||
import android.telephony.IccOpenLogicalChannelResponse
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
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
|
||||
|
||||
class TelephonyManagerApduInterface(
|
||||
private val port: UiccPortInfoCompat,
|
||||
private val tm: TelephonyManager
|
||||
private val tm: TelephonyManager,
|
||||
private val verboseLoggingFlow: Flow<Boolean>
|
||||
): ApduInterface {
|
||||
companion object {
|
||||
const val TAG = "TelephonyManagerApduInterface"
|
||||
}
|
||||
|
||||
private var lastChannel: Int = -1
|
||||
|
||||
override val valid: Boolean
|
||||
|
@ -45,6 +54,10 @@ class TelephonyManagerApduInterface(
|
|||
override fun transmit(tx: ByteArray): ByteArray {
|
||||
check(lastChannel != -1) { "Uninitialized" }
|
||||
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
|
||||
}
|
||||
|
||||
val cla = tx[0].toUByte().toInt()
|
||||
val instruction = tx[1].toUByte().toInt()
|
||||
val p1 = tx[2].toUByte().toInt()
|
||||
|
@ -53,7 +66,17 @@ class TelephonyManagerApduInterface(
|
|||
val p4 = tx.drop(5).toByteArray().encodeHex()
|
||||
|
||||
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
|
||||
cla, instruction, p1, p2, p3, p4)?.decodeHex() ?: byteArrayOf()
|
||||
cla,
|
||||
instruction,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4
|
||||
).also {
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "TelephonyManager APDU response: $it")
|
||||
}
|
||||
}?.decodeHex() ?: byteArrayOf()
|
||||
}
|
||||
|
||||
}
|
|
@ -2,14 +2,28 @@ package im.angry.openeuicc.ui
|
|||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
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
|
||||
|
||||
class LuiActivity : AppCompatActivity() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
enableEdgeToEdge()
|
||||
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() }
|
||||
// TODO: Deactivate LuiActivity if there is no eSIM found.
|
||||
// TODO: Support pre-filled download info (from carrier apps); UX
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/lui_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurfaceVariant">
|
||||
|
|
6
app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?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>
|
6
app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?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>
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "net.typeblog.lpac_jni"
|
||||
compileSdk = 34
|
||||
compileSdk = 35
|
||||
ndkVersion = "26.1.10909125"
|
||||
|
||||
defaultConfig {
|
||||
|
|
|
@ -8,6 +8,13 @@ interface LocalProfileAssistant {
|
|||
// Extended EuiccInfo for use with LUIs, containing information such as firmware version
|
||||
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
|
||||
// The IO context in Kotlin's coroutine library is recommended.
|
||||
fun enableProfile(iccid: String, refresh: Boolean = true): Boolean
|
||||
|
|
|
@ -9,6 +9,7 @@ internal object LpacJni {
|
|||
external fun destroyContext(handle: Long)
|
||||
|
||||
external fun euiccInit(handle: Long): Int
|
||||
external fun euiccSetMss(handle: Long, mss: Byte)
|
||||
external fun euiccFini(handle: Long)
|
||||
|
||||
// es10c
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package net.typeblog.lpac_jni.impl
|
||||
|
||||
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 java.net.URL
|
||||
import java.security.SecureRandom
|
||||
|
@ -9,7 +12,7 @@ import javax.net.ssl.SSLContext
|
|||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
|
||||
class HttpInterfaceImpl: HttpInterface {
|
||||
class HttpInterfaceImpl(private val verboseLoggingFlow: Flow<Boolean>) : HttpInterface {
|
||||
companion object {
|
||||
private const val TAG = "HttpInterfaceImpl"
|
||||
}
|
||||
|
@ -23,6 +26,10 @@ class HttpInterfaceImpl: HttpInterface {
|
|||
): HttpInterface.HttpResponse {
|
||||
Log.d(TAG, "transmit(url = $url)")
|
||||
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "HTTP tx = ${tx.decodeToString(throwOnInvalidSequence = false)}")
|
||||
}
|
||||
|
||||
val parsedUrl = URL(url)
|
||||
if (parsedUrl.protocol != "https") {
|
||||
throw IllegalArgumentException("SM-DP+ servers must use the HTTPS protocol")
|
||||
|
@ -34,6 +41,12 @@ class HttpInterfaceImpl: HttpInterface {
|
|||
|
||||
val conn = parsedUrl.openConnection() as HttpsURLConnection
|
||||
conn.connectTimeout = 2000
|
||||
|
||||
if (url.contains("handleNotification")) {
|
||||
conn.connectTimeout = 1000
|
||||
conn.readTimeout = 1000
|
||||
}
|
||||
|
||||
conn.sslSocketFactory = sslContext.socketFactory
|
||||
conn.requestMethod = "POST"
|
||||
conn.doInput = true
|
||||
|
@ -50,7 +63,16 @@ class HttpInterfaceImpl: HttpInterface {
|
|||
|
||||
Log.d(TAG, "transmit responseCode = ${conn.responseCode}")
|
||||
|
||||
return HttpInterface.HttpResponse(conn.responseCode, conn.inputStream.readBytes())
|
||||
val bytes = conn.inputStream.readBytes().also {
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"HTTP response body = ${it.decodeToString(throwOnInvalidSequence = false)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return HttpInterface.HttpResponse(conn.responseCode, bytes)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
|
|
|
@ -30,6 +30,10 @@ class LocalProfileAssistantImpl(
|
|||
httpInterface.usePublicKeyIds(pkids)
|
||||
}
|
||||
|
||||
override fun setEs10xMss(mss: Byte) {
|
||||
LpacJni.euiccSetMss(contextHandle, mss)
|
||||
}
|
||||
|
||||
override val valid: Boolean
|
||||
get() = !finalized && apduInterface.valid && try {
|
||||
// If we can read both eID and euiccInfo2 properly, we are likely looking at
|
||||
|
@ -42,6 +46,7 @@ class LocalProfileAssistantImpl(
|
|||
}
|
||||
|
||||
override val profiles: List<LocalProfileInfo>
|
||||
@Synchronized
|
||||
get() {
|
||||
val head = LpacJni.es10cGetProfilesInfo(contextHandle)
|
||||
var curr = head
|
||||
|
@ -66,6 +71,7 @@ class LocalProfileAssistantImpl(
|
|||
}
|
||||
|
||||
override val notifications: List<LocalProfileNotification>
|
||||
@Synchronized
|
||||
get() {
|
||||
val head = LpacJni.es10bListNotification(contextHandle)
|
||||
var curr = head
|
||||
|
@ -84,9 +90,11 @@ class LocalProfileAssistantImpl(
|
|||
}
|
||||
|
||||
override val eID: String
|
||||
@Synchronized
|
||||
get() = LpacJni.es10cGetEid(contextHandle)!!
|
||||
|
||||
override val euiccInfo2: EuiccInfo2?
|
||||
@Synchronized
|
||||
get() {
|
||||
val cInfo = LpacJni.es10cexGetEuiccInfo2(contextHandle)
|
||||
if (cInfo == 0L) return null
|
||||
|
@ -122,12 +130,15 @@ class LocalProfileAssistantImpl(
|
|||
return ret
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun enableProfile(iccid: String, refresh: Boolean): Boolean =
|
||||
LpacJni.es10cEnableProfile(contextHandle, iccid, refresh) == 0
|
||||
|
||||
@Synchronized
|
||||
override fun disableProfile(iccid: String, refresh: Boolean): Boolean =
|
||||
LpacJni.es10cDisableProfile(contextHandle, iccid, refresh) == 0
|
||||
|
||||
@Synchronized
|
||||
override fun deleteProfile(iccid: String): Boolean =
|
||||
LpacJni.es10cDeleteProfile(contextHandle, iccid) == 0
|
||||
|
||||
|
@ -144,6 +155,7 @@ class LocalProfileAssistantImpl(
|
|||
) == 0
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun deleteNotification(seqNumber: Long): Boolean =
|
||||
LpacJni.es10bDeleteNotification(contextHandle, seqNumber) == 0
|
||||
|
||||
|
@ -153,6 +165,7 @@ class LocalProfileAssistantImpl(
|
|||
Log.d(TAG, "handleNotification $seqNumber = $it")
|
||||
} == 0
|
||||
|
||||
@Synchronized
|
||||
override fun setNickname(iccid: String, nickname: String): Boolean =
|
||||
LpacJni.es10cSetNickname(contextHandle, iccid, nickname) == 0
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 0011ea6cc4c045c84f7aac839c1cce7804422355
|
||||
Subproject commit a5a0516f084936e7e87cf7420fb99283fa3052ef
|
|
@ -75,6 +75,13 @@ Java_net_typeblog_lpac_1jni_LpacJni_euiccFini(JNIEnv *env, jobject thiz, jlong h
|
|||
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) {
|
||||
jbyteArray bytes = NULL;
|
||||
jstring encoding = NULL;
|
||||
|
|
|
@ -5,5 +5,7 @@
|
|||
<permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" />
|
||||
<permission name="android.permission.MODIFY_PHONE_STATE" />
|
||||
<permission name="android.permission.SECURE_ELEMENT_PRIVILEGED_OPERATION" />
|
||||
<permission name="android.permission.FOREGROUND_SERVICE" />
|
||||
<permission name="android.permission.POST_NOTIFICATIONS" />
|
||||
</privapp-permissions>
|
||||
</permissions>
|
||||
|
|
Loading…
Add table
Reference in a new issue