Element config (#4471)
* Add handy extension "VariantDimension.buildConfigFieldStr" * Update configuration for MapTiler. * Update configuration for Sentry. * Build AnalyticsConfig depending on analytics configuration. * Configure analytics policy url. * Add handy extension "VariantDimension.buildConfigFieldBoolean" * Configure legal urls. * Add a way to disable rageshake / reporting bugs. * Update screenshots * Quality * Fix test * Use `ifBlank` extension * Add missing configuration for PostHog * Update configuration for Rageshake. * Add build log. * Disable crash detection if rageshake feature is not available. Disabled twice. * Hide link to analytics policy if the link is missing. * Fix test when run in enterprise context. * Use RageshakeFeatureAvailability where appropriate. * Rename file. * Move some classes to their correct module. * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
@@ -9,6 +9,8 @@ package io.element.android.x
|
||||
|
||||
import android.app.Application
|
||||
import androidx.startup.AppInitializer
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.appconfig.isEnabled
|
||||
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.x.di.AppComponent
|
||||
@@ -23,7 +25,9 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
AppInitializer.getInstance(this).apply {
|
||||
initializeComponent(CrashInitializer::class.java)
|
||||
if (RageshakeConfig.isEnabled) {
|
||||
initializeComponent(CrashInitializer::class.java)
|
||||
}
|
||||
initializeComponent(PlatformInitializer::class.java)
|
||||
initializeComponent(CacheCleanerInitializer::class.java)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
@@ -10,6 +13,37 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.appconfig"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigFieldStr(
|
||||
name = "URL_POLICY",
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.URL_POLICY ?: ""
|
||||
} else {
|
||||
"https://element.io/cookie-policy"
|
||||
},
|
||||
)
|
||||
buildConfigFieldStr(
|
||||
name = "BUG_REPORT_URL",
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.BUG_REPORT_URL ?: ""
|
||||
} else {
|
||||
"https://riot.im/bugreports/submit"
|
||||
},
|
||||
)
|
||||
buildConfigFieldStr(
|
||||
name = "BUG_REPORT_APP_NAME",
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.BUG_REPORT_APP_NAME ?: ""
|
||||
} else {
|
||||
"element-x-android"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
package io.element.android.appconfig
|
||||
|
||||
object AnalyticsConfig {
|
||||
const val POLICY_LINK = "https://element.io/cookie-policy"
|
||||
const val POLICY_LINK = BuildConfig.URL_POLICY
|
||||
}
|
||||
|
||||
@@ -11,17 +11,23 @@ object RageshakeConfig {
|
||||
/**
|
||||
* The URL to submit bug reports to.
|
||||
*/
|
||||
const val BUG_REPORT_URL = "https://riot.im/bugreports/submit"
|
||||
const val BUG_REPORT_URL = BuildConfig.BUG_REPORT_URL
|
||||
|
||||
/**
|
||||
* As per https://github.com/matrix-org/rageshake:
|
||||
* Identifier for the application (eg 'riot-web').
|
||||
* Should correspond to a mapping configured in the configuration file for github issue reporting to work.
|
||||
*/
|
||||
const val BUG_REPORT_APP_NAME = "element-x-android"
|
||||
const val BUG_REPORT_APP_NAME = BuildConfig.BUG_REPORT_APP_NAME
|
||||
|
||||
/**
|
||||
* The maximum size of the upload request. Default value is just below CloudFlare's max request size.
|
||||
*/
|
||||
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the rageshake feature is enabled.
|
||||
*/
|
||||
val RageshakeConfig.isEnabled: Boolean
|
||||
get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty()
|
||||
|
||||
@@ -13,12 +13,17 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<Analytic
|
||||
override val values: Sequence<AnalyticsPreferencesState>
|
||||
get() = sequenceOf(
|
||||
aAnalyticsPreferencesState().copy(isEnabled = true),
|
||||
aAnalyticsPreferencesState().copy(isEnabled = true, policyUrl = ""),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAnalyticsPreferencesState() = AnalyticsPreferencesState(
|
||||
applicationName = "Element X",
|
||||
isEnabled = false,
|
||||
policyUrl = "https://element.io",
|
||||
fun aAnalyticsPreferencesState(
|
||||
applicationName: String = "Element X",
|
||||
isEnabled: Boolean = false,
|
||||
policyUrl: String = "https://element.io",
|
||||
) = AnalyticsPreferencesState(
|
||||
applicationName = applicationName,
|
||||
isEnabled = isEnabled,
|
||||
policyUrl = policyUrl,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
@@ -36,11 +36,6 @@ fun AnalyticsPreferencesView(
|
||||
id = R.string.screen_analytics_settings_help_us_improve,
|
||||
state.applicationName
|
||||
)
|
||||
val linkText = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_analytics_settings_read_terms,
|
||||
R.string.screen_analytics_settings_read_terms_content_link,
|
||||
tagAndLink = LINK_TAG to state.policyUrl,
|
||||
)
|
||||
Column(modifier) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
@@ -57,7 +52,14 @@ fun AnalyticsPreferencesView(
|
||||
onEnabledChanged(!state.isEnabled)
|
||||
}
|
||||
)
|
||||
ListSupportingText(annotatedString = linkText)
|
||||
if (state.policyUrl.isNotEmpty()) {
|
||||
val linkText = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_analytics_settings_read_terms,
|
||||
R.string.screen_analytics_settings_read_terms_content_link,
|
||||
tagAndLink = LINK_TAG to state.policyUrl,
|
||||
)
|
||||
ListSupportingText(annotatedString = linkText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ package io.element.android.features.analytics.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.appconfig.AnalyticsConfig
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
@@ -36,6 +37,7 @@ class AnalyticsOptInPresenter @Inject constructor(
|
||||
|
||||
return AnalyticsOptInState(
|
||||
applicationName = buildMeta.applicationName,
|
||||
hasPolicyLink = AnalyticsConfig.POLICY_LINK.isNotEmpty(),
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
|
||||
data class AnalyticsOptInState(
|
||||
val applicationName: String,
|
||||
val hasPolicyLink: Boolean,
|
||||
val eventSink: (AnalyticsOptInEvents) -> Unit
|
||||
)
|
||||
|
||||
@@ -14,10 +14,14 @@ open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterP
|
||||
override val values: Sequence<AnalyticsOptInState>
|
||||
get() = sequenceOf(
|
||||
aAnalyticsOptInState(),
|
||||
aAnalyticsOptInState(hasPolicyLink = false),
|
||||
)
|
||||
}
|
||||
|
||||
fun aAnalyticsOptInState() = AnalyticsOptInState(
|
||||
fun aAnalyticsOptInState(
|
||||
hasPolicyLink: Boolean = true,
|
||||
) = AnalyticsOptInState(
|
||||
applicationName = "Element X",
|
||||
hasPolicyLink = hasPolicyLink,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
@@ -95,25 +95,27 @@ private fun AnalyticsOptInHeader(
|
||||
subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Chart())
|
||||
)
|
||||
val text = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_analytics_prompt_read_terms,
|
||||
R.string.screen_analytics_prompt_read_terms_content_link,
|
||||
color = Color.Unspecified,
|
||||
underline = false,
|
||||
bold = true,
|
||||
tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK,
|
||||
)
|
||||
ClickableLinkText(
|
||||
annotatedString = text,
|
||||
onClick = { onClickTerms() },
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
.copy(
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
)
|
||||
if (state.hasPolicyLink) {
|
||||
val text = buildAnnotatedStringWithStyledPart(
|
||||
R.string.screen_analytics_prompt_read_terms,
|
||||
R.string.screen_analytics_prompt_read_terms_content_link,
|
||||
color = Color.Unspecified,
|
||||
underline = false,
|
||||
bold = true,
|
||||
tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK,
|
||||
)
|
||||
ClickableLinkText(
|
||||
annotatedString = text,
|
||||
onClick = { onClickTerms() },
|
||||
modifier = Modifier
|
||||
.padding(8.dp),
|
||||
style = ElementTheme.typography.fontBodyMdRegular
|
||||
.copy(
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appconfig.AnalyticsConfig
|
||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
@@ -35,7 +36,7 @@ class AnalyticsPreferencesPresenterTest {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isEnabled).isTrue()
|
||||
assertThat(initialState.policyUrl).isNotEmpty()
|
||||
assertThat(initialState.policyUrl).isEqualTo(AnalyticsConfig.POLICY_LINK)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.readLocalProperty
|
||||
|
||||
plugins {
|
||||
@@ -16,10 +17,17 @@ plugins {
|
||||
android {
|
||||
namespace = "io.element.android.features.location.api"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "maptiler_api_key",
|
||||
buildConfigFieldStr(
|
||||
name = "MAPTILER_BASE_URL",
|
||||
value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL ?: "https://api.maptiler.com/maps"
|
||||
)
|
||||
buildConfigFieldStr(
|
||||
name = "MAPTILER_API_KEY",
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_MAPTILER_APIKEY
|
||||
} else {
|
||||
@@ -28,9 +36,8 @@ android {
|
||||
}
|
||||
?: ""
|
||||
)
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "maptiler_light_map_id",
|
||||
buildConfigFieldStr(
|
||||
name = "MAPTILER_LIGHT_MAP_ID",
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_MAPTILER_LIGHT_MAPID
|
||||
} else {
|
||||
@@ -40,9 +47,8 @@ android {
|
||||
// fall back to maptiler's default light map.
|
||||
?: "basic-v2"
|
||||
)
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "maptiler_dark_map_id",
|
||||
buildConfigFieldStr(
|
||||
name = "MAPTILER_DARK_MAP_ID",
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_MAPTILER_DARK_MAPID
|
||||
} else {
|
||||
|
||||
@@ -57,7 +57,7 @@ fun StaticMapView(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var retryHash by remember { mutableIntStateOf(0) }
|
||||
val builder = remember { StaticMapUrlBuilder(context) }
|
||||
val builder = remember { StaticMapUrlBuilder() }
|
||||
val painter = rememberAsyncImagePainter(
|
||||
model = if (constraints.isZero) {
|
||||
// Avoid building a URL if any of the size constraints is zero (else it will thrown an exception).
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 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.internal
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.features.location.api.R
|
||||
|
||||
internal const val MAPTILER_BASE_URL = "https://api.maptiler.com/maps"
|
||||
|
||||
internal fun Context.mapId(darkMode: Boolean) = when (darkMode) {
|
||||
true -> getString(R.string.maptiler_dark_map_id)
|
||||
false -> getString(R.string.maptiler_light_map_id)
|
||||
}
|
||||
|
||||
internal val Context.apiKey: String
|
||||
get() = getString(R.string.maptiler_api_key)
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
package io.element.android.features.location.api.internal
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.features.location.api.BuildConfig
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
@@ -16,14 +16,16 @@ import kotlin.math.roundToInt
|
||||
* https://docs.maptiler.com/cloud/api/static-maps/
|
||||
*/
|
||||
internal class MapTilerStaticMapUrlBuilder(
|
||||
private val baseUrl: String,
|
||||
private val apiKey: String,
|
||||
private val lightMapId: String,
|
||||
private val darkMapId: String,
|
||||
) : StaticMapUrlBuilder {
|
||||
constructor(context: Context) : this(
|
||||
apiKey = context.apiKey,
|
||||
lightMapId = context.mapId(darkMode = false),
|
||||
darkMapId = context.mapId(darkMode = true),
|
||||
constructor() : this(
|
||||
baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"),
|
||||
apiKey = BuildConfig.MAPTILER_API_KEY,
|
||||
lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
|
||||
darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID,
|
||||
)
|
||||
|
||||
override fun build(
|
||||
@@ -55,7 +57,7 @@ internal class MapTilerStaticMapUrlBuilder(
|
||||
// image smaller than the available space in pixels.
|
||||
// The resulting image will have to be scaled to fit the available space in order
|
||||
// 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"
|
||||
return "$baseUrl/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft"
|
||||
}
|
||||
|
||||
override fun isServiceAvailable() = apiKey.isNotEmpty()
|
||||
|
||||
@@ -9,21 +9,23 @@
|
||||
|
||||
package io.element.android.features.location.api.internal
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.features.location.api.BuildConfig
|
||||
|
||||
internal class MapTilerTileServerStyleUriBuilder(
|
||||
private val baseUrl: String,
|
||||
private val apiKey: String,
|
||||
private val lightMapId: String,
|
||||
private val darkMapId: String,
|
||||
) : TileServerStyleUriBuilder {
|
||||
constructor(context: Context) : this(
|
||||
apiKey = context.apiKey,
|
||||
lightMapId = context.mapId(darkMode = false),
|
||||
darkMapId = context.mapId(darkMode = true),
|
||||
constructor() : this(
|
||||
baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"),
|
||||
apiKey = BuildConfig.MAPTILER_API_KEY,
|
||||
lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
|
||||
darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID,
|
||||
)
|
||||
|
||||
override fun build(darkMode: Boolean): String {
|
||||
val mapId = if (darkMode) darkMapId else lightMapId
|
||||
return "$MAPTILER_BASE_URL/$mapId/style.json?key=$apiKey"
|
||||
return "$baseUrl/$mapId/style.json?key=$apiKey"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
package io.element.android.features.location.api.internal
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Builds an URL for a 3rd party service provider static maps API.
|
||||
*/
|
||||
@@ -26,4 +24,4 @@ interface StaticMapUrlBuilder {
|
||||
fun isServiceAvailable(): Boolean
|
||||
}
|
||||
|
||||
fun StaticMapUrlBuilder(context: Context): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder(context = context)
|
||||
fun StaticMapUrlBuilder(): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder()
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
|
||||
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.compound.theme.ElementTheme
|
||||
|
||||
/**
|
||||
@@ -24,7 +22,7 @@ interface TileServerStyleUriBuilder {
|
||||
): String
|
||||
}
|
||||
|
||||
fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder(context = context)
|
||||
fun TileServerStyleUriBuilder(): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder()
|
||||
|
||||
/**
|
||||
* Provides and remembers a style URI for a MapLibre compatible tile server.
|
||||
@@ -33,9 +31,8 @@ fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = Map
|
||||
*/
|
||||
@Composable
|
||||
fun rememberTileStyleUrl(): String {
|
||||
val context = LocalContext.current
|
||||
val darkMode = !ElementTheme.isLightTheme
|
||||
return remember(darkMode) {
|
||||
TileServerStyleUriBuilder(context).build(darkMode)
|
||||
TileServerStyleUriBuilder().build(darkMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.junit.Test
|
||||
|
||||
class MapTilerStaticMapUrlBuilderTest {
|
||||
private val builder = MapTilerStaticMapUrlBuilder(
|
||||
baseUrl = "https://base.url",
|
||||
apiKey = "anApiKey",
|
||||
lightMapId = "aLightMapId",
|
||||
darkMapId = "aDarkMapId",
|
||||
@@ -25,6 +26,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
@Test
|
||||
fun `isServiceAvailable returns false if api key is empty`() {
|
||||
val builderWithoutKey = MapTilerStaticMapUrlBuilder(
|
||||
baseUrl = "https://base.url",
|
||||
apiKey = "",
|
||||
lightMapId = "aLightMapId",
|
||||
darkMapId = "aDarkMapId",
|
||||
@@ -44,7 +46,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 600,
|
||||
density = 1f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,7 +61,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 900,
|
||||
density = 1.5f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +76,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 1200,
|
||||
density = 2f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,7 +91,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 1800,
|
||||
density = 3f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,7 +106,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 2048,
|
||||
density = 1f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft")
|
||||
|
||||
assertThat(
|
||||
builder.build(
|
||||
@@ -116,7 +118,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 4096,
|
||||
density = 1f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft")
|
||||
|
||||
assertThat(
|
||||
builder.build(
|
||||
@@ -128,7 +130,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 2048,
|
||||
density = 2f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
|
||||
assertThat(
|
||||
builder.build(
|
||||
@@ -140,7 +142,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 4096,
|
||||
density = 2f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
|
||||
assertThat(
|
||||
builder.build(
|
||||
@@ -152,7 +154,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = Int.MAX_VALUE,
|
||||
density = 2f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -167,7 +169,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 0,
|
||||
density = 1f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||
|
||||
assertThat(
|
||||
builder.build(
|
||||
@@ -179,7 +181,7 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = 0,
|
||||
density = 2f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft")
|
||||
|
||||
assertThat(
|
||||
builder.build(
|
||||
@@ -191,6 +193,6 @@ class MapTilerStaticMapUrlBuilderTest {
|
||||
height = Int.MIN_VALUE,
|
||||
density = 1f,
|
||||
)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||
).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.junit.Test
|
||||
|
||||
class MapTilerTileServerStyleUriBuilderTest {
|
||||
private val builder = MapTilerTileServerStyleUriBuilder(
|
||||
baseUrl = "https://base.url",
|
||||
apiKey = "anApiKey",
|
||||
lightMapId = "aLightMapId",
|
||||
darkMapId = "aDarkMapId",
|
||||
@@ -21,13 +22,13 @@ class MapTilerTileServerStyleUriBuilderTest {
|
||||
fun `light map uri`() {
|
||||
assertThat(
|
||||
builder.build(darkMode = false)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aLightMapId/style.json?key=anApiKey")
|
||||
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dark map uri`() {
|
||||
assertThat(
|
||||
builder.build(darkMode = true)
|
||||
).isEqualTo("https://api.maptiler.com/maps/aDarkMapId/style.json?key=anApiKey")
|
||||
).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ 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)
|
||||
|
||||
@@ -8,17 +8,14 @@
|
||||
package io.element.android.features.location.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.location.api.BuildConfig
|
||||
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 {
|
||||
class DefaultLocationService @Inject constructor() : LocationService {
|
||||
override fun isServiceAvailable(): Boolean {
|
||||
return stringProvider.getString(R.string.maptiler_api_key).isNotEmpty()
|
||||
return BuildConfig.MAPTILER_API_KEY.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,30 +8,15 @@
|
||||
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 io.element.android.features.location.api.BuildConfig
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultLocationServiceTest {
|
||||
@Test
|
||||
fun `if apiKey is empty, isServiceAvailable should return false`() {
|
||||
val fakeStringProvider = FakeStringProvider(
|
||||
defaultResult = ""
|
||||
fun `isServiceAvailable should return value depending on BuildConfig MAPTILER_API_KEY`() {
|
||||
val locationService = DefaultLocationService()
|
||||
assertThat(locationService.isServiceAvailable()).isEqualTo(
|
||||
BuildConfig.MAPTILER_API_KEY.isNotEmpty()
|
||||
)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
||||
@@ -10,7 +10,9 @@ package io.element.android.features.onboarding.impl
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
@@ -24,16 +26,19 @@ import javax.inject.Inject
|
||||
class OnBoardingPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
) : Presenter<OnBoardingState> {
|
||||
@Composable
|
||||
override fun present(): OnBoardingState {
|
||||
val canLoginWithQrCode by produceState(initialValue = false) {
|
||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||
}
|
||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
return OnBoardingState(
|
||||
productionApplicationName = buildMeta.productionApplicationName,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT,
|
||||
canReportBug = canReportBug,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,5 @@ data class OnBoardingState(
|
||||
val productionApplicationName: String,
|
||||
val canLoginWithQrCode: Boolean,
|
||||
val canCreateAccount: Boolean,
|
||||
val canReportBug: Boolean,
|
||||
)
|
||||
|
||||
@@ -16,15 +16,18 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
|
||||
anOnBoardingState(canLoginWithQrCode = true),
|
||||
anOnBoardingState(canCreateAccount = true),
|
||||
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true),
|
||||
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun anOnBoardingState(
|
||||
productionApplicationName: String = "Element",
|
||||
canLoginWithQrCode: Boolean = false,
|
||||
canCreateAccount: Boolean = false
|
||||
canCreateAccount: Boolean = false,
|
||||
canReportBug: Boolean = false,
|
||||
) = OnBoardingState(
|
||||
productionApplicationName = productionApplicationName,
|
||||
canLoginWithQrCode = canLoginWithQrCode,
|
||||
canCreateAccount = canCreateAccount
|
||||
canCreateAccount = canCreateAccount,
|
||||
canReportBug = canReportBug,
|
||||
)
|
||||
|
||||
@@ -144,8 +144,8 @@ private fun OnBoardingButtons(
|
||||
text = stringResource(id = signInButtonStringRes),
|
||||
onClick = onSignIn,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.onBoardingSignIn)
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.onBoardingSignIn)
|
||||
)
|
||||
if (state.canCreateAccount) {
|
||||
TextButton(
|
||||
@@ -155,15 +155,17 @@ private fun OnBoardingButtons(
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
// Add a report problem text button. Use a Text since we need a special theme here.
|
||||
Text(
|
||||
modifier = Modifier
|
||||
if (state.canReportBug) {
|
||||
// Add a report problem text button. Use a Text since we need a special theme here.
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable(onClick = onReportProblem),
|
||||
text = stringResource(id = CommonStrings.common_report_a_problem),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
text = stringResource(id = CommonStrings.common_report_a_problem),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -38,6 +39,7 @@ class OnBoardingPresenterTest {
|
||||
val presenter = OnBoardingPresenter(
|
||||
buildMeta = buildMeta,
|
||||
featureFlagService = featureFlagService,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -46,7 +48,21 @@ class OnBoardingPresenterTest {
|
||||
assertThat(initialState.canLoginWithQrCode).isFalse()
|
||||
assertThat(initialState.productionApplicationName).isEqualTo("B")
|
||||
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
|
||||
assertThat(initialState.canReportBug).isTrue()
|
||||
assertThat(awaitItem().canLoginWithQrCode).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - rageshake not available`() = runTest {
|
||||
val presenter = OnBoardingPresenter(
|
||||
buildMeta = aBuildMeta(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
rageshakeFeatureAvailability = { false },
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().canReportBug).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ package io.element.android.features.onboarding.impl
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
@@ -76,13 +77,28 @@ class OnboardingViewTest {
|
||||
fun `clicking on report a problem calls the sign in callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(),
|
||||
state = anOnBoardingState(
|
||||
canReportBug = true,
|
||||
),
|
||||
onReportProblem = callback,
|
||||
)
|
||||
val text = rule.activity.getString(CommonStrings.common_report_a_problem)
|
||||
rule.onNodeWithText(text).assertExists()
|
||||
rule.clickOn(CommonStrings.common_report_a_problem)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cannot report a problem when the feature is disabled`() {
|
||||
rule.setOnboardingView(
|
||||
state = anOnBoardingState(
|
||||
canReportBug = false,
|
||||
),
|
||||
)
|
||||
val text = rule.activity.getString(CommonStrings.common_report_a_problem)
|
||||
rule.onNodeWithText(text).assertDoesNotExist()
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setOnboardingView(
|
||||
state: OnBoardingState,
|
||||
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
@@ -19,6 +21,25 @@ android {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigFieldStr(
|
||||
name = "URL_COPYRIGHT",
|
||||
value = BuildTimeConfig.URL_COPYRIGHT ?: "https://element.io/copyright",
|
||||
)
|
||||
buildConfigFieldStr(
|
||||
name = "URL_ACCEPTABLE_USE",
|
||||
value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms",
|
||||
)
|
||||
buildConfigFieldStr(
|
||||
name = "URL_PRIVACY",
|
||||
value = BuildTimeConfig.URL_PRIVACY ?: "https://element.io/privacy",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
package io.element.android.features.preferences.impl.about
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import io.element.android.features.preferences.impl.BuildConfig
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
private const val COPYRIGHT_URL = "https://element.io/copyright"
|
||||
private const val USE_POLICY_URL = "https://element.io/acceptable-use-policy-terms"
|
||||
private const val PRIVACY_URL = "https://element.io/privacy"
|
||||
private const val COPYRIGHT_URL = BuildConfig.URL_COPYRIGHT
|
||||
private const val USE_POLICY_URL = BuildConfig.URL_ACCEPTABLE_USE
|
||||
private const val PRIVACY_URL = BuildConfig.URL_PRIVACY
|
||||
|
||||
sealed class ElementLegal(
|
||||
@StringRes val titleRes: Int,
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
@@ -44,6 +45,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
private val indicatorService: IndicatorService,
|
||||
private val directLogoutPresenter: Presenter<DirectLogoutState>,
|
||||
private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
) : Presenter<PreferencesRootState> {
|
||||
@Composable
|
||||
override fun present(): PreferencesRootState {
|
||||
@@ -79,6 +81,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
var canDeactivateAccount by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
LaunchedEffect(Unit) {
|
||||
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
||||
}
|
||||
@@ -114,6 +117,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||
accountManagementUrl = accountManagementUrl.value,
|
||||
devicesManagementUrl = devicesManagementUrl.value,
|
||||
showAnalyticsSettings = hasAnalyticsProviders,
|
||||
canReportBug = canReportBug,
|
||||
showDeveloperSettings = showDeveloperSettings,
|
||||
canDeactivateAccount = canDeactivateAccount,
|
||||
showNotificationSettings = showNotificationSettings.value,
|
||||
|
||||
@@ -20,6 +20,7 @@ data class PreferencesRootState(
|
||||
val showSecureBackupBadge: Boolean,
|
||||
val accountManagementUrl: String?,
|
||||
val devicesManagementUrl: String?,
|
||||
val canReportBug: Boolean,
|
||||
val showAnalyticsSettings: Boolean,
|
||||
val showDeveloperSettings: Boolean,
|
||||
val canDeactivateAccount: Boolean,
|
||||
|
||||
@@ -25,6 +25,7 @@ fun aPreferencesRootState(
|
||||
accountManagementUrl = "aUrl",
|
||||
devicesManagementUrl = "anOtherUrl",
|
||||
showAnalyticsSettings = true,
|
||||
canReportBug = true,
|
||||
showDeveloperSettings = true,
|
||||
showNotificationSettings = true,
|
||||
showLockScreenSettings = true,
|
||||
|
||||
@@ -202,11 +202,13 @@ private fun ColumnScope.GeneralSection(
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())),
|
||||
onClick = onOpenAbout,
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
onClick = onOpenRageShake
|
||||
)
|
||||
if (state.canReportBug) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
|
||||
onClick = onOpenRageShake
|
||||
)
|
||||
}
|
||||
if (state.showAnalyticsSettings) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(id = CommonStrings.common_analytics)) },
|
||||
|
||||
@@ -13,6 +13,7 @@ import app.cash.turbine.ReceiveTurbine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
@@ -78,6 +79,7 @@ class PreferencesRootPresenterTest {
|
||||
assertThat(loadedState.showLockScreenSettings).isTrue()
|
||||
assertThat(loadedState.showNotificationSettings).isTrue()
|
||||
assertThat(loadedState.canDeactivateAccount).isTrue()
|
||||
assertThat(loadedState.canReportBug).isTrue()
|
||||
assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState())
|
||||
assertThat(loadedState.snackbarMessage).isNull()
|
||||
skipItems(1)
|
||||
@@ -92,6 +94,22 @@ class PreferencesRootPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - cannot report bug`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
canDeactivateAccountResult = { true },
|
||||
accountManagementUrlResult = { Result.success("") },
|
||||
)
|
||||
createPresenter(
|
||||
matrixClient = matrixClient,
|
||||
rageshakeFeatureAvailability = { false },
|
||||
).test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.canReportBug).isFalse()
|
||||
skipItems(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can deactivate account is false if the Matrix client say so`() = runTest {
|
||||
createPresenter(
|
||||
@@ -146,6 +164,7 @@ class PreferencesRootPresenterTest {
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
|
||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
|
||||
) = PreferencesRootPresenter(
|
||||
matrixClient = matrixClient,
|
||||
sessionVerificationService = sessionVerificationService,
|
||||
@@ -159,5 +178,6 @@ class PreferencesRootPresenterTest {
|
||||
),
|
||||
directLogoutPresenter = { aDirectLogoutState() },
|
||||
showDeveloperSettingsProvider = showDeveloperSettingsProvider,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.rageshake.api
|
||||
|
||||
fun interface RageshakeFeatureAvailability {
|
||||
fun isAvailable(): Boolean
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
package io.element.android.features.rageshake.api.preferences
|
||||
|
||||
data class RageshakePreferencesState(
|
||||
val isFeatureEnabled: Boolean,
|
||||
val isEnabled: Boolean,
|
||||
val isSupported: Boolean,
|
||||
val sensitivity: Float,
|
||||
|
||||
@@ -12,14 +12,21 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
open class RageshakePreferencesStateProvider : PreviewParameterProvider<RageshakePreferencesState> {
|
||||
override val values: Sequence<RageshakePreferencesState>
|
||||
get() = sequenceOf(
|
||||
aRageshakePreferencesState().copy(isEnabled = true, isSupported = true, sensitivity = 0.5f),
|
||||
aRageshakePreferencesState().copy(isEnabled = true, isSupported = false, sensitivity = 0.5f),
|
||||
aRageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f),
|
||||
aRageshakePreferencesState(isEnabled = true, isSupported = false, sensitivity = 0.5f),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRageshakePreferencesState() = RageshakePreferencesState(
|
||||
isEnabled = false,
|
||||
isSupported = true,
|
||||
sensitivity = 0.3f,
|
||||
eventSink = {}
|
||||
fun aRageshakePreferencesState(
|
||||
isFeatureEnabled: Boolean = true,
|
||||
isEnabled: Boolean = false,
|
||||
isSupported: Boolean = true,
|
||||
sensitivity: Float = 0.3f,
|
||||
eventSink: (RageshakePreferencesEvents) -> Unit = {}
|
||||
) = RageshakePreferencesState(
|
||||
isFeatureEnabled = isFeatureEnabled,
|
||||
isEnabled = isEnabled,
|
||||
isSupported = isSupported,
|
||||
sensitivity = sensitivity,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -36,28 +36,30 @@ fun RageshakePreferencesView(
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
PreferenceCategory(title = stringResource(id = R.string.settings_rageshake)) {
|
||||
if (state.isSupported) {
|
||||
PreferenceSwitch(
|
||||
title = stringResource(id = CommonStrings.preference_rageshake),
|
||||
isChecked = state.isEnabled,
|
||||
onCheckedChange = ::onEnabledChanged
|
||||
)
|
||||
PreferenceSlide(
|
||||
title = stringResource(id = R.string.settings_rageshake_detection_threshold),
|
||||
// summary = stringResource(id = CommonStrings.settings_rageshake_detection_threshold_summary),
|
||||
value = state.sensitivity,
|
||||
enabled = state.isEnabled,
|
||||
// 5 possible values - steps are in ]0, 1[
|
||||
steps = 3,
|
||||
onValueChange = ::onSensitivityChanged
|
||||
)
|
||||
} else {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Rageshaking is not supported by your device")
|
||||
},
|
||||
)
|
||||
if (state.isFeatureEnabled) {
|
||||
PreferenceCategory(title = stringResource(id = R.string.settings_rageshake)) {
|
||||
if (state.isSupported) {
|
||||
PreferenceSwitch(
|
||||
title = stringResource(id = CommonStrings.preference_rageshake),
|
||||
isChecked = state.isEnabled,
|
||||
onCheckedChange = ::onEnabledChanged
|
||||
)
|
||||
PreferenceSlide(
|
||||
title = stringResource(id = R.string.settings_rageshake_detection_threshold),
|
||||
// summary = stringResource(id = CommonStrings.settings_rageshake_detection_threshold_summary),
|
||||
value = state.sensitivity,
|
||||
enabled = state.isEnabled,
|
||||
// 5 possible values - steps are in ]0, 1[
|
||||
steps = 3,
|
||||
onValueChange = ::onSensitivityChanged
|
||||
)
|
||||
} else {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Rageshaking is not supported by your device")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.rageshake.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.appconfig.isEnabled
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability {
|
||||
override fun isAvailable(): Boolean {
|
||||
return RageshakeConfig.isEnabled
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@ import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.api.crash
|
||||
package io.element.android.features.rageshake.impl.crash
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -9,15 +9,17 @@ package io.element.android.features.rageshake.impl.crash
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionEvents
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionState
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -25,12 +27,18 @@ import javax.inject.Inject
|
||||
class DefaultCrashDetectionPresenter @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val crashDataStore: CrashDataStore,
|
||||
) :
|
||||
CrashDetectionPresenter {
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
) : CrashDetectionPresenter {
|
||||
@Composable
|
||||
override fun present(): CrashDetectionState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false)
|
||||
val crashDetected = remember {
|
||||
if (rageshakeFeatureAvailability.isAvailable()) {
|
||||
crashDataStore.appHasCrashed()
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
}.collectAsState(false)
|
||||
|
||||
fun handleEvents(event: CrashDetectionEvents) {
|
||||
when (event) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
||||
@@ -20,9 +20,9 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionPre
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||
import io.element.android.features.rageshake.api.rageshake.RageShake
|
||||
import io.element.android.features.rageshake.api.screenshot.ImageResult
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.rageshake.RageShake
|
||||
import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -75,7 +75,8 @@ class DefaultRageshakeDetectionPresenter @Inject constructor(
|
||||
LaunchedEffect(preferencesState.sensitivity) {
|
||||
rageShake.setSensitivity(preferencesState.sensitivity)
|
||||
}
|
||||
val shouldStart = preferencesState.isEnabled &&
|
||||
val shouldStart = preferencesState.isFeatureEnabled &&
|
||||
preferencesState.isEnabled &&
|
||||
preferencesState.isSupported &&
|
||||
isStarted.value &&
|
||||
!takeScreenshot.value &&
|
||||
|
||||
@@ -11,14 +11,16 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||
import io.element.android.features.rageshake.api.rageshake.RageShake
|
||||
import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore
|
||||
import io.element.android.features.rageshake.impl.rageshake.RageShake
|
||||
import io.element.android.features.rageshake.impl.rageshake.RageshakeDataStore
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -28,6 +30,7 @@ import javax.inject.Inject
|
||||
class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||
private val rageshake: RageShake,
|
||||
private val rageshakeDataStore: RageshakeDataStore,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
) : RageshakePreferencesPresenter {
|
||||
@Composable
|
||||
override fun present(): RageshakePreferencesState {
|
||||
@@ -35,6 +38,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||
val isSupported: MutableState<Boolean> = rememberSaveable {
|
||||
mutableStateOf(rageshake.isAvailable())
|
||||
}
|
||||
val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
val isEnabled = rageshakeDataStore
|
||||
.isEnabled()
|
||||
.collectAsState(initial = false)
|
||||
@@ -51,6 +55,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
return RageshakePreferencesState(
|
||||
isFeatureEnabled = isFeatureAvailable,
|
||||
isEnabled = isEnabled.value,
|
||||
isSupported = isSupported.value,
|
||||
sensitivity = sensitivity.value,
|
||||
|
||||
@@ -13,7 +13,6 @@ import android.hardware.SensorManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import com.squareup.seismic.ShakeDetector
|
||||
import io.element.android.features.rageshake.api.rageshake.RageShake
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.api.rageshake
|
||||
package io.element.android.features.rageshake.impl.rageshake
|
||||
|
||||
interface RageShake {
|
||||
/**
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.api.rageshake
|
||||
package io.element.android.features.rageshake.impl.rageshake
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -13,10 +13,10 @@ import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder
|
||||
import io.element.android.libraries.androidutils.file.compressFile
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
import io.element.android.libraries.androidutils.bitmap.writeBitmap
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.api.screenshot
|
||||
package io.element.android.features.rageshake.impl.screenshot
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
@@ -11,13 +11,13 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporter
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
|
||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||
import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI
|
||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.crash.A_CRASH_DATA
|
||||
import io.element.android.features.rageshake.impl.crash.CrashDataStore
|
||||
import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore
|
||||
import io.element.android.features.rageshake.impl.screenshot.A_SCREENSHOT_URI
|
||||
import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.test.crash
|
||||
package io.element.android.features.rageshake.impl.crash
|
||||
|
||||
import io.element.android.features.rageshake.api.crash.CrashDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -12,9 +12,9 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.rageshake.api.crash.CrashDetectionEvents
|
||||
import io.element.android.features.rageshake.impl.crash.A_CRASH_DATA
|
||||
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
|
||||
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
|
||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||
import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
@@ -51,6 +51,20 @@ class CrashDetectionPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state crash is ignored if the feature is not available`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
FakeCrashDataStore(appHasCrashed = true),
|
||||
isFeatureAvailable = false,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.crashDetected).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - reset app has crashed`() = runTest {
|
||||
val presenter = createPresenter(
|
||||
@@ -86,8 +100,10 @@ class CrashDetectionPresenterTest {
|
||||
private fun createPresenter(
|
||||
crashDataStore: FakeCrashDataStore = FakeCrashDataStore(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
isFeatureAvailable: Boolean = true,
|
||||
) = DefaultCrashDetectionPresenter(
|
||||
buildMeta = buildMeta,
|
||||
crashDataStore = crashDataStore,
|
||||
rageshakeFeatureAvailability = { isFeatureAvailable },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents
|
||||
import io.element.android.features.rageshake.api.screenshot.ImageResult
|
||||
import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.mockk.mockk
|
||||
@@ -52,6 +52,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -76,6 +77,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -101,6 +103,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -135,6 +138,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -169,6 +173,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
||||
@@ -12,9 +12,9 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents
|
||||
import io.element.android.features.rageshake.test.rageshake.A_SENSITIVITY
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY
|
||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
@@ -28,7 +28,8 @@ class RageshakePreferencesPresenterTest {
|
||||
fun `present - initial state available`() = runTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = true),
|
||||
FakeRageshakeDataStore(isEnabled = true)
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -44,7 +45,8 @@ class RageshakePreferencesPresenterTest {
|
||||
fun `present - initial state not available`() = runTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = false),
|
||||
FakeRageshakeDataStore(isEnabled = true)
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -60,7 +62,8 @@ class RageshakePreferencesPresenterTest {
|
||||
fun `present - enable and disable`() = runTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = true),
|
||||
FakeRageshakeDataStore(isEnabled = true)
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -79,7 +82,8 @@ class RageshakePreferencesPresenterTest {
|
||||
fun `present - set sensitivity`() = runTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = true),
|
||||
FakeRageshakeDataStore(isEnabled = true)
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.test.rageshake
|
||||
|
||||
import io.element.android.features.rageshake.api.rageshake.RageShake
|
||||
package io.element.android.features.rageshake.impl.rageshake
|
||||
|
||||
class FakeRageShake(
|
||||
private var isAvailableValue: Boolean = true
|
||||
@@ -5,9 +5,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.test.rageshake
|
||||
package io.element.android.features.rageshake.impl.rageshake
|
||||
|
||||
import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
package io.element.android.features.rageshake.impl.reporter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.features.rageshake.api.reporter.BugReporterListener
|
||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore
|
||||
import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||
@@ -138,7 +139,7 @@ class DefaultBugReporterTest {
|
||||
|
||||
val foundValues = collectValuesFromFormData(request)
|
||||
|
||||
assertThat(foundValues["app"]).isEqualTo("element-x-android")
|
||||
assertThat(foundValues["app"]).isEqualTo(RageshakeConfig.BUG_REPORT_APP_NAME)
|
||||
assertThat(foundValues["can_contact"]).isEqualTo("true")
|
||||
assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH")
|
||||
assertThat(foundValues["sdk_sha"]).isEqualTo("123456789")
|
||||
|
||||
@@ -16,7 +16,9 @@ class DefaultBugReporterUrlProviderTest {
|
||||
@Test
|
||||
fun `test DefaultBugReporterUrlProvider`() {
|
||||
val sut = DefaultBugReporterUrlProvider()
|
||||
val result = sut.provide()
|
||||
assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl())
|
||||
if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) {
|
||||
val result = sut.provide()
|
||||
assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.rageshake.test.screenshot
|
||||
package io.element.android.features.rageshake.impl.screenshot
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
|
||||
|
||||
const val A_SCREENSHOT_URI = "file://content/uri"
|
||||
|
||||
@@ -48,6 +48,7 @@ dependencies {
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.services.analytics.api)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
api(projects.features.roomlist.api)
|
||||
|
||||
@@ -30,6 +30,7 @@ import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
@@ -91,6 +92,7 @@ class RoomListPresenter @Inject constructor(
|
||||
private val notificationCleaner: NotificationCleaner,
|
||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService: EncryptionService = client.encryptionService()
|
||||
|
||||
@@ -103,6 +105,7 @@ class RoomListPresenter @Inject constructor(
|
||||
val filtersState = filtersPresenter.present()
|
||||
val searchState = searchPresenter.present()
|
||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
roomListDataSource.launchIn(this)
|
||||
@@ -163,6 +166,7 @@ class RoomListPresenter @Inject constructor(
|
||||
contextMenu = contextMenu.value,
|
||||
leaveRoomState = leaveRoomState,
|
||||
filtersState = filtersState,
|
||||
canReportBug = canReportBug,
|
||||
searchState = searchState,
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
|
||||
@@ -29,6 +29,7 @@ data class RoomListState(
|
||||
val contextMenu: ContextMenu,
|
||||
val leaveRoomState: LeaveRoomState,
|
||||
val filtersState: RoomListFiltersState,
|
||||
val canReportBug: Boolean,
|
||||
val searchState: RoomListSearchState,
|
||||
val contentState: RoomListContentState,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
|
||||
@@ -57,6 +57,7 @@ internal fun aRoomListState(
|
||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
searchState: RoomListSearchState = aRoomListSearchState(),
|
||||
filtersState: RoomListFiltersState = aRoomListFiltersState(),
|
||||
canReportBug: Boolean = true,
|
||||
contentState: RoomListContentState = aRoomsContentState(),
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||
@@ -69,6 +70,7 @@ internal fun aRoomListState(
|
||||
contextMenu = contextMenu,
|
||||
leaveRoomState = leaveRoomState,
|
||||
filtersState = filtersState,
|
||||
canReportBug = canReportBug,
|
||||
searchState = searchState,
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
|
||||
@@ -127,6 +127,7 @@ private fun RoomListScaffold(
|
||||
displayMenuItems = state.displayActions,
|
||||
displayFilters = state.displayFilters,
|
||||
filtersState = state.filtersState,
|
||||
canReportBug = state.canReportBug,
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
|
||||
@@ -85,6 +85,7 @@ fun RoomListTopBar(
|
||||
displayMenuItems: Boolean,
|
||||
displayFilters: Boolean,
|
||||
filtersState: RoomListFiltersState,
|
||||
canReportBug: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
DefaultRoomListTopBar(
|
||||
@@ -98,6 +99,7 @@ fun RoomListTopBar(
|
||||
displayMenuItems = displayMenuItems,
|
||||
displayFilters = displayFilters,
|
||||
filtersState = filtersState,
|
||||
canReportBug = canReportBug,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -115,6 +117,7 @@ private fun DefaultRoomListTopBar(
|
||||
displayMenuItems: Boolean,
|
||||
displayFilters: Boolean,
|
||||
filtersState: RoomListFiltersState,
|
||||
canReportBug: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// We need this to manually clip the top app bar in preview mode
|
||||
@@ -239,7 +242,7 @@ private fun DefaultRoomListTopBar(
|
||||
}
|
||||
)
|
||||
}
|
||||
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) {
|
||||
if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
@@ -319,6 +322,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview {
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
filtersState = aRoomListFiltersState(),
|
||||
canReportBug = true,
|
||||
onMenuActionClick = {},
|
||||
)
|
||||
}
|
||||
@@ -337,6 +341,7 @@ internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview {
|
||||
displayMenuItems = true,
|
||||
displayFilters = true,
|
||||
filtersState = aRoomListFiltersState(),
|
||||
canReportBug = true,
|
||||
onMenuActionClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
@@ -105,12 +106,14 @@ class RoomListPresenterTest {
|
||||
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
|
||||
val presenter = createRoomListPresenter(
|
||||
client = matrixClient,
|
||||
rageshakeFeatureAvailability = { false },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID))
|
||||
assertThat(initialState.canReportBug).isFalse()
|
||||
val withUserState = awaitItem()
|
||||
assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME)
|
||||
@@ -135,6 +138,7 @@ class RoomListPresenterTest {
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showAvatarIndicator).isTrue()
|
||||
assertThat(initialState.canReportBug).isTrue()
|
||||
sessionVerificationService.emitNeedsSessionVerification(false)
|
||||
encryptionService.emitBackupState(BackupState.ENABLED)
|
||||
val finalState = awaitItem()
|
||||
@@ -675,6 +679,7 @@ class RoomListPresenterTest {
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
syncService = syncService,
|
||||
@@ -705,6 +710,7 @@ class RoomListPresenterTest {
|
||||
notificationCleaner = notificationCleaner,
|
||||
logoutPresenter = { aDirectLogoutState() },
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import config.AnalyticsConfig
|
||||
import config.BuildTimeConfig
|
||||
import config.PushProvidersConfig
|
||||
|
||||
object ModulesConfig {
|
||||
@@ -14,8 +15,27 @@ object ModulesConfig {
|
||||
includeUnifiedPush = true,
|
||||
)
|
||||
|
||||
val analyticsConfig: AnalyticsConfig = AnalyticsConfig.Enabled(
|
||||
withPosthog = true,
|
||||
withSentry = true,
|
||||
)
|
||||
val analyticsConfig: AnalyticsConfig = if (isEnterpriseBuild) {
|
||||
// Is Posthog configuration available?
|
||||
val withPosthog = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.isNullOrEmpty().not() &&
|
||||
BuildTimeConfig.SERVICES_POSTHOG_HOST.isNullOrEmpty().not()
|
||||
// Is Sentry configuration available?
|
||||
val withSentry = BuildTimeConfig.SERVICES_SENTRY_DSN.isNullOrEmpty().not()
|
||||
if (withPosthog || withSentry) {
|
||||
println("Analytics enabled with Posthog: $withPosthog, Sentry: $withSentry")
|
||||
AnalyticsConfig.Enabled(
|
||||
withPosthog = withPosthog,
|
||||
withSentry = withSentry,
|
||||
)
|
||||
} else {
|
||||
println("Analytics disabled")
|
||||
AnalyticsConfig.Disabled
|
||||
}
|
||||
} else {
|
||||
println("Analytics enabled with Posthog and Sentry")
|
||||
AnalyticsConfig.Enabled(
|
||||
withPosthog = true,
|
||||
withSentry = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,21 @@ object BuildTimeConfig {
|
||||
const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c"
|
||||
const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c"
|
||||
|
||||
val METADATA_HOST: String? = null
|
||||
val URL_WEBSITE: String? = null
|
||||
val URL_LOGO: String? = null
|
||||
val URL_COPYRIGHT: String? = null
|
||||
val URL_ACCEPTABLE_USE: String? = null
|
||||
val URL_PRIVACY: String? = null
|
||||
val URL_POLICY: String? = null
|
||||
val SUPPORT_EMAIL_ADDRESS: String? = null
|
||||
val SERVICES_MAPTILER_BASE_URL: String? = null
|
||||
val SERVICES_MAPTILER_APIKEY: String? = null
|
||||
val SERVICES_MAPTILER_LIGHT_MAPID: String? = null
|
||||
val SERVICES_MAPTILER_DARK_MAPID: String? = null
|
||||
val SERVICES_POSTHOG_HOST: String? = null
|
||||
val SERVICES_POSTHOG_APIKEY: String? = null
|
||||
val SERVICES_SENTRY_DSN: String? = null
|
||||
val BUG_REPORT_URL: String? = null
|
||||
val BUG_REPORT_APP_NAME: String? = null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 extension
|
||||
|
||||
import com.android.build.api.dsl.VariantDimension
|
||||
|
||||
fun VariantDimension.buildConfigFieldStr(
|
||||
name: String,
|
||||
value: String,
|
||||
) {
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = name,
|
||||
value = "\"$value\""
|
||||
)
|
||||
}
|
||||
|
||||
fun VariantDimension.buildConfigFieldBoolean(
|
||||
name: String,
|
||||
value: Boolean,
|
||||
) {
|
||||
buildConfigField(
|
||||
type = "boolean",
|
||||
name = name,
|
||||
value = value.toString()
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
@@ -12,6 +14,21 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.services.analyticsproviders.posthog"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigFieldStr(
|
||||
name = "POSTHOG_HOST",
|
||||
value = BuildTimeConfig.SERVICES_POSTHOG_HOST.takeIf { isEnterpriseBuild } ?: ""
|
||||
)
|
||||
buildConfigFieldStr(
|
||||
name = "POSTHOG_APIKEY",
|
||||
value = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.takeIf { isEnterpriseBuild } ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
@@ -21,6 +38,7 @@ dependencies {
|
||||
implementation(libs.posthog) {
|
||||
exclude("com.android.support", "support-annotations")
|
||||
}
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.services.analyticsproviders.api)
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.content.Context
|
||||
import com.posthog.PostHogInterface
|
||||
import com.posthog.android.PostHogAndroid
|
||||
import com.posthog.android.PostHogAndroidConfig
|
||||
import io.element.android.libraries.core.extensions.isElement
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
@@ -22,8 +21,7 @@ class PostHogFactory @Inject constructor(
|
||||
private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider,
|
||||
) {
|
||||
fun createPosthog(): PostHogInterface? {
|
||||
if (!buildMeta.isElement()) return null
|
||||
val endpoint = posthogEndpointConfigProvider.provide()
|
||||
val endpoint = posthogEndpointConfigProvider.provide() ?: return null
|
||||
return PostHogAndroid.with(
|
||||
context,
|
||||
PostHogAndroidConfig(
|
||||
|
||||
@@ -10,4 +10,6 @@ package io.element.android.services.analyticsproviders.posthog
|
||||
data class PosthogEndpointConfig(
|
||||
val host: String,
|
||||
val apiKey: String,
|
||||
)
|
||||
) {
|
||||
val isValid = host.isNotBlank() && apiKey.isNotBlank()
|
||||
}
|
||||
|
||||
@@ -7,24 +7,40 @@
|
||||
|
||||
package io.element.android.services.analyticsproviders.posthog
|
||||
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.core.extensions.isElement
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import javax.inject.Inject
|
||||
|
||||
class PosthogEndpointConfigProvider @Inject constructor(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
) {
|
||||
fun provide(): PosthogEndpointConfig {
|
||||
return when (buildMeta.buildType) {
|
||||
BuildType.RELEASE -> PosthogEndpointConfig(
|
||||
host = "https://posthog.element.io",
|
||||
apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
|
||||
)
|
||||
BuildType.NIGHTLY,
|
||||
BuildType.DEBUG -> PosthogEndpointConfig(
|
||||
host = "https://posthog.element.dev",
|
||||
apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
|
||||
)
|
||||
fun provide(): PosthogEndpointConfig? {
|
||||
return if (enterpriseService.isEnterpriseBuild) {
|
||||
PosthogEndpointConfig(
|
||||
host = BuildConfig.POSTHOG_HOST,
|
||||
apiKey = BuildConfig.POSTHOG_APIKEY,
|
||||
).takeIf {
|
||||
// Note that if the config is invalid, this module will not be included in the build.
|
||||
// So the configuration should be always valid.
|
||||
it.isValid
|
||||
}
|
||||
} else if (buildMeta.isElement()) {
|
||||
when (buildMeta.buildType) {
|
||||
BuildType.RELEASE -> PosthogEndpointConfig(
|
||||
host = "https://posthog.element.io",
|
||||
apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO",
|
||||
)
|
||||
BuildType.NIGHTLY,
|
||||
BuildType.DEBUG -> PosthogEndpointConfig(
|
||||
host = "https://posthog.element.dev",
|
||||
apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import config.BuildTimeConfig
|
||||
import extension.buildConfigFieldStr
|
||||
import extension.readLocalProperty
|
||||
import extension.setupAnvil
|
||||
|
||||
@@ -19,13 +21,15 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
buildConfigFieldStr(
|
||||
name = "SENTRY_DSN",
|
||||
value = (System.getenv("ELEMENT_ANDROID_SENTRY_DSN")
|
||||
?: readLocalProperty("services.analyticsproviders.sentry.dsn")
|
||||
value = if (isEnterpriseBuild) {
|
||||
BuildTimeConfig.SERVICES_SENTRY_DSN
|
||||
} else {
|
||||
System.getenv("ELEMENT_ANDROID_SENTRY_DSN")
|
||||
?: readLocalProperty("services.analyticsproviders.sentry.dsn")
|
||||
}
|
||||
?: ""
|
||||
).let { "\"$it\"" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,7 @@ class SentryAnalyticsProvider @Inject constructor(
|
||||
Timber.tag(analyticsTag.value).d("Initializing Sentry")
|
||||
if (Sentry.isEnabled()) return
|
||||
|
||||
val dsn = if (SentryConfig.DSN.isNotBlank()) {
|
||||
SentryConfig.DSN
|
||||
} else {
|
||||
val dsn = SentryConfig.DSN.ifBlank {
|
||||
Timber.w("No Sentry DSN provided, Sentry will not be initialized")
|
||||
return
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user