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/README.md b/README.md index caa8fff8ef..5e5227c17b 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-poc Proof Of Concept to run a Matrix client on Android devices using the Matrix Rust Sdk and Jetpack compose. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 70ef693426..f5a0799676 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,3 @@ - - /* * Copyright (c) 2022 New Vector Ltd * diff --git a/build.gradle.kts b/build.gradle.kts index fa9822913a..f2f16714ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,8 @@ plugins { alias(libs.plugins.detekt) alias(libs.plugins.ktlint) alias(libs.plugins.dependencygraph) + alias(libs.plugins.sonarqube) + alias(libs.plugins.kover) } tasks.register("clean").configure { @@ -108,3 +110,68 @@ allprojects { plugin("org.owasp.dependencycheck") } } + +// To run a sonar analysis: +// Run './gradlew sonar -Dsonar.login=' +// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma +// Sonar result can be found here: https://sonarcloud.io/project/overview?id=vector-im_element-x-android +sonar { + properties { + property("sonar.projectName", "element-x-android") + property("sonar.projectKey", "vector-im_element-x-android") + property("sonar.host.url", "https://sonarcloud.io") + property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName) + property("sonar.sourceEncoding", "UTF-8") + property("sonar.links.homepage", "https://github.com/vector-im/element-x-android/") + property("sonar.links.ci", "https://github.com/vector-im/element-x-android/actions") + property("sonar.links.scm", "https://github.com/vector-im/element-x-android/") + property("sonar.links.issue", "https://github.com/vector-im/element-x-android/issues") + property("sonar.organization", "new_vector_ltd_organization") + property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid") + + // exclude source code from analyses separated by a colon (:) + // Exclude Java source + property("sonar.exclusions", "**/BugReporterMultipartBody.java") + } +} + +allprojects { + val projectDir = projectDir.toString() + sonar { + properties { + // Note: folders `kotlin` are not supported (yet), I asked on their side: https://community.sonarsource.com/t/82824 + // As a workaround provide the path in `sonar.sources` property. + if (File("$projectDir/src/main/kotlin").exists()) { + property("sonar.sources", "src/main/kotlin") + } + if (File("$projectDir/src/test/kotlin").exists()) { + property("sonar.tests", "src/test/kotlin") + } + } + } +} + +allprojects { + apply(plugin = "kover") +} + +// Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover +// Run `./gradlew koverMergedReport` to also get XML report +koverMerged { + enable() + + filters { + classes { + excludes.addAll( + listOf( + /* + "*Fragment", + "*Fragment\$*", + "*Activity", + "*Activity\$*", + */ + ) + ) + } + } +} diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 42282ffa8c..fcefcb0068 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.architecture) implementation(projects.libraries.testtags) + implementation(projects.libraries.androidutils) implementation(libs.accompanist.pager) implementation(libs.accompanist.pagerindicator) testImplementation(libs.test.junit) diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt index 9ed2bc9354..1d8d0b349b 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/logs/VectorFileLogger.kt @@ -18,7 +18,9 @@ package io.element.android.features.rageshake.logs import android.content.Context import android.util.Log +import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.data.tryOrNull +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -37,6 +39,7 @@ import java.util.logging.Logger class VectorFileLogger( context: Context, // private val vectorPreferences: VectorPreferences + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : Timber.Tree() { companion object { @@ -82,7 +85,7 @@ class VectorFileLogger( for (i in 0..15) { val file = File(cacheDirectory, "elementLogs.${i}.txt") - tryOrNull { file.delete() } + file.safeDelete() } fileHandler = tryOrNull( @@ -101,14 +104,14 @@ class VectorFileLogger( fun reset() { // Delete all files getLogFiles().map { - tryOrNull { it.delete() } + it.safeDelete() } } @OptIn(DelicateCoroutinesApi::class) override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { fileHandler ?: return - GlobalScope.launch(Dispatchers.IO) { + GlobalScope.launch(dispatcher) { if (skipLog(priority)) return@launch if (t != null) { logToFile(t) diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt index f3770be719..6cf888a44e 100755 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/reporter/BugReporter.kt @@ -23,11 +23,12 @@ import io.element.android.features.rageshake.crash.CrashDataStore import io.element.android.features.rageshake.logs.VectorFileLogger import io.element.android.features.rageshake.screenshot.ScreenshotHolder import io.element.android.libraries.androidutils.file.compressFile +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.toOnOff import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -54,6 +55,7 @@ class BugReporter @Inject constructor( @ApplicationContext private val context: Context, private val screenshotHolder: ScreenshotHolder, private val crashDataStore: CrashDataStore, + private val coroutineDispatchers: CoroutineDispatchers, /* private val activeSessionHolder: ActiveSessionHolder, private val versionProvider: VersionProvider, @@ -168,7 +170,7 @@ class BugReporter @Inject constructor( coroutineScope.launch { var serverError: String? = null var reportURL: String? = null - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { var bugDescription = theBugDescription val crashCallStack = crashDataStore.crashInfo().first() @@ -418,12 +420,12 @@ class BugReporter @Inject constructor( } } - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { mBugReportCall = null // delete when the bug report has been successfully sent for (file in mBugReportFiles) { - file.delete() + file.safeDelete() } if (null != listener) { @@ -498,7 +500,7 @@ class BugReporter @Inject constructor( val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME) if (logCatErrFile.exists()) { - logCatErrFile.delete() + logCatErrFile.safeDelete() } try { diff --git a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt index 410c63b93a..33674c07fb 100644 --- a/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt +++ b/features/rageshake/src/main/kotlin/io/element/android/features/rageshake/screenshot/ScreenshotHolder.kt @@ -19,6 +19,7 @@ package io.element.android.features.rageshake.screenshot import android.content.Context import android.graphics.Bitmap import io.element.android.libraries.androidutils.bitmap.writeBitmap +import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn @@ -38,6 +39,6 @@ class ScreenshotHolder @Inject constructor( fun getFile() = file.takeIf { it.exists() && it.length() > 0 } fun reset() { - file.delete() + file.safeDelete() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2fdcdbf06a..c3b00a39a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -144,3 +144,5 @@ dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencyc stem = { id = "com.likethesalad.stem", version.ref = "stem" } stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" } paparazzi = "app.cash.paparazzi:1.2.0" +sonarqube = "org.sonarqube:3.5.0.2730" +kover = "org.jetbrains.kotlinx.kover:0.6.1" diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index 6aa8911587..13da553bbf 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -26,4 +26,5 @@ android { dependencies { implementation(libs.timber) implementation(libs.androidx.corektx) + implementation(projects.libraries.core) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt new file mode 100644 index 0000000000..b12e4d9986 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -0,0 +1,34 @@ +/* + * 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.libraries.androidutils.file + +import io.element.android.libraries.core.data.tryOrNull +import timber.log.Timber +import java.io.File + +fun File.safeDelete() { + tryOrNull( + onError = { + Timber.e(it, "Error, unable to delete file $path") + }, + operation = { + if (delete().not()) { + Timber.w("Warning, unable to delete file $path") + } + } + ) +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/compressFile.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/compressFile.kt index 587c1aed75..7e55e5e62d 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/compressFile.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/compressFile.kt @@ -32,7 +32,7 @@ fun compressFile(file: File): File? { val dstFile = file.resolveSibling(file.name + ".gz") if (dstFile.exists()) { - dstFile.delete() + dstFile.safeDelete() } return try { diff --git a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/RustMatrixClient.kt b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/RustMatrixClient.kt index b9631cd6ca..c6b9bef6e0 100644 --- a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/RustMatrixClient.kt +++ b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/RustMatrixClient.kt @@ -87,7 +87,7 @@ internal class RustMatrixClient internal constructor( .addView(slidingSyncView) .build() - private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope, dispatchers) + private val slidingSyncObserverProxy = SlidingSyncObserverProxy(coroutineScope) private val roomSummaryDataSource: RustRoomSummaryDataSource = RustRoomSummaryDataSource( slidingSyncObserverProxy.updateSummaryFlow, diff --git a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/sync/SlidingSyncObserverProxy.kt b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/sync/SlidingSyncObserverProxy.kt index 5774e23cb4..1922eb985f 100644 --- a/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/sync/SlidingSyncObserverProxy.kt +++ b/libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/sync/SlidingSyncObserverProxy.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.sync -import io.element.android.libraries.core.coroutine.CoroutineDispatchers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -30,7 +29,6 @@ private const val BUFFER_SIZE = 64 class SlidingSyncObserverProxy( private val coroutineScope: CoroutineScope, - private val coroutineDispatchers: CoroutineDispatchers ) : SlidingSyncObserver { private val updateSummaryMutableFlow = @@ -39,7 +37,7 @@ class SlidingSyncObserverProxy( override fun didReceiveSyncUpdate(summary: UpdateSummary) { if (summary.rooms.isEmpty()) return - coroutineScope.launch(coroutineDispatchers.io) { + coroutineScope.launch { updateSummaryMutableFlow.emit(summary) } }