Skip to content

Commit

Permalink
Merge branch 'develop' into renovate/core
Browse files Browse the repository at this point in the history
  • Loading branch information
bmarty authored May 3, 2024
2 parents e56f613 + e9c262e commit c1cd8e2
Show file tree
Hide file tree
Showing 302 changed files with 1,827 additions and 466 deletions.
39 changes: 39 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,45 @@

<data android:scheme="io.element" />
</intent-filter>
<!--
Element web links
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" />
<data android:host="*.element.io" />
</intent-filter>
<!--
matrix.to links
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
https://developer.android.com/training/app-links#web-links
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https" />
<data android:host="matrix.to" />
</intent-filter>
<!--
links from matrix.to website
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="element" />
<data android:host="user" />
<data android:host="room" />
</intent-filter>
</activity>

<provider
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/kotlin/io/element/android/x/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ 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.call.ui.ElementCallActivity
import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.features.lockscreen.api.isLocked
import io.element.android.libraries.architecture.bindings
Expand All @@ -58,6 +59,13 @@ class MainActivity : NodeActivity() {
Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}")
installSplashScreen()
super.onCreate(savedInstanceState)
if (ElementCallActivity.maybeStart(this, intent)) {
Timber.tag(loggerTag.value).w("Starting Element Call Activity")
if (savedInstanceState == null) {
finish()
return
}
}
appBindings = bindings()
appBindings.lockScreenService().handleSecureFlag(this)
enableEdgeToEdge()
Expand Down Expand Up @@ -135,11 +143,18 @@ class MainActivity : NodeActivity() {
* Called when:
* - the launcher icon is clicked (if the app is already running);
* - a notification is clicked.
* - a deep link have been clicked
* - the app is going to background (<- this is strange)
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Timber.tag(loggerTag.value).w("onNewIntent")

if (ElementCallActivity.maybeStart(this, intent)) {
Timber.tag(loggerTag.value).w("Starting Element Call Activity")
return
}

// If the mainNode is not init yet, keep the intent for later.
// It can happen when the activity is killed by the system. The methods are called in this order :
// onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
Expand All @@ -57,16 +56,20 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForNavTargetAttached
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState
Expand All @@ -91,6 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
private val ftueEntryPoint: FtueEntryPoint,
private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
Expand Down Expand Up @@ -197,6 +201,11 @@ class LoggedInFlowNode @AssistedInject constructor(
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
) : NavTarget

@Parcelize
data class UserProfile(
val userId: UserId,
) : NavTarget

@Parcelize
data class Settings(
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
Expand Down Expand Up @@ -270,14 +279,14 @@ class LoggedInFlowNode @AssistedInject constructor(
}

override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
}

override fun onPermalinkClicked(data: PermalinkData) {
when (data) {
is PermalinkData.UserLink -> {
// FIXME Add a user profile screen.
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
// Should not happen (handled by MessagesNode)
Timber.e("User link clicked: ${data.userId}.")
}
is PermalinkData.RoomLink -> {
backstack.push(
Expand Down Expand Up @@ -306,6 +315,17 @@ class LoggedInFlowNode @AssistedInject constructor(
)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
userProfileEntryPoint.nodeBuilder(this, buildContext)
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
.callback(callback)
.build()
}
is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback {
override fun onOpenBugReport() {
Expand All @@ -321,7 +341,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
return preferencesEntryPoint.nodeBuilder(this, buildContext)
preferencesEntryPoint.nodeBuilder(this, buildContext)
.params(inputs)
.callback(callback)
.build()
Expand All @@ -345,11 +365,6 @@ class LoggedInFlowNode @AssistedInject constructor(
}
NavTarget.Ftue -> {
ftueEntryPoint.nodeBuilder(this, buildContext)
.callback(object : FtueEntryPoint.Callback {
override fun onFtueFlowFinished() {
lifecycleScope.launch { attachRoomList() }
}
})
.build()
}
NavTarget.RoomDirectorySearch -> {
Expand All @@ -368,32 +383,42 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}

suspend fun attachRoomList() {
if (!canShowRoomList()) return
attachChild<Node> {
backstack.singleTop(NavTarget.RoomList)
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
}

suspend fun attachRoom(roomId: RoomId) {
if (!canShowRoomList()) return
attachChild<RoomFlowNode> {
backstack.singleTop(NavTarget.RoomList)
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
backstack.push(
NavTarget.Room(
roomIdOrAlias = roomIdOrAlias,
initialElement = RoomNavigationTarget.Messages(
focusedEventId = eventId
)
)
)
}
}

private fun canShowRoomList(): Boolean {
return ftueService.state.value is FtueState.Complete
suspend fun attachUser(userId: UserId) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
attachChild<Node> {
backstack.push(
NavTarget.UserProfile(
userId = userId,
)
)
}
}

@Composable
override fun View(modifier: Modifier) {
Box(modifier = modifier) {
val lockScreenState by lockScreenStateService.lockState.collectAsState()
val isFtueDisplayed by ftueService.state.collectAsState()
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (isFtueDisplayed is FtueState.Complete) {
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
if (lockScreenState == LockScreenLockState.Locked) {
Expand Down
36 changes: 30 additions & 6 deletions appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.sessionstorage.api.LoggedInState
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
Expand Down Expand Up @@ -279,17 +281,37 @@ class RootFlowNode @AssistedInject constructor(
when (resolvedIntent) {
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
}
}

private suspend fun navigateTo(permalinkData: PermalinkData) {
Timber.d("Navigating to $permalinkData")
attachSession(null)
.apply {
when (permalinkData) {
is PermalinkData.FallbackLink -> Unit
is PermalinkData.RoomEmailInviteLink -> Unit
is PermalinkData.RoomLink -> {
attachRoom(
roomIdOrAlias = permalinkData.roomIdOrAlias,
eventId = permalinkData.eventId,
)
}
is PermalinkData.UserLink -> {
attachUser(permalinkData.userId)
}
}
}
}

private suspend fun navigateTo(deeplinkData: DeeplinkData) {
Timber.d("Navigating to $deeplinkData")
attachSession(deeplinkData.sessionId)
.attachSession()
.apply {
when (deeplinkData) {
is DeeplinkData.Root -> attachRoomList()
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
}
}
}
Expand All @@ -298,10 +320,12 @@ class RootFlowNode @AssistedInject constructor(
oidcActionFlow.post(oidcAction)
}

private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
// [sessionId] will be null for permalink.
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
// TODO handle multi-session
return waitForChildAttached { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
}
.attachSession()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcIntentResolver
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import timber.log.Timber
import javax.inject.Inject

sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
}

class IntentResolver @Inject constructor(
private val deeplinkParser: DeeplinkParser,
private val oidcIntentResolver: OidcIntentResolver
private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser,
) {
fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null

// Coming from a notification?
val deepLinkData = deeplinkParser.getFromIntent(intent)
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)

// Coming during login using Oidc?
val oidcAction = oidcIntentResolver.resolve(intent)
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)

// External link clicked? (matrix.to, element.io, etc.)
val permalinkData = intent
.takeIf { it.action == Intent.ACTION_VIEW }
?.dataString
?.let { permalinkParser.parse(it) }
?.takeIf { it !is PermalinkData.FallbackLink }
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)

// Unknown intent
Timber.w("Unknown intent")
return null
Expand Down
Loading

0 comments on commit c1cd8e2

Please sign in to comment.