EuiccChannelManagerService.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. package im.angry.openeuicc.service
  2. import android.content.Intent
  3. import android.content.pm.PackageManager
  4. import android.os.Binder
  5. import android.os.IBinder
  6. import android.util.Log
  7. import androidx.core.app.NotificationChannelCompat
  8. import androidx.core.app.NotificationCompat
  9. import androidx.core.app.NotificationManagerCompat
  10. import androidx.lifecycle.LifecycleService
  11. import androidx.lifecycle.lifecycleScope
  12. import im.angry.openeuicc.common.R
  13. import im.angry.openeuicc.core.EuiccChannelManager
  14. import im.angry.openeuicc.util.*
  15. import kotlinx.coroutines.Dispatchers
  16. import kotlinx.coroutines.NonCancellable
  17. import kotlinx.coroutines.delay
  18. import kotlinx.coroutines.flow.Flow
  19. import kotlinx.coroutines.flow.MutableSharedFlow
  20. import kotlinx.coroutines.flow.MutableStateFlow
  21. import kotlinx.coroutines.flow.collect
  22. import kotlinx.coroutines.flow.first
  23. import kotlinx.coroutines.flow.onCompletion
  24. import kotlinx.coroutines.flow.onStart
  25. import kotlinx.coroutines.flow.takeWhile
  26. import kotlinx.coroutines.flow.transformWhile
  27. import kotlinx.coroutines.isActive
  28. import kotlinx.coroutines.launch
  29. import kotlinx.coroutines.withContext
  30. import kotlinx.coroutines.withTimeoutOrNull
  31. import kotlinx.coroutines.yield
  32. import net.typeblog.lpac_jni.ProfileDownloadCallback
  33. /**
  34. * An Android Service wrapper for EuiccChannelManager.
  35. * The purpose of this wrapper is mainly lifecycle-wise: having a Service allows the manager
  36. * instance to have its own independent lifecycle. This way it can be created as requested and
  37. * destroyed when no other components are bound to this service anymore.
  38. * This behavior allows us to avoid keeping the APDU channels open at all times. For example,
  39. * the EuiccService implementation should *only* bind to this service when it requires an
  40. * instance of EuiccChannelManager. UI components can keep being bound to this service for
  41. * their entire lifecycles, since the whole purpose of them is to expose the current state
  42. * to the user.
  43. *
  44. * Additionally, this service is also responsible for long-running "foreground" tasks that
  45. * are not suitable to be managed by UI components. This includes profile downloading, etc.
  46. * When a UI component needs to run one of these tasks, they have to bind to this service
  47. * and call one of the `launch*` methods, which will run the task inside this service's
  48. * lifecycle context and return a Flow instance for the UI component to subscribe to its
  49. * progress.
  50. */
  51. class EuiccChannelManagerService : LifecycleService(), OpenEuiccContextMarker {
  52. companion object {
  53. private const val TAG = "EuiccChannelManagerService"
  54. private const val CHANNEL_ID = "tasks"
  55. private const val FOREGROUND_ID = 1000
  56. private const val TASK_FAILURE_ID = 1001
  57. }
  58. inner class LocalBinder : Binder() {
  59. val service = this@EuiccChannelManagerService
  60. }
  61. private val euiccChannelManagerDelegate = lazy {
  62. appContainer.euiccChannelManagerFactory.createEuiccChannelManager(this)
  63. }
  64. val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
  65. /**
  66. * The state of a "foreground" task (named so due to the need to startForeground())
  67. */
  68. sealed interface ForegroundTaskState {
  69. data object Idle : ForegroundTaskState
  70. data class InProgress(val progress: Int) : ForegroundTaskState
  71. data class Done(val error: Throwable?) : ForegroundTaskState
  72. }
  73. /**
  74. * This flow emits whenever the service has had a start command, from startService()
  75. * The service self-starts when foreground is required, because other components
  76. * only bind to this service and do not start it per-se.
  77. */
  78. private val foregroundStarted: MutableSharedFlow<Unit> = MutableSharedFlow()
  79. /**
  80. * This flow is used to emit progress updates when a foreground task is running.
  81. */
  82. private val foregroundTaskState: MutableStateFlow<ForegroundTaskState> =
  83. MutableStateFlow(ForegroundTaskState.Idle)
  84. override fun onBind(intent: Intent): IBinder {
  85. super.onBind(intent)
  86. return LocalBinder()
  87. }
  88. override fun onDestroy() {
  89. super.onDestroy()
  90. if (euiccChannelManagerDelegate.isInitialized()) {
  91. euiccChannelManager.invalidate()
  92. }
  93. }
  94. override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  95. return super.onStartCommand(intent, flags, startId).also {
  96. lifecycleScope.launch {
  97. foregroundStarted.emit(Unit)
  98. }
  99. }
  100. }
  101. private fun ensureForegroundTaskNotificationChannel() {
  102. val nm = NotificationManagerCompat.from(this)
  103. if (nm.getNotificationChannelCompat(CHANNEL_ID) == null) {
  104. val channel =
  105. NotificationChannelCompat.Builder(
  106. CHANNEL_ID,
  107. NotificationManagerCompat.IMPORTANCE_LOW
  108. )
  109. .setName(getString(R.string.task_notification))
  110. .setVibrationEnabled(false)
  111. .build()
  112. nm.createNotificationChannel(channel)
  113. }
  114. }
  115. private suspend fun updateForegroundNotification(title: String, iconRes: Int) {
  116. ensureForegroundTaskNotificationChannel()
  117. val nm = NotificationManagerCompat.from(this)
  118. val state = foregroundTaskState.value
  119. if (state is ForegroundTaskState.InProgress) {
  120. val notification = NotificationCompat.Builder(this, CHANNEL_ID)
  121. .setContentTitle(title)
  122. .setProgress(100, state.progress, state.progress == 0)
  123. .setSmallIcon(iconRes)
  124. .setPriority(NotificationCompat.PRIORITY_LOW)
  125. .setOngoing(true)
  126. .setOnlyAlertOnce(true)
  127. .build()
  128. if (state.progress == 0) {
  129. startForeground(FOREGROUND_ID, notification)
  130. } else if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
  131. nm.notify(FOREGROUND_ID, notification)
  132. }
  133. // Yield out so that the main looper can handle the notification event
  134. // Without this yield, the notification sent above will not be shown in time.
  135. yield()
  136. } else {
  137. stopForeground(STOP_FOREGROUND_REMOVE)
  138. }
  139. }
  140. private fun postForegroundTaskFailureNotification(title: String) {
  141. if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
  142. return
  143. }
  144. val notification = NotificationCompat.Builder(this, CHANNEL_ID)
  145. .setContentTitle(title)
  146. .setSmallIcon(R.drawable.ic_x_black)
  147. .build()
  148. NotificationManagerCompat.from(this).notify(TASK_FAILURE_ID, notification)
  149. }
  150. /**
  151. * Launch a potentially blocking foreground task in this service's lifecycle context.
  152. * This function does not block, but returns a Flow that emits ForegroundTaskState
  153. * updates associated with this task. The last update the returned flow will emit is
  154. * always ForegroundTaskState.Done. The returned flow MUST be started in order for the
  155. * foreground task to run.
  156. *
  157. * The task closure is expected to update foregroundTaskState whenever appropriate.
  158. * If a foreground task is already running, this function returns null.
  159. *
  160. * To wait for foreground tasks to be available, use waitForForegroundTask().
  161. *
  162. * The function will set the state back to Idle once it sees ForegroundTaskState.Done.
  163. */
  164. private fun launchForegroundTask(
  165. title: String,
  166. failureTitle: String,
  167. iconRes: Int,
  168. task: suspend EuiccChannelManagerService.() -> Unit
  169. ): Flow<ForegroundTaskState>? {
  170. // Atomically set the state to InProgress. If this returns true, we are
  171. // the only task currently in progress.
  172. if (!foregroundTaskState.compareAndSet(
  173. ForegroundTaskState.Idle,
  174. ForegroundTaskState.InProgress(0)
  175. )
  176. ) {
  177. return null
  178. }
  179. lifecycleScope.launch(Dispatchers.Main) {
  180. // Wait until our self-start command has succeeded.
  181. // We can only call startForeground() after that
  182. val res = withTimeoutOrNull(30 * 1000) {
  183. foregroundStarted.first()
  184. }
  185. if (res == null) {
  186. // The only case where the wait above could time out is if the subscriber
  187. // to the flow is stuck. Or we failed to start foreground.
  188. // In that case, we should just set our state back to Idle -- setting it
  189. // to Done wouldn't help much because nothing is going to then set it Idle.
  190. foregroundTaskState.value = ForegroundTaskState.Idle
  191. return@launch
  192. }
  193. updateForegroundNotification(title, iconRes)
  194. try {
  195. withContext(Dispatchers.IO + NonCancellable) { // Any LPA-related task must always complete
  196. this@EuiccChannelManagerService.task()
  197. }
  198. // This update will be sent by the subscriber (as shown below)
  199. foregroundTaskState.value = ForegroundTaskState.Done(null)
  200. } catch (t: Throwable) {
  201. Log.e(TAG, "Foreground task encountered an error")
  202. Log.e(TAG, Log.getStackTraceString(t))
  203. foregroundTaskState.value = ForegroundTaskState.Done(t)
  204. if (isActive) {
  205. postForegroundTaskFailureNotification(failureTitle)
  206. }
  207. } finally {
  208. if (isActive) {
  209. stopSelf()
  210. }
  211. }
  212. }
  213. // We should be the only task running, so we can subscribe to foregroundTaskState
  214. // until we encounter ForegroundTaskState.Done.
  215. // Then, we complete the returned flow, but we also set the state back to Idle.
  216. // The state update back to Idle won't show up in the returned stream, because
  217. // it has been completed by that point.
  218. return foregroundTaskState.transformWhile {
  219. // Also update our notification when we see an update
  220. // But ignore the first progress = 0 update -- that is the current value.
  221. // we need that to be handled by the main coroutine after it finishes.
  222. if (it !is ForegroundTaskState.InProgress || it.progress != 0) {
  223. withContext(Dispatchers.Main) {
  224. updateForegroundNotification(title, iconRes)
  225. }
  226. }
  227. emit(it)
  228. it !is ForegroundTaskState.Done
  229. }.onStart {
  230. // When this Flow is started, we unblock the coroutine launched above by
  231. // self-starting as a foreground service.
  232. withContext(Dispatchers.Main) {
  233. startForegroundService(
  234. Intent(
  235. this@EuiccChannelManagerService,
  236. this@EuiccChannelManagerService::class.java
  237. )
  238. )
  239. }
  240. }.onCompletion { foregroundTaskState.value = ForegroundTaskState.Idle }
  241. }
  242. val isForegroundTaskRunning: Boolean
  243. get() = foregroundTaskState.value != ForegroundTaskState.Idle
  244. suspend fun waitForForegroundTask() {
  245. foregroundTaskState.takeWhile { it != ForegroundTaskState.Idle }
  246. .collect()
  247. }
  248. fun launchProfileDownloadTask(
  249. slotId: Int,
  250. portId: Int,
  251. smdp: String,
  252. matchingId: String?,
  253. confirmationCode: String?,
  254. imei: String?
  255. ): Flow<ForegroundTaskState>? =
  256. launchForegroundTask(
  257. getString(R.string.task_profile_download),
  258. getString(R.string.task_profile_download_failure),
  259. R.drawable.ic_task_sim_card_download
  260. ) {
  261. euiccChannelManager.beginTrackedOperation(slotId, portId) {
  262. val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)
  263. val res = channel!!.lpa.downloadProfile(
  264. smdp,
  265. matchingId,
  266. imei,
  267. confirmationCode,
  268. object : ProfileDownloadCallback {
  269. override fun onStateUpdate(state: ProfileDownloadCallback.DownloadState) {
  270. if (state.progress == 0) return
  271. foregroundTaskState.value =
  272. ForegroundTaskState.InProgress(state.progress)
  273. }
  274. })
  275. if (!res) {
  276. // TODO: Provide more details on the error
  277. throw RuntimeException("Failed to download profile; this is typically caused by another error happened before.")
  278. }
  279. preferenceRepository.notificationDownloadFlow.first()
  280. }
  281. }
  282. fun launchProfileRenameTask(
  283. slotId: Int,
  284. portId: Int,
  285. iccid: String,
  286. name: String
  287. ): Flow<ForegroundTaskState>? =
  288. launchForegroundTask(
  289. getString(R.string.task_profile_rename),
  290. getString(R.string.task_profile_rename_failure),
  291. R.drawable.ic_task_rename
  292. ) {
  293. val res = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!.lpa.setNickname(
  294. iccid,
  295. name
  296. )
  297. if (!res) {
  298. throw RuntimeException("Profile not renamed")
  299. }
  300. }
  301. fun launchProfileDeleteTask(
  302. slotId: Int,
  303. portId: Int,
  304. iccid: String
  305. ): Flow<ForegroundTaskState>? =
  306. launchForegroundTask(
  307. getString(R.string.task_profile_delete),
  308. getString(R.string.task_profile_delete_failure),
  309. R.drawable.ic_task_delete
  310. ) {
  311. euiccChannelManager.beginTrackedOperation(slotId, portId) {
  312. euiccChannelManager.findEuiccChannelByPort(
  313. slotId,
  314. portId
  315. )!!.lpa.deleteProfile(iccid)
  316. preferenceRepository.notificationDeleteFlow.first()
  317. }
  318. }
  319. class SwitchingProfilesRefreshException : Exception()
  320. fun launchProfileSwitchTask(
  321. slotId: Int,
  322. portId: Int,
  323. iccid: String,
  324. enable: Boolean, // Enable or disable the profile indicated in iccid
  325. reconnectTimeoutMillis: Long = 0 // 0 = do not wait for reconnect, useful for USB readers
  326. ): Flow<ForegroundTaskState>? =
  327. launchForegroundTask(
  328. getString(R.string.task_profile_switch),
  329. getString(R.string.task_profile_switch_failure),
  330. R.drawable.ic_task_switch
  331. ) {
  332. euiccChannelManager.beginTrackedOperation(slotId, portId) {
  333. val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)!!
  334. val (res, refreshed) =
  335. if (!channel.lpa.switchProfile(iccid, enable, refresh = true)) {
  336. // Sometimes, we *can* enable or disable the profile, but we cannot
  337. // send the refresh command to the modem because the profile somehow
  338. // makes the modem "busy". In this case, we can still switch by setting
  339. // refresh to false, but then the switch cannot take effect until the
  340. // user resets the modem manually by toggling airplane mode or rebooting.
  341. Pair(channel.lpa.switchProfile(iccid, enable, refresh = false), false)
  342. } else {
  343. Pair(true, true)
  344. }
  345. if (!res) {
  346. throw RuntimeException("Could not switch profile")
  347. }
  348. if (!refreshed) {
  349. // We may have switched the profile, but we could not refresh. Tell the caller about this
  350. throw SwitchingProfilesRefreshException()
  351. }
  352. if (reconnectTimeoutMillis > 0) {
  353. // Add an unconditional delay first to account for any race condition between
  354. // the card sending the refresh command and the modem actually refreshing
  355. delay(reconnectTimeoutMillis / 10)
  356. // This throws TimeoutCancellationException if timed out
  357. euiccChannelManager.waitForReconnect(
  358. slotId,
  359. portId,
  360. reconnectTimeoutMillis / 10 * 9
  361. )
  362. }
  363. preferenceRepository.notificationSwitchFlow.first()
  364. }
  365. }
  366. }