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:
parent
c6b99c853c
commit
3c1deff79c
95 changed files with 613 additions and 273 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a6241486ded79ef2c079dcc45a0fe97bf01db66c05712ec7f74db035d76a91b5
|
||||
size 18115
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2ca4b3953ec0c460052305d9cfa5c0b1db2a97e8d9a27b483e8b5d15d873d32c
|
||||
size 17500
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:232fcbe898588450577fca29090873626c2df7cc4afba91a87d3956d29f0afbc
|
||||
size 81150
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26f4a151850c4c21ed57d0a1d36ea72aece585ed5ec6d54303edbeaa91d06df9
|
||||
size 73959
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ce6f663ca845a54b4e1aca55f1405e00d29084a95b6a61721e5136efe30f7fc
|
||||
size 311755
|
||||
oid sha256:0ab623f806fc90bef41beccc5d5f444a882e6634cdadecbbc341e10369bbcfdb
|
||||
size 315380
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c7b714d78970f7bfc32c0bb8a226b2896681134a6eb329c871c0270511e8e81f
|
||||
size 306543
|
||||
oid sha256:b73088b5af32e47d18d6961fe5622411f5342388d8a0d78c8f3fb5e27213146a
|
||||
size 315116
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:563c2a20cbf651c88e85dfba98c02fa60197b298ee621b5b4b1bdc4af51a456e
|
||||
size 310172
|
||||
oid sha256:24735f133066c55c6d88b7f9930ad983cca431611c7e6833ca5a4d657eeb14a5
|
||||
size 313116
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:171e41e96dbb6495c667a57047cf66e5059a19b8dba7d56e799113cd5a8690e4
|
||||
size 304775
|
||||
oid sha256:1d02a7ff9ff40f73d8c72b9378e10ba1f6580524cbdefcf290066b8047fdc33c
|
||||
size 307633
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:171e41e96dbb6495c667a57047cf66e5059a19b8dba7d56e799113cd5a8690e4
|
||||
size 304775
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e41a284f2eedb0c7158079f059e05f8f505436ba91a82194faaff459bb30e4b4
|
||||
size 392983
|
||||
oid sha256:f5ac9f2f168b895e8262181f789b0fe2ad2c97b1296a50c1453879438bfe436a
|
||||
size 395942
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12691cbb9890570cb2081921224b13529e256feff1171d24e77cae11afc676e8
|
||||
size 380258
|
||||
oid sha256:f7c1c7d34935986d01113069c25849fc860133de582fe94ee63f2e28b181e87f
|
||||
size 397378
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4f2c6932790cd4bbdff4dd07a728290aeb2eca47811c0b3d220e17aed5e8e34e
|
||||
size 383588
|
||||
oid sha256:a11f8383fc481255fb3fa6f1a5475d23c13c0460c33b04bc190cf3b5931893a3
|
||||
size 395108
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee7b3e5bb14da106ecedbe4827cded220d7222b68837c3c63ae3dfacd66ec2f2
|
||||
size 365133
|
||||
oid sha256:36424ccefbbbd35201800dbcbb743a2d22aa6fe7a606054fc3acf8b98aead272
|
||||
size 381588
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee7b3e5bb14da106ecedbe4827cded220d7222b68837c3c63ae3dfacd66ec2f2
|
||||
size 365133
|
||||
Loading…
Add table
Add a link
Reference in a new issue