Let element enterprise be able to configure id for mapTiler. (#4446)
* Let element enterprise configure the ids for maptiler service. * Disable location sharing and location viewer is the service is not available. * Fix compilation issue on connected test * Do not allow to reload the map if the mapId is not available. * Update screenshots * Rename file. * Better to inject a string provider here, so we can unit test DefaultLocationService. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import config.BuildTimeConfig
|
||||
import extension.readLocalProperty
|
||||
|
||||
plugins {
|
||||
@@ -19,24 +20,36 @@ android {
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "maptiler_api_key",
|
||||
value = System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY")
|
||||
?: readLocalProperty("services.maptiler.apikey")
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_MAPTILER_APIKEY
|
||||
} else {
|
||||
System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY")
|
||||
?: readLocalProperty("services.maptiler.apikey")
|
||||
}
|
||||
?: ""
|
||||
)
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "maptiler_light_map_id",
|
||||
value = System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID")
|
||||
?: readLocalProperty("services.maptiler.lightMapId")
|
||||
// fall back to maptiler's default light map.
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_MAPTILER_LIGHT_MAPID
|
||||
} else {
|
||||
System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID")
|
||||
?: readLocalProperty("services.maptiler.lightMapId")
|
||||
}
|
||||
// fall back to maptiler's default light map.
|
||||
?: "basic-v2"
|
||||
)
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "maptiler_dark_map_id",
|
||||
value = System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID")
|
||||
?: readLocalProperty("services.maptiler.darkMapId")
|
||||
// fall back to maptiler's default dark map.
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_MAPTILER_DARK_MAPID
|
||||
} else {
|
||||
System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID")
|
||||
?: readLocalProperty("services.maptiler.darkMapId")
|
||||
}
|
||||
// fall back to maptiler's default dark map.
|
||||
?: "basic-v2-dark"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.api
|
||||
|
||||
interface LocationService {
|
||||
fun isServiceAvailable(): Boolean
|
||||
}
|
||||
@@ -103,6 +103,7 @@ fun StaticMapView(
|
||||
} else {
|
||||
StaticMapPlaceholder(
|
||||
showProgress = collectedState.value.isLoading(),
|
||||
canReload = builder.isServiceAvailable(),
|
||||
contentDescription = contentDescription,
|
||||
width = maxWidth,
|
||||
height = maxHeight,
|
||||
|
||||
@@ -57,6 +57,8 @@ internal class MapTilerStaticMapUrlBuilder(
|
||||
// to keep the perceived content size constant at the expense of sharpness.
|
||||
return "$MAPTILER_BASE_URL/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft"
|
||||
}
|
||||
|
||||
override fun isServiceAvailable() = apiKey.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun coerceWidthAndHeight(width: Int, height: Int, is2x: Boolean): Pair<Int, Int> {
|
||||
|
||||
@@ -9,8 +9,10 @@ package io.element.android.features.location.api.internal
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -18,7 +20,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
@@ -28,12 +29,12 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.BooleanProvider
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun StaticMapPlaceholder(
|
||||
showProgress: Boolean,
|
||||
canReload: Boolean,
|
||||
contentDescription: String?,
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
@@ -54,7 +55,7 @@ internal fun StaticMapPlaceholder(
|
||||
)
|
||||
if (showProgress) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
} else if (canReload) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
@@ -70,14 +71,24 @@ internal fun StaticMapPlaceholder(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun StaticMapPlaceholderPreview(
|
||||
@PreviewParameter(BooleanProvider::class) values: Boolean
|
||||
) = ElementPreview {
|
||||
StaticMapPlaceholder(
|
||||
showProgress = values,
|
||||
contentDescription = null,
|
||||
width = 400.dp,
|
||||
height = 400.dp,
|
||||
onLoadMapClick = {},
|
||||
)
|
||||
internal fun StaticMapPlaceholderPreview() = ElementPreview {
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
listOf(
|
||||
true to false,
|
||||
false to true,
|
||||
false to false,
|
||||
).forEach { (showProgress, canReload) ->
|
||||
StaticMapPlaceholder(
|
||||
showProgress = showProgress,
|
||||
canReload = canReload,
|
||||
contentDescription = null,
|
||||
width = 400.dp,
|
||||
height = 200.dp,
|
||||
onLoadMapClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ interface StaticMapUrlBuilder {
|
||||
height: Int,
|
||||
density: Float,
|
||||
): String
|
||||
|
||||
fun isServiceAvailable(): Boolean
|
||||
}
|
||||
|
||||
fun StaticMapUrlBuilder(context: Context): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder(context = context)
|
||||
|
||||
@@ -17,6 +17,21 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
darkMapId = "aDarkMapId",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `isServiceAvailable returns true if api key is not empty`() {
|
||||
assertThat(builder.isServiceAvailable()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isServiceAvailable returns false if api key is empty`() {
|
||||
val builderWithoutKey = MapTilerStaticMapUrlBuilder(
|
||||
apiKey = "",
|
||||
lightMapId = "aLightMapId",
|
||||
darkMapId = "aDarkMapId",
|
||||
)
|
||||
assertThat(builderWithoutKey.isServiceAvailable()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `static map 1x density`() {
|
||||
assertThat(
|
||||
|
||||
@@ -49,6 +49,7 @@ dependencies {
|
||||
testImplementation(projects.libraries.testtags)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.location.api.LocationService
|
||||
import io.element.android.features.location.api.R
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLocationService @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
) : LocationService {
|
||||
override fun isServiceAvailable(): Boolean {
|
||||
return stringProvider.getString(R.string.maptiler_api_key).isNotEmpty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.location.api.R
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultLocationServiceTest {
|
||||
@Test
|
||||
fun `if apiKey is empty, isServiceAvailable should return false`() {
|
||||
val fakeStringProvider = FakeStringProvider(
|
||||
defaultResult = ""
|
||||
)
|
||||
val locationService = DefaultLocationService(
|
||||
stringProvider = fakeStringProvider,
|
||||
)
|
||||
assertThat(locationService.isServiceAvailable()).isFalse()
|
||||
assertThat(fakeStringProvider.lastResIdParam).isEqualTo(R.string.maptiler_api_key)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if apiKey is not empty, isServiceAvailable should return true`() {
|
||||
val locationService = DefaultLocationService(
|
||||
stringProvider = FakeStringProvider(
|
||||
defaultResult = "aKey"
|
||||
)
|
||||
)
|
||||
assertThat(locationService.isServiceAvailable()).isTrue()
|
||||
}
|
||||
}
|
||||
18
features/location/test/build.gradle.kts
Normal file
18
features/location/test/build.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.location.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.location.api)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.test
|
||||
|
||||
import io.element.android.features.location.api.LocationService
|
||||
|
||||
class FakeLocationService(
|
||||
private val isServiceAvailable: Boolean,
|
||||
) : LocationService {
|
||||
override fun isServiceAvailable() = isServiceAvailable
|
||||
}
|
||||
@@ -75,6 +75,7 @@ dependencies {
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.features.location.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.LocationService
|
||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
@@ -96,6 +97,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val locationService: LocationService,
|
||||
private val room: MatrixRoom,
|
||||
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
||||
private val mentionSpanTheme: MentionSpanTheme,
|
||||
@@ -409,7 +411,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
NavTarget.LocationViewer(
|
||||
location = event.content.location,
|
||||
description = event.content.description,
|
||||
)
|
||||
).takeIf { locationService.isServiceAvailable() }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.location.api.LocationService
|
||||
import io.element.android.features.messages.impl.MessagesNavigator
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
@@ -104,6 +105,7 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||
private val mediaSender: MediaSender,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val locationService: LocationService,
|
||||
private val messageComposerContext: DefaultMessageComposerContext,
|
||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||
private val roomAliasSuggestionsDataSource: RoomAliasSuggestionsDataSource,
|
||||
@@ -155,7 +157,8 @@ class MessageComposerPresenter @AssistedInject constructor(
|
||||
|
||||
val canShareLocation = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)
|
||||
canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) &&
|
||||
locationService.isServiceAvailable()
|
||||
}
|
||||
|
||||
val canCreatePoll = remember { mutableStateOf(false) }
|
||||
|
||||
@@ -18,6 +18,8 @@ import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.location.api.LocationService
|
||||
import io.element.android.features.location.test.FakeLocationService
|
||||
import io.element.android.features.messages.impl.FakeMessagesNavigator
|
||||
import io.element.android.features.messages.impl.MessagesNavigator
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
@@ -1536,6 +1538,7 @@ class MessageComposerPresenterTest {
|
||||
navigator: MessagesNavigator = FakeMessagesNavigator(),
|
||||
pickerProvider: PickerProvider = this.pickerProvider,
|
||||
featureFlagService: FeatureFlagService = this.featureFlagService,
|
||||
locationService: LocationService = FakeLocationService(true),
|
||||
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||
@@ -1558,6 +1561,7 @@ class MessageComposerPresenterTest {
|
||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
analyticsService = analyticsService,
|
||||
locationService = locationService,
|
||||
messageComposerContext = DefaultMessageComposerContext(),
|
||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||
roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(),
|
||||
|
||||
@@ -46,5 +46,4 @@ dependencies {
|
||||
androidTestImplementation(libs.test.junit)
|
||||
androidTestImplementation(libs.test.truth)
|
||||
androidTestImplementation(libs.test.runner)
|
||||
androidTestImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ package io.element.android.libraries.pushstore.impl
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
@@ -28,7 +27,7 @@ class DefaultUserPushStoreFactoryTest {
|
||||
fun testParallelCreation() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
val sessionId = SessionId("@alice:server.org")
|
||||
val userPushStoreFactory = DefaultUserPushStoreFactory(context, NoOpSessionObserver())
|
||||
val userPushStoreFactory = DefaultUserPushStoreFactory(context)
|
||||
var userPushStore1: UserPushStore? = null
|
||||
val thread1 = thread {
|
||||
userPushStore1 = userPushStoreFactory.getOrCreate(sessionId)
|
||||
|
||||
@@ -13,4 +13,8 @@ object BuildTimeConfig {
|
||||
const val GOOGLE_APP_ID_RELEASE = "1:912726360885:android:d097de99a4c23d2700427c"
|
||||
const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c"
|
||||
const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c"
|
||||
|
||||
val SERVICES_MAPTILER_APIKEY: String? = null
|
||||
val SERVICES_MAPTILER_LIGHT_MAPID: String? = null
|
||||
val SERVICES_MAPTILER_DARK_MAPID: String? = null
|
||||
}
|
||||
|
||||
@@ -12,15 +12,19 @@ import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
class FakeStringProvider(
|
||||
private val defaultResult: String = "A string"
|
||||
) : StringProvider {
|
||||
var lastResIdParam: Int? = null
|
||||
override fun getString(resId: Int): String {
|
||||
lastResIdParam = resId
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
override fun getString(resId: Int, vararg formatArgs: Any?): String {
|
||||
lastResIdParam = resId
|
||||
return defaultResult + formatArgs.joinToString()
|
||||
}
|
||||
|
||||
override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String {
|
||||
lastResIdParam = resId
|
||||
return defaultResult + " ($quantity) " + formatArgs.joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user