Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,3 @@ fun WireApplicationGraph.createSessionViewModelGraph(
): AppSessionViewModelGraph {
return asContribution<AppSessionViewModelGraph.Factory>().createAppSessionViewModelGraph(currentAccount, userSessionScope)
}

/**
* Compatibility path for secondary activities while their preparation entry points are migrated
* in the next stack slice.
*/
fun WireApplicationGraph.createSessionViewModelGraph(currentAccount: UserId): AppSessionViewModelGraph =
createSessionViewModelGraph(currentAccount, coreLogic.getSessionScope(currentAccount))
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package com.wire.android.feature

import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.session.AppUserSessionPreparationResult
import com.wire.android.session.UserSessionPreparationGate
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import kotlinx.coroutines.flow.Flow
Expand All @@ -36,13 +38,21 @@ class ObserveAppLockConfigUseCase @Inject constructor(
private val globalDataStore: GlobalDataStore,
@KaliumCoreLogic private val coreLogic: CoreLogic
) {
private val userSessionPreparationGate by lazy { UserSessionPreparationGate(coreLogic) }

operator fun invoke(): Flow<AppLockConfig> = channelFlow {
coreLogic.getGlobalScope().session.currentSessionFlow().collectLatest { sessionResult ->
when {
sessionResult is CurrentSessionResult.Success && sessionResult.accountInfo.isValid() -> {
val userId = sessionResult.accountInfo.userId
val appLockTeamFeatureConfigFlow =
coreLogic.getSessionScope(userId).appLockTeamFeatureConfigObserver
val sessionScope = when (val preparation = userSessionPreparationGate.prepare(userId)) {
is AppUserSessionPreparationResult.Ready -> preparation.sessionScope
is AppUserSessionPreparationResult.Failed -> {
send(AppLockConfig.Disabled(DEFAULT_APP_LOCK_TIMEOUT))
return@collectLatest
}
}
val appLockTeamFeatureConfigFlow = sessionScope.appLockTeamFeatureConfigObserver

appLockTeamFeatureConfigFlow().combineTransform(
globalDataStore.isAppLockPasscodeSetFlow()
Expand Down
75 changes: 73 additions & 2 deletions app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@
*/
package com.wire.android.ui

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.biometric.BiometricManager
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.lifecycleScope
import com.ramcosta.composedestinations.generated.app.destinations.AppUnlockWithBiometricsScreenDestination
import com.ramcosta.composedestinations.generated.app.destinations.EnterLockCodeScreenDestination
import com.ramcosta.composedestinations.generated.app.destinations.SetLockCodeScreenDestination
Expand All @@ -35,33 +41,91 @@ import com.wire.android.model.LocalWireSessionImageLoader
import com.wire.android.navigation.LoginTypeSelector
import com.wire.android.navigation.MainNavHost
import com.wire.android.navigation.rememberNavigator
import com.wire.android.session.AppUserSessionPreparationResult
import com.wire.android.session.UserSessionPreparationGate
import com.wire.android.ui.common.setupOrientationForDevice
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.SupportPage
import com.wire.android.util.SupportUrlResolver
import com.wire.android.util.launchUpdateTheApp
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.UserSessionScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

class AppLockActivity : BaseActivity() {

@Inject
lateinit var loginTypeSelector: LoginTypeSelector

private val qualifiedIdMapper = QualifiedIdMapper(null)
private var preparationState by mutableStateOf<UserSessionPreparationUiState>(
UserSessionPreparationUiState.ResolvingSession
)
private var preparationJob: Job? = null

override fun onCreate(savedInstanceState: Bundle?) {
wireApplicationGraph.inject(this)
super.onCreate(savedInstanceState)
val sessionViewModelGraph = intent.getStringExtra(EXTRA_USER_ID)
val userId = intent.getStringExtra(EXTRA_USER_ID)
?.let(qualifiedIdMapper::fromStringToQualifiedID)
?.let(wireApplicationGraph::createSessionViewModelGraph)
?: run {
appLogger.e("appLock: missing session user id, closing app lock activity")
finish()
return
}
setupOrientationForDevice()
enableEdgeToEdge()
showPreparation(userId)
}

private fun showPreparation(userId: UserId) {
setContent {
WireTheme {
UserSessionPreparationScreen(
state = preparationState,
onRetry = { prepareSession(userId) },
onUpdate = ::updateTheApp,
onContactSupport = ::openSupport,
)
}
}
prepareSession(userId)
}

private fun prepareSession(userId: UserId) {
if (preparationJob?.isActive == true) return
preparationState = UserSessionPreparationUiState.ResolvingSession
preparationJob = lifecycleScope.launch {
val gate = UserSessionPreparationGate(wireApplicationGraph.coreLogic)
val result = coroutineScope {
val observer = launch(start = CoroutineStart.UNDISPATCHED) {
gate.observe(userId).collect { preparationState = it.toUiState() }
}
try {
gate.prepare(userId)
} finally {
observer.cancelAndJoin()
}
}
when (result) {
is AppUserSessionPreparationResult.Ready -> showAppLock(userId, result.sessionScope)
is AppUserSessionPreparationResult.Failed -> {
preparationState = UserSessionPreparationUiState.Failed(result.reason.toUiFailure())
}
}
}
}

private fun showAppLock(userId: UserId, userSessionScope: UserSessionScope) {
val sessionViewModelGraph = wireApplicationGraph.createSessionViewModelGraph(userId, userSessionScope)
setContent {
val snackbarHostState = remember { SnackbarHostState() }
val rememberedSessionViewModelGraph = remember { sessionViewModelGraph }
Expand Down Expand Up @@ -102,6 +166,13 @@ class AppLockActivity : BaseActivity() {
}
}

private fun openSupport() {
val supportUrl = SupportUrlResolver.resolve(resources, SupportPage.SUPPORT)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(supportUrl)))
}

private fun updateTheApp() = launchUpdateTheApp()

companion object {
const val SET_TEAM_APP_LOCK = "set_team_app_lock"
const val EXTRA_USER_ID = "user_id"
Expand Down
88 changes: 81 additions & 7 deletions app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.wire.android.ui.calling

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
Expand All @@ -32,7 +33,10 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
Expand All @@ -41,29 +45,44 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.wire.android.appLogger
import com.wire.android.di.metro.LocalWireViewModelScopeKey
import com.wire.android.di.metro.AppSessionViewModelGraph
import com.wire.android.di.metro.createSessionViewModelGraph
import com.wire.android.di.metro.wireApplicationGraph
import com.wire.android.model.LocalWireSessionImageLoader
import com.wire.android.ui.AppLockActivity
import com.wire.android.ui.BaseActivity
import com.wire.android.ui.LocalActivity
import com.wire.android.ui.UserSessionPreparationScreen
import com.wire.android.ui.UserSessionPreparationUiState
import com.wire.android.ui.toUiFailure
import com.wire.android.ui.toUiState
import com.wire.android.ui.calling.common.ProximitySensorManager
import com.wire.android.ui.common.setupOrientationForDevice
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topappbar.CommonTopAppBarParams
import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel
import com.wire.android.ui.common.topappbar.WireTopAppBar
import com.wire.android.ui.theme.WireTheme
import com.wire.android.session.AppUserSessionPreparationResult
import com.wire.android.session.UserSessionPreparationGate
import com.wire.android.util.SupportPage
import com.wire.android.util.SupportUrlResolver
import com.wire.android.util.SwitchAccountObserver
import com.wire.android.util.launchUpdateTheApp
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.UserSessionScope
import dev.zacsweers.metro.HasMemberInjections
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.Provider
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

@HasMemberInjections
@Suppress("TooManyFunctions")
abstract class CallActivity : BaseActivity() {

@Inject
Expand Down Expand Up @@ -101,6 +120,10 @@ abstract class CallActivity : BaseActivity() {
}
}
protected val qualifiedIdMapper = QualifiedIdMapper(null)
private var preparationState by mutableStateOf<UserSessionPreparationUiState>(
UserSessionPreparationUiState.ResolvingSession
)
private var preparationJob: Job? = null

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Expand All @@ -112,18 +135,65 @@ abstract class CallActivity : BaseActivity() {
wireApplicationGraph.inject(this)
super.onCreate(savedInstanceState)
setupOrientationForDevice()
setUpScreenshotPreventionFlag()
setUpCallingFlags()

enableEdgeToEdge()

val sessionViewModelGraph = createCallSessionViewModelGraph(intent) ?: run {
val userId = intent.getStringExtra(EXTRA_USER_ID)
?.let(qualifiedIdMapper::fromStringToQualifiedID)
?: run {
appLogger.e("$TAG missing call session user id, closing call activity")
finish()
return
}

showPreparation(userId)
}

private fun showPreparation(userId: UserId) {
setContent {
WireTheme {
UserSessionPreparationScreen(
state = preparationState,
onRetry = { prepareSession(userId) },
onUpdate = ::updateTheApp,
onContactSupport = ::openSupport,
)
}
}
prepareSession(userId)
}

private fun prepareSession(userId: UserId) {
if (preparationJob?.isActive == true) return
preparationState = UserSessionPreparationUiState.ResolvingSession
preparationJob = lifecycleScope.launch {
val gate = UserSessionPreparationGate(wireApplicationGraph.coreLogic)
val result = coroutineScope {
val observer = launch(start = CoroutineStart.UNDISPATCHED) {
gate.observe(userId).collect { preparationState = it.toUiState() }
}
try {
gate.prepare(userId)
} finally {
observer.cancelAndJoin()
}
}
when (result) {
is AppUserSessionPreparationResult.Ready -> showCall(userId, result.sessionScope)
is AppUserSessionPreparationResult.Failed -> {
preparationState = UserSessionPreparationUiState.Failed(result.reason.toUiFailure())
}
}
}
}

private fun showCall(userId: UserId, userSessionScope: UserSessionScope) {
val sessionViewModelGraph = wireApplicationGraph.createSessionViewModelGraph(userId, userSessionScope)

handleNewIntent(intent)
onSessionPrepared()
setUpScreenshotPreventionFlag()

appLogger.i("$TAG Initializing proximity sensor..")
proximitySensorManager.initialize()
Expand Down Expand Up @@ -159,13 +229,17 @@ abstract class CallActivity : BaseActivity() {

protected abstract fun handleNewIntent(intent: Intent)

protected open fun onSessionPrepared() = Unit

@Composable
protected abstract fun Content()

private fun createCallSessionViewModelGraph(intent: Intent): AppSessionViewModelGraph? =
intent.getStringExtra(EXTRA_USER_ID)
?.let(qualifiedIdMapper::fromStringToQualifiedID)
?.let(wireApplicationGraph::createSessionViewModelGraph)
private fun updateTheApp() = launchUpdateTheApp()

private fun openSupport() {
val supportUrl = SupportUrlResolver.resolve(resources, SupportPage.SUPPORT)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(supportUrl)))
}

fun switchAccountIfNeeded(userId: String?) {
userId?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import androidx.appcompat.app.AppCompatActivity
import com.wire.android.appLogger
import com.wire.android.di.ApplicationScope
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.session.AppUserSessionPreparationResult
import com.wire.android.session.UserSessionPreparationGate
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.session.CurrentSessionUseCase
Expand All @@ -44,6 +46,7 @@ class ProximitySensorManager @Inject constructor(
@KaliumCoreLogic private val coreLogic: Lazy<CoreLogic>,
@ApplicationScope private val appCoroutineScope: CoroutineScope
) {
private val userSessionPreparationGate by lazy { UserSessionPreparationGate(coreLogic.value) }

private lateinit var sensorManager: SensorManager
private var proximity: Sensor? = null
Expand Down Expand Up @@ -78,7 +81,14 @@ class ProximitySensorManager @Inject constructor(
when {
currentSession is CurrentSessionResult.Success && currentSession.accountInfo.isValid() -> {
val userId = currentSession.accountInfo.userId
val isCallRunning = coreLogic.value.getSessionScope(userId).calls.isCallRunning()
val sessionScope = when (val preparation = userSessionPreparationGate.prepare(userId)) {
is AppUserSessionPreparationResult.Ready -> preparation.sessionScope
is AppUserSessionPreparationResult.Failed -> {
if (wakeLock.isHeld) wakeLock.release()
return@launch
}
}
val isCallRunning = sessionScope.calls.isCallRunning()
val distance = event.values.first()
val shouldTurnOffScreen = distance == NEAR_DISTANCE && isCallRunning
appLogger.i(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ class OngoingCallActivity : CallActivity() {
switchAccountIfNeeded(userId)
}

@SuppressLint("UnusedContentLambdaTargetStateParameter")
override fun onCreate(savedInstanceState: Bundle?) {
wireApplicationGraph.inject(this)
super.onCreate(savedInstanceState)
}

override fun onSessionPrepared() {
if (shouldAnswerCall && userId != null && conversationId != null) {
callNotificationManager.hideIncomingCallNotification(userId!!, conversationId!!)
servicesManager.startCallServiceToAnswer(
Expand Down
Loading
Loading