Skip to content
Draft
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ Multiplatform (Android, iOS and Desktop) version of the Probe app.
- `dwMain` News Media Scan specific Branding and customization
- `ooniMain` OONI Probe specific Branding and customization

* `probeCore` is a UI-free Kotlin Multiplatform library holding the engine, passport, storage, and
run/upload orchestration shared by the apps and the CLI. It carries no Compose/UI/navigation
dependencies, so the command-line front-end can reuse it.

* `cliApp` is the `ooniprobe` JVM command-line application (Clikt). It depends **only** on `probeCore`
(never `composeApp`). See `cliApp/README.md`.

* `androidApp` and `desktopApp` are the Android and desktop application shells around `composeApp`.

* `iosApp` contains the iOS application configuration and the engine integration written in Swift

* `.github` contains the Continuous Integration configuration for Github
Expand Down Expand Up @@ -101,6 +110,12 @@ Choosing the option `android (local)` won't work. This is a current
[issue](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html#f03e048) with
the official testing library.

The UI-free `probeCore` and `cliApp` tests run on the JVM without a device or simulator:

```
./gradlew :cliApp:test :probeCore:desktopTest
```

## Translations

[![Translation status](https://localizationlab.weblate.cloud/widget/ooni/app-strings-common-xml/287x66-grey.png)](https://localizationlab.weblate.cloud/engage/ooni/)
Expand Down
1 change: 1 addition & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ android {
}

dependencies {
implementation(project(":probeCore"))
implementation(project(":composeApp"))

// Compose — MainActivity hosts setContent { App() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import org.ooni.probe.config.OrganizationConfig
import org.ooni.probe.config.ProxyConfig
import org.ooni.probe.data.models.BatteryState
import org.ooni.probe.data.models.PlatformAction
import org.ooni.probe.di.Dependencies
import org.ooni.probe.di.ComposeDependencies
import org.ooni.probe.di.CoreDependencies
import org.ooni.probe.shared.LanguageSupport
import org.ooni.probe.shared.Platform
import org.ooni.probe.shared.PlatformInfo
Expand All @@ -54,7 +55,7 @@ import java.util.Locale
*/
class AndroidApplication : Application() {
val dependencies by lazy {
Dependencies(
ComposeDependencies(
platformInfo = platformInfo,
oonimkallBridge = AndroidOonimkallBridge(),
passportBridge = AndroidPassportBridge(),
Expand Down Expand Up @@ -138,9 +139,9 @@ class AndroidApplication : Application() {
AndroidSqliteDriver(Database.Schema, this, "v2.db")

private fun buildDataStore(): DataStore<Preferences> =
Dependencies.getDataStore(
CoreDependencies.getDataStore(
producePath = {
filesDir.resolve(Dependencies.DATA_STORE_FILE_NAME).absolutePath
filesDir.resolve(CoreDependencies.DATA_STORE_FILE_NAME).absolutePath
},
migrations = listOf(
SharedPreferencesMigration(this, "${packageName}_preferences"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import org.jetbrains.compose.resources.getString
import org.ooni.probe.AndroidApplication
import org.ooni.probe.shared.R
import org.ooni.probe.data.models.Descriptor
import org.ooni.probe.di.Dependencies
import org.ooni.probe.di.CoreDependencies
import org.ooni.probe.domain.descriptors.DescriptorUpdateOutcome

class DescriptorUpdateWorker(
Expand Down Expand Up @@ -143,7 +143,7 @@ class DescriptorUpdateWorker(
companion object {
fun buildWorkData(descriptors: List<Descriptor.Id>): Data =
workDataOf(
DATA_KEY_DESCRIPTORS to Dependencies.buildJson().encodeToString(descriptors),
DATA_KEY_DESCRIPTORS to CoreDependencies.buildJson().encodeToString(descriptors),
)

private const val NOTIFICATION_CHANNEL_ID = "UPDATES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ import ooniprobe.composeapp.generated.resources.Notification_StopTest
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.Results_UploadingMissing
import org.jetbrains.compose.resources.getString
import org.ooni.engine.models.displayNameSuspended
import org.ooni.probe.AndroidApplication
import org.ooni.probe.MainActivity
import org.ooni.probe.shared.R
import org.ooni.probe.data.models.RunBackgroundState
import org.ooni.probe.data.models.RunSpecification
import org.ooni.probe.di.Dependencies
import org.ooni.probe.data.models.color
import org.ooni.probe.di.CoreDependencies
import org.ooni.probe.domain.UploadMissingMeasurements
import org.ooni.probe.ui.primaryLight
import kotlin.coroutines.cancellation.CancellationException
Expand Down Expand Up @@ -274,7 +276,7 @@ class RunWorker(

fun buildWorkData(spec: RunSpecification): Data {
val specWithoutInstalledInputs = spec.stripInstalledInputs()
val specJson = Dependencies.buildJson().encodeToString(specWithoutInstalledInputs)
val specJson = CoreDependencies.buildJson().encodeToString(specWithoutInstalledInputs)
return workDataOf(DATA_KEY_SPEC to specJson)
}
}
Expand Down
20 changes: 7 additions & 13 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ plugins {
alias(libs.plugins.cocoapods)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ktlint)
alias(libs.plugins.sqldelight)
alias(libs.plugins.javafx) apply false
// The Sentry Android Gradle plugin instruments the Android application and
// uploads ProGuard mappings, so it lives in the :androidApp module now.
Expand Down Expand Up @@ -96,6 +95,10 @@ kotlin {
baseName = "composeApp"
isStatic = true
binaryOption("bundleId", "composeApp")
// Export :probeCore (passport bridge/models, engine, domain types moved out of
// composeApp) into the framework header. Without export they resolve for Kotlin but are
// invisible to Swift in iosApp.
export(project(":probeCore"))
}

// See https://github.com/getsentry/sentry-kotlin-multiplatform?tab=readme-ov-file#cocoa-sdk-version-compatibility-table
Expand Down Expand Up @@ -139,6 +142,9 @@ kotlin {
}
commonMain {
dependencies {
// `api` (not `implementation`) so the framework can `export(project(":probeCore"))`
// and expose probeCore's public API (passport/engine/domain types) to Swift.
api(project(":probeCore"))
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
Expand All @@ -162,7 +168,6 @@ kotlin {
kotlin.srcDir(tasks.named("generateSharedBuildConfig"))
}
iosMain.dependencies {
implementation(libs.sqldelight.native)
implementation(libs.bundles.mobile)
implementation(libs.bundles.ios)
}
Expand Down Expand Up @@ -224,7 +229,6 @@ kotlin {
// actuals (in-memory DB / DataStore / SecureStorage); these need the
// JVM SQLDelight + DataStore drivers on the test classpath.
dependencies {
implementation(libs.sqldelight.jvm)
implementation(libs.androidx.datastore.preferences.core)
implementation(libs.androidx.datastore.core.okio)
implementation(libs.kotlinx.coroutines.test)
Expand Down Expand Up @@ -287,16 +291,6 @@ androidComponents {
}
}

sqldelight {
databases {
create("Database") {
packageName = "org.ooni.probe"
schemaOutputDirectory = file("src/commonMain/sqldelight/databases")
verifyMigrations = true
}
}
}

ktlint {
filter {
exclude {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
package org.ooni.probe.shared.monitoring

actual object Instrumentation {
actual suspend fun <T> withTransaction(
operation: String,
name: String?,
data: Map<String, Any>,
block: suspend () -> T,
): T = block()
}
actual fun createInstrumentationDelegate(): InstrumentationDelegate = NoOpInstrumentationDelegate
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package org.ooni.probe.shared.monitoring
import io.sentry.Sentry
import io.sentry.TransactionOptions

actual object Instrumentation {
actual suspend fun <T> withTransaction(
actual fun createInstrumentationDelegate(): InstrumentationDelegate = SentryInstrumentationDelegate

object SentryInstrumentationDelegate : InstrumentationDelegate {
override suspend fun <T> withTransaction(
operation: String,
name: String?,
data: Map<String, Any>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import android.app.Application
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.test.core.app.ApplicationProvider
import org.ooni.probe.di.Dependencies
import org.ooni.probe.di.CoreDependencies

internal actual fun createPreferenceDataStore(): DataStore<Preferences> {
val app = ApplicationProvider.getApplicationContext<Application>()
return Dependencies.getDataStore(
producePath = { app.filesDir.resolve("test" + Dependencies.Companion.DATA_STORE_FILE_NAME).absolutePath },
return CoreDependencies.getDataStore(
producePath = { app.filesDir.resolve("test" + CoreDependencies.Companion.DATA_STORE_FILE_NAME).absolutePath },
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
package org.ooni.probe.shared.monitoring

actual object Instrumentation {
actual suspend fun <T> withTransaction(
import io.sentry.Sentry
import io.sentry.TransactionOptions

actual fun createInstrumentationDelegate(): InstrumentationDelegate = SentryInstrumentationDelegate

object SentryInstrumentationDelegate : InstrumentationDelegate {
override suspend fun <T> withTransaction(
operation: String,
name: String?,
data: Map<String, Any>,
block: suspend () -> T,
): T = block()
): T {
if (!Sentry.isEnabled()) return block()

val span = Sentry.getSpan()
return if (span == null) {
val transaction = Sentry.startTransaction(
name ?: operation,
operation,
TransactionOptions().also { it.isBindToScope = true },
)
data.forEach { (key, value) -> transaction.setData(key, value) }
val result = block()
transaction.finish()
result
} else {
val innerSpan = span.startChild(operation)
data.forEach { (key, value) -> innerSpan.setData(key, value) }
val result = block()
innerSpan.finish()
result
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.ooni.engine

import org.ooni.engine.models.TaskOrigin
import org.ooni.probe.config.OrganizationConfig
import org.ooni.probe.shared.PlatformInfo

/**
* Flavor-aware software-name helpers used by the UI app's check-in path.
*
* These live in `composeApp` because they read the flavor `OrganizationConfig.baseSoftwareName`.
* `probeCore`'s `Engine` computes the same value from the injected `CoreConfig` instead.
*/
val PlatformInfo.softwareName
get() = OrganizationConfig.baseSoftwareName + "-" + platform.engineName

fun PlatformInfo.buildSoftwareName(taskOrigin: TaskOrigin) =
softwareName + (if (taskOrigin == TaskOrigin.AutoRun) "-" + "unattended" else "")
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.ooni.engine.models

import androidx.compose.runtime.Composable
import ooniprobe.composeapp.generated.resources.Res
import ooniprobe.composeapp.generated.resources.Test_Dash_Fullname
import ooniprobe.composeapp.generated.resources.Test_Experimental_Fullname
import ooniprobe.composeapp.generated.resources.Test_FacebookMessenger_Fullname
import ooniprobe.composeapp.generated.resources.Test_HTTPHeaderFieldManipulation_Fullname
import ooniprobe.composeapp.generated.resources.Test_HTTPInvalidRequestLine_Fullname
import ooniprobe.composeapp.generated.resources.Test_NDT_Fullname
import ooniprobe.composeapp.generated.resources.Test_Psiphon_Fullname
import ooniprobe.composeapp.generated.resources.Test_Signal_Fullname
import ooniprobe.composeapp.generated.resources.Test_Telegram_Fullname
import ooniprobe.composeapp.generated.resources.Test_Tor_Fullname
import ooniprobe.composeapp.generated.resources.Test_WebConnectivity_Fullname
import ooniprobe.composeapp.generated.resources.Test_WhatsApp_Fullname
import ooniprobe.composeapp.generated.resources.test_experimental
import ooniprobe.composeapp.generated.resources.test_facebook_messenger
import ooniprobe.composeapp.generated.resources.test_psiphon
import ooniprobe.composeapp.generated.resources.test_signal
import ooniprobe.composeapp.generated.resources.test_telegram
import ooniprobe.composeapp.generated.resources.test_tor
import ooniprobe.composeapp.generated.resources.test_websites
import ooniprobe.composeapp.generated.resources.test_whatsapp
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource

val TestType.labelRes: StringResource
get() = when (this) {
is TestType.Dash -> Res.string.Test_Dash_Fullname
is TestType.Experimental -> Res.string.Test_Experimental_Fullname
is TestType.FacebookMessenger -> Res.string.Test_FacebookMessenger_Fullname
is TestType.HttpHeaderFieldManipulation -> Res.string.Test_HTTPHeaderFieldManipulation_Fullname
is TestType.HttpInvalidRequestLine -> Res.string.Test_HTTPInvalidRequestLine_Fullname
is TestType.Ndt -> Res.string.Test_NDT_Fullname
is TestType.Psiphon -> Res.string.Test_Psiphon_Fullname
is TestType.Signal -> Res.string.Test_Signal_Fullname
is TestType.Telegram -> Res.string.Test_Telegram_Fullname
is TestType.Tor -> Res.string.Test_Tor_Fullname
is TestType.WebConnectivity -> Res.string.Test_WebConnectivity_Fullname
is TestType.Whatsapp -> Res.string.Test_WhatsApp_Fullname
}

val TestType.iconRes: DrawableResource?
get() = when (this) {
is TestType.Experimental -> Res.drawable.test_experimental
is TestType.FacebookMessenger -> Res.drawable.test_facebook_messenger
is TestType.Psiphon -> Res.drawable.test_psiphon
is TestType.Signal -> Res.drawable.test_signal
is TestType.Telegram -> Res.drawable.test_telegram
is TestType.Tor -> Res.drawable.test_tor
is TestType.WebConnectivity -> Res.drawable.test_websites
is TestType.Whatsapp -> Res.drawable.test_whatsapp
is TestType.Dash,
is TestType.HttpHeaderFieldManipulation,
is TestType.HttpInvalidRequestLine,
is TestType.Ndt,
-> null
}

val TestType.displayName: String
@Composable
get() = if (this is TestType.Experimental) name else stringResource(labelRes)

suspend fun TestType.displayNameSuspended() = if (this is TestType.Experimental) name else getString(labelRes)
Loading