Location expanded view: show own location (#916)
If the location permission is granted: - Shows the user's own location - Shows a button to center the map on it Part of: - https://github.com/vector-im/element-meta/issues/1678
This commit is contained in:
parent
2ccedc1e67
commit
4162d16f52
16 changed files with 188 additions and 38 deletions
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.location.impl
|
||||
|
||||
import android.Manifest
|
||||
import android.view.Gravity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
|
|
@ -60,4 +61,6 @@ object MapDefaults {
|
|||
.build()
|
||||
|
||||
const val DEFAULT_ZOOM = 15.0
|
||||
|
||||
val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
package io.element.android.features.location.impl.send
|
||||
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
|
|
@ -56,9 +54,7 @@ class SendLocationPresenter @Inject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
) : Presenter<SendLocationState> {
|
||||
|
||||
private val permissionsPresenter = permissionsPresenterFactory.create(
|
||||
listOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
|
||||
)
|
||||
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
|
||||
|
||||
@Composable
|
||||
override fun present(): SendLocationState {
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ package io.element.android.features.location.impl.show
|
|||
|
||||
sealed interface ShowLocationEvents {
|
||||
object Share : ShowLocationEvents
|
||||
data class TrackMyLocation(val enabled: Boolean) : ShowLocationEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,21 @@
|
|||
package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.libraries.architecture.Presenter
|
||||
|
||||
class ShowLocationPresenter @AssistedInject constructor(
|
||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||
private val actions: LocationActions,
|
||||
@Assisted private val location: Location,
|
||||
@Assisted private val description: String?
|
||||
|
|
@ -34,15 +42,26 @@ class ShowLocationPresenter @AssistedInject constructor(
|
|||
fun create(location: Location, description: String?): ShowLocationPresenter
|
||||
}
|
||||
|
||||
private val permissionsPresenter = permissionsPresenterFactory.create(MapDefaults.permissions)
|
||||
|
||||
@Composable
|
||||
override fun present(): ShowLocationState {
|
||||
return ShowLocationState(
|
||||
location = location,
|
||||
description = description
|
||||
) {
|
||||
when (it) {
|
||||
val permissionsState: PermissionsState = permissionsPresenter.present()
|
||||
var isTrackMyLocation by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleEvents(event: ShowLocationEvents) {
|
||||
when (event) {
|
||||
ShowLocationEvents.Share -> actions.share(location, description)
|
||||
is ShowLocationEvents.TrackMyLocation -> isTrackMyLocation = event.enabled
|
||||
}
|
||||
}
|
||||
|
||||
return ShowLocationState(
|
||||
location = location,
|
||||
description = description,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
isTrackMyLocation = isTrackMyLocation,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,7 @@ import io.element.android.features.location.api.Location
|
|||
data class ShowLocationState(
|
||||
val location: Location,
|
||||
val description: String?,
|
||||
val hasLocationPermission: Boolean,
|
||||
val isTrackMyLocation: Boolean,
|
||||
val eventSink: (ShowLocationEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,17 +25,37 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
|||
ShowLocationState(
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = true,
|
||||
isTrackMyLocation = false,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = null,
|
||||
hasLocationPermission = true,
|
||||
isTrackMyLocation = true,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
Location(1.23, 2.34, 4f),
|
||||
description = "My favourite place!",
|
||||
hasLocationPermission = false,
|
||||
isTrackMyLocation = false,
|
||||
eventSink = {},
|
||||
),
|
||||
ShowLocationState(
|
||||
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,
|
||||
eventSink = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.LocationSearching
|
||||
import androidx.compose.material.icons.filled.MyLocation
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -37,15 +40,19 @@ 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.send.SendLocationState
|
||||
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
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.maplibre.compose.CameraMode
|
||||
import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason
|
||||
import io.element.android.libraries.maplibre.compose.IconAnchor
|
||||
import io.element.android.libraries.maplibre.compose.MapboxMap
|
||||
import io.element.android.libraries.maplibre.compose.Symbol
|
||||
|
|
@ -64,7 +71,33 @@ fun ShowLocationView(
|
|||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
Scaffold(modifier,
|
||||
val cameraPositionState = rememberCameraPositionState {
|
||||
position = CameraPosition.Builder()
|
||||
.target(LatLng(state.location.lat, state.location.lon))
|
||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||
.build()
|
||||
}
|
||||
|
||||
LaunchedEffect(state.isTrackMyLocation) {
|
||||
when (state.isTrackMyLocation) {
|
||||
false -> cameraPositionState.cameraMode = CameraMode.NONE
|
||||
true -> {
|
||||
cameraPositionState.position = CameraPosition.Builder()
|
||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||
.build()
|
||||
cameraPositionState.cameraMode = CameraMode.TRACKING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraPositionState.isMoving) {
|
||||
if (cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
|
||||
state.eventSink(ShowLocationEvents.TrackMyLocation(false))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
|
|
@ -82,7 +115,19 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -107,14 +152,12 @@ fun ShowLocationView(
|
|||
styleUri = rememberTileStyleUrl(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
images = mapOf(PIN_ID to DesignSystemR.drawable.pin).toImmutableMap(),
|
||||
cameraPositionState = rememberCameraPositionState {
|
||||
position = CameraPosition.Builder()
|
||||
.target(LatLng(state.location.lat, state.location.lon))
|
||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||
.build()
|
||||
},
|
||||
cameraPositionState = cameraPositionState,
|
||||
uiSettings = MapDefaults.uiSettings,
|
||||
symbolManagerSettings = MapDefaults.symbolManagerSettings,
|
||||
locationSettings = MapDefaults.locationSettings.copy(
|
||||
locationEnabled = state.hasLocationPermission,
|
||||
),
|
||||
) {
|
||||
Symbol(
|
||||
iconId = PIN_ID,
|
||||
|
|
|
|||
|
|
@ -21,21 +21,43 @@ 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 kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class ShowLocationPresenterTest {
|
||||
|
||||
private val permissionsPresenterFake = PermissionsPresenterFake()
|
||||
private val actions = FakeLocationActions()
|
||||
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,
|
||||
location,
|
||||
A_DESCRIPTION,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `emits initial state`() = runTest {
|
||||
val presenter = ShowLocationPresenter(
|
||||
actions,
|
||||
location,
|
||||
A_DESCRIPTION,
|
||||
)
|
||||
fun `emits initial state with no location permission`() = runTest {
|
||||
moleculeFlow(RecompositionClock.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 with location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -43,17 +65,28 @@ class ShowLocationPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.location).isEqualTo(location)
|
||||
Truth.assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
||||
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true)
|
||||
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits initial state with partial location permission`() = runTest {
|
||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||
|
||||
moleculeFlow(RecompositionClock.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(true)
|
||||
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses action to share location`() = runTest {
|
||||
val presenter = ShowLocationPresenter(
|
||||
actions,
|
||||
location,
|
||||
A_DESCRIPTION,
|
||||
)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -65,6 +98,27 @@ class ShowLocationPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `centers on user location`() = runTest {
|
||||
permissionsPresenterFake.givenState(PermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true)
|
||||
Truth.assertThat(initialState.isTrackMyLocation).isEqualTo(false)
|
||||
|
||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||
val trackMyLocationState = awaitItem()
|
||||
|
||||
delay(1)
|
||||
|
||||
Truth.assertThat(trackMyLocationState.hasLocationPermission).isEqualTo(true)
|
||||
Truth.assertThat(trackMyLocationState.isTrackMyLocation).isEqualTo(true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val A_DESCRIPTION = "My happy place"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f19878925f3b5b377a91885540fb15d29a5b78d8be2282d64e8809af0bbf5ff4
|
||||
size 12195
|
||||
oid sha256:8d0a5de3c4e09d76b6453ccc6ace5c690d540d945a62e630474932734209058a
|
||||
size 11716
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c936d2d804bc9e98fcc49430f11ddaa572b05fc8d3a0df93ad6521ee8e78f708
|
||||
size 21806
|
||||
oid sha256:9218d1d514342c400051bebb10ef458c99103fda618bba45e61a524c5a58eb63
|
||||
size 11903
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f19878925f3b5b377a91885540fb15d29a5b78d8be2282d64e8809af0bbf5ff4
|
||||
size 12195
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c936d2d804bc9e98fcc49430f11ddaa572b05fc8d3a0df93ad6521ee8e78f708
|
||||
size 21806
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:065d2a09680e35540b870862ec0ba5c54182e016017938ef64e510b6132333c9
|
||||
size 13389
|
||||
oid sha256:e39c5ba30983b034886a6adc330d1510b3a48c40511697edcaf6530716c7ba2e
|
||||
size 12500
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0bb88c64bc68b10b1fb709135f445de5a5e4d78623448d0fef97504e025d5f6d
|
||||
size 24551
|
||||
oid sha256:e4ff75e74c19280308878e3001a8791aaa735439cc667946fefd21070611630e
|
||||
size 12698
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:065d2a09680e35540b870862ec0ba5c54182e016017938ef64e510b6132333c9
|
||||
size 13389
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0bb88c64bc68b10b1fb709135f445de5a5e4d78623448d0fef97504e025d5f6d
|
||||
size 24551
|
||||
Loading…
Add table
Add a link
Reference in a new issue