Let enterprise build be able to override (or disable) the bug report URL.
This commit is contained in:
@@ -8,15 +8,19 @@
|
||||
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.features.rageshake.impl.reporter.BugReporterUrlProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability {
|
||||
override fun isAvailable(): Boolean {
|
||||
return RageshakeConfig.isEnabled
|
||||
class DefaultRageshakeFeatureAvailability @Inject constructor(
|
||||
private val bugReporterUrlProvider: BugReporterUrlProvider,
|
||||
) : RageshakeFeatureAvailability {
|
||||
override fun isAvailable(): Flow<Boolean> {
|
||||
return bugReporterUrlProvider.provide()
|
||||
.map { it != null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.rageshake.impl.crash
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
@@ -19,6 +22,8 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionState
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -32,12 +37,15 @@ class DefaultCrashDetectionPresenter @Inject constructor(
|
||||
@Composable
|
||||
override fun present(): CrashDetectionState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val crashDetected = remember {
|
||||
if (rageshakeFeatureAvailability.isAvailable()) {
|
||||
crashDataStore.appHasCrashed()
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
val crashDetected by remember {
|
||||
rageshakeFeatureAvailability.isAvailable()
|
||||
.flatMapLatest { isAvailable ->
|
||||
if (isAvailable) {
|
||||
crashDataStore.appHasCrashed()
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
}
|
||||
}.collectAsState(false)
|
||||
|
||||
fun handleEvents(event: CrashDetectionEvents) {
|
||||
@@ -49,7 +57,7 @@ class DefaultCrashDetectionPresenter @Inject constructor(
|
||||
|
||||
return CrashDetectionState(
|
||||
appName = buildMeta.applicationName,
|
||||
crashDetected = crashDetected.value,
|
||||
crashDetected = crashDetected,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||
val isSupported: MutableState<Boolean> = rememberSaveable {
|
||||
mutableStateOf(rageshake.isAvailable())
|
||||
}
|
||||
val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() }
|
||||
val isFeatureAvailable by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||
val isEnabled by remember {
|
||||
rageshakeDataStore.isEnabled()
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
@@ -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.reporter
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
fun interface BugReportAppNameProvider {
|
||||
fun provide(): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultBugReportAppNameProvider @Inject constructor() : BugReportAppNameProvider {
|
||||
override fun provide(): String = RageshakeConfig.BUG_REPORT_APP_NAME
|
||||
}
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
package io.element.android.features.rageshake.impl.reporter
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
fun interface BugReporterUrlProvider {
|
||||
fun provide(): HttpUrl
|
||||
fun provide(): Flow<HttpUrl?>
|
||||
}
|
||||
|
||||
@@ -114,6 +114,12 @@ class DefaultBugReporter @Inject constructor(
|
||||
canContact: Boolean,
|
||||
listener: BugReporterListener,
|
||||
) {
|
||||
val url = bugReporterUrlProvider.provide().first()
|
||||
if (url == null) {
|
||||
// It should not happen, but if the URL is null, we cannot proceed
|
||||
Timber.e("## sendBugReport() : bug report URL is null")
|
||||
error("Bug report URL is null, cannot send bug report")
|
||||
}
|
||||
// enumerate files to delete
|
||||
val bugReportFiles: MutableList<File> = ArrayList()
|
||||
var response: Response? = null
|
||||
@@ -243,7 +249,7 @@ class DefaultBugReporter @Inject constructor(
|
||||
}
|
||||
// build the request
|
||||
val request = Request.Builder()
|
||||
.url(bugReporterUrlProvider.provide())
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
var errorMessage: String? = null
|
||||
|
||||
@@ -9,14 +9,31 @@ package io.element.android.features.rageshake.impl.reporter
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultBugReporterUrlProvider @Inject constructor() : BugReporterUrlProvider {
|
||||
override fun provide(): HttpUrl {
|
||||
return RageshakeConfig.BUG_REPORT_URL.toHttpUrl()
|
||||
class DefaultBugReporterUrlProvider @Inject constructor(
|
||||
private val bugReportAppNameProvider: BugReportAppNameProvider,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
) : BugReporterUrlProvider {
|
||||
override fun provide(): Flow<HttpUrl?> {
|
||||
if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null)
|
||||
return enterpriseService.bugReportUrlFlow
|
||||
.map { bugReportUrl ->
|
||||
when (bugReportUrl) {
|
||||
is BugReportUrl.Custom -> bugReportUrl.url
|
||||
BugReportUrl.Disabled -> null
|
||||
BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
.map { it?.toHttpUrl() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -104,6 +105,6 @@ class CrashDetectionPresenterTest {
|
||||
) = DefaultCrashDetectionPresenter(
|
||||
buildMeta = buildMeta,
|
||||
crashDataStore = crashDataStore,
|
||||
rageshakeFeatureAvailability = { isFeatureAvailable },
|
||||
rageshakeFeatureAvailability = { flowOf(isFeatureAvailable) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
@@ -52,7 +53,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -77,7 +78,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -103,7 +104,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -138,7 +139,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
@@ -173,7 +174,7 @@ class RageshakeDetectionPresenterTest {
|
||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||
rageshake = rageshake,
|
||||
rageshakeDataStore = rageshakeDataStore,
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY
|
||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageShake
|
||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -29,7 +30,7 @@ class RageshakePreferencesPresenterTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = true),
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -46,7 +47,7 @@ class RageshakePreferencesPresenterTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = false),
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -63,7 +64,7 @@ class RageshakePreferencesPresenterTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = true),
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
@@ -83,7 +84,7 @@ class RageshakePreferencesPresenterTest {
|
||||
val presenter = DefaultRageshakePreferencesPresenter(
|
||||
FakeRageShake(isAvailableValue = true),
|
||||
FakeRageshakeDataStore(isEnabled = true),
|
||||
rageshakeFeatureAvailability = { true },
|
||||
rageshakeFeatureAvailability = { flowOf(true) },
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
|
||||
@@ -31,6 +31,7 @@ import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionSt
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.MultipartReader
|
||||
@@ -464,7 +465,7 @@ class DefaultBugReporterTest {
|
||||
userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")),
|
||||
sessionStore = sessionStore,
|
||||
buildMeta = buildMeta,
|
||||
bugReporterUrlProvider = { server.url("/") },
|
||||
bugReporterUrlProvider = { flowOf(server.url("/")) },
|
||||
sdkMetadata = FakeSdkMetadata("123456789"),
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
tracingService = tracingService,
|
||||
|
||||
@@ -7,18 +7,44 @@
|
||||
|
||||
package io.element.android.features.rageshake.impl.reporter
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.appconfig.RageshakeConfig
|
||||
import io.element.android.features.enterprise.api.BugReportUrl
|
||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultBugReporterUrlProviderTest {
|
||||
@Test
|
||||
fun `test DefaultBugReporterUrlProvider`() {
|
||||
val sut = DefaultBugReporterUrlProvider()
|
||||
if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) {
|
||||
val result = sut.provide()
|
||||
assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl())
|
||||
fun `provide return values when there is an rageshake app name`() = runTest {
|
||||
val enterpriseService = FakeEnterpriseService()
|
||||
val sut = DefaultBugReporterUrlProvider(
|
||||
bugReportAppNameProvider = { "rageshakeAppName" },
|
||||
enterpriseService = enterpriseService,
|
||||
)
|
||||
sut.provide().test {
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }?.toHttpUrl()
|
||||
)
|
||||
enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Disabled)
|
||||
assertThat(awaitItem()).isNull()
|
||||
enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Custom("https://aURL.org"))
|
||||
assertThat(awaitItem()).isEqualTo("https://aURL.org".toHttpUrl())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `provide return null when there is no rageshake app name`() = runTest {
|
||||
val enterpriseService = FakeEnterpriseService()
|
||||
val sut = DefaultBugReporterUrlProvider(
|
||||
bugReportAppNameProvider = { "" },
|
||||
enterpriseService = enterpriseService,
|
||||
)
|
||||
sut.provide().test {
|
||||
assertThat(awaitItem()).isNull()
|
||||
awaitComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user