Merge pull request #963 from vector-im/julioromano/showLocationRequestPermission

Show location request permission in expanded location view
This commit is contained in:
Benoit Marty
2023-08-18 16:58:51 +02:00
committed by GitHub
37 changed files with 416 additions and 102 deletions

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl
package io.element.android.features.location.impl.common
import android.Manifest
import android.view.Gravity

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.impl.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PermissionDeniedDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.location.impl.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PermissionRationaleDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl
package io.element.android.features.location.impl.common.actions
import android.content.Context
import android.content.Intent
@@ -22,7 +22,6 @@ import android.net.Uri
import androidx.annotation.VisibleForTesting
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.show.LocationActions
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.show
package io.element.android.features.location.impl.common.actions
import io.element.android.features.location.api.Location

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
sealed interface PermissionsEvents {
object RequestPermissions : PermissionsEvents

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
import io.element.android.libraries.architecture.Presenter

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable
import com.google.accompanist.permissions.ExperimentalPermissionsApi

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
data class PermissionsState(
val permissions: Permissions = Permissions.NoneGranted,

View File

@@ -25,11 +25,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.permissions.PermissionsEvents
import io.element.android.features.location.impl.permissions.PermissionsPresenter
import io.element.android.features.location.impl.permissions.PermissionsState
import io.element.android.features.location.impl.show.LocationActions
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta

View File

@@ -47,10 +47,11 @@ import com.mapbox.mapboxsdk.camera.CameraPosition
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.R
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
@@ -232,33 +233,3 @@ internal fun SendLocationViewPreview(
navigateUp = {},
)
}
@Composable
private fun PermissionRationaleDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}
@Composable
private fun PermissionDeniedDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClicked = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View File

@@ -19,4 +19,9 @@ package io.element.android.features.location.impl.show
sealed interface ShowLocationEvents {
object Share : ShowLocationEvents
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
object DismissDialog : ShowLocationEvents
object RequestPermissions : ShowLocationEvents
object OpenAppSettings : ShowLocationEvents
}

View File

