diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 91352bb27b..a88a5faa9d 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@11.2.2 + uses: danger/danger-js@11.2.3 with: args: "--dangerfile ./tools/danger/dangerfile.js" env: diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 0b9939b133..486094760e 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v3 - name: Assemble debug APK run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES - - uses: mobile-dev-inc/action-maestro-cloud@v1.1.1 + - uses: mobile-dev-inc/action-maestro-cloud@v1.2.3 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} app-file: app/build/outputs/apk/debug/app-debug.apk diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 47400648a5..78c407cde1 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -37,7 +37,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.2.2 + uses: danger/danger-js@11.2.3 with: args: "--dangerfile ./tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 678a4268af..f4a12c9ec2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,17 @@ jobs: - uses: actions/checkout@v3 - name: Run tests run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES + - name: Generate kover report + if: always() + run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES + + - name: Archive kover report + if: always() + uses: actions/upload-artifact@v3 + with: + name: kover-results + path: | + **/build/reports/kover/merged - name: Archive test results on error if: failure() @@ -32,3 +43,17 @@ jobs: path: | **/out/failures/ **/build/reports/tests/*UnitTest/ + + - name: Publish results to Sonar + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} + if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} + run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES + + # https://github.com/codecov/codecov-action + - name: Upload coverage reports to codecov + if: always() + uses: codecov/codecov-action@v3 + # with: + # files: build/reports/kover/merged/xml/report.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8a46..7d1c62f24e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,8 @@ - + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 54d5acd7d7..55ded342f7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/README.md b/README.md index 2b3385c34b..6406ffbb6b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +[![Latest build](https://github.com/vector-im/element-x-android/actions/workflows/build.yml/badge.svg?query=branch%3Adevelop)](https://github.com/vector-im/element-x-android/actions/workflows/build.yml?query=branch%3Adevelop) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector-im_element-x-android&metric=bugs)](https://sonarcloud.io/summary/new_code?id=vector-im_element-x-android) +[![codecov](https://codecov.io/github/vector-im/element-x-android/branch/develop/graph/badge.svg?token=ecwvia7amV)](https://codecov.io/github/vector-im/element-x-android) +[![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) +[![Weblate](https://translate.element.io/widgets/element-android/-/svg-badge.svg)](https://translate.element.io/engage/element-android/?utm_source=widget) + # element-x-android ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/). diff --git a/anvilannotations/src/main/kotlin/io/element/android/x/anvilannotations/ContributesNode.kt b/anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt similarity index 95% rename from anvilannotations/src/main/kotlin/io/element/android/x/anvilannotations/ContributesNode.kt rename to anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt index 1367dc0dc7..cf9f2f3684 100644 --- a/anvilannotations/src/main/kotlin/io/element/android/x/anvilannotations/ContributesNode.kt +++ b/anvilannotations/src/main/kotlin/io/element/android/anvilannotations/ContributesNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.x.anvilannotations +package io.element.android.anvilannotations import kotlin.reflect.KClass diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index da817a5edb..e8b6ab285c 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } dependencies { - implementation(project(":anvilannotations")) + implementation(projects.anvilannotations) api(libs.anvil.compiler.api) implementation(libs.anvil.compiler.utils) implementation("com.squareup:kotlinpoet:1.12.0") diff --git a/anvilcodegen/src/main/kotlin/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt similarity index 96% rename from anvilcodegen/src/main/kotlin/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt rename to anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt index 0ca9da53af..576a52df89 100644 --- a/anvilcodegen/src/main/kotlin/io/element/android/x/anvilcodegen/ContributesNodeCodeGenerator.kt +++ b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeCodeGenerator.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalAnvilApi::class) -package io.element.android.x.anvilcodegen +package io.element.android.anvilcodegen import com.google.auto.service.AutoService import com.squareup.anvil.annotations.ContributesTo @@ -46,7 +46,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.multibindings.IntoMap -import io.element.android.x.anvilannotations.ContributesNode +import io.element.android.anvilannotations.ContributesNode import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtFile @@ -148,7 +148,7 @@ class ContributesNodeCodeGenerator : CodeGenerator { } companion object { - private val assistedNodeFactoryFqName = FqName("io.element.android.x.architecture.AssistedNodeFactory") - private val nodeKeyFqName = FqName("io.element.android.x.architecture.NodeKey") + private val assistedNodeFactoryFqName = FqName("io.element.android.libraries.architecture.AssistedNodeFactory") + private val nodeKeyFqName = FqName("io.element.android.libraries.architecture.NodeKey") } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3818bf972..f5a0799676 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,3 @@ - - /* * Copyright (c) 2022 New Vector Ltd * @@ -164,14 +162,14 @@ knit { dependencies { allLibraries() allFeatures() - implementation(project(":tests:uitests")) - implementation(project(":anvilannotations")) - anvil(project(":anvilcodegen")) + implementation(projects.tests.uitests) + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) // https://developer.android.com/studio/write/java8-support#library-desugaring-versions coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2") implementation(libs.appyx.core) - + implementation(libs.androidx.splash) implementation(libs.androidx.corektx) implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.activity.compose) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 61a03241a1..aa64ea3f53 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,8 @@ tools:targetApi="33"> diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index 4532276e11..76cf15e660 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -18,9 +18,10 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer -import io.element.android.x.di.DaggerComponentOwner +import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent +import io.element.android.x.info.logApplicationInfo import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.MatrixInitializer import io.element.android.x.initializer.TimberInitializer @@ -40,5 +41,6 @@ class ElementXApplication : Application(), DaggerComponentOwner { initializeComponent(TimberInitializer::class.java) initializeComponent(MatrixInitializer::class.java) } + logApplicationInfo() } } diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index 2630c78c7e..ebd9494017 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -22,20 +22,23 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeComponentActivity -import io.element.android.x.architecture.bindings -import io.element.android.x.di.DaggerComponentOwner -import io.element.android.x.designsystem.ElementXTheme +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.designsystem.ElementXTheme +import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppBindings import io.element.android.x.node.RootFlowNode class MainActivity : NodeComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() super.onCreate(savedInstanceState) val appBindings = bindings() + appBindings.matrixClientsHolder().restore(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { ElementXTheme { @@ -48,11 +51,17 @@ class MainActivity : NodeComponentActivity() { buildContext = it, appComponentOwner = applicationContext as DaggerComponentOwner, authenticationService = appBindings.authenticationService(), - rootPresenter = appBindings.rootPresenter() + presenter = appBindings.rootPresenter(), + matrixClientsHolder = appBindings.matrixClientsHolder() ) } } } } } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + bindings().matrixClientsHolder().onSaveInstanceState(outState) + } } diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index 78f40f0322..de8b29682e 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -17,13 +17,14 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo -import io.element.android.x.matrix.auth.MatrixAuthenticationService +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.auth.MatrixAuthenticationService import io.element.android.x.root.RootPresenter import kotlinx.coroutines.CoroutineScope @ContributesTo(AppScope::class) interface AppBindings { - fun coroutineScope(): CoroutineScope fun rootPresenter(): RootPresenter fun authenticationService(): MatrixAuthenticationService + fun matrixClientsHolder(): MatrixClientsHolder } diff --git a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt b/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt index 342c18e3a4..ded3c333d7 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppComponent.kt @@ -20,7 +20,10 @@ import android.content.Context import com.squareup.anvil.annotations.MergeComponent import dagger.BindsInstance import dagger.Component -import io.element.android.x.architecture.NodeFactoriesBindings +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn @SingleIn(AppScope::class) @MergeComponent(AppScope::class) diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index c1b3ff42e7..7cb3fb55c3 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -20,7 +20,10 @@ import android.content.Context import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides -import io.element.android.x.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/kotlin/io/element/android/x/di/MatrixClientsHolder.kt b/app/src/main/kotlin/io/element/android/x/di/MatrixClientsHolder.kt new file mode 100644 index 0000000000..22dfe8a3ef --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/di/MatrixClientsHolder.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 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.x.di + +import android.os.Bundle +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.MatrixClient +import io.element.android.libraries.matrix.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.core.SessionId +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" + +@SingleIn(AppScope::class) +class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) { + + private val sessionIdsToMatrixClient = ConcurrentHashMap() + + fun add(matrixClient: MatrixClient) { + sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient + } + + fun removeAll() { + sessionIdsToMatrixClient.clear() + } + + fun remove(sessionId: SessionId) { + sessionIdsToMatrixClient.remove(sessionId) + } + + fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty() + + fun knowSession(sessionId: SessionId): Boolean = sessionIdsToMatrixClient.containsKey(sessionId) + + fun getOrNull(sessionId: SessionId): MatrixClient? { + return sessionIdsToMatrixClient[sessionId] + } + + @Suppress("DEPRECATION") + fun restore(savedInstanceState: Bundle?) { + if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return + val sessionIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array + if (sessionIds.isNullOrEmpty()) return + // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. + runBlocking { + sessionIds.forEach { sessionId -> + Timber.v("Restore matrix session: $sessionId") + val matrixClient = authenticationService.restoreSession(sessionId) + if (matrixClient != null) { + add(matrixClient) + } + } + } + } + + fun onSaveInstanceState(outState: Bundle) { + val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() + Timber.v("Save matrix session keys = $sessionKeys") + outState.putSerializable(SAVE_INSTANCE_KEY, sessionKeys) + } +} diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt index cfd7eee471..c60eba1575 100644 --- a/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/RoomComponent.kt @@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.MergeSubcomponent import dagger.BindsInstance import dagger.Subcomponent -import io.element.android.x.architecture.NodeFactoriesBindings -import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.room.MatrixRoom @SingleIn(RoomScope::class) @MergeSubcomponent(RoomScope::class) diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt index 8da31df8eb..be558f6f63 100644 --- a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt @@ -20,8 +20,11 @@ import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.MergeSubcomponent import dagger.BindsInstance import dagger.Subcomponent -import io.element.android.x.architecture.NodeFactoriesBindings -import io.element.android.x.matrix.MatrixClient +import io.element.android.libraries.architecture.NodeFactoriesBindings +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.MatrixClient @SingleIn(SessionScope::class) @MergeSubcomponent(SessionScope::class) diff --git a/app/src/main/kotlin/io/element/android/x/info/Logs.kt b/app/src/main/kotlin/io/element/android/x/info/Logs.kt new file mode 100644 index 0000000000..9e96f48e2d --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/info/Logs.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 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.x.info + +import io.element.android.x.BuildConfig +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +fun logApplicationInfo() { + val appVersion = buildString { + append(BuildConfig.VERSION_NAME) + append(" (") + append(BuildConfig.VERSION_CODE) + append(") - ") + append(BuildConfig.BUILD_TYPE) + } + // TODO Get SDK version somehow + val sdkVersion = "SDK VERSION (TODO)" + val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date()) + + Timber.d("----------------------------------------------------------------") + Timber.d("----------------------------------------------------------------") + Timber.d(" Application version: $appVersion") + Timber.d(" SDK version: $sdkVersion") + Timber.d(" Local time: $date") + Timber.d("----------------------------------------------------------------") + Timber.d("----------------------------------------------------------------\n\n\n\n") +} diff --git a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt index df3dcacdbb..fa7e904e72 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt @@ -18,7 +18,7 @@ package io.element.android.x.initializer import android.content.Context import androidx.startup.Initializer -import io.element.android.x.features.rageshake.crash.VectorUncaughtExceptionHandler +import io.element.android.features.rageshake.crash.VectorUncaughtExceptionHandler class CrashInitializer : Initializer { diff --git a/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt index bfcdaea612..9af7738fa8 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/MatrixInitializer.kt @@ -18,9 +18,9 @@ package io.element.android.x.initializer import android.content.Context import androidx.startup.Initializer +import io.element.android.libraries.matrix.tracing.TracingConfigurations +import io.element.android.libraries.matrix.tracing.setupTracing import io.element.android.x.BuildConfig -import io.element.android.x.matrix.tracing.TracingConfigurations -import io.element.android.x.matrix.tracing.setupTracing class MatrixInitializer : Initializer { diff --git a/app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt index 10b7e17ffd..23286c683d 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/TimberInitializer.kt @@ -18,8 +18,8 @@ package io.element.android.x.initializer import android.content.Context import androidx.startup.Initializer +import io.element.android.features.rageshake.logs.VectorFileLogger import io.element.android.x.BuildConfig -import io.element.android.x.features.rageshake.logs.VectorFileLogger import timber.log.Timber class TimberInitializer : Initializer { diff --git a/app/src/main/kotlin/io/element/android/x/node/BackstackExt.kt b/app/src/main/kotlin/io/element/android/x/node/BackstackExt.kt new file mode 100644 index 0000000000..0476e50553 --- /dev/null +++ b/app/src/main/kotlin/io/element/android/x/node/BackstackExt.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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.x.node + +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.NewRoot + +/** + * Don't process NewRoot if the nav target already exists in the stack. + */ +fun BackStack.safeRoot(element: T) { + val containsRoot = elements.value.any { + it.key.navTarget == element + } + if (containsRoot) return + accept(NewRoot(element)) +} diff --git a/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt index 8a5a967875..b2e2aca077 100644 --- a/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/LoggedInFlowNode.kt @@ -32,16 +32,17 @@ import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.node import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push -import io.element.android.x.architecture.bindings -import io.element.android.x.architecture.createNode -import io.element.android.x.di.DaggerComponentOwner +import io.element.android.features.preferences.PreferencesFlowNode +import io.element.android.features.roomlist.RoomListNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.matrix.MatrixClient +import io.element.android.libraries.matrix.core.RoomId +import io.element.android.libraries.matrix.core.SessionId +import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.x.di.SessionComponent -import io.element.android.x.features.preferences.PreferencesFlowNode -import io.element.android.x.features.roomlist.RoomListNode -import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.core.RoomId -import io.element.android.x.matrix.core.SessionId -import io.element.android.x.matrix.ui.di.MatrixUIBindings import kotlinx.parcelize.Parcelize class LoggedInFlowNode( @@ -124,6 +125,11 @@ class LoggedInFlowNode( @Composable override fun View(modifier: Modifier) { - Children(navModel = backstack) + Children( + navModel = backstack, + modifier = modifier, + // Animate navigation to settings and to a room + transitionHandler = rememberDefaultTransitionHandler(), + ) } } diff --git a/app/src/main/kotlin/io/element/android/x/node/NotLoggedInFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/NotLoggedInFlowNode.kt index 3c619c5e38..b9e38d1063 100644 --- a/app/src/main/kotlin/io/element/android/x/node/NotLoggedInFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/NotLoggedInFlowNode.kt @@ -26,9 +26,10 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.node import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.replace -import io.element.android.x.features.login.LoginFlowNode -import io.element.android.x.features.onboarding.OnBoardingScreen +import com.bumble.appyx.navmodel.backstack.operation.push +import io.element.android.features.login.LoginFlowNode +import io.element.android.features.onboarding.OnBoardingScreen +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -62,7 +63,7 @@ class NotLoggedInFlowNode( return when (navTarget) { NavTarget.OnBoarding -> node(buildContext) { OnBoardingScreen( - onSignIn = { backstack.replace(NavTarget.LoginFlow) } + onSignIn = { backstack.push(NavTarget.LoginFlow) } ) } NavTarget.LoginFlow -> LoginFlowNode(buildContext) @@ -71,6 +72,11 @@ class NotLoggedInFlowNode( @Composable override fun View(modifier: Modifier) { - Children(navModel = backstack) + Children( + navModel = backstack, + modifier = modifier, + // Animate navigation to login screen + transitionHandler = rememberDefaultTransitionHandler(), + ) } } diff --git a/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt index 9db5f5cfb5..bae60841d6 100644 --- a/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/RoomFlowNode.kt @@ -25,12 +25,12 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.navmodel.backstack.BackStack -import io.element.android.x.architecture.bindings -import io.element.android.x.architecture.createNode -import io.element.android.x.di.DaggerComponentOwner +import io.element.android.features.messages.MessagesNode +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.x.di.RoomComponent -import io.element.android.x.features.messages.MessagesNode -import io.element.android.x.matrix.room.MatrixRoom import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -70,6 +70,9 @@ class RoomFlowNode( @Composable override fun View(modifier: Modifier) { - Children(navModel = backstack) + Children( + navModel = backstack, + modifier = modifier, + ) } } diff --git a/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt b/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt index 9dee857470..ae3c31008d 100644 --- a/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt +++ b/app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt @@ -21,13 +21,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children -import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode @@ -36,13 +33,13 @@ import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push -import io.element.android.x.architecture.createNode -import io.element.android.x.architecture.presenterConnector -import io.element.android.x.di.DaggerComponentOwner -import io.element.android.x.features.rageshake.bugreport.BugReportNode -import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.auth.MatrixAuthenticationService -import io.element.android.x.matrix.core.SessionId +import io.element.android.features.rageshake.bugreport.BugReportNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.matrix.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.core.SessionId +import io.element.android.x.di.MatrixClientsHolder import io.element.android.x.root.RootPresenter import io.element.android.x.root.RootView import kotlinx.coroutines.flow.distinctUntilChanged @@ -50,66 +47,96 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap class RootFlowNode( - buildContext: BuildContext, + private val buildContext: BuildContext, private val backstack: BackStack = BackStack( initialElement = NavTarget.SplashScreen, savedStateMap = buildContext.savedStateMap, ), private val appComponentOwner: DaggerComponentOwner, private val authenticationService: MatrixAuthenticationService, - rootPresenter: RootPresenter + private val matrixClientsHolder: MatrixClientsHolder, + private val presenter: RootPresenter ) : ParentNode( navModel = backstack, - buildContext = buildContext, + buildContext = buildContext ), DaggerComponentOwner by appComponentOwner { - private val matrixClientsHolder = ConcurrentHashMap() - private val presenterConnector = presenterConnector(rootPresenter) - override fun onBuilt() { super.onBuilt() - whenChildAttached(LoggedInFlowNode::class) { _, child -> - child.lifecycle.subscribe( - onDestroy = { matrixClientsHolder.remove(child.sessionId) } - ) - } + observeLoggedInState() + } + + private fun observeLoggedInState() { authenticationService.isLoggedIn() .distinctUntilChanged() .onEach { isLoggedIn -> Timber.v("isLoggedIn=$isLoggedIn") if (isLoggedIn) { - val matrixClient = authenticationService.restoreSession() - if (matrixClient == null) { - backstack.newRoot(NavTarget.NotLoggedInFlow) - } else { - matrixClientsHolder[matrixClient.sessionId] = matrixClient - backstack.newRoot(NavTarget.LoggedInFlow(matrixClient.sessionId)) - } + tryToRestoreLatestSession( + onSuccess = { switchToLoggedInFlow(it) }, + onFailure = { switchToLogoutFlow() } + ) } else { - backstack.newRoot(NavTarget.NotLoggedInFlow) + switchToLogoutFlow() } } .launchIn(lifecycleScope) } + private fun switchToLoggedInFlow(sessionId: SessionId) { + backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId)) + } + + private fun switchToLogoutFlow() { + matrixClientsHolder.removeAll() + backstack.safeRoot(NavTarget.NotLoggedInFlow) + } + + private suspend fun tryToRestoreLatestSession( + onSuccess: (SessionId) -> Unit = {}, + onFailure: () -> Unit = {} + ) { + val latestKnownSessionId = authenticationService.getLatestSessionId() + if (latestKnownSessionId == null) { + onFailure() + return + } + if (matrixClientsHolder.knowSession(latestKnownSessionId)) { + onSuccess(latestKnownSessionId) + return + } + val matrixClient = authenticationService.restoreSession(latestKnownSessionId) + if (matrixClient == null) { + Timber.v("Failed to restore session...") + onFailure() + } else { + matrixClientsHolder.add(matrixClient) + onSuccess(matrixClient.sessionId) + } + } + private fun onOpenBugReport() { backstack.push(NavTarget.BugReport) } @Composable override fun View(modifier: Modifier) { - val state by presenterConnector.stateFlow.collectAsState() + val state = presenter.present() RootView( state = state, + modifier = modifier, onOpenBugReport = this::onOpenBugReport, ) { - Children(navModel = backstack) + Children( + navModel = backstack, + // Animate opening the bug report screen + transitionHandler = rememberDefaultTransitionHandler(), + ) } } @@ -136,8 +163,10 @@ class RootFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - val matrixClient = - matrixClientsHolder[navTarget.sessionId] ?: throw IllegalStateException("Makes sure to give a matrixClient with the given sessionId") + val matrixClient = matrixClientsHolder.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { + Timber.w("Couldn't find any session, go through SplashScreen") + backstack.newRoot(NavTarget.SplashScreen) + } LoggedInFlowNode( buildContext = buildContext, sessionId = navTarget.sessionId, @@ -146,12 +175,14 @@ class RootFlowNode( ) } NavTarget.NotLoggedInFlow -> NotLoggedInFlowNode(buildContext) - NavTarget.SplashScreen -> node(buildContext) { - Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } + NavTarget.SplashScreen -> splashNode(buildContext) NavTarget.BugReport -> createNode(buildContext, plugins = listOf(bugReportNodeCallback)) } } + + private fun splashNode(buildContext: BuildContext) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } } diff --git a/app/src/main/kotlin/io/element/android/x/root/RootPresenter.kt b/app/src/main/kotlin/io/element/android/x/root/RootPresenter.kt index 2bcf49505a..63881df5ae 100644 --- a/app/src/main/kotlin/io/element/android/x/root/RootPresenter.kt +++ b/app/src/main/kotlin/io/element/android/x/root/RootPresenter.kt @@ -19,10 +19,10 @@ package io.element.android.x.root import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.x.architecture.Presenter -import io.element.android.x.features.rageshake.bugreport.BugReportPresenter -import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter -import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter +import io.element.android.features.rageshake.bugreport.BugReportPresenter +import io.element.android.features.rageshake.crash.ui.CrashDetectionPresenter +import io.element.android.features.rageshake.detection.RageshakeDetectionPresenter +import io.element.android.libraries.architecture.Presenter import javax.inject.Inject class RootPresenter @Inject constructor( diff --git a/app/src/main/kotlin/io/element/android/x/root/RootState.kt b/app/src/main/kotlin/io/element/android/x/root/RootState.kt index 6a062d2925..6d516c5c70 100644 --- a/app/src/main/kotlin/io/element/android/x/root/RootState.kt +++ b/app/src/main/kotlin/io/element/android/x/root/RootState.kt @@ -17,9 +17,9 @@ package io.element.android.x.root import androidx.compose.runtime.Stable -import io.element.android.x.features.rageshake.bugreport.BugReportState -import io.element.android.x.features.rageshake.crash.ui.CrashDetectionState -import io.element.android.x.features.rageshake.detection.RageshakeDetectionState +import io.element.android.features.rageshake.bugreport.BugReportState +import io.element.android.features.rageshake.crash.ui.CrashDetectionState +import io.element.android.features.rageshake.detection.RageshakeDetectionState @Stable data class RootState( diff --git a/app/src/main/kotlin/io/element/android/x/root/RootView.kt b/app/src/main/kotlin/io/element/android/x/root/RootView.kt index b767fb164f..f8cbdea24f 100644 --- a/app/src/main/kotlin/io/element/android/x/root/RootView.kt +++ b/app/src/main/kotlin/io/element/android/x/root/RootView.kt @@ -24,12 +24,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import io.element.android.features.rageshake.crash.ui.CrashDetectionEvents +import io.element.android.features.rageshake.crash.ui.CrashDetectionView +import io.element.android.features.rageshake.detection.RageshakeDetectionEvents +import io.element.android.features.rageshake.detection.RageshakeDetectionView +import io.element.android.tests.uitests.openShowkase import io.element.android.x.component.ShowkaseButton -import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents -import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView -import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents -import io.element.android.x.features.rageshake.detection.RageshakeDetectionView -import io.element.android.x.tests.uitests.openShowkase @Composable fun RootView( diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 0000000000..9b852a657f --- /dev/null +++ b/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000000..6763f9b016 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,26 @@ + + + + + + +