diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 7440ecd2bf..ce9764360b 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.oauth.api) + implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) @@ -61,6 +62,7 @@ dependencies { testImplementation(projects.features.share.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oauth.test) + testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt index d2a9c9298e..66fffce9e8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -12,4 +12,7 @@ sealed interface LoggedInEvents { data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents data object CheckSlidingSyncProxyAvailability : LoggedInEvents data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents + data object DismissLocalNetworkPermissionPrompt : LoggedInEvents + + data object RequestLocationNetworkPermission : LoggedInEvents } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 752d10e7a9..fab9797c76 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.appnav.loggedin +import android.Manifest import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -15,6 +16,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -36,6 +38,9 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.PermissionsEvent +import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.services.analytics.api.AnalyticsService @@ -58,7 +63,12 @@ class LoggedInPresenter( private val encryptionService: EncryptionService, private val buildMeta: BuildMeta, private val networkMonitor: NetworkMonitor, + private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor, + permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { + private val localNetworkPermissionsPresenter: PermissionsPresenter = + permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK) + @Composable override fun present(): LoggedInState { val coroutineScope = rememberCoroutineScope() @@ -117,6 +127,21 @@ class LoggedInPresenter( } } + val localNetworkPermissionState = localNetworkPermissionsPresenter.present() + var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) } + val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) { + value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl) + } + LaunchedEffect(localNetworkPermissionState.permissionGranted) { + if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false + } + val localNetworkPermissionDialog = when { + !advisorRequestsPrompt -> LocalNetworkPermissionDialog.None + localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None + localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale + else -> LocalNetworkPermissionDialog.Settings + } + fun handleEvent(event: LoggedInEvents) { when (event) { is LoggedInEvents.CloseErrorDialog -> { @@ -134,6 +159,17 @@ class LoggedInPresenter( // Force the logout since Native Sliding Sync is already enforced by the SDK matrixClient.logout(userInitiated = true, ignoreSdkError = true) } + LoggedInEvents.DismissLocalNetworkPermissionPrompt -> { + localNetworkPromptDismissedThisSession = true + } + LoggedInEvents.RequestLocationNetworkPermission -> { + if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) { + localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog) + } else { + localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions) + } + localNetworkPromptDismissedThisSession = true + } } } @@ -143,6 +179,7 @@ class LoggedInPresenter( ignoreRegistrationError = ignoreRegistrationError, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, appName = buildMeta.applicationName, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = ::handleEvent, ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index b066f9f867..fdfff9bf75 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -16,5 +16,12 @@ data class LoggedInState( val ignoreRegistrationError: Boolean, val forceNativeSlidingSyncMigration: Boolean, val appName: String, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (LoggedInEvents) -> Unit, ) + +enum class LocalNetworkPermissionDialog { + None, + Rationale, + Settings, +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index b2f5407519..afe09ec78b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -19,6 +19,8 @@ open class LoggedInStateProvider : PreviewParameterProvider { aLoggedInState(showSyncSpinner = true), aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())), aLoggedInState(forceNativeSlidingSyncMigration = true), + aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale), + aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings), ) } @@ -27,11 +29,13 @@ fun aLoggedInState( pusherRegistrationState: AsyncData = AsyncData.Uninitialized, forceNativeSlidingSyncMigration: Boolean = false, appName: String = "Element X", + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, ) = LoggedInState( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState, ignoreRegistrationError = false, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, appName = appName, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = {}, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 62d8de8c29..198a731e49 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.Lifecycle import io.element.android.appnav.R import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain import io.element.android.libraries.designsystem.preview.ElementPreview @@ -82,6 +83,39 @@ fun LoggedInView( } ) } + + LocalNetworkPermissionDialog(state = state) +} + +@Composable +private fun LocalNetworkPermissionDialog(state: LoggedInState) { + when (state.localNetworkPermissionDialog) { + LocalNetworkPermissionDialog.None -> Unit + LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(CommonStrings.dialog_allow_access), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = { + state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) + }, + onDismiss = { + state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) + }, + ) + LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(CommonStrings.action_open_settings), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = { + state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) + }, + onDismiss = { + state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) + }, + ) + } } private fun Throwable.getReason(): String? { diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 18c8cfd7b9..faa19d9e61 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.test.FakePushService @@ -124,6 +126,8 @@ class LoggedInPresenterTest { encryptionService = encryptionService, buildMeta = buildMeta, networkMonitor = networkMonitor, + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(), ).test { encryptionService.emitRecoveryState(RecoveryState.UNKNOWN) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) @@ -368,6 +372,8 @@ class LoggedInPresenterTest { encryptionService = encryptionService, buildMeta = buildMeta, networkMonitor = networkMonitor, + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(), ) } }