From 278f8ae4c668f0b54641c50a5b9b53b84223fcfa Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 19 Jul 2023 11:58:13 +0200 Subject: [PATCH] Send My Location (#770) - https://github.com/vector-im/element-meta/issues/1682 --- .../android/features/location/api/Location.kt | 4 + .../features/location/api/StaticMapView.kt | 1 + .../features/location/api/internal/MapUrls.kt | 21 +- .../features/location/api/LocationKtTest.kt | 5 + features/location/impl/build.gradle.kts | 10 +- .../impl/{show => }/AndroidLocationActions.kt | 15 +- .../features/location/impl/MapDefaults.kt | 63 +++ .../location/impl/SendLocationPresenter.kt | 71 --- .../location/impl/SendLocationView.kt | 148 ------ .../impl/location/LocationUpdatesFlowImpl.kt | 96 ---- .../features/location/impl/map/MapView.kt | 299 ------------ .../PermissionsEvents.kt} | 7 +- .../PermissionsPresenter.kt} | 14 +- .../permissions/PermissionsPresenterImpl.kt | 60 +++ .../impl/permissions/PermissionsState.kt} | 29 +- .../{ => send}/SendLocationEntryPointImpl.kt | 2 +- .../location/impl/send/SendLocationEvents.kt | 42 ++ .../impl/{ => send}/SendLocationNode.kt | 17 +- .../impl/send/SendLocationPresenter.kt | 168 +++++++ .../SendLocationState.kt} | 24 +- .../impl/send/SendLocationStateProvider.kt | 57 +++ .../location/impl/send/SendLocationView.kt | 257 ++++++++++ .../location/impl/show/LocationActions.kt | 1 + .../location/impl/show/ShowLocationView.kt | 45 +- .../impl/SendLocationPresenterTest.kt | 64 --- .../permissions/PermissionsPresenterFake.kt | 40 ++ .../impl/send/SendLocationPresenterTest.kt | 461 ++++++++++++++++++ .../impl/show/AndroidLocationActionsTest.kt | 1 + .../location/impl/show/FakeLocationActions.kt | 7 + .../matrix/test/room/FakeMatrixRoom.kt | 14 +- .../libraries/textcomposer/TextComposer.kt | 2 +- .../src/main/res/values/localazy.xml | 1 + .../src/main/res/values-sk/translations.xml | 1 - .../src/main/res/values/localazy.xml | 3 +- .../toolbox/api/systemclock/SystemClock.kt | 2 +- ...ViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 - ...iewLightPreview_0_null,NEXUS_5,1.0,en].png | 3 - ...ewPreview-D-0_1_null_0,NEXUS_5,1.0,en].png | 3 + ...ewPreview-D-0_1_null_1,NEXUS_5,1.0,en].png | 3 + ...ewPreview-D-0_1_null_2,NEXUS_5,1.0,en].png | 3 + ...ewPreview-D-0_1_null_3,NEXUS_5,1.0,en].png | 3 + ...ewPreview-D-0_1_null_4,NEXUS_5,1.0,en].png | 3 + ...ewPreview-N-0_2_null_0,NEXUS_5,1.0,en].png | 3 + ...ewPreview-N-0_2_null_1,NEXUS_5,1.0,en].png | 3 + ...ewPreview-N-0_2_null_2,NEXUS_5,1.0,en].png | 3 + ...ewPreview-N-0_2_null_3,NEXUS_5,1.0,en].png | 3 + ...ewPreview-N-0_2_null_4,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 - ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 - 55 files changed, 1351 insertions(+), 767 deletions(-) rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/{show => }/AndroidLocationActions.kt (84%) create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/MapDefaults.kt delete mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationPresenter.kt delete mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationView.kt delete mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowImpl.kt delete mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/{SendLocationEvents.kt => permissions/PermissionsEvents.kt} (70%) rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/{SendLocationState.kt => permissions/PermissionsPresenter.kt} (68%) create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterImpl.kt rename features/location/impl/src/{test/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowFake.kt => main/kotlin/io/element/android/features/location/impl/permissions/PermissionsState.kt} (53%) rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/{ => send}/SendLocationEntryPointImpl.kt (95%) create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/{ => send}/SendLocationNode.kt (73%) create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt rename features/location/impl/src/main/kotlin/io/element/android/features/location/impl/{SendLocationStateProvider.kt => send/SendLocationState.kt} (51%) create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt create mode 100644 features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt delete mode 100644 features/location/impl/src/test/kotlin/io/element/android/features/location/impl/SendLocationPresenterTest.kt create mode 100644 features/location/impl/src/test/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterFake.kt create mode 100644 features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt index d09e163c30..8d801b37a8 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/Location.kt @@ -37,4 +37,8 @@ data class Location( ) } } + + fun toGeoUri(): String { + return "geo:$lat,$lon;u=$accuracy" + } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt index db86263379..14390d0f4f 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -107,6 +107,7 @@ fun StaticMapView( contentDescription = null, tint = Color.Unspecified, modifier = Modifier.align { size, space, _ -> + // Center bottom edge of pin (i.e. its arrow) to center of screen IntOffset( x = (space.width - size.width) / 2, y = (space.height / 2) - size.height, diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt index 355741dbaa..b6f21a4512 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt @@ -17,7 +17,11 @@ package io.element.android.features.location.api.internal import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import io.element.android.features.location.api.R +import io.element.android.libraries.theme.ElementTheme /** * Provides the URL to an image that contains a statically-generated map of the given location. @@ -34,10 +38,25 @@ fun staticMapUrl( return "${baseUrl(darkMode)}/static/${lon},${lat},${zoom}/${width}x${height}@2x.webp?key=${context.apiKey}&attribution=bottomleft" } +/** + * Utility function to remember the tile server URL based on the current theme. + */ +@Composable +fun rememberTileStyleUrl(): String { + val context = LocalContext.current + val darkMode = !ElementTheme.isLightTheme + return remember(darkMode) { + tileStyleUrl( + context = context, + darkMode = darkMode + ) + } +} + /** * Provides the URL to a MapLibre style document, used for rendering dynamic maps. */ -fun tileStyleUrl( +private fun tileStyleUrl( context: Context, darkMode: Boolean, ): String { diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt index 61d9bd0351..f3d1f72f22 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/LocationKtTest.kt @@ -76,4 +76,9 @@ internal class LocationKtTest { )) } + @Test + fun `encode geoUri - returns geoUri from a Location`() { + assertThat(Location(1.0,2.0,3.0f).toGeoUri()) + .isEqualTo("geo:1.0,2.0;u=3.0") + } } diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index dfb5192bea..1158b5f152 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -30,18 +30,22 @@ anvil { dependencies { api(projects.features.location.api) + implementation(projects.features.messages.api) + implementation(projects.libraries.maplibreCompose) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.di) implementation(projects.libraries.designsystem) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrixui) + implementation(projects.libraries.androidutils) implementation(projects.services.analytics.api) - implementation(libs.maplibre) - implementation(libs.maplibre.annotation) + implementation(libs.accompanist.permission) implementation(projects.libraries.uiStrings) implementation(libs.dagger) implementation(projects.anvilannotations) + implementation(projects.services.toolbox.api) anvil(projects.anvilcodegen) ksp(libs.showkase.processor) @@ -52,4 +56,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.features.analytics.test) + testImplementation(projects.features.messages.test) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/AndroidLocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/AndroidLocationActions.kt similarity index 84% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/AndroidLocationActions.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/AndroidLocationActions.kt index fb23f72557..da88598251 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/AndroidLocationActions.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/AndroidLocationActions.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.location.impl.show +package io.element.android.features.location.impl import android.content.Context import android.content.Intent @@ -22,6 +22,8 @@ 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 import timber.log.Timber @@ -29,24 +31,25 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class AndroidLocationActions @Inject constructor( - @ApplicationContext private val appContext: Context + @ApplicationContext private val context: Context ) : LocationActions { - - private var activityContext: Context? = null - override fun share(location: Location, label: String?) { runCatching { val uri = Uri.parse(buildUrl(location, label)) val showMapsIntent = Intent(Intent.ACTION_VIEW).setData(uri) val chooserIntent = Intent.createChooser(showMapsIntent, null) chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - appContext.startActivity(chooserIntent) + context.startActivity(chooserIntent) }.onSuccess { Timber.v("Open location succeed") }.onFailure { Timber.e(it, "Open location failed") } } + + override fun openSettings() { + context.openAppSettingsPage() + } } @VisibleForTesting diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/MapDefaults.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/MapDefaults.kt new file mode 100644 index 0000000000..62a620b0c1 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/MapDefaults.kt @@ -0,0 +1,63 @@ +/* + * 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 + +import android.view.Gravity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.geometry.LatLng +import io.element.android.libraries.maplibre.compose.MapLocationSettings +import io.element.android.libraries.maplibre.compose.MapSymbolManagerSettings +import io.element.android.libraries.maplibre.compose.MapUiSettings +import io.element.android.libraries.theme.ElementTheme + +/** + * Common configuration values for the map. + */ +object MapDefaults { + val uiSettings: MapUiSettings + @Composable + @ReadOnlyComposable + get() = MapUiSettings( + compassEnabled = false, + rotationGesturesEnabled = false, + scrollGesturesEnabled = true, + tiltGesturesEnabled = false, + zoomGesturesEnabled = true, + logoGravity = Gravity.TOP, + attributionGravity = Gravity.TOP, + attributionTintColor = ElementTheme.colors.iconPrimary + ) + + val symbolManagerSettings: MapSymbolManagerSettings + get() = MapSymbolManagerSettings( + iconAllowOverlap = true + ) + + val locationSettings: MapLocationSettings + get() = MapLocationSettings( + locationEnabled = false, + ) + + val centerCameraPosition = CameraPosition.Builder() + .target(LatLng(49.843, 9.902056)) + .zoom(2.7) + .build() + + const val DEFAULT_ZOOM = 15.0 +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationPresenter.kt deleted file mode 100644 index ce5eb10817..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationPresenter.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.room.location.AssetType -import kotlinx.coroutines.launch -import javax.inject.Inject - -class SendLocationPresenter @Inject constructor( - private val room: MatrixRoom, -) : Presenter { - @Composable - override fun present(): SendLocationState { - - val scope = rememberCoroutineScope() - - var mode by remember { - mutableStateOf(SendLocationState.Mode.ALocation) - } - - fun handleEvents(event: SendLocationEvents) { - when (event) { - is SendLocationEvents.ShareLocation -> scope.launch { - shareLocation(event) - } - is SendLocationEvents.SwitchMode -> { - mode = event.mode - } - } - } - - return SendLocationState( - mode = mode, - eventSink = ::handleEvents, - ) - } - - private suspend fun shareLocation( - event: SendLocationEvents.ShareLocation - ) { - room.sendLocation( - body = "Location at latitude: ${event.lat}, longitude: ${event.lng}", - geoUri = "geo:${event.lat},${event.lng}", - description = null, - zoomLevel = 15, // Send default zoom level for now. - assetType = AssetType.PIN, - ) - } -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationView.kt deleted file mode 100644 index acf90d546d..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationView.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.LocationOn -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ListItem -import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import io.element.android.features.location.impl.map.MapView -import io.element.android.features.location.impl.map.rememberMapState -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.BottomSheetScaffold -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.libraries.designsystem.R as DesignSystemR - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -@Composable -fun SendLocationView( - state: SendLocationState, - modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, -) { - val mapState = rememberMapState() - BottomSheetScaffold( - sheetContent = { - Spacer(modifier = Modifier.height(16.dp)) - ListItem( - headlineContent = { - Text(stringResource(CommonStrings.screen_share_this_location_action)) - }, - modifier = Modifier.clickable { - state.eventSink( - SendLocationEvents.ShareLocation( - lat = mapState.position.lat, - lng = mapState.position.lon - ) - ) - onBackPressed() - }, - leadingContent = { - Icon(Icons.Default.LocationOn, null) - }, - ) - Spacer(modifier = Modifier.height(16.dp)) - }, - modifier = modifier, - scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded), - ), - sheetDragHandle = {}, - sheetSwipeEnabled = false, - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(CommonStrings.screen_share_location_title), - style = ElementTheme.typography.aliasScreenTitle, - ) - }, - navigationIcon = { - BackButton(onClick = onBackPressed) - }, - ) - }, - ) { - Box( - modifier = Modifier - .padding(it) - .consumeWindowInsets(it), - contentAlignment = Alignment.Center - ) { - MapView( - modifier = Modifier.fillMaxSize(), - mapState = mapState, - ) - Icon( - resourceId = DesignSystemR.drawable.pin, - contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier.align { size, space, _ -> - IntOffset( - x = (space.width - size.width) / 2, - y = (space.height / 2) - size.height, - ) - } - ) - } - } -} - -@Preview -@Composable -internal fun SendLocationViewLightPreview(@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun SendLocationViewDarkPreview(@PreviewParameter(SendLocationStateProvider::class) state: SendLocationState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: SendLocationState) { - SendLocationView( - state = state, - onBackPressed = {}, - ) -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowImpl.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowImpl.kt deleted file mode 100644 index f4fcd25de1..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowImpl.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.location - -import android.Manifest -import android.content.Context -import android.location.LocationManager -import androidx.annotation.RequiresPermission -import androidx.core.content.getSystemService -import androidx.core.location.LocationListenerCompat -import androidx.core.location.LocationManagerCompat -import androidx.core.location.LocationRequestCompat -import io.element.android.features.location.api.Location -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -/** - * Returns a cold [Flow] that, once collected, emits [Location] updates every second. - */ -@RequiresPermission( - anyOf = [ - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - ] -) -fun locationUpdatesFlow( - context: Context, - coroutineDispatchers: CoroutineDispatchers, -): Flow = callbackFlow { - val locationManager: LocationManager = checkNotNull(context.getSystemService()) - val provider = locationManager.bestAvailableProvider() - // Try to eagerly emit the last known location as fast as possible - locationManager.getLastKnownLocation(provider)?.let { location -> - trySendBlocking( - Location( - lat = location.latitude, - lon = location.longitude, - accuracy = location.accuracy - ) - ) - } - val locationListener = LocationListenerCompat { location -> - trySendBlocking( - Location( - lat = location.latitude, - lon = location.longitude, - accuracy = location.accuracy - ) - ) - } - LocationManagerCompat.requestLocationUpdates( - locationManager, - provider, - buildLocationRequest(), - coroutineDispatchers.io.asExecutor(), - locationListener, - ) - awaitClose { - LocationManagerCompat.removeUpdates(locationManager, locationListener) - } -} - -private fun LocationManager.bestAvailableProvider(): String = - checkNotNull(getProviders(true).maxByOrNull { providerPriority(it) }) { - "No location provider available" - } - -private fun providerPriority(provider: String): Int = when (provider) { - LocationManager.FUSED_PROVIDER -> 4 - LocationManager.GPS_PROVIDER -> 3 - LocationManager.NETWORK_PROVIDER -> 2 - LocationManager.PASSIVE_PROVIDER -> 1 - else -> 0 -} - -private fun buildLocationRequest() = LocationRequestCompat.Builder(1_000).apply { - setMinUpdateIntervalMillis(1_000) -}.build() diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt deleted file mode 100644 index a344d8571e..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt +++ /dev/null @@ -1,299 +0,0 @@ -/* - * 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.map - -import android.annotation.SuppressLint -import android.view.Gravity -import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import com.mapbox.mapboxsdk.Mapbox -import com.mapbox.mapboxsdk.camera.CameraPosition -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory -import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.maps.MapView -import com.mapbox.mapboxsdk.maps.MapboxMap -import com.mapbox.mapboxsdk.maps.Style -import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager -import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions -import com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM -import io.element.android.features.location.api.Location -import io.element.android.features.location.api.internal.tileStyleUrl -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.theme.ElementTheme -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import io.element.android.libraries.designsystem.R as DesignSystemR - -/** - * Composable wrapper around MapLibre's [MapView]. - */ -@SuppressLint("MissingPermission") -@Composable -fun MapView( - modifier: Modifier = Modifier, - mapState: MapState = rememberMapState(), - darkMode: Boolean = !ElementTheme.isLightTheme, -) { - // When in preview, early return a Box with the received modifier preserving layout - if (LocalInspectionMode.current) { - @Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return. - Box( - modifier = modifier.background(Color.DarkGray) - ) { - Text("[MapView]", modifier = Modifier.align(Alignment.Center)) - } - return - } - - val context = LocalContext.current - val mapView = remember { - Mapbox.getInstance(context) - MapView(context) - } - var mapRefs by remember { mutableStateOf(null) } - - val attributionColour = ElementTheme.colors.iconPrimary - - // Build map - LaunchedEffect(darkMode) { - mapView.awaitMap().let { map -> - map.uiSettings.apply { - attributionGravity = Gravity.TOP - setAttributionTintColor(attributionColour.toArgb()) - logoGravity = Gravity.TOP - isCompassEnabled = false - isRotateGesturesEnabled = false - } - map.setStyle(tileStyleUrl(context, darkMode)) { style -> - mapRefs = MapRefs( - map = map, - symbolManager = SymbolManager(mapView, map, style).apply { - iconAllowOverlap = true - }, - style = style - ) - } - } - } - - // Update state position when moving map - DisposableEffect(mapRefs) { - var listener: MapboxMap.OnCameraIdleListener? = null - - mapRefs?.let { mapRefs -> - listener = MapboxMap.OnCameraIdleListener { - mapRefs.map.cameraPosition.target?.let { target -> - val position = MapState.CameraPosition( - lat = target.latitude, - lon = target.longitude, - zoom = mapRefs.map.cameraPosition.zoom - ) - mapState.position = position - Timber.d("Camera moved to: $position") - } - }.apply { - mapRefs.map.addOnCameraIdleListener(this) - Timber.d("Added OnCameraIdleListener $this") - } - } - - onDispose { - mapRefs?.let { mapRefs -> - listener?.let { - mapRefs.map.removeOnCameraIdleListener(it).apply { - Timber.d("Removed OnCameraIdleListener $it") - } - } - } - } - } - - // Move map to given position when state has changed - LaunchedEffect(mapRefs, mapState.position) { - mapRefs?.map?.moveCamera( - CameraUpdateFactory.newCameraPosition( - CameraPosition.Builder() - .target(LatLng(mapState.position.lat, mapState.position.lon)) - .zoom(mapState.position.zoom).build() - ) - ) - Timber.d("Camera position updated to: ${mapState.position}") - } - - // Draw pin - LaunchedEffect(mapRefs, mapState.location) { - mapRefs?.let { mapRefs -> - mapState.location?.let { location -> - context.getDrawable(DesignSystemR.drawable.pin)?.let { mapRefs.style.addImage("pin", it) } - mapRefs.symbolManager.create( - SymbolOptions() - .withLatLng(LatLng(location.lat, location.lon)) - .withIconImage("pin") - .withIconSize(1.3f) - .withIconAnchor(ICON_ANCHOR_BOTTOM) - ) - Timber.d("Shown pin at location: $location") - } - } - - } - - // Draw markers - LaunchedEffect(mapRefs, mapState.markers) { - mapRefs?.let { mapRefs -> - mapState.markers.forEachIndexed { index, marker -> - context.getDrawable(marker.drawable)?.let { mapRefs.style.addImage("marker_$index", it) } - mapRefs.symbolManager.create( - SymbolOptions() - .withLatLng(LatLng(marker.lat, marker.lon)) - .withIconImage("marker_$index") - .withIconSize(1.0f) - ) - Timber.d("Shown marker at location: $marker") - } - } - } - - @Suppress("ModifierReused") - AndroidView( - factory = { mapView }, - modifier = modifier - ) -} - -@Composable -fun rememberMapState( - position: MapState.CameraPosition = MapState.CameraPosition(lat = 0.0, lon = 0.0, zoom = 0.0), - location: Location? = null, - markers: ImmutableList = emptyList().toImmutableList(), -): MapState = remember { - MapState( - position = position, - location = location, - markers = markers, - ) -} // TODO(Use remember saveable with Parcelable custom saver) - -@Stable -class MapState( - position: CameraPosition, // The position of the camera, it's what will be shared - location: Location? = null, // The location retrieved by the location subsystem, if any. - markers: ImmutableList = emptyList().toImmutableList(), // The pin's location, if any. -) { - var position: CameraPosition by mutableStateOf(position) - var location: Location? by mutableStateOf(location) - var markers: ImmutableList by mutableStateOf(markers) - - override fun toString(): String { - return "MapState(position=$position, location=$location, markers=$markers)" - } - - @Stable - data class CameraPosition( - val lat: Double, - val lon: Double, - val zoom: Double, - ) - - @Stable - data class Marker( - @DrawableRes val drawable: Int, - val lat: Double, - val lon: Double, - ) -} - -private class MapRefs( - val map: MapboxMap, - val symbolManager: SymbolManager, - val style: Style -) - -/** - * A suspending function that provides an instance of [MapboxMap] from this [MapView]. This is - * an alternative to [MapView.getMapAsync] by using coroutines to obtain the [MapboxMap]. - * - * Inspired from [com.google.maps.android.ktx.awaitMap] - * - * @return the [MapboxMap] instance - */ -private suspend inline fun MapView.awaitMap(): MapboxMap = - suspendCoroutine { continuation -> - getMapAsync { - continuation.resume(it) - } - } - -@Preview -@Composable -fun MapViewLightPreview() = - ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -fun MapViewDarkPreview() = - ElementPreviewDark { ContentToPreview() } - -@Composable -private fun ContentToPreview() { - MapView( - modifier = Modifier.size(400.dp), - mapState = rememberMapState( - position = MapState.CameraPosition( - lat = 0.0, - lon = 0.0, - zoom = 0.0, - ), - location = Location( - lat = 0.0, - lon = 0.0, - accuracy = 0.0f, - ), - markers = listOf( - MapState.Marker( - drawable = DesignSystemR.drawable.pin, - lat = 0.0, - lon = 0.0, - ) - ).toImmutableList() - ), - ) -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsEvents.kt similarity index 70% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationEvents.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsEvents.kt index 360d77d879..194bf31df7 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsEvents.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package io.element.android.features.location.impl +package io.element.android.features.location.impl.permissions -sealed interface SendLocationEvents { - data class ShareLocation(val lat: Double, val lng: Double) : SendLocationEvents - data class SwitchMode(val mode: SendLocationState.Mode) : SendLocationEvents +sealed interface PermissionsEvents { + object RequestPermissions : PermissionsEvents } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenter.kt similarity index 68% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationState.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenter.kt index c09f391b7f..ccff16159e 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenter.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package io.element.android.features.location.impl +package io.element.android.features.location.impl.permissions -data class SendLocationState( - val mode: Mode = Mode.ALocation, - val eventSink: (SendLocationEvents) -> Unit = {}, -) { - sealed interface Mode { - object MyLocation : Mode - object ALocation : Mode +import io.element.android.libraries.architecture.Presenter + +interface PermissionsPresenter : Presenter { + interface Factory { + fun create(permissions: List): PermissionsPresenter } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterImpl.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterImpl.kt new file mode 100644 index 0000000000..85941ab7d3 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterImpl.kt @@ -0,0 +1,60 @@ +/* + * 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.permissions + +import androidx.compose.runtime.Composable +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.squareup.anvil.annotations.ContributesBinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.di.AppScope + +class PermissionsPresenterImpl @AssistedInject constructor( + @Assisted private val permissions: List +) : PermissionsPresenter { + + @AssistedFactory + @ContributesBinding(AppScope::class) + interface Factory : PermissionsPresenter.Factory { + override fun create(permissions: List): PermissionsPresenterImpl + } + + @OptIn(ExperimentalPermissionsApi::class) + @Composable + override fun present(): PermissionsState { + val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions) + + fun handleEvents(event: PermissionsEvents) { + when (event) { + PermissionsEvents.RequestPermissions -> multiplePermissionsState.launchMultiplePermissionRequest() + } + } + + return PermissionsState( + permissions = when { + multiplePermissionsState.allPermissionsGranted -> PermissionsState.Permissions.AllGranted + multiplePermissionsState.permissions.any { it.status.isGranted } -> PermissionsState.Permissions.SomeGranted + else -> PermissionsState.Permissions.NoneGranted + }, + shouldShowRationale = multiplePermissionsState.shouldShowRationale, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowFake.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsState.kt similarity index 53% rename from features/location/impl/src/test/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowFake.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsState.kt index 861657a7e7..626cf93c23 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/location/LocationUpdatesFlowFake.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/permissions/PermissionsState.kt @@ -14,22 +14,19 @@ * limitations under the License. */ -package io.element.android.features.location.impl.location +package io.element.android.features.location.impl.permissions -import io.element.android.features.location.api.Location -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -fun fakeLocationUpdatesFlow(): Flow = flow { - while (true) { - delay(1_000) - emit(aLocation()) +data class PermissionsState( + val permissions: Permissions = Permissions.NoneGranted, + val shouldShowRationale: Boolean = false, + val eventSink: (PermissionsEvents) -> Unit = {}, +) { + sealed interface Permissions { + object AllGranted : Permissions + object SomeGranted : Permissions + object NoneGranted : Permissions } -} -private fun aLocation() = Location( - lat = 51.49404, - lon = -0.25484, - accuracy = 5f -) + val isAnyGranted: Boolean + get() = permissions is Permissions.SomeGranted || permissions is Permissions.AllGranted +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationEntryPointImpl.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEntryPointImpl.kt similarity index 95% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationEntryPointImpl.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEntryPointImpl.kt index 345fcef1a2..9edf195e28 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationEntryPointImpl.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEntryPointImpl.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.location.impl +package io.element.android.features.location.impl.send import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt new file mode 100644 index 0000000000..2f0686da27 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationEvents.kt @@ -0,0 +1,42 @@ +/* + * 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.send + +import io.element.android.features.location.api.Location + +sealed interface SendLocationEvents { + data class SendLocation( + val cameraPosition: CameraPosition, + val location: Location?, + ) : SendLocationEvents { + data class CameraPosition( + val lat: Double, + val lon: Double, + val zoom: Double, + ) + } + + object SwitchToMyLocationMode : SendLocationEvents + + object SwitchToPinLocationMode : SendLocationEvents + + object DismissDialog : SendLocationEvents + + object RequestPermissions : SendLocationEvents + + object OpenAppSettings : SendLocationEvents +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationNode.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt similarity index 73% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationNode.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt index fb6f723a65..be4d3f0764 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationNode.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationNode.kt @@ -14,30 +14,43 @@ * limitations under the License. */ -package io.element.android.features.location.impl +package io.element.android.features.location.impl.send import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.RoomScope +import io.element.android.services.analytics.api.AnalyticsService @ContributesNode(RoomScope::class) class SendLocationNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SendLocationPresenter, + analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { + + init { + lifecycle.subscribe( + onResume = { + analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.LocationSend)) + } + ) + } + @Composable override fun View(modifier: Modifier) { SendLocationView( state = presenter.present(), modifier = modifier, - onBackPressed = ::navigateUp, + navigateUp = ::navigateUp, ) } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt new file mode 100644 index 0000000000..f5a91f0bf9 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationPresenter.kt @@ -0,0 +1,168 @@ +/* + * 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.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 +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.messages.api.MessageComposerContext +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.launch +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +class SendLocationPresenter @Inject constructor( + permissionsPresenterFactory: PermissionsPresenter.Factory, + private val room: MatrixRoom, + private val analyticsService: AnalyticsService, + private val messageComposerContext: MessageComposerContext, + private val locationActions: LocationActions, + private val systemClock: SystemClock, + private val buildMeta: BuildMeta, +) : Presenter { + + private val permissionsPresenter = permissionsPresenterFactory.create( + listOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) + ) + + @Composable + override fun present(): SendLocationState { + val permissionsState: PermissionsState = permissionsPresenter.present() + var mode: SendLocationState.Mode by remember { + mutableStateOf( + if (permissionsState.isAnyGranted) SendLocationState.Mode.SenderLocation + else SendLocationState.Mode.PinLocation + ) + } + val appName by remember { derivedStateOf { buildMeta.applicationName } } + var permissionDialog: SendLocationState.Dialog by remember { + mutableStateOf(SendLocationState.Dialog.None) + } + val scope = rememberCoroutineScope() + + LaunchedEffect(permissionsState.permissions) { + if (permissionsState.isAnyGranted) { + mode = SendLocationState.Mode.SenderLocation + permissionDialog = SendLocationState.Dialog.None + } + } + + fun handleEvents(event: SendLocationEvents) { + when (event) { + is SendLocationEvents.SendLocation -> scope.launch { + sendLocation(event, mode) + } + SendLocationEvents.SwitchToMyLocationMode -> when { + permissionsState.isAnyGranted -> mode = SendLocationState.Mode.SenderLocation + permissionsState.shouldShowRationale -> permissionDialog = SendLocationState.Dialog.PermissionRationale + else -> permissionDialog = SendLocationState.Dialog.PermissionDenied + } + SendLocationEvents.SwitchToPinLocationMode -> mode = SendLocationState.Mode.PinLocation + SendLocationEvents.DismissDialog -> permissionDialog = SendLocationState.Dialog.None + SendLocationEvents.OpenAppSettings -> { + locationActions.openSettings() + permissionDialog = SendLocationState.Dialog.None + } + SendLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) + } + } + + return SendLocationState( + permissionDialog = permissionDialog, + mode = mode, + hasLocationPermission = permissionsState.isAnyGranted, + appName = appName, + eventSink = ::handleEvents, + ) + } + + private suspend fun sendLocation( + event: SendLocationEvents.SendLocation, + mode: SendLocationState.Mode, + ) { + when (mode) { + SendLocationState.Mode.PinLocation -> { + val geoUri = event.cameraPosition.toGeoUri() + room.sendLocation( + body = generateBody(geoUri, systemClock.epochMillis()), + geoUri = geoUri, + description = null, + zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(), + assetType = AssetType.PIN + ) + analyticsService.capture( + Composer( + inThread = messageComposerContext.composerMode.inThread, + isEditing = messageComposerContext.composerMode.isEditing, + isLocation = true, + isReply = messageComposerContext.composerMode.isReply, + locationType = Composer.LocationType.PinDrop, + ) + ) + } + SendLocationState.Mode.SenderLocation -> { + val geoUri = event.toGeoUri() + room.sendLocation( + body = generateBody(geoUri, systemClock.epochMillis()), + geoUri = geoUri, + description = null, + zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(), + assetType = AssetType.SENDER + ) + analyticsService.capture( + Composer( + inThread = messageComposerContext.composerMode.inThread, + isEditing = messageComposerContext.composerMode.isEditing, + isLocation = true, + isReply = messageComposerContext.composerMode.isReply, + locationType = Composer.LocationType.MyLocation, + ) + ) + } + } + } +} + +private fun SendLocationEvents.SendLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri() + +private fun SendLocationEvents.SendLocation.CameraPosition.toGeoUri(): String = "geo:$lat,$lon" + +private fun generateBody(uri: String, epochMillis: Long): String { + val timestamp = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT) + return "Location was shared at $uri as of $timestamp" +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt similarity index 51% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationStateProvider.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt index 1337f809e4..3aeec5f046 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/SendLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationState.kt @@ -14,13 +14,23 @@ * limitations under the License. */ -package io.element.android.features.location.impl +package io.element.android.features.location.impl.send -import androidx.compose.ui.tooling.preview.PreviewParameterProvider +data class SendLocationState( + val permissionDialog: Dialog = Dialog.None, + val mode: Mode = Mode.PinLocation, + val hasLocationPermission: Boolean = false, + val appName: String = "AppName", + val eventSink: (SendLocationEvents) -> Unit = {}, +) { + sealed interface Mode { + object SenderLocation : Mode + object PinLocation : Mode + } -class SendLocationStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - SendLocationState(), - ) + sealed interface Dialog { + object None : Dialog + object PermissionRationale : Dialog + object PermissionDenied : Dialog + } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt new file mode 100644 index 0000000000..15f16f593a --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationStateProvider.kt @@ -0,0 +1,57 @@ +/* + * 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.send + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +private const val APP_NAME = "ApplicationName" + +class SendLocationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SendLocationState( + permissionDialog = SendLocationState.Dialog.None, + mode = SendLocationState.Mode.PinLocation, + hasLocationPermission = false, + appName = APP_NAME, + ), + SendLocationState( + permissionDialog = SendLocationState.Dialog.PermissionDenied, + mode = SendLocationState.Mode.PinLocation, + hasLocationPermission = false, + appName = APP_NAME, + ), + SendLocationState( + permissionDialog = SendLocationState.Dialog.PermissionRationale, + mode = SendLocationState.Mode.PinLocation, + hasLocationPermission = false, + appName = APP_NAME, + ), + SendLocationState( + permissionDialog = SendLocationState.Dialog.None, + mode = SendLocationState.Mode.PinLocation, + hasLocationPermission = true, + appName = APP_NAME, + ), + SendLocationState( + permissionDialog = SendLocationState.Dialog.None, + mode = SendLocationState.Mode.SenderLocation, + hasLocationPermission = true, + appName = APP_NAME, + ), + ) +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt new file mode 100644 index 0000000000..2c79dff5ad --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt @@ -0,0 +1,257 @@ +/* + * 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.send + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.LocationSearching +import androidx.compose.material.icons.filled.MyLocation +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.mapbox.mapboxsdk.camera.CameraPosition +import io.element.android.features.location.api.Location +import io.element.android.features.location.api.internal.rememberTileStyleUrl +import io.element.android.features.location.impl.MapDefaults +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 +import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold +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.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.MapboxMap +import io.element.android.libraries.maplibre.compose.rememberCameraPositionState +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.designsystem.R as DesignSystemR + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun SendLocationView( + state: SendLocationState, + modifier: Modifier = Modifier, + navigateUp: () -> Unit = {}, +) { + LaunchedEffect(Unit) { + state.eventSink(SendLocationEvents.RequestPermissions) + } + + when (state.permissionDialog) { + SendLocationState.Dialog.None -> Unit + SendLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog( + onContinue = { state.eventSink(SendLocationEvents.OpenAppSettings) }, + onDismiss = { state.eventSink(SendLocationEvents.DismissDialog) }, + appName = state.appName, + ) + SendLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog( + onContinue = { state.eventSink(SendLocationEvents.RequestPermissions) }, + onDismiss = { state.eventSink(SendLocationEvents.DismissDialog) }, + appName = state.appName, + ) + } + + val cameraPositionState = rememberCameraPositionState { + position = MapDefaults.centerCameraPosition + } + + LaunchedEffect(state.mode) { + when (state.mode) { + SendLocationState.Mode.PinLocation -> { + cameraPositionState.cameraMode = CameraMode.NONE + } + SendLocationState.Mode.SenderLocation -> { + cameraPositionState.position = CameraPosition.Builder() + .zoom(MapDefaults.DEFAULT_ZOOM) + .build() + cameraPositionState.cameraMode = CameraMode.TRACKING + } + } + } + + LaunchedEffect(cameraPositionState.isMoving) { + if (cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) { + state.eventSink(SendLocationEvents.SwitchToPinLocationMode) + } + } + + BottomSheetScaffold( + sheetContent = { + Spacer(modifier = Modifier.height(16.dp)) + ListItem( + headlineContent = { + Text( + stringResource( + when (state.mode) { + SendLocationState.Mode.PinLocation -> CommonStrings.screen_share_this_location_action + SendLocationState.Mode.SenderLocation -> CommonStrings.screen_share_my_location_action + } + ) + ) + }, + modifier = Modifier.clickable { + state.eventSink( + SendLocationEvents.SendLocation( + cameraPosition = SendLocationEvents.SendLocation.CameraPosition( + lat = cameraPositionState.position.target!!.latitude, + lon = cameraPositionState.position.target!!.longitude, + zoom = cameraPositionState.position.zoom, + ), + cameraPositionState.location?.let { + Location( + lat = it.latitude, + lon = it.longitude, + accuracy = it.accuracy, + ) + } + ) + ) + navigateUp() + }, + leadingContent = { + Icon(Icons.Default.LocationOn, null) + }, + ) + Spacer(modifier = Modifier.height(28.dp)) + }, + modifier = modifier, + scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded), + ), + sheetDragHandle = {}, + sheetSwipeEnabled = false, + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(CommonStrings.screen_share_location_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = navigateUp) + }, + ) + }, + ) { + Box( + modifier = Modifier + .padding(it) + .consumeWindowInsets(it), + contentAlignment = Alignment.Center + ) { + MapboxMap( + styleUri = rememberTileStyleUrl(), + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + uiSettings = MapDefaults.uiSettings, + symbolManagerSettings = MapDefaults.symbolManagerSettings, + locationSettings = MapDefaults.locationSettings.copy( + locationEnabled = state.hasLocationPermission, + ), + ) + Icon( + resourceId = DesignSystemR.drawable.pin, + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align { size, space, _ -> + // Center bottom edge of pin (i.e. its arrow) to center of screen + IntOffset( + x = (space.width - size.width) / 2, + y = (space.height / 2) - size.height, + ) + } + ) + FloatingActionButton( + onClick = { state.eventSink(SendLocationEvents.SwitchToMyLocationMode) }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 16.dp, bottom = 72.dp), + ) { + when (state.mode) { + SendLocationState.Mode.PinLocation -> Icon(imageVector = Icons.Default.LocationSearching, contentDescription = null) + SendLocationState.Mode.SenderLocation -> Icon(imageVector = Icons.Default.MyLocation, contentDescription = null) + } + } + } + } +} + +@DayNightPreviews +@Composable +fun SendLocationViewPreview( + @PreviewParameter(SendLocationStateProvider::class) state: SendLocationState +) = ElementPreview { + SendLocationView( + state = state, + 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), + ) +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LocationActions.kt index 7e38bd65fa..d93b15e5c5 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LocationActions.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/LocationActions.kt @@ -20,4 +20,5 @@ import io.element.android.features.location.api.Location interface LocationActions { fun share(location: Location, label: String?) + fun openSettings() } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 4e328fe3e6..764f384b0b 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -33,9 +33,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.location.impl.map.MapState -import io.element.android.features.location.impl.map.MapView -import io.element.android.features.location.impl.map.rememberMapState +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.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -45,9 +46,16 @@ 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.IconAnchor +import io.element.android.libraries.maplibre.compose.MapboxMap +import io.element.android.libraries.maplibre.compose.Symbol +import io.element.android.libraries.maplibre.compose.rememberCameraPositionState +import io.element.android.libraries.maplibre.compose.rememberSymbolState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.compound.generated.TypographyTokens import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.toImmutableMap +import io.element.android.libraries.designsystem.R as DesignSystemR @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable @@ -56,11 +64,6 @@ fun ShowLocationView( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { - val mapState = rememberMapState( - location = state.location, - position = MapState.CameraPosition(state.location.lat, state.location.lon, 15.0), - ) - Scaffold(modifier, topBar = { TopAppBar( @@ -100,10 +103,27 @@ fun ShowLocationView( ) } - MapView( - mapState = mapState, + MapboxMap( + 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() + }, + uiSettings = MapDefaults.uiSettings, + symbolManagerSettings = MapDefaults.symbolManagerSettings, + ) { + Symbol( + iconId = PIN_ID, + state = rememberSymbolState( + position = LatLng(state.location.lat, state.location.lon) + ), + iconAnchor = IconAnchor.BOTTOM, + ) + } } } } @@ -125,3 +145,6 @@ private fun ContentToPreview(state: ShowLocationState) { onBackPressed = {}, ) } + +private const val PIN_ID = "pin" + diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/SendLocationPresenterTest.kt deleted file mode 100644 index 5d35f06861..0000000000 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/SendLocationPresenterTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 - -import app.cash.molecule.RecompositionClock -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class SendLocationPresenterTest { - - private val room = FakeMatrixRoom() - private val presenter = SendLocationPresenter(room) - - @Test - fun `emits initial state`() = runTest { - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - Truth.assertThat(awaitItem().mode).isEqualTo(SendLocationState.Mode.ALocation) - } - } - - @Test - fun `share location event shares a location`() = runTest { - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(SendLocationEvents.ShareLocation(1.0, 2.0)) - delay(1) - Truth.assertThat(room.sendLocationCount).isEqualTo(1) - } - } - - @Test - fun `switches mode`() = runTest { - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(SendLocationEvents.SwitchMode(SendLocationState.Mode.MyLocation)) - Truth.assertThat(awaitItem().mode).isEqualTo(SendLocationState.Mode.MyLocation) - } - } -} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterFake.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterFake.kt new file mode 100644 index 0000000000..a18e4cf2bf --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/permissions/PermissionsPresenterFake.kt @@ -0,0 +1,40 @@ +/* + * 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.permissions + +import androidx.compose.runtime.Composable + +class PermissionsPresenterFake : PermissionsPresenter { + + val events = mutableListOf() + + private fun handleEvent(event: PermissionsEvents) { + events += event + } + + private var state = PermissionsState(eventSink = ::handleEvent) + set(value) { + field = value.copy(eventSink = ::handleEvent) + } + + fun givenState(state: PermissionsState) { + this.state = state + } + + @Composable + override fun present(): PermissionsState = state +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt new file mode 100644 index 0000000000..45c99b556a --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -0,0 +1,461 @@ +/* + * 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.send + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +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.messages.test.MessageComposerContextFake +import io.element.android.libraries.matrix.api.room.location.AssetType +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.SendLocationInvocation +import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SendLocationPresenterTest { + + private val permissionsPresenterFake = PermissionsPresenterFake() + private val fakeMatrixRoom = FakeMatrixRoom() + private val fakeAnalyticsService = FakeAnalyticsService() + private val messageComposerContextFake = MessageComposerContextFake() + private val fakeLocationActions = FakeLocationActions() + private val fakeSystemClock = SystemClock { 0L } + private val fakeBuildMeta = aBuildMeta(applicationName = "app name") + private val sendLocationPresenter: SendLocationPresenter = SendLocationPresenter( + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permissions: List): PermissionsPresenter = permissionsPresenterFake + }, + room = fakeMatrixRoom, + analyticsService = fakeAnalyticsService, + messageComposerContext = messageComposerContextFake, + locationActions = fakeLocationActions, + systemClock = fakeSystemClock, + buildMeta = fakeBuildMeta, + ) + + @Test + fun `initial state with permissions granted`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + + val initialState = awaitItem() + Truth.assertThat(initialState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(initialState.mode).isEqualTo(SendLocationState.Mode.SenderLocation) + Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true) + + // Swipe the map to switch mode + initialState.eventSink(SendLocationEvents.SwitchToPinLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(true) + } + } + + @Test + fun `initial state with permissions partially granted`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.SomeGranted, + shouldShowRationale = false, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + + val initialState = awaitItem() + Truth.assertThat(initialState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(initialState.mode).isEqualTo(SendLocationState.Mode.SenderLocation) + Truth.assertThat(initialState.hasLocationPermission).isEqualTo(true) + + // Swipe the map to switch mode + initialState.eventSink(SendLocationEvents.SwitchToPinLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(true) + } + } + + @Test + fun `initial state with permissions denied`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(initialState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(initialState.hasLocationPermission).isEqualTo(false) + + // Click on the button to switch mode + initialState.eventSink(SendLocationEvents.SwitchToMyLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.PermissionDenied) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(false) + } + } + + @Test + fun `initial state with permissions denied once`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = true, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(initialState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(initialState.hasLocationPermission).isEqualTo(false) + + // Click on the button to switch mode + initialState.eventSink(SendLocationEvents.SwitchToMyLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.PermissionRationale) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(false) + } + } + + @Test + fun `rationale dialog dismiss`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = true, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + // Click on the button to switch mode + initialState.eventSink(SendLocationEvents.SwitchToMyLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.PermissionRationale) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(false) + + // Dismiss the dialog + myLocationState.eventSink(SendLocationEvents.DismissDialog) + val dialogDismissedState = awaitItem() + Truth.assertThat(dialogDismissedState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(dialogDismissedState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(dialogDismissedState.hasLocationPermission).isEqualTo(false) + } + } + + @Test + fun `rationale dialog continue`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = true, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + // Click on the button to switch mode + initialState.eventSink(SendLocationEvents.SwitchToMyLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.PermissionRationale) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(false) + + // Continue the dialog sends permission request to the permissions presenter + myLocationState.eventSink(SendLocationEvents.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(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + // Click on the button to switch mode + initialState.eventSink(SendLocationEvents.SwitchToMyLocationMode) + val myLocationState = awaitItem() + Truth.assertThat(myLocationState.permissionDialog).isEqualTo(SendLocationState.Dialog.PermissionDenied) + Truth.assertThat(myLocationState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(myLocationState.hasLocationPermission).isEqualTo(false) + + // Dismiss the dialog + myLocationState.eventSink(SendLocationEvents.DismissDialog) + val dialogDismissedState = awaitItem() + Truth.assertThat(dialogDismissedState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(dialogDismissedState.mode).isEqualTo(SendLocationState.Mode.PinLocation) + Truth.assertThat(dialogDismissedState.hasLocationPermission).isEqualTo(false) + } + } + + @Test + fun `share sender location`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.AllGranted, + shouldShowRationale = false, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + // Send location + initialState.eventSink( + SendLocationEvents.SendLocation( + cameraPosition = SendLocationEvents.SendLocation.CameraPosition( + lat = 0.0, + lon = 1.0, + zoom = 2.0, + ), + location = Location( + lat = 3.0, + lon = 4.0, + accuracy = 5.0f, + ) + ) + ) + + delay(1) // Wait for the coroutine to finish + + Truth.assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1) + Truth.assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo( + SendLocationInvocation( + body = "Location was shared at geo:3.0,4.0;u=5.0 as of 1970-01-01T00:00:00Z", + geoUri = "geo:3.0,4.0;u=5.0", + description = null, + zoomLevel = 15, + assetType = AssetType.SENDER + ) + ) + + Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) + Truth.assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo( + Composer( + inThread = false, + isEditing = false, + isLocation = true, + isReply = false, + locationType = Composer.LocationType.MyLocation, + ) + ) + } + } + + @Test + fun `share pin location`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + ) + ) + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + // Send location + initialState.eventSink( + SendLocationEvents.SendLocation( + cameraPosition = SendLocationEvents.SendLocation.CameraPosition( + lat = 0.0, + lon = 1.0, + zoom = 2.0, + ), + location = Location( + lat = 3.0, + lon = 4.0, + accuracy = 5.0f, + ) + ) + ) + + delay(1) // Wait for the coroutine to finish + + Truth.assertThat(fakeMatrixRoom.sentLocations.size).isEqualTo(1) + Truth.assertThat(fakeMatrixRoom.sentLocations.last()).isEqualTo( + SendLocationInvocation( + body = "Location was shared at geo:0.0,1.0 as of 1970-01-01T00:00:00Z", + geoUri = "geo:0.0,1.0", + description = null, + zoomLevel = 15, + assetType = AssetType.PIN + ) + ) + + Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) + Truth.assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo( + Composer( + inThread = false, + isEditing = false, + isLocation = true, + isReply = false, + locationType = Composer.LocationType.PinDrop, + ) + ) + } + } + + @Test + fun `composer context passes through analytics`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + ) + ) + messageComposerContextFake.apply { + composerMode = MessageComposerMode.Edit( + eventId = null, defaultContent = "", transactionId = null + ) + } + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + // Send location + initialState.eventSink( + SendLocationEvents.SendLocation( + cameraPosition = SendLocationEvents.SendLocation.CameraPosition( + lat = 0.0, + lon = 1.0, + zoom = 2.0, + ), + location = null + ) + ) + + delay(1) // Wait for the coroutine to finish + + Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(1) + Truth.assertThat(fakeAnalyticsService.capturedEvents.last()).isEqualTo( + Composer( + inThread = false, + isEditing = true, + isLocation = true, + isReply = false, + locationType = Composer.LocationType.PinDrop, + ) + ) + } + } + + @Test + fun `open settings activity`() = runTest { + permissionsPresenterFake.givenState( + PermissionsState( + permissions = PermissionsState.Permissions.NoneGranted, + shouldShowRationale = false, + ) + ) + messageComposerContextFake.apply { + composerMode = MessageComposerMode.Edit( + eventId = null, defaultContent = "", transactionId = null + ) + } + + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + // Skip initial state + val initialState = awaitItem() + + initialState.eventSink(SendLocationEvents.SwitchToMyLocationMode) + val dialogShownState = awaitItem() + + // Open settings + dialogShownState.eventSink(SendLocationEvents.OpenAppSettings) + val settingsOpenedState = awaitItem() + + Truth.assertThat(settingsOpenedState.permissionDialog).isEqualTo(SendLocationState.Dialog.None) + Truth.assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `application name is in state`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + sendLocationPresenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.appName).isEqualTo("app name") + } + } +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/AndroidLocationActionsTest.kt index 14dd983f34..29c0ba4d58 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/AndroidLocationActionsTest.kt @@ -18,6 +18,7 @@ package io.element.android.features.location.impl.show 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 diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/FakeLocationActions.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/FakeLocationActions.kt index 411863f725..c54aab6f28 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/FakeLocationActions.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/FakeLocationActions.kt @@ -26,8 +26,15 @@ class FakeLocationActions : LocationActions { var sharedLabel: String? = null private set + var openSettingsInvocationsCount = 0 + private set + override fun share(location: Location, label: String?) { sharedLocation = location sharedLabel = label } + + override fun openSettings() { + openSettingsInvocationsCount++ + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index d2b4d621ed..7fe7de5b9f 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -97,8 +97,8 @@ class FakeMatrixRoom( var reportedContentCount: Int = 0 private set - var sendLocationCount: Int = 0 - private set + private val _sentLocations = mutableListOf() + val sentLocations: List = _sentLocations var invitedUserId: UserId? = null @@ -279,7 +279,7 @@ class FakeMatrixRoom( zoomLevel: Int?, assetType: AssetType?, ): Result = simulateLongTask { - sendLocationCount++ + _sentLocations.add(SendLocationInvocation(body, geoUri, description, zoomLevel, assetType)) return sendLocationResult } @@ -381,3 +381,11 @@ class FakeMatrixRoom( progressCallbackValues = values } } + +data class SendLocationInvocation( + val body: String, + val geoUri: String, + val description: String?, + val zoomLevel: Int?, + val assetType: AssetType?, +) diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index e33e20d690..8c4c361707 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -359,7 +359,7 @@ private fun AttachmentButton( Image( modifier = Modifier.size(12.5f.dp), painter = painterResource(R.drawable.ic_add_attachment), - contentDescription = null, + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), contentScale = ContentScale.Inside, colorFilter = ColorFilter.tint( LocalContentColor.current diff --git a/libraries/textcomposer/src/main/res/values/localazy.xml b/libraries/textcomposer/src/main/res/values/localazy.xml index a94173c1d2..0bd53c3bee 100644 --- a/libraries/textcomposer/src/main/res/values/localazy.xml +++ b/libraries/textcomposer/src/main/res/values/localazy.xml @@ -1,5 +1,6 @@ + "Add attachment" "Toggle bullet list" "Toggle code block" "Message…" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 336ae9144c..654f3d7cbe 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -143,7 +143,6 @@ "%1$s nedokázal načítať mapu. Skúste to prosím neskôr." "Načítanie správ zlyhalo" "%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr." - "%1$s nemá povolenie na prístup k vašej polohe. Prístup môžete povoliť v Nastavenia > Poloha" "Niektoré správy neboli odoslané" "Prepáčte, vyskytla sa chyba" "🔐️ Pripojte sa ku mne na %1$s" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index c73284e700..10952f4194 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -143,7 +143,8 @@ "%1$s could not load the map. Please try again later." "Failed loading messages" "%1$s could not access your location. Please try again later." - "%1$s does not have permission to access your location. You can enable access in Settings > Location" + "To send a location, allow %1$s to access your location from its settings screen." + "To send a location, allow %1$s to access your location in the next dialog." "Some messages have not been sent" "Sorry, an error occurred" "🔐️ Join me on %1$s" diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt index 7cde29d7c3..c9498a846f 100644 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt @@ -16,6 +16,6 @@ package io.element.android.services.toolbox.api.systemclock -interface SystemClock { +fun interface SystemClock { fun epochMillis(): Long } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index fe31f40122..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewDarkPreview_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dcd8ccab99efd822f085614edb7296e5d73f3c1ae8d84ec0ccf930ead56bfa13 -size 7803 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7200bc82d1..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.map_null_DefaultGroup_MapViewLightPreview_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6040743cf442f6e3069eb77f562d6803eb9243edc959c0db7b5631ad00c583b -size 7103 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a2898e4ffb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e45014ab4619ebeb90de5c69320b3ec4be51b6f1dbf8d6c8da9fec44ec0e8a2f +size 20840 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e4f95bb4a5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c0a503af0cbcf66a71fd93fdba8cc710a5f6a3ad847bc5af36b68b2c2e7eef3 +size 34482 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e907574552 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b90f17d03c0200600356dc2f694c92703ad34dcf3721fcfb3e4f37fcfbb6e55 +size 33552 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a2898e4ffb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e45014ab4619ebeb90de5c69320b3ec4be51b6f1dbf8d6c8da9fec44ec0e8a2f +size 20840 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..505d052921 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-D-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db911f98b01b92cff861787a2cdc2a01fa6d9fc0281a19075b2bf2ebc9f8f109 +size 20971 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e203e2a1b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90000e70e71b582c32f1699c1a968725e7b6a281af4b5b2e3e64eed33d5f7916 +size 19358 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c45d402d70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:890f5dfb312fdbc3f942b7e719e2e1f12759d1348917b80d57b500332eb0bf4c +size 32109 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9fc4c56bb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:718b3c631a73d60950f1c945e8c0c31891eb9734223a8aecbcd7407baaa3bd5f +size 31290 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e203e2a1b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90000e70e71b582c32f1699c1a968725e7b6a281af4b5b2e3e64eed33d5f7916 +size 19358 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..50564a10f5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.send_null_DefaultGroup_SendLocationViewPreview-N-0_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7cc4fb19532e0c88107f0adabfef4859d4ab190ace800523490a81687ca674e +size 19435 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 10ea448514..5eb7294406 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56ef973e3f944265aac38f15a1e5f339e5b8e6923a1db783fda20a846aeea149 -size 10521 +oid sha256:364f107ffaa4844d0141361642ce3a494a187588f27be50b5fd27d44be21fa64 +size 8887 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 38533e7649..471e59a5b0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5760854d139072805765e75856e19947df525c8ab1e93d85b73a4db82e778d66 -size 13825 +oid sha256:f19878925f3b5b377a91885540fb15d29a5b78d8be2282d64e8809af0bbf5ff4 +size 12195 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 9da1d32d75..b56214c49f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aba8f92a7d7924e98efeb17b5d0c3a83e555edb94809df6446a28e2ea38a3fb2 -size 23287 +oid sha256:c936d2d804bc9e98fcc49430f11ddaa572b05fc8d3a0df93ad6521ee8e78f708 +size 21806 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 15bc69a1d6..ea8bf3f1bd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f1203a54cc36ceb6bfd50ddb1db9e55efdfa2fe09ad03eaca685bb306ad5c64 -size 10506 +oid sha256:ac01bc1992e3fa27950c7071cd3e8a06b94a608238d55972816fa2a1a3175e7c +size 9448 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 18193b118f..8ecfebb614 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b35436aff6308e353bf52f837f0d267e39b73a529523cef8c6df937792b61be8 -size 14330 +oid sha256:065d2a09680e35540b870862ec0ba5c54182e016017938ef64e510b6132333c9 +size 13389 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index b2c694902a..7701a371c3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl.show_null_DefaultGroup_ShowLocationViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f68bec570d74cbf375688b40ec27ac2b6a1048c315cb54472db78260edcc112f -size 25297 +oid sha256:0bb88c64bc68b10b1fb709135f445de5a5e4d78623448d0fef97504e025d5f6d +size 24551 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 17f6fa8481..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c67fe85270b99a4a23a63b65554c73c95ad4761cb8e4acaad6239817b22c5de7 -size 18034 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index b1f91f5599..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.location.impl_null_DefaultGroup_SendLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b6e542d637b348b22982d528c93ff9a3e70f2f833342b1d0941b9f265a5c2aa -size 18798