Browse Source

Fix modal contents overlapping screen lock pin #2692

pull/2874/head
ganfra 4 months ago
parent
commit
b0c369ee1a
  1. 4
      app/src/main/AndroidManifest.xml
  2. 41
      app/src/main/kotlin/io/element/android/x/MainActivity.kt
  3. 19
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  4. 1
      changelog.d/2692.bugfix
  5. 6
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt
  6. 71
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt
  7. 26
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt
  8. 40
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt
  9. 21
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.kt
  10. 28
      features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt
  11. 3
      features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt

4
app/src/main/AndroidManifest.xml

@ -138,6 +138,10 @@ @@ -138,6 +138,10 @@
android:resource="@xml/file_providers" />
</provider>
<activity
android:name="io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"/>
</application>
</manifest>

41
app/src/main/kotlin/io/element/android/x/MainActivity.kt

@ -32,6 +32,9 @@ import androidx.compose.runtime.remember @@ -32,6 +32,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver
@ -39,13 +42,16 @@ import io.element.android.compound.theme.ElementTheme @@ -39,13 +42,16 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.isDark
import io.element.android.compound.theme.mapToTheme
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.features.lockscreen.api.isLocked
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler
import kotlinx.coroutines.launch
import timber.log.Timber
private val loggerTag = LoggerTag("MainActivity")
@ -59,27 +65,13 @@ class MainActivity : NodeActivity() { @@ -59,27 +65,13 @@ class MainActivity : NodeActivity() {
installSplashScreen()
super.onCreate(savedInstanceState)
appBindings = bindings()
appBindings.lockScreenService().handleSecureFlag(this)
setupLockManagement(appBindings.lockScreenService())
enableEdgeToEdge()
setContent {
MainContent(appBindings)
}
}
@Deprecated("")
override fun onBackPressed() {
// If the app is locked, we need to intercept onBackPressed before it goes to OnBackPressedDispatcher.
// Indeed, otherwise we would need to trick Appyx backstack management everywhere.
// Without this trick, we would get pop operations on the hidden backstack.
if (appBindings.lockScreenService().isLocked) {
// Do not kill the app in this case, just go to background.
moveTaskToBack(false)
} else {
@Suppress("DEPRECATION")
super.onBackPressed()
}
}
@Composable
private fun MainContent(appBindings: AppBindings) {
val theme by remember {
@ -96,8 +88,8 @@ class MainActivity : NodeActivity() { @@ -96,8 +88,8 @@ class MainActivity : NodeActivity() {
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
) {
if (migrationState.migrationAction.isSuccess()) {
MainNodeHost()
@ -131,6 +123,19 @@ class MainActivity : NodeActivity() { @@ -131,6 +123,19 @@ class MainActivity : NodeActivity() {
}
}
private fun setupLockManagement(lockScreenService: LockScreenService) {
lockScreenService.handleSecureFlag(this)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Locked) {
startActivity(PinUnlockActivity.newIntent(this@MainActivity))
}
}
}
}
}
/**
* Called when:
* - the launcher icon is clicked (if the app is already running);

19
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -47,9 +47,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint @@ -47,9 +47,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint
@ -100,8 +97,6 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -100,8 +97,6 @@ class LoggedInFlowNode @AssistedInject constructor(
private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
private val ftueService: FtueService,
private val lockScreenEntryPoint: LockScreenEntryPoint,
private val lockScreenStateService: LockScreenService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val matrixClient: MatrixClient,
snackbarDispatcher: SnackbarDispatcher,
@ -111,7 +106,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -111,7 +106,7 @@ class LoggedInFlowNode @AssistedInject constructor(
savedStateMap = buildContext.savedStateMap,
),
permanentNavModel = PermanentNavModel(
navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent),
navTargets = setOf(NavTarget.LoggedInPermanent),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@ -189,9 +184,6 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -189,9 +184,6 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
data object LoggedInPermanent : NavTarget
@Parcelize
data object LockPermanent : NavTarget
@Parcelize
data object RoomList : NavTarget
@ -235,11 +227,6 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -235,11 +227,6 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.LoggedInPermanent -> {
createNode<LoggedInNode>(buildContext)
}
NavTarget.LockPermanent -> {
lockScreenEntryPoint.nodeBuilder(this, buildContext)
.target(LockScreenEntryPoint.Target.Unlock)
.build()
}
NavTarget.RoomList -> {
val callback = object : RoomListEntryPoint.Callback {
override fun onRoomClicked(roomId: RoomId) {
@ -430,15 +417,11 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -430,15 +417,11 @@ class LoggedInFlowNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
Box(modifier = modifier) {
val lockScreenState by lockScreenStateService.lockState.collectAsState()
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
if (lockScreenState == LockScreenLockState.Locked) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent)
}
}
}

1
changelog.d/2692.bugfix

@ -0,0 +1 @@ @@ -0,0 +1 @@
Fix modal contents overlapping screen lock pin.

6
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt

@ -29,11 +29,11 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana @@ -29,11 +29,11 @@ import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockMana
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
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.matrix.api.MatrixClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -41,7 +41,7 @@ import javax.inject.Inject @@ -41,7 +41,7 @@ import javax.inject.Inject
class PinUnlockPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
private val biometricUnlockManager: BiometricUnlockManager,
private val matrixClient: MatrixClient,
private val signOut: SignOut,
private val coroutineScope: CoroutineScope,
private val pinUnlockHelper: PinUnlockHelper,
) : Presenter<PinUnlockState> {
@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor( @@ -179,7 +179,7 @@ class PinUnlockPresenter @Inject constructor(
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncData<String?>>) = launch {
suspend {
matrixClient.logout(ignoreSdkError = true)
signOut()
}.runCatchingUpdatingState(signOutAction)
}
}

71
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
/*
* 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.lockscreen.impl.unlock.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
import io.element.android.libraries.architecture.bindings
import kotlinx.coroutines.launch
import javax.inject.Inject
class PinUnlockActivity : AppCompatActivity() {
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, PinUnlockActivity::class.java)
}
}
@Inject lateinit var presenter: PinUnlockPresenter
@Inject lateinit var lockScreenService: LockScreenService
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this)
setContent {
ElementTheme {
val state = presenter.present()
PinUnlockView(state = state, isInAppUnlock = false)
}
}
lifecycleScope.launch {
lockScreenService.lockState.collect { state ->
if (state == LockScreenLockState.Unlocked) {
finish()
}
}
}
val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
moveTaskToBack(true)
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
}

26
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/di/PinUnlockBindings.kt

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/*
* 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.lockscreen.impl.unlock.di
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.lockscreen.impl.unlock.activity.PinUnlockActivity
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface PinUnlockBindings {
fun inject(activity: PinUnlockActivity)
}

40
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/DefaultSignOut.kt

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/*
* 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.lockscreen.impl.unlock.signout
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultSignOut @Inject constructor(
private val authenticationService: MatrixAuthenticationService,
private val matrixClientProvider: MatrixClientProvider,
) : SignOut {
override suspend fun invoke(): String? {
val currentSession = authenticationService.getLatestSessionId()
return if (currentSession != null) {
matrixClientProvider.getOrRestore(currentSession)
.getOrThrow()
.logout(ignoreSdkError = true)
} else {
error("No session to sign out")
}
}
}

21
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/signout/SignOut.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.lockscreen.impl.unlock.signout
interface SignOut {
suspend operator fun invoke(): String?
}

28
features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/FakeSignOut.kt

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
/*
* 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.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.unlock.signout.SignOut
import io.element.android.tests.testutils.simulateLongTask
class FakeSignOut(
var lambda: () -> String? = { null }
) : SignOut {
override suspend fun invoke(): String? = simulateLongTask {
lambda()
}
}

3
features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt

@ -29,7 +29,6 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry @@ -29,7 +29,6 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.pin.model.assertText
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -150,7 +149,7 @@ class PinUnlockPresenterTest { @@ -150,7 +149,7 @@ class PinUnlockPresenterTest {
return PinUnlockPresenter(
pinCodeManager = pinCodeManager,
biometricUnlockManager = biometricUnlockManager,
matrixClient = FakeMatrixClient(),
signOut = FakeSignOut(),
coroutineScope = scope,
pinUnlockHelper = PinUnlockHelper(biometricUnlockManager, pinCodeManager),
)

Loading…
Cancel
Save