feat: Notification handling [2/2]

This commit is contained in:
Peter Cai 2023-12-31 20:17:17 -05:00
parent 2fc84146da
commit 3357a90f91
10 changed files with 143 additions and 1 deletions

View file

@ -39,6 +39,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import im.angry.openeuicc.core.EuiccChannelManager import im.angry.openeuicc.core.EuiccChannelManager
import im.angry.openeuicc.util.PreferenceRepository
open class OpenEuiccApplication : Application() { open class OpenEuiccApplication : Application() {
val telephonyManager by lazy { val telephonyManager by lazy {
@ -17,4 +18,8 @@ open class OpenEuiccApplication : Application() {
val subscriptionManager by lazy { val subscriptionManager by lazy {
getSystemService(SubscriptionManager::class.java)!! getSystemService(SubscriptionManager::class.java)!!
} }
val preferenceRepository by lazy {
PreferenceRepository(this)
}
} }

View file

@ -26,8 +26,10 @@ import net.typeblog.lpac_jni.LocalProfileInfo
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileNotification
import java.lang.Exception import java.lang.Exception
open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener { open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
@ -152,11 +154,17 @@ open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfi
private suspend fun doEnableProfile(iccid: String) = private suspend fun doEnableProfile(iccid: String) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
channel.lpa.enableProfile(iccid) channel.lpa.enableProfile(iccid)
if (preferenceRepository.notificationEnableFlow.first()) {
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Enable)
}
} }
private suspend fun doDisableProfile(iccid: String) = private suspend fun doDisableProfile(iccid: String) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
channel.lpa.disableProfile(iccid) channel.lpa.disableProfile(iccid)
if (preferenceRepository.notificationDisableFlow.first()) {
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Disable)
}
} }
sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {

View file

@ -7,9 +7,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.preferenceRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileNotification
import java.lang.Exception import java.lang.Exception
class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker { class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
@ -58,6 +61,9 @@ class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
doDelete() doDelete()
if (preferenceRepository.notificationDeleteFlow.first()) {
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Delete)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d(ProfileDownloadFragment.TAG, "Error deleting profile") Log.d(ProfileDownloadFragment.TAG, "Error deleting profile")
Log.d(ProfileDownloadFragment.TAG, Log.getStackTraceString(e)) Log.d(ProfileDownloadFragment.TAG, Log.getStackTraceString(e))

View file

@ -18,10 +18,13 @@ import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.openEuiccApplication import im.angry.openeuicc.util.openEuiccApplication
import im.angry.openeuicc.util.preferenceRepository
import im.angry.openeuicc.util.setWidthPercent import im.angry.openeuicc.util.setWidthPercent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.typeblog.lpac_jni.LocalProfileNotification
import net.typeblog.lpac_jni.ProfileDownloadCallback import net.typeblog.lpac_jni.ProfileDownloadCallback
import kotlin.Exception import kotlin.Exception
@ -168,6 +171,9 @@ class ProfileDownloadFragment : DialogFragment(), EuiccFragmentMarker, Toolbar.O
lifecycleScope.launch { lifecycleScope.launch {
try { try {
doDownloadProfile(server, code, confirmationCode, imei) doDownloadProfile(server, code, confirmationCode, imei)
if (preferenceRepository.notificationDownloadFlow.first()) {
channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Install)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "Error downloading profile") Log.d(TAG, "Error downloading profile")
Log.d(TAG, Log.getStackTraceString(e)) Log.d(TAG, Log.getStackTraceString(e))

View file

@ -3,10 +3,16 @@ package im.angry.openeuicc.ui
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.datastore.preferences.core.Preferences
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class SettingsFragment: PreferenceFragmentCompat() { class SettingsFragment: PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -20,5 +26,30 @@ class SettingsFragment: PreferenceFragmentCompat() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.summary.toString()))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.summary.toString())))
true true
} }
findPreference<CheckBoxPreference>("pref_notifications_download")
?.bindBooleanFlow(preferenceRepository.notificationDownloadFlow, PreferenceKeys.NOTIFICATION_DOWNLOAD)
findPreference<CheckBoxPreference>("pref_notifications_delete")
?.bindBooleanFlow(preferenceRepository.notificationDeleteFlow, PreferenceKeys.NOTIFICATION_DELETE)
findPreference<CheckBoxPreference>("pref_notifications_enable")
?.bindBooleanFlow(preferenceRepository.notificationEnableFlow, PreferenceKeys.NOTIFICATION_ENABLE)
findPreference<CheckBoxPreference>("pref_notifications_disable")
?.bindBooleanFlow(preferenceRepository.notificationDisableFlow, PreferenceKeys.NOTIFICATION_DISABLE)
}
private fun CheckBoxPreference.bindBooleanFlow(flow: Flow<Boolean>, key: Preferences.Key<Boolean>) {
lifecycleScope.launch {
flow.collect { isChecked = it }
}
setOnPreferenceChangeListener { _, newValue ->
runBlocking {
preferenceRepository.updatePreference(key, newValue as Boolean)
}
true
}
} }
} }

View file

