ソースを参照

feat: Notification handling [2/2]

Peter Cai 2 年 前
コミット
3357a90f91

+ 1 - 0
app-common/build.gradle

@@ -39,6 +39,7 @@ dependencies {
     implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
     implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.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'
     testImplementation 'junit:junit:4.13.2'
     androidTestImplementation 'androidx.test.ext:junit:1.1.5'

+ 5 - 0
app-common/src/main/java/im/angry/openeuicc/OpenEuiccApplication.kt

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

+ 8 - 0
app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -26,8 +26,10 @@ import net.typeblog.lpac_jni.LocalProfileInfo
 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
+import net.typeblog.lpac_jni.LocalProfileNotification
 import java.lang.Exception
 
 open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfilesChangedListener {
@@ -152,11 +154,17 @@ open class EuiccManagementFragment : Fragment(), EuiccFragmentMarker, EuiccProfi
     private suspend fun doEnableProfile(iccid: String) =
         withContext(Dispatchers.IO) {
             channel.lpa.enableProfile(iccid)
+            if (preferenceRepository.notificationEnableFlow.first()) {
+                channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Enable)
+            }
         }
 
     private suspend fun doDisableProfile(iccid: String) =
         withContext(Dispatchers.IO) {
             channel.lpa.disableProfile(iccid)
+            if (preferenceRepository.notificationDisableFlow.first()) {
+                channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Disable)
+            }
         }
 
     sealed class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {

+ 6 - 0
app-common/src/main/java/im/angry/openeuicc/ui/ProfileDeleteFragment.kt

@@ -7,9 +7,12 @@ 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.preferenceRepository
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import net.typeblog.lpac_jni.LocalProfileNotification
 import java.lang.Exception
 
 class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
@@ -58,6 +61,9 @@ class ProfileDeleteFragment : DialogFragment(), EuiccFragmentMarker {
         lifecycleScope.launch {
             try {
                 doDelete()
+                if (preferenceRepository.notificationDeleteFlow.first()) {
+                    channel.lpa.handleLatestNotification(LocalProfileNotification.Operation.Delete)
+                }
             } catch (e: Exception) {
                 Log.d(ProfileDownloadFragment.TAG, "Error deleting profile")
                 Log.d(ProfileDownloadFragment.TAG, Log.getStackTraceString(e))

+ 6 - 0
app-common/src/main/java/im/angry/openeuicc/ui/ProfileDownloadFragment.kt

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

+ 31 - 0
app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt

@@ -3,10 +3,16 @@ package im.angry.openeuicc.ui
 import android.content.Intent
 import android.net.Uri
 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.PreferenceFragmentCompat
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.util.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
 
 class SettingsFragment: PreferenceFragmentCompat() {
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -20,5 +26,30 @@ class SettingsFragment: PreferenceFragmentCompat() {
                 startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.summary.toString())))
                 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
+        }
     }
 }

+ 52 - 0
app-common/src/main/java/im/angry/openeuicc/util/PreferenceUtils.kt

@@ -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
+        }
+    }
+}

+ 8 - 0
app-common/src/main/res/values/strings.xml

@@ -48,6 +48,14 @@
     <string name="pref_settings">Settings</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_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_app_version">App Version</string>
     <string name="pref_info_source_code">Source Code</string>

+ 20 - 1
app-common/src/main/res/xml/pref_settings.xml

@@ -4,7 +4,26 @@
         app:title="@string/pref_notifications"
         app:summary="@string/pref_notifications_desc"
         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>
 
     <PreferenceCategory

+ 6 - 0
libs/lpac-jni/src/main/java/net/typeblog/lpac_jni/impl/LocalProfileAssistantImpl.kt

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