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