From 7cc5a1040b3fb1449cb426b5b930e23d32208622 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 16 Apr 2024 11:37:03 +0200 Subject: [PATCH 01/14] Added `canAccessWifiSsidLive` Signed-off-by: Arnau Mora Gras --- .../ui/account/AccountSettingsActivity.kt | 12 +-- .../bitfire/davdroid/util/PermissionUtils.kt | 88 +++++++++++++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt index 953072690..757fcd065 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt @@ -49,7 +49,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringArrayResource @@ -78,15 +77,16 @@ import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.util.TaskUtils import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod +import com.google.accompanist.permissions.ExperimentalPermissionsApi import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.openid.appauth.AuthState -import javax.inject.Inject @AndroidEntryPoint class AccountSettingsActivity: AppCompatActivity() { @@ -211,6 +211,7 @@ class AccountSettingsActivity: AppCompatActivity() { } } + @OptIn(ExperimentalPermissionsApi::class) @Composable fun SyncSettings( contactsSyncInterval: Long?, @@ -292,12 +293,7 @@ class AccountSettingsActivity: AppCompatActivity() { onDismiss = { showWifiOnlySsidsDialog = false } ) - // TODO make canAccessWifiSsid live-capable - val canAccessWifiSsid = - if (LocalInspectionMode.current) - false - else - PermissionUtils.canAccessWifiSsid(context) + val canAccessWifiSsid by PermissionUtils.canAccessWifiSsidLive() if (onlyOnSsids != null && !canAccessWifiSsid) ActionCard( icon = Icons.Default.SyncProblem, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 3c08cd97b..1c59f2e46 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -6,12 +6,23 @@ package at.bitfire.davdroid.util import android.Manifest import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.location.LocationManager import android.net.Uri import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -23,6 +34,9 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.PermissionsActivity +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import kotlinx.coroutines.flow.MutableStateFlow object PermissionUtils { @@ -73,6 +87,80 @@ object PermissionUtils { locationAvailable } + /** + * Checks whether all conditions to access the current WiFi's SSID are met: + * + * 1. location permissions ([WIFI_SSID_PERMISSIONS]) granted (Android 8.1+) + * 2. location enabled (Android 9+) + * + * @return *true* if SSID can be obtained; *false* if the SSID will be or something like that + */ + @Composable + @ExperimentalPermissionsApi + fun canAccessWifiSsidLive(): State { + // If preview, cannot access WiFi SSID + if (LocalInspectionMode.current) + return remember { derivedStateOf { false } } + + // before Android 8.1, SSIDs are always readable + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) + return remember { derivedStateOf { true } } + + val context = LocalContext.current + + val locationAvailable = MutableStateFlow(false) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val br = LocationProviderChangedReceiver(locationAvailable) + DisposableEffect(Unit) { + val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION) + context.registerReceiver(br, filter) + + onDispose { context.unregisterReceiver(br) } + } + } else { + LaunchedEffect(Unit) { + // Android <9 doesn't require active location services + locationAvailable.tryEmit(true) + } + } + + val permissions = rememberMultiplePermissionsState( + permissions = WIFI_SSID_PERMISSIONS.toList() + ) + return produceState( + initialValue = false, + permissions.allPermissionsGranted, + locationAvailable + ) { + val granted = permissions.allPermissionsGranted + val location = locationAvailable.value + value = granted && location + } + } + + /** + * Used by [canAccessWifiSsidLive] to listen for location provider changes. + */ + internal class LocationProviderChangedReceiver( + val state: MutableStateFlow + ) : BroadcastReceiver() { + + private var isGpsEnabled: Boolean = false + private var isNetworkEnabled: Boolean = false + + override fun onReceive(context: Context, intent: Intent) { + intent.action?.let { act -> + if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + + state.tryEmit(isGpsEnabled || isNetworkEnabled) + } + } + } + } + /** * Checks whether at least one of the given permissions is granted. * From 531df44107cef5c6a695fb24a1c13e9596b08458 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 16 Apr 2024 11:55:53 +0200 Subject: [PATCH 02/14] Fixed initial state Signed-off-by: Arnau Mora Gras --- .../bitfire/davdroid/util/PermissionUtils.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 1c59f2e46..5cfe3abe8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -110,7 +110,7 @@ object PermissionUtils { val locationAvailable = MutableStateFlow(false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val br = LocationProviderChangedReceiver(locationAvailable) + val br = LocationProviderChangedReceiver(context, locationAvailable) DisposableEffect(Unit) { val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION) context.registerReceiver(br, filter) @@ -140,22 +140,35 @@ object PermissionUtils { /** * Used by [canAccessWifiSsidLive] to listen for location provider changes. + * + * State will be true either if GPS is enabled or if network location is enabled. + * + * @param context The context to use for setting the initial state. Afterwards the receiver will + * use its own Context. + * @param state The state to update with the current location provider state. */ internal class LocationProviderChangedReceiver( + context: Context, val state: MutableStateFlow ) : BroadcastReceiver() { private var isGpsEnabled: Boolean = false private var isNetworkEnabled: Boolean = false + init { updateState(context) } + + private fun updateState(context: Context) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + + state.tryEmit(isGpsEnabled || isNetworkEnabled) + } + override fun onReceive(context: Context, intent: Intent) { intent.action?.let { act -> if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) - isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - - state.tryEmit(isGpsEnabled || isNetworkEnabled) + updateState(context) } } } From d34930be717b9fd1ff5ce80c91b3f90b62bf6bc3 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 16 Apr 2024 12:01:00 +0200 Subject: [PATCH 03/14] Cleaned up code Signed-off-by: Arnau Mora Gras --- .../bitfire/davdroid/util/PermissionUtils.kt | 57 +++++++------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 5cfe3abe8..c2f33bc27 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -110,11 +110,30 @@ object PermissionUtils { val locationAvailable = MutableStateFlow(false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val br = LocationProviderChangedReceiver(context, locationAvailable) + fun updateState(context: Context) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + + locationAvailable.tryEmit(isGpsEnabled || isNetworkEnabled) + } + + val br = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + intent?.action?.let { act -> + if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) { + updateState(context) + } + } + } + } DisposableEffect(Unit) { val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION) context.registerReceiver(br, filter) + // Update the initial state + updateState(context) + onDispose { context.unregisterReceiver(br) } } } else { @@ -138,42 +157,6 @@ object PermissionUtils { } } - /** - * Used by [canAccessWifiSsidLive] to listen for location provider changes. - * - * State will be true either if GPS is enabled or if network location is enabled. - * - * @param context The context to use for setting the initial state. Afterwards the receiver will - * use its own Context. - * @param state The state to update with the current location provider state. - */ - internal class LocationProviderChangedReceiver( - context: Context, - val state: MutableStateFlow - ) : BroadcastReceiver() { - - private var isGpsEnabled: Boolean = false - private var isNetworkEnabled: Boolean = false - - init { updateState(context) } - - private fun updateState(context: Context) { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) - isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - - state.tryEmit(isGpsEnabled || isNetworkEnabled) - } - - override fun onReceive(context: Context, intent: Intent) { - intent.action?.let { act -> - if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) { - updateState(context) - } - } - } - } - /** * Checks whether at least one of the given permissions is granted. * From 5da246169cbf30161a661e120faad94f8aeb1447 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Tue, 16 Apr 2024 12:04:04 +0200 Subject: [PATCH 04/14] More cleanup Signed-off-by: Arnau Mora Gras --- .../at/bitfire/davdroid/util/PermissionUtils.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index c2f33bc27..46d9e1839 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -16,7 +16,6 @@ import android.net.Uri import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.produceState @@ -108,8 +107,12 @@ object PermissionUtils { val context = LocalContext.current - val locationAvailable = MutableStateFlow(false) + val locationAvailable = MutableStateFlow( + // Android <9 doesn't require active location services, otherwise set initial state to false + Build.VERSION.SDK_INT < Build.VERSION_CODES.P + ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // Only if Android >= 9, we need to check for location services, so add listener fun updateState(context: Context) { val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) @@ -136,11 +139,6 @@ object PermissionUtils { onDispose { context.unregisterReceiver(br) } } - } else { - LaunchedEffect(Unit) { - // Android <9 doesn't require active location services - locationAvailable.tryEmit(true) - } } val permissions = rememberMultiplePermissionsState( From 13133bf24659317a703d53df06fdc19815d0d261 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sun, 28 Apr 2024 10:38:46 +0200 Subject: [PATCH 05/14] Removed inspection check Signed-off-by: Arnau Mora --- .../main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 46d9e1839..4c1c74f3a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -97,10 +96,6 @@ object PermissionUtils { @Composable @ExperimentalPermissionsApi fun canAccessWifiSsidLive(): State { - // If preview, cannot access WiFi SSID - if (LocalInspectionMode.current) - return remember { derivedStateOf { false } } - // before Android 8.1, SSIDs are always readable if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return remember { derivedStateOf { true } } From 39726d524c7f0f40b53e6f39528c94ed56c6ca74 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sun, 28 Apr 2024 10:40:43 +0200 Subject: [PATCH 06/14] Renamed function Signed-off-by: Arnau Mora --- .../at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt | 2 +- app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt index 757fcd065..0bf49df4c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt @@ -293,7 +293,7 @@ class AccountSettingsActivity: AppCompatActivity() { onDismiss = { showWifiOnlySsidsDialog = false } ) - val canAccessWifiSsid by PermissionUtils.canAccessWifiSsidLive() + val canAccessWifiSsid by PermissionUtils.canAccessWifiSsidLiveState() if (onlyOnSsids != null && !canAccessWifiSsid) ActionCard( icon = Icons.Default.SyncProblem, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 4c1c74f3a..fb9bf9534 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -95,7 +95,7 @@ object PermissionUtils { */ @Composable @ExperimentalPermissionsApi - fun canAccessWifiSsidLive(): State { + fun canAccessWifiSsidLiveState(): State { // before Android 8.1, SSIDs are always readable if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return remember { derivedStateOf { true } } From 3b78f5c4326de5237e2eac5555e2f999f947bebb Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sun, 28 Apr 2024 10:42:55 +0200 Subject: [PATCH 07/14] Using `broadcastReceiverFlow` Signed-off-by: Arnau Mora --- .../bitfire/davdroid/util/PermissionUtils.kt | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index fb9bf9534..260044c23 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.util import android.Manifest import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -15,7 +14,7 @@ import android.location.LocationManager import android.net.Uri import android.os.Build import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.produceState @@ -108,32 +107,22 @@ object PermissionUtils { ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // Only if Android >= 9, we need to check for location services, so add listener - fun updateState(context: Context) { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) - val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - - locationAvailable.tryEmit(isGpsEnabled || isNetworkEnabled) - } - - val br = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent?) { - intent?.action?.let { act -> + LaunchedEffect(Unit) { + // Update the initial state + locationAvailable.tryEmit(canAccessWifiSsid(context)) + + broadcastReceiverFlow( + context, + IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION), + null + ).collect { intent -> + intent.action?.let { act -> if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) { - updateState(context) + locationAvailable.tryEmit(canAccessWifiSsid(context)) } } } } - DisposableEffect(Unit) { - val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION) - context.registerReceiver(br, filter) - - // Update the initial state - updateState(context) - - onDispose { context.unregisterReceiver(br) } - } } val permissions = rememberMultiplePermissionsState( From 34097d6aa96a5307e1f389edc49e3ffffcdb8249 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sun, 28 Apr 2024 10:43:43 +0200 Subject: [PATCH 08/14] Simplified check Signed-off-by: Arnau Mora --- .../kotlin/at/bitfire/davdroid/util/PermissionUtils.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 260044c23..5bce1c4cb 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -115,12 +115,9 @@ object PermissionUtils { context, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION), null - ).collect { intent -> - intent.action?.let { act -> - if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) { - locationAvailable.tryEmit(canAccessWifiSsid(context)) - } - } + ).collect { + // Update the state when location services change + locationAvailable.tryEmit(canAccessWifiSsid(context)) } } } From 44b630b1992016670eba4e5ecda85e1be541d088 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sun, 28 Apr 2024 10:44:03 +0200 Subject: [PATCH 09/14] Using `MODE_CHANGED_ACTION` Signed-off-by: Arnau Mora --- app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 5bce1c4cb..10eae39ba 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -113,7 +113,7 @@ object PermissionUtils { broadcastReceiverFlow( context, - IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION), + IntentFilter(LocationManager.MODE_CHANGED_ACTION), null ).collect { // Update the state when location services change From c2aacae2d3f73a84d4a1e284e3df97ed536a3080 Mon Sep 17 00:00:00 2001 From: Arnau Mora Gras Date: Sun, 28 Apr 2024 10:45:26 +0200 Subject: [PATCH 10/14] Updated comment Signed-off-by: Arnau Mora --- .../main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 10eae39ba..6913a518c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -90,7 +90,10 @@ object PermissionUtils { * 1. location permissions ([WIFI_SSID_PERMISSIONS]) granted (Android 8.1+) * 2. location enabled (Android 9+) * - * @return *true* if SSID can be obtained; *false* if the SSID will be or something like that + * @return An state that will be: + * - `true` if SSID can be obtained + * - `false` if the SSID will be _unknown_ or something like that + * - `null` never, the state will always have a value */ @Composable @ExperimentalPermissionsApi From 65f2665ad02b5175d5b5c15c26090d467eb59c93 Mon Sep 17 00:00:00 2001 From: Arnau Mora Date: Mon, 29 Apr 2024 18:59:50 +0200 Subject: [PATCH 11/14] Set default value for immediate Signed-off-by: Arnau Mora Gras --- .../kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt index 9ad7fb24f..849c5f933 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt @@ -27,7 +27,7 @@ fun broadcastReceiverFlow( context: Context, filter: IntentFilter, flags: Int? = null, - immediate: Boolean + immediate: Boolean = true ): Flow = callbackFlow { val receiver = object: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { From c7d301b4c122f8b63fb689ad49489458661221d3 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 11 May 2024 14:31:05 +0200 Subject: [PATCH 12/14] Use derivedStateOf instead of produceState; correctly collect broadcastReceiverFlow --- .../ui/account/AccountSettingsActivity.kt | 6 +- .../davdroid/util/BroadcastReceiverFlow.kt | 2 +- .../bitfire/davdroid/util/PermissionUtils.kt | 69 ++++++++----------- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt index 0bf49df4c..d8245134e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt @@ -77,16 +77,15 @@ import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.util.TaskUtils import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod -import com.google.accompanist.permissions.ExperimentalPermissionsApi import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.openid.appauth.AuthState +import javax.inject.Inject @AndroidEntryPoint class AccountSettingsActivity: AppCompatActivity() { @@ -211,7 +210,6 @@ class AccountSettingsActivity: AppCompatActivity() { } } - @OptIn(ExperimentalPermissionsApi::class) @Composable fun SyncSettings( contactsSyncInterval: Long?, @@ -293,7 +291,7 @@ class AccountSettingsActivity: AppCompatActivity() { onDismiss = { showWifiOnlySsidsDialog = false } ) - val canAccessWifiSsid by PermissionUtils.canAccessWifiSsidLiveState() + val canAccessWifiSsid by PermissionUtils.rememberCanAccessWifiSsid() if (onlyOnSsids != null && !canAccessWifiSsid) ActionCard( icon = Icons.Default.SyncProblem, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt index 849c5f933..9ad7fb24f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/BroadcastReceiverFlow.kt @@ -27,7 +27,7 @@ fun broadcastReceiverFlow( context: Context, filter: IntentFilter, flags: Int? = null, - immediate: Boolean = true + immediate: Boolean ): Flow = callbackFlow { val receiver = object: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 6913a518c..186b289e7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -14,10 +14,10 @@ import android.location.LocationManager import android.net.Uri import android.os.Build import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.produceState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.core.app.NotificationCompat @@ -25,6 +25,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.location.LocationManagerCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger @@ -33,7 +34,8 @@ import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.PermissionsActivity import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map object PermissionUtils { @@ -85,57 +87,44 @@ object PermissionUtils { } /** - * Checks whether all conditions to access the current WiFi's SSID are met: + * Returns a live state of whether all conditions to access the current WiFi's SSID are met: * * 1. location permissions ([WIFI_SSID_PERMISSIONS]) granted (Android 8.1+) * 2. location enabled (Android 9+) * - * @return An state that will be: - * - `true` if SSID can be obtained - * - `false` if the SSID will be _unknown_ or something like that - * - `null` never, the state will always have a value + * @return `true` if SSID can be obtained reliably; `false` otherwise (SSID will be "unknown" or something like that) */ @Composable - @ExperimentalPermissionsApi - fun canAccessWifiSsidLiveState(): State { + @OptIn(ExperimentalPermissionsApi::class) + fun rememberCanAccessWifiSsid(): State { // before Android 8.1, SSIDs are always readable if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) - return remember { derivedStateOf { true } } - - val context = LocalContext.current - - val locationAvailable = MutableStateFlow( - // Android <9 doesn't require active location services, otherwise set initial state to false - Build.VERSION.SDK_INT < Build.VERSION_CODES.P - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // Only if Android >= 9, we need to check for location services, so add listener - LaunchedEffect(Unit) { - // Update the initial state - locationAvailable.tryEmit(canAccessWifiSsid(context)) + return remember { mutableStateOf(true) } + val locationAvailableFlow = + // Android 9+: dynamically check whether Location is enabled + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val context = LocalContext.current broadcastReceiverFlow( context, IntentFilter(LocationManager.MODE_CHANGED_ACTION), - null - ).collect { - // Update the state when location services change - locationAvailable.tryEmit(canAccessWifiSsid(context)) + null, + immediate = true + ).map { + val locationManager = context.getSystemService()!! + LocationManagerCompat.isLocationEnabled(locationManager) } - } - } + } else + // Android <9 doesn't require active Location to read the SSID + flowOf(true) + val locationAvailable by locationAvailableFlow.collectAsStateWithLifecycle(false) + + val permissions = rememberMultiplePermissionsState(WIFI_SSID_PERMISSIONS.toList()) - val permissions = rememberMultiplePermissionsState( - permissions = WIFI_SSID_PERMISSIONS.toList() - ) - return produceState( - initialValue = false, - permissions.allPermissionsGranted, - locationAvailable - ) { - val granted = permissions.allPermissionsGranted - val location = locationAvailable.value - value = granted && location + return remember { + derivedStateOf { + locationAvailable && permissions.allPermissionsGranted + } } } From 02273b2343b7f6c01239384748ca673a168dc81e Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 11 May 2024 14:35:18 +0200 Subject: [PATCH 13/14] Always show WifiSSID Card in Preview (otherwise Preview won't render) --- .../at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt index d8245134e..aef7ac91a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt @@ -49,6 +49,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringArrayResource @@ -292,7 +293,7 @@ class AccountSettingsActivity: AppCompatActivity() { ) val canAccessWifiSsid by PermissionUtils.rememberCanAccessWifiSsid() - if (onlyOnSsids != null && !canAccessWifiSsid) + if (LocalInspectionMode.current || (onlyOnSsids != null && !canAccessWifiSsid)) ActionCard( icon = Icons.Default.SyncProblem, actionText = stringResource(R.string.settings_sync_wifi_only_ssids_permissions_action), From c5daa426d1d1685eacdbe68592415056eae685f1 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 11 May 2024 14:45:55 +0200 Subject: [PATCH 14/14] Don't call flow.map in Composable --- .../bitfire/davdroid/util/PermissionUtils.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index 186b289e7..f3261c167 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -34,6 +34,7 @@ import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible import at.bitfire.davdroid.ui.PermissionsActivity import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -103,18 +104,9 @@ object PermissionUtils { val locationAvailableFlow = // Android 9+: dynamically check whether Location is enabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val context = LocalContext.current - broadcastReceiverFlow( - context, - IntentFilter(LocationManager.MODE_CHANGED_ACTION), - null, - immediate = true - ).map { - val locationManager = context.getSystemService()!! - LocationManagerCompat.isLocationEnabled(locationManager) - } - } else + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + locationEnabledFlow(LocalContext.current) + else // Android <9 doesn't require active Location to read the SSID flowOf(true) val locationAvailable by locationAvailableFlow.collectAsStateWithLifecycle(false) @@ -128,6 +120,17 @@ object PermissionUtils { } } + private fun locationEnabledFlow(context: Context): Flow = + broadcastReceiverFlow( + context, + IntentFilter(LocationManager.MODE_CHANGED_ACTION), + null, + immediate = true + ).map { + val locationManager = context.getSystemService()!! + LocationManagerCompat.isLocationEnabled(locationManager) + } + /** * Checks whether at least one of the given permissions is granted. *