@@ -17,6 +17,8 @@
package io.element.android.features.location.impl.show
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -25,14 +27,18 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.permissions.PermissionsPresenter
import io.element.android.features.location.impl.permissions.PermissionsState
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
class ShowLocationPresenter @AssistedInject constructor(
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val actions: LocationActions,
private val locationActions: LocationActions,
private val buildMeta: BuildMeta,
@Assisted private val location: Location,
@Assisted private val description: String?
) : Presenter<ShowLocationState> {
@@ -48,19 +54,47 @@ class ShowLocationPresenter @AssistedInject constructor(
override fun present(): ShowLocationState {
val permissionsState: PermissionsState = permissionsPresenter.present()
var isTrackMyLocation by remember { mutableStateOf(false) }
val appName by remember { derivedStateOf { buildMeta.applicationName } }
var permissionDialog: ShowLocationState.Dialog by remember {
mutableStateOf(ShowLocationState.Dialog.None)
}
LaunchedEffect(permissionsState.permissions) {
if (permissionsState.isAnyGranted) {
permissionDialog = ShowLocationState.Dialog.None
}
}
fun handleEvents(event: ShowLocationEvents) {
when (event) {
ShowLocationEvents.Share -> actions.share(location, description)
is ShowLocationEvents.TrackMyLocation -> isTrackMyLocation = event.enabled
ShowLocationEvents.Share -> locationActions.share(location, description)
is ShowLocationEvents.TrackMyLocation -> {
if (event.enabled) {
when {
permissionsState.isAnyGranted -> isTrackMyLocation = true
permissionsState.shouldShowRationale -> permissionDialog = ShowLocationState.Dialog.PermissionRationale
else -> permissionDialog = ShowLocationState.Dialog.PermissionDenied
}
} else {
isTrackMyLocation = false
}
}
ShowLocationEvents.DismissDialog -> permissionDialog = ShowLocationState.Dialog.None
ShowLocationEvents.OpenAppSettings -> {
locationActions.openSettings()
permissionDialog = ShowLocationState.Dialog.None
}
ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
}
return ShowLocationState(
permissionDialog = permissionDialog,
location = location,
description = description,
hasLocationPermission = permissionsState.isAnyGranted,
isTrackMyLocation = isTrackMyLocation,
appName = appName,
eventSink = ::handleEvents,
)
}

View File

@@ -19,9 +19,17 @@ package io.element.android.features.location.impl.show
import io.element.android.features.location.api.Location
data class ShowLocationState(
val permissionDialog: Dialog,
val location: Location,
val description: String?,
val hasLocationPermission: Boolean,
val isTrackMyLocation: Boolean,
val appName: String,
val eventSink: (ShowLocationEvents) -> Unit,
)
) {
sealed interface Dialog {
object None : Dialog
object PermissionRationale : Dialog
object PermissionDenied : Dialog
}
}

View File

@@ -19,50 +19,82 @@ package io.element.android.features.location.impl.show
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location
private const val APP_NAME = "ApplicationName"
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
override val values: Sequence<ShowLocationState>
get() = sequenceOf(
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.PermissionDenied,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.PermissionRationale,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = true,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = null,
hasLocationPermission = true,
isTrackMyLocation = true,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = "My favourite place!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = "For some reason I decided to to write a small essay that wraps at just two lines!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
ShowLocationState(
ShowLocationState.Dialog.None,
Location(1.23, 2.34, 4f),
description = "For some reason I decided to write a small essay in the location description. " +
"It is so long that it will wrap onto more than two lines!",
hasLocationPermission = false,
isTrackMyLocation = false,
appName = APP_NAME,
eventSink = {},
),
)

View File

@@ -39,7 +39,9 @@ import androidx.compose.ui.unit.dp
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng
import io.element.android.features.location.api.internal.rememberTileStyleUrl
import io.element.android.features.location.impl.MapDefaults
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@@ -70,6 +72,20 @@ fun ShowLocationView(
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
when (state.permissionDialog) {
ShowLocationState.Dialog.None -> Unit
ShowLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
onContinue = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
appName = state.appName,
)
ShowLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog(
onContinue = { state.eventSink(ShowLocationEvents.RequestPermissions) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
appName = state.appName,
)
}
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.Builder()
.target(LatLng(state.location.lat, state.location.lon))
@@ -116,14 +132,12 @@ fun ShowLocationView(
)
},
floatingActionButton = {
if (state.hasLocationPermission) {
FloatingActionButton(
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
) {
when (state.isTrackMyLocation) {
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
}
FloatingActionButton(
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
) {
when (state.isTrackMyLocation) {
false -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null)
true -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null)
}
}
},

View File

@@ -14,11 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.show
package io.element.android.features.location.impl.common.actions
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.buildUrl
import org.junit.Test
import java.net.URLEncoder

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.show
package io.element.android.features.location.impl.common.actions
import io.element.android.features.location.api.Location

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.features.location.impl.permissions
package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable

View File

@@ -23,11 +23,11 @@ import com.google.common.truth.Truth
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.permissions.PermissionsEvents
import io.element.android.features.location.impl.permissions.PermissionsPresenter
import io.element.android.features.location.impl.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.permissions.PermissionsState
import io.element.android.features.location.impl.show.FakeLocationActions
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.messages.test.MessageComposerContextFake
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.test.core.aBuildMeta

View File

@@ -21,9 +21,12 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.permissions.PermissionsPresenter
import io.element.android.features.location.impl.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.permissions.PermissionsState
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsPresenterFake
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.libraries.matrix.test.core.aBuildMeta
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -31,19 +34,48 @@ import org.junit.Test
class ShowLocationPresenterTest {
private val permissionsPresenterFake = PermissionsPresenterFake()
private val actions = FakeLocationActions()
private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val location = Location(1.23, 4.56, 7.8f)
private val presenter = ShowLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = permissionsPresenterFake
},
actions,
fakeLocationActions,
fakeBuildMeta,
location,
A_DESCRIPTION,
)
@Test
fun `emits initial state with no location permission`() = runTest {
permissionsPresenterFake.givenState(
PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.location).isEqualTo(location)
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(false)
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
}
}
@Test
fun `emits initial state location permission denied once`() = runTest {
permissionsPresenterFake.givenState(
PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -93,8 +125,8 @@ class ShowLocationPresenterTest {
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvents.Share)
Truth.assertThat(actions.sharedLocation).isEqualTo(location)
Truth.assertThat(actions.sharedLabel).isEqualTo(A_DESCRIPTION)
Truth.assertThat(fakeLocationActions.sharedLocation).isEqualTo(location)
Truth.assertThat(fakeLocationActions.sharedLabel).isEqualTo(A_DESCRIPTION)
}
}
@@ -116,11 +148,145 @@ class ShowLocationPresenterTest {
Truth.assertThat(trackMyLocationState.hasLocationPermission).isEqualTo(true)
Truth.assertThat(trackMyLocationState.isTrackMyLocation).isEqualTo(true)
// Swipe the map to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(false))
val trackLocationDisabledState = awaitItem()
Truth.assertThat(trackLocationDisabledState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
Truth.assertThat(trackLocationDisabledState.isTrackMyLocation).isEqualTo(false)
Truth.assertThat(trackLocationDisabledState.hasLocationPermission).isEqualTo(true)
}
}
@Test
fun `rationale dialog dismiss`() = runTest {
permissionsPresenterFake.givenState(
PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip initial state
val initialState = awaitItem()
// Click on the button to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val trackLocationState = awaitItem()
Truth.assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
Truth.assertThat(trackLocationState.isTrackMyLocation).isEqualTo(false)
Truth.assertThat(trackLocationState.hasLocationPermission).isEqualTo(false)
// Dismiss the dialog
initialState.eventSink(ShowLocationEvents.DismissDialog)
val dialogDismissedState = awaitItem()
Truth.assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
Truth.assertThat(dialogDismissedState.isTrackMyLocation).isEqualTo(false)
Truth.assertThat(dialogDismissedState.hasLocationPermission).isEqualTo(false)
}
}
@Test
fun `rationale dialog continue`() = runTest {
permissionsPresenterFake.givenState(
PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip initial state
val initialState = awaitItem()
// Click on the button to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val trackLocationState = awaitItem()
Truth.assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
Truth.assertThat(trackLocationState.isTrackMyLocation).isEqualTo(false)
Truth.assertThat(trackLocationState.hasLocationPermission).isEqualTo(false)
// Continue the dialog sends permission request to the permissions presenter
trackLocationState.eventSink(ShowLocationEvents.RequestPermissions)
Truth.assertThat(permissionsPresenterFake.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
}
}
@Test
fun `permission denied dialog dismiss`() = runTest {
permissionsPresenterFake.givenState(
PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip initial state
val initialState = awaitItem()
// Click on the button to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val trackLocationState = awaitItem()
Truth.assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionDenied)
Truth.assertThat(trackLocationState.isTrackMyLocation).isEqualTo(false)
Truth.assertThat(trackLocationState.hasLocationPermission).isEqualTo(false)
// Dismiss the dialog
initialState.eventSink(ShowLocationEvents.DismissDialog)
val dialogDismissedState = awaitItem()
Truth.assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
Truth.assertThat(dialogDismissedState.isTrackMyLocation).isEqualTo(false)
Truth.assertThat(dialogDismissedState.hasLocationPermission).isEqualTo(false)
}
}
@Test
fun `open settings activity`() = runTest {
permissionsPresenterFake.givenState(
PermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Skip initial state
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val dialogShownState = awaitItem()
// Open settings
dialogShownState.eventSink(ShowLocationEvents.OpenAppSettings)
val settingsOpenedState = awaitItem()
Truth.assertThat(settingsOpenedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
Truth.assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
}
}
@Test
fun `application name is in state`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.appName).isEqualTo("app name")
}
}
companion object {
private const val A_DESCRIPTION = "My happy place"
}
}