@ -0,0 +1,52 @@
package im.angry.openeuicc.util
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import androidx.fragment.app.Fragment
import im.angry.openeuicc.OpenEuiccApplication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "prefs")
val Context.preferenceRepository: PreferenceRepository
get() = (applicationContext as OpenEuiccApplication).preferenceRepository
val Fragment.preferenceRepository: PreferenceRepository
get() = requireContext().preferenceRepository
object PreferenceKeys {
val NOTIFICATION_DOWNLOAD = booleanPreferencesKey("notification_download")
val NOTIFICATION_DELETE = booleanPreferencesKey("notification_delete")
val NOTIFICATION_ENABLE = booleanPreferencesKey("notification_enable")
val NOTIFICATION_DISABLE = booleanPreferencesKey("notification_disable")
}
class PreferenceRepository(context: Context) {
private val dataStore = context.dataStore
// Expose flows so that we can also handle default values
// ---- Profile Notifications ----
val notificationDownloadFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DOWNLOAD] ?: true }
val notificationDeleteFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DELETE] ?: true }
// Enabling / disabling notifications are not sent by default
val notificationEnableFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_ENABLE] ?: false }
val notificationDisableFlow: Flow<Boolean> =
dataStore.data.map { it[PreferenceKeys.NOTIFICATION_DISABLE] ?: false }
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
dataStore.edit {
it[key] = value
}
}
}

View file

@ -48,6 +48,14 @@
<string name="pref_settings">Settings</string> <string name="pref_settings">Settings</string>
<string name="pref_notifications">Notifications</string> <string name="pref_notifications">Notifications</string>
<string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string> <string name="pref_notifications_desc">eSIM profile operations send notifications to the carrier. Fine-tune this behavior as needed here.</string>
<string name="pref_notifications_download">Downloads</string>
<string name="pref_notifications_download_desc">Send notifications for <i>downloading</i> profiles</string>
<string name="pref_notifications_delete">Deletion</string>
<string name="pref_notifications_delete_desc">Send notifications for <i>deleting</i> profiles</string>
<string name="pref_notifications_enable">Enabling</string>
<string name="pref_notifications_enable_desc">Send notifications for <i>enabling</i> profiles\nNote that this type of notification is unreliable.</string>
<string name="pref_notifications_disable">Disabling</string>
<string name="pref_notifications_disable_desc">Send notifications for <i>disabling</i> profiles\nNote that this type of notification is unreliable.</string>
<string name="pref_info">Info</string> <string name="pref_info">Info</string>
<string name="pref_info_app_version">App Version</string> <string name="pref_info_app_version">App Version</string>
<string name="pref_info_source_code">Source Code</string> <string name="pref_info_source_code">Source Code</string>

View file

@ -4,7 +4,26 @@
app:title="@string/pref_notifications" app:title="@string/pref_notifications"
app:summary="@string/pref_notifications_desc" app:summary="@string/pref_notifications_desc"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<CheckBoxPreference
app:iconSpaceReserved="false"
app:title="@string/pref_notifications_download"
app:summary="@string/pref_notifications_download_desc"
app:key="pref_notifications_download" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:title="@string/pref_notifications_delete"
app:summary="@string/pref_notifications_delete_desc"
app:key="pref_notifications_delete" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:title="@string/pref_notifications_enable"
app:summary="@string/pref_notifications_enable_desc"
app:key="pref_notifications_enable" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:title="@string/pref_notifications_disable"
app:summary="@string/pref_notifications_disable_desc"
app:key="pref_notifications_disable" />
</im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory> </im.angry.openeuicc.ui.preference.LongSummaryPreferenceCategory>
<PreferenceCategory <PreferenceCategory

View file

@ -1,5 +1,6 @@
package net.typeblog.lpac_jni.impl package net.typeblog.lpac_jni.impl
import android.util.Log
import net.typeblog.lpac_jni.LpacJni import net.typeblog.lpac_jni.LpacJni
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.EuiccInfo2 import net.typeblog.lpac_jni.EuiccInfo2
@ -13,6 +14,10 @@ class LocalProfileAssistantImpl(
apduInterface: ApduInterface, apduInterface: ApduInterface,
httpInterface: HttpInterface httpInterface: HttpInterface
): LocalProfileAssistant { ): LocalProfileAssistant {
companion object {
val TAG = "LocalProfileAssistantImpl"
}
private val contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface) private val contextHandle: Long = LpacJni.createContext(apduInterface, httpInterface)
init { init {
if (LpacJni.es10xInit(contextHandle) < 0) { if (LpacJni.es10xInit(contextHandle) < 0) {
@ -59,6 +64,7 @@ class LocalProfileAssistantImpl(
override fun handleLatestNotification(operation: LocalProfileNotification.Operation) { override fun handleLatestNotification(operation: LocalProfileNotification.Operation) {
notifications.find { it.profileManagementOperation == operation }?.let { notifications.find { it.profileManagementOperation == operation }?.let {
Log.d(TAG, "handleLatestNotification: $it")
handleNotification(it.seqNumber) handleNotification(it.seqNumber)
} }
} }