1
1
package im.angry.openeuicc.service
2
2
3
- import android.app.Service
4
3
import android.content.Intent
4
+ import android.content.pm.PackageManager
5
5
import android.os.Binder
6
6
import android.os.IBinder
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
7
13
import im.angry.openeuicc.core.EuiccChannelManager
8
14
import im.angry.openeuicc.util.*
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.flow.Flow
17
+ import kotlinx.coroutines.flow.MutableSharedFlow
18
+ import kotlinx.coroutines.flow.MutableStateFlow
19
+ import kotlinx.coroutines.flow.first
20
+ import kotlinx.coroutines.flow.onCompletion
21
+ import kotlinx.coroutines.flow.transformWhile
22
+ import kotlinx.coroutines.launch
23
+ import kotlinx.coroutines.withContext
24
+ import net.typeblog.lpac_jni.ProfileDownloadCallback
9
25
10
26
/* *
11
27
* An Android Service wrapper for EuiccChannelManager.
@@ -17,8 +33,20 @@ import im.angry.openeuicc.util.*
17
33
* instance of EuiccChannelManager. UI components can keep being bound to this service for
18
34
* their entire lifecycles, since the whole purpose of them is to expose the current state
19
35
* to the user.
36
+ *
37
+ * Additionally, this service is also responsible for long-running "foreground" tasks that
38
+ * are not suitable to be managed by UI components. This includes profile downloading, etc.
39
+ * When a UI component needs to run one of these tasks, they have to bind to this service
40
+ * and call one of the `launch*` methods, which will run the task inside this service's
41
+ * lifecycle context and return a Flow instance for the UI component to subscribe to its
42
+ * progress.
20
43
*/
21
- class EuiccChannelManagerService : Service (), OpenEuiccContextMarker {
44
+ class EuiccChannelManagerService : LifecycleService (), OpenEuiccContextMarker {
45
+ companion object {
46
+ private const val CHANNEL_ID = " tasks"
47
+ private const val FOREGROUND_ID = 1000
48
+ }
49
+
22
50
inner class LocalBinder : Binder () {
23
51
val service = this @EuiccChannelManagerService
24
52
}
@@ -28,14 +56,167 @@ class EuiccChannelManagerService : Service(), OpenEuiccContextMarker {
28
56
}
29
57
val euiccChannelManager: EuiccChannelManager by euiccChannelManagerDelegate
30
58
31
- override fun onBind (intent : Intent ? ): IBinder = LocalBinder ()
59
+ /* *
60
+ * The state of a "foreground" task (named so due to the need to startForeground())
61
+ */
62
+ sealed interface ForegroundTaskState {
63
+ data object Idle : ForegroundTaskState
64
+ data class InProgress (val progress : Int ) : ForegroundTaskState
65
+ data class Done (val error : Throwable ? ) : ForegroundTaskState
66
+ }
67
+
68
+ /* *
69
+ * This flow emits whenever the service has had a start command, from startService()
70
+ * The service self-starts when foreground is required, because other components
71
+ * only bind to this service and do not start it per-se.
72
+ */
73
+ private val foregroundStarted: MutableSharedFlow <Unit > = MutableSharedFlow ()
74
+
75
+ /* *
76
+ * This flow is used to emit progress updates when a foreground task is running.
77
+ */
78
+ private val foregroundTaskState: MutableStateFlow <ForegroundTaskState > =
79
+ MutableStateFlow (ForegroundTaskState .Idle )
80
+
81
+ override fun onBind (intent : Intent ): IBinder {
82
+ super .onBind(intent)
83
+ return LocalBinder ()
84
+ }
32
85
33
86
override fun onDestroy () {
34
87
super .onDestroy()
35
- // This is the whole reason of the existence of this service:
36
- // we can clean up opened channels when no one is using them
37
88
if (euiccChannelManagerDelegate.isInitialized()) {
38
89
euiccChannelManager.invalidate()
39
90
}
40
91
}
92
+
93
+ override fun onStartCommand (intent : Intent ? , flags : Int , startId : Int ): Int {
94
+ return super .onStartCommand(intent, flags, startId).also {
95
+ lifecycleScope.launch {
96
+ foregroundStarted.emit(Unit )
97
+ }
98
+ }
99
+ }
100
+
101
+ private fun updateForegroundNotification (title : String , iconRes : Int ) {
102
+ val channel =
103
+ NotificationChannelCompat .Builder (CHANNEL_ID , NotificationManagerCompat .IMPORTANCE_LOW )
104
+ .setName(getString(R .string.task_notification))
105
+ .setVibrationEnabled(false )
106
+ .build()
107
+ NotificationManagerCompat .from(this ).createNotificationChannel(channel)
108
+
109
+ val state = foregroundTaskState.value
110
+
111
+ if (state is ForegroundTaskState .InProgress ) {
112
+ val notification = NotificationCompat .Builder (this , CHANNEL_ID )
113
+ .setContentTitle(title)
114
+ .setProgress(100 , state.progress, state.progress == 0 )
115
+ .setSmallIcon(iconRes)
116
+ .setPriority(NotificationCompat .PRIORITY_LOW )
117
+ .build()
118
+
119
+ if (state.progress == 0 ) {
120
+ startForeground(FOREGROUND_ID , notification)
121
+ } else if (checkSelfPermission(android.Manifest .permission.POST_NOTIFICATIONS ) == PackageManager .PERMISSION_GRANTED ) {
122
+ NotificationManagerCompat .from(this ).notify(FOREGROUND_ID , notification)
123
+ }
124
+ } else {
125
+ stopForeground(STOP_FOREGROUND_REMOVE )
126
+ }
127
+ }
128
+
129
+ /* *
130
+ * Launch a potentially blocking foreground task in this service's lifecycle context.
131
+ * This function does not block, but returns a Flow that emits ForegroundTaskState
132
+ * updates associated with this task.
133
+ * The task closure is expected to update foregroundTaskState whenever appropriate.
134
+ * If a foreground task is already running, this function returns null.
135
+ */
136
+ private fun launchForegroundTask (
137
+ title : String ,
138
+ iconRes : Int ,
139
+ task : suspend EuiccChannelManagerService .() -> Unit
140
+ ): Flow <ForegroundTaskState >? {
141
+ // Atomically set the state to InProgress. If this returns true, we are
142
+ // the only task currently in progress.
143
+ if (! foregroundTaskState.compareAndSet(
144
+ ForegroundTaskState .Idle ,
145
+ ForegroundTaskState .InProgress (0 )
146
+ )
147
+ ) {
148
+ return null
149
+ }
150
+
151
+ lifecycleScope.launch(Dispatchers .Main ) {
152
+ // Wait until our self-start command has succeeded.
153
+ // We can only call startForeground() after that
154
+ foregroundStarted.first()
155
+ updateForegroundNotification(title, iconRes)
156
+
157
+ try {
158
+ withContext(Dispatchers .IO ) {
159
+ this @EuiccChannelManagerService.task()
160
+ }
161
+ foregroundTaskState.value = ForegroundTaskState .Done (null )
162
+ } catch (t: Throwable ) {
163
+ foregroundTaskState.value = ForegroundTaskState .Done (t)
164
+ } finally {
165
+ stopSelf()
166
+ }
167
+
168
+ updateForegroundNotification(title, iconRes)
169
+ }
170
+
171
+ // We 've launched the coroutine, now we can self-start
172
+ // This is required in order to use startForeground()
173
+ // This will end up calling onStartCommand(), which will emit
174
+ // into foregroundStarted and unblock the coroutine above
175
+ startForegroundService(Intent (this , this ::class .java))
176
+
177
+ // We should be the only task running, so we can subscribe to foregroundTaskState
178
+ // until we encounter ForegroundTaskState.Done.
179
+ return foregroundTaskState.transformWhile {
180
+ // Also update our notification when we see an update
181
+ updateForegroundNotification(title, iconRes)
182
+ emit(it)
183
+ it !is ForegroundTaskState .Done
184
+ }.onCompletion { foregroundTaskState.value = ForegroundTaskState .Idle }
185
+ }
186
+
187
+ fun launchProfileDownloadTask (
188
+ slotId : Int ,
189
+ portId : Int ,
190
+ smdp : String ,
191
+ matchingId : String? ,
192
+ confirmationCode : String? ,
193
+ imei : String?
194
+ ): Flow <ForegroundTaskState >? =
195
+ launchForegroundTask(
196
+ getString(R .string.task_profile_download),
197
+ R .drawable.ic_task_sim_card_download
198
+ ) {
199
+ euiccChannelManager.beginTrackedOperationBlocking(slotId, portId) {
200
+ val channel = euiccChannelManager.findEuiccChannelByPort(slotId, portId)
201
+ val res = channel!! .lpa.downloadProfile(
202
+ smdp,
203
+ matchingId,
204
+ imei,
205
+ confirmationCode,
206
+ object : ProfileDownloadCallback {
207
+ override fun onStateUpdate (state : ProfileDownloadCallback .DownloadState ) {
208
+ if (state.progress == 0 ) return
209
+ foregroundTaskState.value =
210
+ ForegroundTaskState .InProgress (state.progress)
211
+ }
212
+ })
213
+
214
+ if (! res) {
215
+ // TODO: Provide more details on the error
216
+ throw RuntimeException (" Failed to download profile; this is typically caused by another error happened before." )
217
+ }
218
+
219
+ preferenceRepository.notificationDownloadFlow.first()
220
+ }
221
+ }
41
222
}
0 commit comments