Browse Source

Add hidden developer settings in release builds too (#3020)

* Add hidden developer settings to release builds

* Add changelog
pull/3036/head
Jorge Martin Espinosa 3 months ago committed by GitHub
parent
commit
cd045027dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      changelog.d/3020.misc
  2. 11
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
  3. 21
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt
  4. 16
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt
  5. 1
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt
  6. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt
  7. 18
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
  8. 46
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/utils/ShowDeveloperSettingsProvider.kt
  9. 18
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
  10. 77
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt

1
changelog.d/3020.misc

@ -0,0 +1 @@ @@ -0,0 +1 @@
Enable hidden access to developer options in release mode apps.

11
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt

@ -36,6 +36,8 @@ import io.element.android.libraries.architecture.AsyncData @@ -36,6 +36,8 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
@ -52,6 +54,7 @@ class DeveloperSettingsPresenter @Inject constructor( @@ -52,6 +54,7 @@ class DeveloperSettingsPresenter @Inject constructor(
private val clearCacheUseCase: ClearCacheUseCase,
private val rageshakePresenter: RageshakePreferencesPresenter,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
@ -76,6 +79,14 @@ class DeveloperSettingsPresenter @Inject constructor( @@ -76,6 +79,14 @@ class DeveloperSettingsPresenter @Inject constructor(
LaunchedEffect(Unit) {
FeatureFlags.entries
.filter { it.isFinished.not() }
.run {
// Never display room directory search in release builds for Play Store
if (buildMeta.flavorDescription == "GooglePlay" && buildMeta.buildType == BuildType.RELEASE) {
filterNot { it.key == FeatureFlags.RoomDirectorySearch.key }
} else {
this
}
}
.forEach { feature ->
features[feature.key] = feature
enabledFeatures[feature.key] = featureFlagService.isFeatureEnabled(feature)

21
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootEvents.kt

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.root
sealed interface PreferencesRootEvents {
data object OnVersionInfoClick : PreferencesRootEvents
}

16
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt

@ -25,8 +25,8 @@ import androidx.compose.runtime.mutableStateOf @@ -25,8 +25,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.libraries.architecture.Presenter
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.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
@ -46,12 +46,12 @@ class PreferencesRootPresenter @Inject constructor( @@ -46,12 +46,12 @@ class PreferencesRootPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val sessionVerificationService: SessionVerificationService,
private val analyticsService: AnalyticsService,
private val buildType: BuildType,
private val versionFormatter: VersionFormatter,
private val snackbarDispatcher: SnackbarDispatcher,
private val featureFlagService: FeatureFlagService,
private val indicatorService: IndicatorService,
private val directLogoutPresenter: DirectLogoutPresenter,
private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider,
) : Presenter<PreferencesRootState> {
@Composable
override fun present(): PreferencesRootState {
@ -97,7 +97,16 @@ class PreferencesRootPresenter @Inject constructor( @@ -97,7 +97,16 @@ class PreferencesRootPresenter @Inject constructor(
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
}
val showDeveloperSettings = buildType != BuildType.RELEASE
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
fun handleEvent(event: PreferencesRootEvents) {
when (event) {
is PreferencesRootEvents.OnVersionInfoClick -> {
showDeveloperSettingsProvider.unlockDeveloperSettings()
}
}
}
return PreferencesRootState(
myUser = matrixUser.value,
version = versionFormatter.get(),
@ -113,6 +122,7 @@ class PreferencesRootPresenter @Inject constructor( @@ -113,6 +122,7 @@ class PreferencesRootPresenter @Inject constructor(
showBlockedUsersItem = showBlockedUsersItem,
directLogoutState = directLogoutState,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvent,
)
}

1
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt

@ -35,4 +35,5 @@ data class PreferencesRootState( @@ -35,4 +35,5 @@ data class PreferencesRootState(
val showBlockedUsersItem: Boolean,
val directLogoutState: DirectLogoutState,
val snackbarMessage: SnackbarMessage?,
val eventSink: (PreferencesRootEvents) -> Unit,
)

2
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt

@ -23,6 +23,7 @@ import io.element.android.libraries.ui.strings.CommonStrings @@ -23,6 +23,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun aPreferencesRootState(
myUser: MatrixUser,
eventSink: (PreferencesRootEvents) -> Unit = { _ -> },
) = PreferencesRootState(
myUser = myUser,
version = "Version 1.1 (1)",
@ -38,4 +39,5 @@ fun aPreferencesRootState( @@ -38,4 +39,5 @@ fun aPreferencesRootState(
showBlockedUsersItem = true,
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
directLogoutState = aDirectLogoutState(),
eventSink = eventSink,
)

18
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt

@ -18,10 +18,10 @@ package io.element.android.features.preferences.impl.root @@ -18,10 +18,10 @@ package io.element.android.features.preferences.impl.root
import androidx.compose.foundation.clickable
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
import androidx.compose.ui.text.style.TextAlign
@ -112,6 +112,11 @@ fun PreferencesRootView( @@ -112,6 +112,11 @@ fun PreferencesRootView(
Footer(
version = state.version,
deviceId = state.deviceId,
onClick = if (!state.showDeveloperSettings) {
{ state.eventSink(PreferencesRootEvents.OnVersionInfoClick) }
} else {
null
}
)
}
}
@ -231,9 +236,10 @@ private fun ColumnScope.GeneralSection( @@ -231,9 +236,10 @@ private fun ColumnScope.GeneralSection(
}
@Composable
private fun Footer(
private fun ColumnScope.Footer(
version: String,
deviceId: String?
deviceId: String?,
onClick: (() -> Unit)?,
) {
val text = remember(version, deviceId) {
buildString {
@ -246,8 +252,10 @@ private fun Footer( @@ -246,8 +252,10 @@ private fun Footer(
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, top = 40.dp, bottom = 24.dp),
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp)
.clickable(enabled = onClick != null, onClick = onClick ?: {})
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 24.dp),
textAlign = TextAlign.Center,
text = text,
style = ElementTheme.typography.fontBodySmRegular,

46
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/utils/ShowDeveloperSettingsProvider.kt

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.utils
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
class ShowDeveloperSettingsProvider @Inject constructor(
buildMeta: BuildMeta,
) {
companion object {
const val DEVELOPER_SETTINGS_COUNTER = 7
}
private var counter = DEVELOPER_SETTINGS_COUNTER
private val isDeveloperBuild = buildMeta.buildType != BuildType.RELEASE
private val _showDeveloperSettings = MutableStateFlow(isDeveloperBuild)
val showDeveloperSettings: StateFlow<Boolean> = _showDeveloperSettings
fun unlockDeveloperSettings() {
if (counter == 0) {
return
}
counter--
if (counter == 0) {
_showDeveloperSettings.value = true
}
}
}

18
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt

@ -27,8 +27,11 @@ import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePr @@ -27,8 +27,11 @@ import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePr
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
@ -73,6 +76,19 @@ class DeveloperSettingsPresenterTest { @@ -73,6 +76,19 @@ class DeveloperSettingsPresenterTest {
}
}
@Test
fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest {
val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay")
val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = awaitLastSequentialItem()
assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val presenter = createDeveloperSettingsPresenter()
@ -150,6 +166,7 @@ class DeveloperSettingsPresenterTest { @@ -150,6 +166,7 @@ class DeveloperSettingsPresenterTest {
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
buildMeta: BuildMeta = aBuildMeta(),
): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter(
featureFlagService = featureFlagService,
@ -157,6 +174,7 @@ class DeveloperSettingsPresenterTest { @@ -157,6 +174,7 @@ class DeveloperSettingsPresenterTest {
clearCacheUseCase = clearCacheUseCase,
rageshakePresenter = rageshakePresenter,
appPreferencesStore = preferencesStore,
buildMeta = buildMeta,
)
}
}

77
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt

@ -23,6 +23,7 @@ import app.cash.turbine.test @@ -23,6 +23,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutPresenter
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -32,6 +33,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser @@ -32,6 +33,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.services.analytics.test.FakeAnalyticsService
@ -53,24 +55,7 @@ class PreferencesRootPresenterTest { @@ -53,24 +55,7 @@ class PreferencesRootPresenterTest {
@Test
fun `present - initial state`() = runTest {
val matrixClient = FakeMatrixClient()
val sessionVerificationService = FakeSessionVerificationService()
val presenter = PreferencesRootPresenter(
matrixClient = matrixClient,
sessionVerificationService = sessionVerificationService,
analyticsService = FakeAnalyticsService(),
buildType = BuildType.DEBUG,
versionFormatter = FakeVersionFormatter(),
snackbarDispatcher = SnackbarDispatcher(),
featureFlagService = FakeFeatureFlagService(),
indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = FakeEncryptionService(),
),
directLogoutPresenter = object : DirectLogoutPresenter {
@Composable
override fun present() = aDirectLogoutState
},
)
val presenter = createPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -104,4 +89,60 @@ class PreferencesRootPresenterTest { @@ -104,4 +89,60 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.snackbarMessage).isNull()
}
}
@Test
fun `present - developer settings is hidden by default in release builds`() = runTest {
val presenter = createPresenter(
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val loadedState = awaitItem()
assertThat(loadedState.showDeveloperSettings).isFalse()
}
}
@Test
fun `present - developer settings can be enabled in release builds`() = runTest {
val presenter = createPresenter(
showDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.RELEASE))
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val loadedState = awaitItem()
repeat(times = ShowDeveloperSettingsProvider.DEVELOPER_SETTINGS_COUNTER) {
assertThat(loadedState.showDeveloperSettings).isFalse()
loadedState.eventSink(PreferencesRootEvents.OnVersionInfoClick)
}
assertThat(awaitItem().showDeveloperSettings).isTrue()
}
}
private fun createPresenter(
matrixClient: FakeMatrixClient = FakeMatrixClient(),
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
) = PreferencesRootPresenter(
matrixClient = matrixClient,
sessionVerificationService = sessionVerificationService,
analyticsService = FakeAnalyticsService(),
versionFormatter = FakeVersionFormatter(),
snackbarDispatcher = SnackbarDispatcher(),
featureFlagService = FakeFeatureFlagService(),
indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = FakeEncryptionService(),
),
directLogoutPresenter = object : DirectLogoutPresenter {
@Composable
override fun present() = aDirectLogoutState
},
showDeveloperSettingsProvider = showDeveloperSettingsProvider,
)
}

Loading…
Cancel
Save