Merge pull request #7241 from element-hq/feature/bma/about

Move deviceId and git details to developer settings
This commit is contained in:
Benoit Marty
2026-08-06 16:36:18 +02:00
committed by GitHub
23 changed files with 110 additions and 52 deletions
@@ -31,6 +31,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.data.ByteUnit
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
@@ -41,6 +42,7 @@ import kotlinx.coroutines.launch
class DeveloperSettingsPresenter(
private val appDeveloperSettingsPresenter: Presenter<AppDeveloperSettingsState>,
private val sessionId: SessionId,
private val deviceId: DeviceId,
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase,
private val enterpriseService: EnterpriseService,
@@ -117,6 +119,7 @@ class DeveloperSettingsPresenter(
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
showColorPicker = showColorPicker,
deviceId = deviceId,
eventSink = ::handleEvent,
)
}
@@ -11,6 +11,7 @@ package io.element.android.features.preferences.impl.developer
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.DeviceId
import kotlinx.collections.immutable.ImmutableMap
data class DeveloperSettingsState(
@@ -21,6 +22,7 @@ data class DeveloperSettingsState(
val markAllRoomsAsReadAction: AsyncAction<Unit>,
val isEnterpriseBuild: Boolean,
val showColorPicker: Boolean,
val deviceId: DeviceId,
val eventSink: (DeveloperSettingsEvents) -> Unit
) {
val showLoader = clearCacheAction is AsyncAction.Loading || markAllRoomsAsReadAction is AsyncAction.Loading
@@ -13,6 +13,7 @@ import io.element.android.features.preferences.impl.developer.appsettings.AppDev
import io.element.android.features.preferences.impl.developer.appsettings.anAppDeveloperSettingsState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.DeviceId
import kotlinx.collections.immutable.persistentMapOf
open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSettingsState> {
@@ -40,6 +41,7 @@ fun aDeveloperSettingsState(
markAllRoomsAsReadAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
isEnterpriseBuild: Boolean = false,
showColorPicker: Boolean = false,
deviceId: DeviceId = DeviceId("ILAKNDNASDLK"),
eventSink: (DeveloperSettingsEvents) -> Unit = {},
) = DeveloperSettingsState(
appDeveloperSettingsState = appDeveloperSettingsState,
@@ -49,5 +51,6 @@ fun aDeveloperSettingsState(
markAllRoomsAsReadAction = markAllRoomsAsReadAction,
isEnterpriseBuild = isEnterpriseBuild,
showColorPicker = showColorPicker,
deviceId = deviceId,
eventSink = eventSink,
)
@@ -15,12 +15,14 @@ import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView
import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
@@ -31,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.ui.strings.CommonStrings
import io.mhssn.colorpicker.ColorPickerDialog
import io.mhssn.colorpicker.ColorPickerType
@@ -75,6 +78,7 @@ fun DeveloperSettingsView(
state = state.appDeveloperSettingsState,
onOpenShowkase = onOpenShowkase,
)
SessionCategory(deviceId = state.deviceId)
NotificationCategory(onPushHistoryClick)
MarkAllRoomsAsReadCategory(state)
@@ -168,6 +172,24 @@ fun DeveloperSettingsView(
)
}
@Composable
private fun SessionCategory(deviceId: DeviceId) {
PreferenceCategory(title = "Session") {
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
val context = LocalContext.current
ListItem(
headlineContent = { Text("DeviceId") },
supportingContent = { Text(text = deviceId.value) },
onClick = {
context.copyToClipboard(
text = deviceId.value,
toastMessage = toastMessage,
)
}
)
}
}
@Composable
private fun MarkAllRoomsAsReadCategory(state: DeveloperSettingsState) {
PreferenceCategory(title = "Room list") {
@@ -25,6 +25,7 @@ import io.element.android.features.rageshake.api.preferences.RageshakePreference
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@@ -42,6 +43,7 @@ class AppDeveloperSettingsPresenter(
private val featureFlagService: FeatureFlagService,
private val rageshakePresenter: Presenter<RageshakePreferencesState>,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
) : Presenter<AppDeveloperSettingsState> {
@Composable
override fun present(): AppDeveloperSettingsState {
@@ -110,6 +112,8 @@ class AppDeveloperSettingsPresenter(
),
tracingLogLevel = tracingLogLevel,
tracingLogPacks = tracingLogPacks,
gitBranch = buildMeta.gitBranchName,
gitSha = buildMeta.gitRevision,
eventSink = ::handleEvent,
)
}
@@ -20,6 +20,8 @@ data class AppDeveloperSettingsState(
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val tracingLogLevel: AsyncData<LogLevelItem>,
val tracingLogPacks: ImmutableList<TraceLogPack>,
val gitBranch: String,
val gitSha: String,
val eventSink: (AppDeveloperSettingsEvent) -> Unit
)
@@ -37,6 +37,8 @@ fun anAppDeveloperSettingsState(
customElementCallBaseUrlState = customElementCallBaseUrlState,
tracingLogLevel = AsyncData.Success(LogLevelItem.INFO),
tracingLogPacks = traceLogPacks.toImmutableList(),
gitBranch = "feature/awesome-feature",
gitSha = "1234567890",
eventSink = eventSink,
)
@@ -14,6 +14,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -22,6 +23,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
@@ -33,6 +35,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalComposeUiApi::class)
@@ -98,6 +101,41 @@ fun AppDeveloperSettingsView(
onClick = { error("This crash is a test.") }
)
}
GitCategory(
gitBranch = state.gitBranch,
gitSha = state.gitSha,
)
}
}
@Composable
private fun GitCategory(
gitBranch: String,
gitSha: String,
) {
PreferenceCategory(title = "Git") {
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
val context = LocalContext.current
ListItem(
headlineContent = { Text("Git branch") },
supportingContent = { Text(text = gitBranch) },
onClick = {
context.copyToClipboard(
text = gitBranch,
toastMessage = toastMessage,
)
}
)
ListItem(
headlineContent = { Text("Git SHA") },
supportingContent = { Text(text = gitSha) },
onClick = {
context.copyToClipboard(
text = gitSha,
toastMessage = toastMessage,
)
}
)
}
}
@@ -142,7 +142,6 @@ class PreferencesRootPresenter(
myUser = matrixUser.value,
userStatusState = userStatusState,
version = remember { versionFormatter.get() },
deviceId = matrixClient.deviceId,
isMultiAccountEnabled = isMultiAccountEnabled,
otherSessions = otherSessions,
showSecureBackup = !canVerifyUserSession,
@@ -11,7 +11,6 @@ package io.element.android.features.preferences.impl.root
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
@@ -19,7 +18,6 @@ data class PreferencesRootState(
val myUser: MatrixUser,
val userStatusState: UserStatusState?,
val version: String,
val deviceId: DeviceId?,
val isMultiAccountEnabled: Boolean,
val otherSessions: ImmutableList<MatrixUser>,
val showSecureBackup: Boolean,
@@ -15,7 +15,6 @@ import io.element.android.features.preferences.impl.userstatus.UserStatusPickerS
import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
@@ -29,7 +28,6 @@ open class PreferencesRootStateProvider : PreviewParameterProvider<PreferencesRo
aPreferencesRootState(
myUser = aMatrixUser(avatarUrl = "anAvatarUrl"),
version = "Version 1.1 (1)",
deviceId = DeviceId("ILAKNDNASDLK"),
isMultiAccountEnabled = true,
otherSessions = aMatrixUserList().drop(1).take(1),
showSecureBackup = true,
@@ -55,7 +53,6 @@ open class PreferencesRootStateProvider : PreviewParameterProvider<PreferencesRo
showSecureBackupBadge = true,
),
aPreferencesRootState(
deviceId = DeviceId("ILAKNDNASDLK"),
showLabsItem = true,
canReportBug = true,
nbOfBlockedUsers = 3,
@@ -76,7 +73,6 @@ open class PreferencesRootStateProvider : PreviewParameterProvider<PreferencesRo
fun aPreferencesRootState(
myUser: MatrixUser = aMatrixUser(),
version: String = "Version 1.1 (1)",
deviceId: DeviceId? = null,
isMultiAccountEnabled: Boolean = false,
otherSessions: List<MatrixUser> = emptyList(),
showSecureBackup: Boolean = false,
@@ -96,7 +92,6 @@ fun aPreferencesRootState(
) = PreferencesRootState(
myUser = myUser,
version = version,
deviceId = deviceId,
isMultiAccountEnabled = isMultiAccountEnabled,
otherSessions = otherSessions.toImmutableList(),
showSecureBackup = showSecureBackup,
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -43,7 +42,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
import io.element.android.libraries.ui.strings.CommonStrings
@@ -129,7 +127,6 @@ fun PreferencesRootView(
// Version
Footer(
version = state.version,
deviceId = state.deviceId,
onClick = if (!state.showDeveloperSettings) {
{ state.eventSink(PreferencesRootEvent.OnVersionInfoClick) }
} else {
@@ -326,25 +323,15 @@ private fun ColumnScope.GeneralSection(
@Composable
private fun ColumnScope.Footer(
version: String,
deviceId: DeviceId?,
onClick: (() -> Unit)?,
) {
val text = remember(version, deviceId) {
buildString {
append(version)
if (deviceId != null) {
append("\n")
append(deviceId)
}
}
}
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable(enabled = onClick != null, onClick = onClick ?: {})
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 24.dp),
textAlign = TextAlign.Center,
text = text,
text = version,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
@@ -24,16 +24,10 @@ class DefaultVersionFormatter(
private val buildMeta: BuildMeta,
) : VersionFormatter {
override fun get(): String {
val base = stringProvider.getString(
return stringProvider.getString(
CommonStrings.settings_version_number,
buildMeta.versionName,
buildMeta.versionCode.toString()
)
return if (buildMeta.gitBranchName == "main") {
base
} else {
// In case of a build not from main, we display the branch name and the revision
"$base\n${buildMeta.gitBranchName} (${buildMeta.gitRevision})"
}
}
}
@@ -25,7 +25,9 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.data.megaBytes
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -57,6 +59,7 @@ class DeveloperSettingsPresenterTest {
assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
assertThat(state.isEnterpriseBuild).isFalse()
assertThat(state.showColorPicker).isFalse()
assertThat(state.deviceId).isEqualTo(A_DEVICE_ID)
}
awaitItem().also { state ->
assertThat(state.cacheSize.isLoading()).isTrue()
@@ -169,6 +172,7 @@ class DeveloperSettingsPresenterTest {
private fun createDeveloperSettingsPresenter(
sessionId: SessionId = A_SESSION_ID,
deviceId: DeviceId = A_DEVICE_ID,
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
@@ -179,6 +183,7 @@ class DeveloperSettingsPresenterTest {
return DeveloperSettingsPresenter(
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
sessionId = sessionId,
deviceId = deviceId,
computeCacheSizeUseCase = cacheSizeUseCase,
clearCacheUseCase = clearCacheUseCase,
enterpriseService = enterpriseService,
@@ -15,6 +15,7 @@ import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.preferences.impl.R
import io.element.android.tests.testutils.EnsureNeverCalled
@@ -41,7 +42,7 @@ class DeveloperSettingsViewTest : RobolectricTest() {
}
}
@Config(qualifiers = "h2000dp")
@Config(qualifiers = "h2400dp")
@Test
fun `clicking on push history notification invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false)
@@ -80,7 +81,7 @@ class DeveloperSettingsViewTest : RobolectricTest() {
eventSink = eventsRecorder
),
)
onNodeWithText("Clear cache").performClick()
onNodeWithText("Clear cache").performScrollTo().performClick()
eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache)
}
}
@@ -16,6 +16,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.test.FakeFeature
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -53,6 +54,8 @@ class AppDeveloperSettingsPresenterTest {
assertThat(state.rageshakeState.isSupported).isTrue()
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized)
assertThat(state.gitBranch).isEqualTo("feature/awesome-feature")
assertThat(state.gitSha).isEqualTo("1234567890")
}
awaitItem().also { state ->
assertThat(state.features).isNotEmpty()
@@ -145,6 +148,10 @@ class AppDeveloperSettingsPresenterTest {
featureFlagService = featureFlagService,
rageshakePresenter = { aRageshakePreferencesState() },
appPreferencesStore = preferencesStore,
buildMeta = aBuildMeta(
gitRevision = "1234567890",
gitBranchName = "feature/awesome-feature"
)
)
}
}
@@ -16,7 +16,7 @@ import org.junit.Test
class VersionFormatterTest {
@Test
fun `version formatter should return simplified version for main branch`() = runTest {
fun `version formatter should return formatted version`() = runTest {
val sut = DefaultVersionFormatter(
stringProvider = FakeStringProvider(defaultResult = VERSION),
buildMeta = aBuildMeta(
@@ -28,21 +28,7 @@ class VersionFormatterTest {
assertThat(sut.get()).isEqualTo("${VERSION}versionName, 123")
}
@Test
fun `version formatter should return simplified version for other branch`() = runTest {
val sut = DefaultVersionFormatter(
stringProvider = FakeStringProvider(defaultResult = VERSION),
buildMeta = aBuildMeta(
versionName = "versionName",
versionCode = 123,
gitBranchName = "branch",
gitRevision = "1234567890",
)
)
assertThat(sut.get()).isEqualTo("${VERSION}versionName, 123\nbranch (1234567890)")
}
companion object {
const val VERSION = "version"
const val VERSION = "version: "
}
}
@@ -185,6 +185,7 @@ class DefaultBugReporter(
.addFormDataPart("device", Build.MODEL.trim())
.addFormDataPart("locale", Locale.getDefault().toString())
.addFormDataPart("sdk_sha", sdkMetadata.sdkGitSha)
.addFormDataPart("sha", buildMeta.gitRevision)
.addFormDataPart("local_time", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
.addFormDataPart("utc_time", LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC).format(DateTimeFormatter.ISO_DATE_TIME))
.addFormDataPart("app_id", buildMeta.applicationId)
@@ -534,6 +534,6 @@ class DefaultBugReporterTest : RobolectricTest() {
}
companion object {
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 17
private const val EXPECTED_NUMBER_OF_PROGRESS_VALUE = 18
}
}
@@ -15,6 +15,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
@@ -37,6 +38,11 @@ object SessionMatrixModule {
return matrixClient.sessionId
}
@Provides
fun providesDeviceId(matrixClient: MatrixClient): DeviceId {
return matrixClient.deviceId
}
@Provides
fun providesSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService {
return matrixClient.sessionVerificationService
@@ -5,6 +5,8 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.matrix.impl.search
import com.google.common.truth.Truth.assertThat
@@ -14,6 +16,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSearchServ
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle