Add hidden developer settings in release builds too (#3020)
* Add hidden developer settings to release builds * Add changelog
This commit is contained in:
committed by
GitHub
parent
f3fa7e57a5
commit
1a0a28d966
1
changelog.d/3020.misc
Normal file
1
changelog.d/3020.misc
Normal file
@@ -0,0 +1 @@
|
||||
Enable hidden access to developer options in release mode apps.
|
||||
@@ -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(
|
||||
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(
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
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(
|
||||
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(
|
||||
showBlockedUsersItem = showBlockedUsersItem,
|
||||
directLogoutState = directLogoutState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,4 +35,5 @@ data class PreferencesRootState(
|
||||
val showBlockedUsersItem: Boolean,
|
||||
val directLogoutState: DirectLogoutState,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (PreferencesRootEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
showBlockedUsersItem = true,
|
||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||
directLogoutState = aDirectLogoutState(),
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
}
|
||||
|
||||
@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(
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
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 {
|
||||
clearCacheUseCase = clearCacheUseCase,
|
||||
rageshakePresenter = rageshakePresenter,
|
||||
appPreferencesStore = preferencesStore,
|
||||
buildMeta = buildMeta,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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 {
|
||||
@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 {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user