diff --git a/.fleet/receipt.json b/.fleet/receipt.json
index f1485118..a76ba042 100644
--- a/.fleet/receipt.json
+++ b/.fleet/receipt.json
@@ -12,11 +12,6 @@
"ui": [
"compose"
]
- },
- "desktop": {
- "ui": [
- "compose"
- ]
}
}
},
diff --git a/.gitignore b/.gitignore
index 33e4c75d..a968e342 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ captures
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
+/iosApp/Pods/
+.kotlin
diff --git a/build.gradle.kts b/build.gradle.kts
index 52cf9fab..f49eedf2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,5 +4,7 @@ plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrainsCompose) apply false
+ alias(libs.plugins.jetbrainsComposeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
+ alias(libs.plugins.cocoapods) apply false
}
\ No newline at end of file
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 50bb0b9d..7df336d5 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -1,42 +1,47 @@
-import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsCompose)
- kotlin("plugin.serialization") version "1.9.22"
+ alias(libs.plugins.jetbrainsComposeCompiler)
+ alias(libs.plugins.cocoapods)
+ alias(libs.plugins.kotlinSerialization)
}
kotlin {
androidTarget {
- compilations.all {
- kotlinOptions {
- jvmTarget = "11"
- }
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
}
}
-
- jvm("desktop")
-
- listOf(
- iosX64(),
- iosArm64(),
- iosSimulatorArm64()
- ).forEach { iosTarget ->
- iosTarget.binaries.framework {
- baseName = "ComposeApp"
+
+ iosX64()
+ iosArm64()
+ iosSimulatorArm64()
+
+ cocoapods {
+ ios.deploymentTarget = "9.0"
+
+ version = "1.0"
+ summary = "Compose App"
+ homepage = "https://github.com/ooni/probe-multiplatform"
+
+ framework {
+ baseName = "composeApp"
isStatic = true
}
+
+ podfile = project.file("../iosApp/Podfile")
}
sourceSets {
- val desktopMain by getting
-
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
- implementation("io.insert-koin:koin-android:3.5.6")
+ implementation(libs.android.oonimkall)
}
commonMain.dependencies {
implementation(compose.runtime)
@@ -46,71 +51,24 @@ kotlin {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
-
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
- implementation("io.github.aakira:napier:2.7.1")
- implementation("io.insert-koin:koin-core:3.5.6")
- // TODO(r2): When koin 3.6.x comes out we can drop this.
- // https://github.com/InsertKoinIO/koin/issues/1803
- implementation("io.insert-koin:koin-compose:1.1.5")
-
- val voyagerVersion = "1.0.0"
- implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion")
- implementation("cafe.adriel.voyager:voyager-screenmodel:$voyagerVersion")
- implementation("cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyagerVersion")
- implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion")
- implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
- implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion")
-
- implementation("com.russhwolf:multiplatform-settings-no-arg:1.1.1")
- implementation("com.russhwolf:multiplatform-settings-coroutines:1.1.1")
-
- }
- desktopMain.dependencies {
- implementation(compose.desktop.currentOs)
+ implementation(libs.kotlin.serialization)
}
}
+
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ compilerOptions {
+ // Common compiler options applied to all Kotlin source sets
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ }
+ composeCompiler {
+ enableStrongSkippingMode = true
+ }
}
android {
namespace = "org.ooni.probe"
compileSdk = libs.versions.android.compileSdk.get().toInt()
- sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
- sourceSets["main"].res.srcDirs("src/androidMain/res")
- sourceSets["main"].resources.srcDirs("src/commonMain/resources")
-
- externalNativeBuild {
- cmake {
- path("src/androidMain/kotlin/org/ooni/libooniprobe-android/CMakeLists.txt")
- }
- }
-
- buildTypes {
- all {
- externalNativeBuild {
- cmake {
- targets("libooniprobe.so")
- arguments("-DGRADLE_USER_HOME=${project.gradle.gradleUserHomeDir}")
- }
- }
- }
- release {
- externalNativeBuild {
- cmake {
- arguments("-DANDROID_PACKAGE_NAME=${namespace}")
- }
- }
- }
- debug {
- externalNativeBuild {
- cmake {
- arguments("-DANDROID_PACKAGE_NAME=${namespace}.debug")
- }
- }
- }
- }
-
defaultConfig {
applicationId = "org.ooni.probe"
minSdk = libs.versions.android.minSdk.get().toInt()
@@ -124,27 +82,18 @@ android {
}
}
buildTypes {
+ getByName("debug") {
+ applicationIdSuffix = ".debug"
+ }
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
}
-
-compose.desktop {
- application {
- mainClass = "MainKt"
-
- nativeDistributions {
- targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
- packageName = "org.ooni.probe"
- packageVersion = "1.0.0"
- }
- }
-}
diff --git a/composeApp/composeApp.podspec b/composeApp/composeApp.podspec
new file mode 100644
index 00000000..73870841
--- /dev/null
+++ b/composeApp/composeApp.podspec
@@ -0,0 +1,54 @@
+Pod::Spec.new do |spec|
+ spec.name = 'composeApp'
+ spec.version = '1.0'
+ spec.homepage = 'https://github.com/ooni/probe-multiplatform'
+ spec.source = { :http=> ''}
+ spec.authors = ''
+ spec.license = ''
+ spec.summary = 'Compose App'
+ spec.vendored_frameworks = 'build/cocoapods/framework/composeApp.framework'
+ spec.libraries = 'c++'
+ spec.ios.deployment_target = '9.0'
+
+
+ if !Dir.exist?('build/cocoapods/framework/composeApp.framework') || Dir.empty?('build/cocoapods/framework/composeApp.framework')
+ raise "
+
+ Kotlin framework 'composeApp' doesn't exist yet, so a proper Xcode project can't be generated.
+ 'pod install' should be executed after running ':generateDummyFramework' Gradle task:
+
+ ./gradlew :composeApp:generateDummyFramework
+
+ Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
+ end
+
+ spec.xcconfig = {
+ 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
+ }
+
+ spec.pod_target_xcconfig = {
+ 'KOTLIN_PROJECT_PATH' => ':composeApp',
+ 'PRODUCT_MODULE_NAME' => 'composeApp',
+ }
+
+ spec.script_phases = [
+ {
+ :name => 'Build composeApp',
+ :execution_position => :before_compile,
+ :shell_path => '/bin/sh',
+ :script => <<-SCRIPT
+ if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
+ echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
+ exit 0
+ fi
+ set -ev
+ REPO_ROOT="$PODS_TARGET_SRCROOT"
+ "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
+ -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
+ -Pkotlin.native.cocoapods.archs="$ARCHS" \
+ -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
+ SCRIPT
+ }
+ ]
+ spec.resources = ['build/compose/cocoapods/compose-resources']
+end
\ No newline at end of file
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 543d724a..112aadd0 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -3,7 +3,7 @@
-#include
-#include
-
-struct go_string { const char *str; long n; };
-extern char *apiCall(struct go_string fname);
-extern char *apiCallWithArgs(struct go_string fname, struct go_string args);
-
-JNIEXPORT jstring JNICALL Java_org_ooni_probe_GoOONIProbeClient_apiCall(JNIEnv *env, jclass c, jstring fname)
-{
- jstring ret;
- const char *fname_str = (*env)->GetStringUTFChars(env, fname, 0);
- size_t fname_len = (*env)->GetStringUTFLength(env, fname);
- char *d = apiCall((struct go_string){
- .str = fname_str,
- .n = fname_len
- });
- (*env)->ReleaseStringUTFChars(env, fname, fname_str);
- if (!d) {
- return NULL;
- }
- ret = (*env)->NewStringUTF(env, d);
- free(d);
- return ret;
-}
-
-JNIEXPORT jstring JNICALL Java_org_ooni_probe_GoOONIProbeClient_apiCallWithArgs(JNIEnv *env, jclass c, jstring fname, jstring args)
-{
- jstring ret;
- const char *fname_str = (*env)->GetStringUTFChars(env, fname, 0);
- size_t fname_len = (*env)->GetStringUTFLength(env, fname);
-
- const char *args_str = (*env)->GetStringUTFChars(env, args, 0);
- size_t args_len = (*env)->GetStringUTFLength(env, args);
-
- char *d = apiCallWithArgs((struct go_string){
- .str = fname_str,
- .n = fname_len
- }, (struct go_string){
- .str = args_str,
- .n = args_len
- });
- (*env)->ReleaseStringUTFChars(env, fname, fname_str);
- if (!d) {
- return NULL;
- }
- ret = (*env)->NewStringUTF(env, d);
- free(d);
- return ret;
-}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/libooniprobe-android/libooniprobe-android.go b/composeApp/src/androidMain/kotlin/org/ooni/libooniprobe-android/libooniprobe-android.go
deleted file mode 100644
index 62a59036..00000000
--- a/composeApp/src/androidMain/kotlin/org/ooni/libooniprobe-android/libooniprobe-android.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package main
-
-// #cgo LDFLAGS: -llog
-// #include
-
-import "C"
-import (
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
-)
-
-/** BEGIN API section **/
-// In reality the code in here should live inside of probe-cli
-// It represents the API contract that exists between OONI Probe CLI and the apps.
-
-// pkg/ooniprobe/api/
-func DoHTTPRequest(url string, retryCount int) (string, error) {
- fmt.Printf("we don't actually implement %d retries\n", retryCount)
- response, err := http.Get(url)
- if err != nil {
- return "", fmt.Errorf("failed to perform request: %v", err)
- }
- defer response.Body.Close()
-
- resp, err := io.ReadAll(response.Body)
- if err != nil {
- return "", fmt.Errorf("failed to read response: %v", err)
- }
-
- return string(resp), nil
-}
-
-func GetPublicIP() (string, error) {
- return DoHTTPRequest("https://api.ipify.org", 1)
-}
-
-// pkg/ooniprobe/mobileapi/
-var ErrUnknownFuncName = errors.New("invalid function name")
-
-// Any represents a value of any type.
-type Any interface{}
-
-// ParseAnyList parses a JSON string into a list of Any.
-func ParseAnyList(s string) ([]Any, error) {
- var data []Any
- err := json.Unmarshal([]byte(s), &data)
- if err != nil {
- return nil, err
- }
- return data, nil
-}
-
-type API struct {
-}
-
-func (a API) Init() {
- // Do any initialization needed to get the API running
-}
-
-func (a API) Call(funcName string) (string, error) {
- switch funcName {
- case "GetPublicIP":
- return GetPublicIP()
- default:
- return "", ErrUnknownFuncName
- }
-}
-
-type returnValueHTTPResponse struct {
- Body string `json:"body"`
-}
-
-func (a API) CallWithArgs(funcName string, args []Any) (string, error) {
- switch funcName {
- case "DoHTTPRequest":
- if len(args) != 2 {
- return "", errors.New("DoHTTPRequest takes exactly 2 arguments")
- }
-
- url, ok := args[0].(string)
- if !ok {
- return "", errors.New("DoHTTPRequest: args[0](name) must be a string")
- }
-
- retryCount, ok := args[1].(float64)
- if !ok {
- return "", errors.New("DoHTTPRequest: args[1](count) must be a number")
- }
- body, err := DoHTTPRequest(url, int(retryCount))
- if err != nil {
- return "", err
- }
-
- ret, err := json.Marshal(returnValueHTTPResponse{Body: body})
- return string(ret), err
- default:
- return "", fmt.Errorf("%s: %v", funcName, ErrUnknownFuncName)
- }
-}
-
-/** END API section **/
-
-var api API
-
-type ReturnValue struct {
- Value interface{} `json:"return_value"`
- Err interface{} `json:"error"`
-}
-
-func SerializeReturnValue(v string, err error) *C.char {
- var errVal interface{}
- if err != nil {
- errVal = fmt.Sprintf("%v", err)
- }
- rv := ReturnValue{
- Value: v,
- Err: errVal,
- }
- b, err := json.Marshal(rv)
- if err != nil {
- return nil
- }
- return C.CString(string(b))
-}
-
-func init() {
- api = API{}
- api.Init()
-}
-
-//export apiCall
-func apiCall(funcName string) *C.char {
- rv, err := api.Call(funcName)
- return SerializeReturnValue(rv, err)
-}
-
-//export apiCallWithArgs
-func apiCallWithArgs(funcName string, argsJSON string) *C.char {
- args, err := ParseAnyList(argsJSON)
- if err != nil {
- return SerializeReturnValue("", err)
- }
- rv, err := api.CallWithArgs(funcName, args)
- return SerializeReturnValue(rv, err)
-}
-
-func main() {}
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt
new file mode 100644
index 00000000..e1a28d8b
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/AndroidApplication.kt
@@ -0,0 +1,5 @@
+package org.ooni.probe
+
+import android.app.Application
+
+class AndroidApplication : Application()
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/Application.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/Application.kt
deleted file mode 100644
index aa74cc33..00000000
--- a/composeApp/src/androidMain/kotlin/org/ooni/probe/Application.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.ooni.probe
-
-import di.KoinInit
-import di.androidModule
-import org.koin.android.ext.koin.androidContext
-import org.koin.android.ext.koin.androidLogger
-import org.koin.core.logger.Level
-import io.github.aakira.napier.DebugAntilog
-import io.github.aakira.napier.Napier
-
-class Application : android.app.Application() {
- override fun onCreate() {
- super.onCreate()
- Napier.base(DebugAntilog())
- KoinInit().init {
- androidLogger(level = Level.DEBUG)
- androidContext(androidContext = this@Application)
- modules(
- listOf(
- androidModule,
- )
- )
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/GoOONIProbeClient.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/GoOONIProbeClient.kt
deleted file mode 100644
index c8d58227..00000000
--- a/composeApp/src/androidMain/kotlin/org/ooni/probe/GoOONIProbeClient.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.ooni.probe
-
-import android.content.Context
-import io.github.aakira.napier.Napier
-
-class GoOONIProbeClient(context: Context) {
- init {
- Napier.d("loading ooniprobe in appContext=$context")
- // System.loadLibrary() is the most basic shared library loading strategy.
- // I have seen code like the one in Wireguard make use of many more:
- // https://github.com/WireGuard/wireguard-android/blob/master/tunnel/src/main/java/com/wireguard/android/util/SharedLibraryLoader.java
- // TODO: do testing of this and unnderstand if these strategies are needed or if it only
- // applies to older versions of android and/or loading happening inside of a VPNService
- System.loadLibrary("ooniprobe")
- }
- companion object {
- @JvmStatic private external fun apiCall(funcName: String): String
- @JvmStatic private external fun apiCallWithArgs(funcName: String, args : String): String
- }
- fun call(funcName: String) : String{
- return apiCall(funcName)
- }
- fun callWithArgs(funcName: String, args : String) : String{
- return apiCallWithArgs(funcName, args)
- }
-
-}
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt
index 184c3bbb..5034f72c 100644
--- a/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/MainActivity.kt
@@ -1,24 +1,20 @@
package org.ooni.probe
-import App
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
+import org.ooni.engine.AndroidOonimkallBridge
+import org.ooni.probe.di.Dependencies
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val bridge = AndroidOonimkallBridge()
+ val dependencies = Dependencies(bridge, filesDir.absolutePath)
+
setContent {
- App()
+ App(dependencies)
}
}
}
-
-@Preview
-@Composable
-fun AppAndroidPreview() {
- App()
-}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/platform/GoOONIProbeClientBridge.kt b/composeApp/src/androidMain/kotlin/platform/GoOONIProbeClientBridge.kt
deleted file mode 100644
index 44e0b1e8..00000000
--- a/composeApp/src/androidMain/kotlin/platform/GoOONIProbeClientBridge.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package platform
-
-import android.content.Context
-import io.github.aakira.napier.Napier
-import org.ooni.probe.GoOONIProbeClient
-
-actual class GoOONIProbeClientBridge(context: Context) {
- private val client = GoOONIProbeClient(context)
- init {
- Napier.d("running laoder")
- }
-
- actual fun apiCall(funcName: String): String {
- Napier.d("running API call ${funcName}")
- return client.call(funcName)
- }
- actual fun apiCallWithArgs(funcName: String, args : String): String {
- Napier.d("running API call with args ${funcName} ${args}")
- return client.callWithArgs(funcName, args)
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/platform/MultiplatformSettings.kt b/composeApp/src/androidMain/kotlin/platform/MultiplatformSettings.kt
deleted file mode 100644
index 6ac31dea..00000000
--- a/composeApp/src/androidMain/kotlin/platform/MultiplatformSettings.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package platform
-
-
-import android.content.Context
-import com.russhwolf.settings.Settings
-import com.russhwolf.settings.SharedPreferencesSettings
-
-actual class MultiplatformSettings(private val context: Context) {
- actual fun createSettings() : Settings {
- val delegate = context.getSharedPreferences("ooniprobe_settings", Context.MODE_PRIVATE)
- return SharedPreferencesSettings(delegate)
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt
deleted file mode 100644
index 4d66e796..00000000
--- a/composeApp/src/commonMain/kotlin/App.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-import androidx.compose.foundation.layout.fillMaxSize
-
-import androidx.compose.runtime.*
-import androidx.compose.ui.Modifier
-import androidx.compose.material3.Surface
-import androidx.compose.material3.MaterialTheme
-import cafe.adriel.voyager.navigator.Navigator
-import io.github.aakira.napier.Napier
-
-import org.jetbrains.compose.ui.tooling.preview.Preview
-
-
-import org.koin.compose.koinInject
-import org.koin.compose.KoinContext
-
-import main.MainView
-import main.MainViewModel
-import main.OnboardingState
-import ui.Theme
-import ui.screens.onboarding.OnboardingView
-
-@Composable
-@Preview
-fun App(
- mainViewModel: MainViewModel = koinInject(),
-) {
- KoinContext {
- val onboardingState = mainViewModel.onboardingState.collectAsState().value
- Theme() {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background,
- ) {
- when (onboardingState) {
- is OnboardingState.Complete -> {
- Navigator(
- screen = MainView()
- )
- }
- is OnboardingState.Incomplete -> {
- Navigator(
- screen = OnboardingView()
- )
- }
- else -> {}
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/core/probe/OONIProbeClient.kt b/composeApp/src/commonMain/kotlin/core/probe/OONIProbeClient.kt
deleted file mode 100644
index 5e7d01c0..00000000
--- a/composeApp/src/commonMain/kotlin/core/probe/OONIProbeClient.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package core.probe
-
-import io.github.aakira.napier.Napier
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonPrimitive
-import kotlinx.serialization.json.buildJsonArray
-import platform.GoOONIProbeClientBridge
-
-val laxJson = Json {ignoreUnknownKeys = true}
-@Serializable
-data class ApiCallValue(
- val return_value: String,
- val error: String?,
-)
-@Serializable
-data class HTTPResponse(
- val body: String
-)
-
-class OONIProbeClient(ooniprobeClientBridge: GoOONIProbeClientBridge) {
- private val ooniprobeClientBridge = ooniprobeClientBridge
-
- fun doHTTPRequest(url : String, retryCount : Int) : HTTPResponse {
- val args = buildJsonArray {
- add(JsonPrimitive(url))
- add(JsonPrimitive(retryCount))
- }
-
- val apiCallValue = laxJson.decodeFromString(
- ooniprobeClientBridge.apiCallWithArgs("DoHTTPRequest", args.toString())
- )
- if (apiCallValue.error != null) {
- throw Error(apiCallValue.error)
- }
- return laxJson.decodeFromString(apiCallValue.return_value)
- }
-
- fun getPublicIP() : String {
- val apiCallValue = laxJson.decodeFromString(
- ooniprobeClientBridge.apiCall("GetPublicIP")
- )
- Napier.i("getPublicIP: return_value=${apiCallValue.return_value} error=${apiCallValue.error}")
- if (apiCallValue.error != null) {
- throw Error(apiCallValue.error)
- }
- return apiCallValue.return_value
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/core/settings/SettingsManager.kt b/composeApp/src/commonMain/kotlin/core/settings/SettingsManager.kt
deleted file mode 100644
index 349357f7..00000000
--- a/composeApp/src/commonMain/kotlin/core/settings/SettingsManager.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package core.settings
-
-import com.russhwolf.settings.ExperimentalSettingsApi
-import com.russhwolf.settings.ObservableSettings
-import com.russhwolf.settings.Settings
-import com.russhwolf.settings.set
-import com.russhwolf.settings.coroutines.getBooleanFlow
-import com.russhwolf.settings.coroutines.getIntFlow
-import com.russhwolf.settings.coroutines.getIntOrNullFlow
-import com.russhwolf.settings.coroutines.getLongFlow
-import com.russhwolf.settings.coroutines.getStringOrNullFlow
-import kotlinx.coroutines.flow.Flow
-
-class SettingsManager constructor(private val settings: Settings) {
- private val observableSettings: ObservableSettings by lazy { settings as ObservableSettings }
-
- fun setString(key: String, value: String) {
- observableSettings.set(key = key, value = value)
- }
-
- fun getNonFlowString(key: String) = observableSettings.getString(
- key = key,
- defaultValue = "",
- )
-
- fun getString(key: String) = observableSettings.getStringOrNullFlow(key = key)
-
- fun setInt(key: String, value: Int) {
- observableSettings.set(key = key, value = value)
- }
- fun getInt(key: String) = observableSettings.getIntOrNullFlow(key = key)
-
- fun getIntFlow(key: String) = observableSettings.getIntFlow(key = key, defaultValue = 0)
-
- companion object {
- const val PROBE_CREDENTIALS = "probe_credentials_key"
- }
-
- fun clearAllSettings() {
- observableSettings.clear()
- }
-
- @OptIn(ExperimentalSettingsApi::class)
- fun getBoolean(key: String): Flow {
- return observableSettings.getBooleanFlow(
- key = key,
- defaultValue = false,
- )
- }
-
- fun setBoolean(key: String, value: Boolean) {
- observableSettings.set(key = key, value = value)
- }
-
- @OptIn(ExperimentalSettingsApi::class)
- fun getLong(key: Any): Flow {
- return observableSettings.getLongFlow(
- key = key.toString(),
- defaultValue = 0,
- )
- }
-
- fun setLong(key: String, value: Long) {
- observableSettings.set(key = key, value = value)
- }
-}
diff --git a/composeApp/src/commonMain/kotlin/core/settings/SettingsStore.kt b/composeApp/src/commonMain/kotlin/core/settings/SettingsStore.kt
deleted file mode 100644
index 107d7ca5..00000000
--- a/composeApp/src/commonMain/kotlin/core/settings/SettingsStore.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package core.settings
-
-import kotlinx.coroutines.flow.Flow
-
-interface SettingsStore {
- fun clearAll()
- fun saveProbeCredentials(value: String)
- fun getProbeCredentials(): Flow
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/core/settings/SettingsStoreImpl.kt b/composeApp/src/commonMain/kotlin/core/settings/SettingsStoreImpl.kt
deleted file mode 100644
index 54ee20cf..00000000
--- a/composeApp/src/commonMain/kotlin/core/settings/SettingsStoreImpl.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package core.settings
-
-import kotlinx.coroutines.flow.Flow
-
-class SettingsStoreImpl(
- private val settingsManager: SettingsManager,
-) : SettingsStore {
- override fun clearAll() {
- return settingsManager.clearAllSettings()
- }
- override fun saveProbeCredentials(value: String) {
- return settingsManager.setString(key = SettingsManager.PROBE_CREDENTIALS, value = value)
- }
- override fun getProbeCredentials(): Flow {
- return settingsManager.getString(key = SettingsManager.PROBE_CREDENTIALS)
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/di/KoinInit.kt b/composeApp/src/commonMain/kotlin/di/KoinInit.kt
deleted file mode 100644
index 04af7de2..00000000
--- a/composeApp/src/commonMain/kotlin/di/KoinInit.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package di
-
-import org.koin.core.Koin
-import org.koin.core.context.startKoin
-import org.koin.dsl.KoinAppDeclaration
-
-class KoinInit {
- fun init(appDeclaration: KoinAppDeclaration = {}): Koin {
- return startKoin {
- modules(
- listOf(
- platformModule(),
- commonModule(),
- ),
- )
- appDeclaration()
- }.koin
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/di/commonModules.kt b/composeApp/src/commonMain/kotlin/di/commonModules.kt
deleted file mode 100644
index 20216c8e..00000000
--- a/composeApp/src/commonMain/kotlin/di/commonModules.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package di
-
-import core.settings.SettingsManager
-import core.settings.SettingsStore
-import core.settings.SettingsStoreImpl
-import org.koin.core.module.Module
-import org.koin.dsl.module
-import ui.screens.home.HomeScreenModel
-import main.MainViewModel
-import ui.screens.onboarding.OnboardingViewModel
-
-fun commonModule() = module {
- single {
- SettingsManager(settings = get())
- }
-
- /**
- * Stores or repositories in Android speak
- */
- single {
- SettingsStoreImpl(
- settingsManager = get(),
- )
- }
-
- /**
- * ScreenModels
- */
- single {
- HomeScreenModel(
- settingsStore = get(),
- ooniProbeClient = get()
- )
- }
- single {
- OnboardingViewModel(
- settingsStore = get()
- )
- }
-
- single {
- MainViewModel(
- settingsStore = get()
- )
- }
-
-}
-expect fun platformModule(): Module
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/main/MainVIew.kt b/composeApp/src/commonMain/kotlin/main/MainVIew.kt
deleted file mode 100644
index 626eb60b..00000000
--- a/composeApp/src/commonMain/kotlin/main/MainVIew.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package main
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.material.BottomNavigation
-import androidx.compose.material.BottomNavigationItem
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-
-import cafe.adriel.voyager.core.screen.Screen
-import cafe.adriel.voyager.navigator.CurrentScreen
-import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
-import cafe.adriel.voyager.navigator.tab.Tab
-import cafe.adriel.voyager.navigator.tab.TabNavigator
-import io.github.aakira.napier.Napier
-
-import ui.components.AppTab
-
-class MainView: Screen {
- @Composable
- override fun Content() {
- TabNavigator(
- AppTab.HomeTab,
- ) {
- Scaffold(
- content = { CurrentScreen() },
- bottomBar = {
- BottomNavigation(
- backgroundColor = MaterialTheme.colorScheme.background,
- ) {
- TabNavigationItem(AppTab.HomeTab)
- }
- },
- )
- }
- }
-}
-
-@Composable
-private fun RowScope.TabNavigationItem(tab: Tab) {
- val tabNavigator = LocalTabNavigator.current
-
- // TODO(art): There doesn't seem to be a nice way to pick selected and
- // unselected icons in Voyager, so we don't implement it for the moment.
- // See:
- // https://github.com/adrielcafe/voyager/issues/141
- // https://github.com/adrielcafe/voyager/issues/313
- val isSelected = tabNavigator.current == tab
-
- BottomNavigationItem(
- selected = tabNavigator.current == tab,
- onClick = { tabNavigator.current = tab },
- icon = { tab.options.icon?.let {
- Icon(
- painter = it,
- contentDescription = tab.options.title,
- tint = if (isSelected) {
- MaterialTheme.colorScheme.primary
- } else {
- MaterialTheme.colorScheme.onBackground
- }
- )
- }
- }
- )
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/main/MainViewModel.kt b/composeApp/src/commonMain/kotlin/main/MainViewModel.kt
deleted file mode 100644
index 78bc3262..00000000
--- a/composeApp/src/commonMain/kotlin/main/MainViewModel.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package main
-
-import cafe.adriel.voyager.core.model.ScreenModel
-import core.settings.SettingsStore
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import cafe.adriel.voyager.core.model.screenModelScope
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-class MainViewModel(
- settingsStore: SettingsStore,
-) : ScreenModel {
- val onboardingState: StateFlow =
- settingsStore.getProbeCredentials().map {
- if (it.isNullOrEmpty().not()) {
- return@map OnboardingState.Complete
- }
- OnboardingState.Incomplete
- }.stateIn(
- scope = screenModelScope,
- started=SharingStarted.WhileSubscribed(),
- initialValue = OnboardingState.Loading,
- )
-}
-
-sealed class OnboardingState {
- data object Loading: OnboardingState()
- data object Complete : OnboardingState()
- data object Incomplete: OnboardingState()
-}
-
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
new file mode 100644
index 00000000..e3acc575
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
@@ -0,0 +1,70 @@
+package org.ooni.engine
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlin.math.roundToInt
+
+class Engine(
+ private val bridge: OonimkallBridge,
+ private val json: Json,
+ private val baseFilePath: String
+) {
+
+ fun startTask(taskSettings: TaskSettings): Flow = channelFlow {
+ val finalSettings = taskSettings.copy(
+ stateDir = baseFilePath,
+ tunnelDir = baseFilePath,
+ tempDir = baseFilePath,
+ assetsDir = baseFilePath,
+ )
+
+ val task = bridge.startTask(json.encodeToString(finalSettings))
+
+ while (!task.isDone()) {
+ val eventJson = task.waitForNextEvent()
+ val eventResult = json.decodeFromString(eventJson)
+ eventResult.toTaskEvent()?.let { send(it) }
+ }
+
+ invokeOnClose {
+ if (it is CancellationException) {
+ task.interrupt()
+ }
+ }
+ }
+
+ private fun EventResult.toTaskEvent(): TaskEvent? =
+ when (key) {
+ "status.started" -> TaskEvent.Started
+
+ "status.end" -> TaskEvent.StatusEnd
+
+ "status.progress" ->
+ value?.percentage?.let { percentageValue ->
+ TaskEvent.Progress(
+ percentage = (percentageValue * 100.0).roundToInt(),
+ message = value?.message
+ )
+ }
+
+ "log" -> value?.message?.let { message ->
+ TaskEvent.Log(
+ level = value?.log_level,
+ message = message
+ )
+ }
+
+ "status.report_create" -> value?.report_id?.let {
+ TaskEvent.ReportCreate(reportId = it)
+ }
+
+ "task_terminated" -> TaskEvent.TaskTerminated
+
+ "failure.startup" -> TaskEvent.FailureStartup(message = value?.failure)
+
+ else -> null
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/EventResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/EventResult.kt
new file mode 100644
index 00000000..e48ca6a6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/EventResult.kt
@@ -0,0 +1,29 @@
+package org.ooni.engine
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class EventResult {
+ var key: String? = null
+ var value: Value? = null
+
+ @Serializable
+ class Value {
+ var key: Double = 0.0
+ var log_level: String? = null
+ var message: String? = null
+ var percentage: Double = 0.0
+ var json_str: String? = null
+ var idx: Int = 0
+ var report_id: String? = null
+ var probe_ip: String? = null
+ var probe_asn: String? = null
+ var probe_cc: String? = null
+ var probe_network_name: String? = null
+ var downloaded_kb: Double = 0.0
+ var uploaded_kb: Double = 0.0
+ var input: String? = null
+ var failure: String? = null
+ var orig_key: String? = null
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt
new file mode 100644
index 00000000..c498c54f
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt
@@ -0,0 +1,11 @@
+package org.ooni.engine
+
+interface OonimkallBridge {
+ fun startTask(settingsSerialized: String): Task
+
+ interface Task {
+ fun interrupt()
+ fun isDone(): Boolean
+ fun waitForNextEvent(): String
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEvent.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEvent.kt
new file mode 100644
index 00000000..b4a8c210
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEvent.kt
@@ -0,0 +1,27 @@
+package org.ooni.engine
+
+sealed interface TaskEvent {
+ data class Log(
+ val level: String?,
+ val message: String
+ ): TaskEvent
+
+ data object Started : TaskEvent
+
+ data class ReportCreate(
+ val reportId: String
+ ) : TaskEvent
+
+ data class Progress(
+ val percentage: Int,
+ val message: String?
+ ): TaskEvent
+
+ data object StatusEnd : TaskEvent
+
+ data object TaskTerminated : TaskEvent
+
+ data class FailureStartup(
+ val message: String?
+ ): TaskEvent
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskSettings.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskSettings.kt
new file mode 100644
index 00000000..5b250811
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskSettings.kt
@@ -0,0 +1,24 @@
+package org.ooni.engine
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TaskSettings(
+ @SerialName("name") val name: String,
+ @SerialName("inputs") val inputs: List,
+ @SerialName("version") val version: Int = 1,
+ @SerialName("log_level") val logLevel: String,
+ @SerialName("state_dir") val stateDir: String? = null,
+ @SerialName("temp_dir") val tempDir: String? = null,
+ @SerialName("tunnel_dir") val tunnelDir: String? = null,
+ @SerialName("assets_dir") val assetsDir: String? = null,
+ @SerialName("options") val options: Options = Options()
+) {
+ @Serializable
+ data class Options(
+ @SerialName("no_collector") val noCollector: Boolean = true,
+ @SerialName("software_name") val softwareName: String = "Probe Multiplatform",
+ @SerialName("software_version") val softwareVersion: String = "1.0"
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
new file mode 100644
index 00000000..d28fc18c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
@@ -0,0 +1,28 @@
+package org.ooni.probe
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import org.ooni.probe.di.Dependencies
+import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.ooni.probe.ui.Theme
+import org.ooni.probe.ui.main.MainScreen
+
+@Composable
+@Preview
+fun App(
+ dependencies: Dependencies
+) {
+ Theme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ MainScreen(
+ dependencies.mainViewModel
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
new file mode 100644
index 00000000..65759bca
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
@@ -0,0 +1,24 @@
+package org.ooni.probe.di
+
+import kotlinx.serialization.json.Json
+import org.ooni.engine.Engine
+import org.ooni.engine.OonimkallBridge
+import org.ooni.probe.ui.main.MainViewModel
+
+class Dependencies(
+ private val oonimkallBridge: OonimkallBridge,
+ private val baseFileDir: String,
+) {
+
+ private val json by lazy {
+ Json {
+ encodeDefaults = true
+ ignoreUnknownKeys = true
+ }
+ }
+
+ private val engine by lazy { Engine(oonimkallBridge, json, baseFileDir) }
+
+ val mainViewModel by lazy { MainViewModel(engine) }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/Theme.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/Theme.kt
similarity index 99%
rename from composeApp/src/commonMain/kotlin/ui/Theme.kt
rename to composeApp/src/commonMain/kotlin/org/ooni/probe/ui/Theme.kt
index b4bad519..aabcb76d 100644
--- a/composeApp/src/commonMain/kotlin/ui/Theme.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/Theme.kt
@@ -1,4 +1,4 @@
-package ui
+package org.ooni.probe.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainScreen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainScreen.kt
new file mode 100644
index 00000000..753f7a87
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainScreen.kt
@@ -0,0 +1,35 @@
+package org.ooni.probe.ui.main
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+
+@Composable
+fun MainScreen(
+ viewModel: MainViewModel
+) {
+ val state by viewModel.state.collectAsState()
+
+ Column {
+ Button(
+ onClick = { viewModel.onEvent(MainViewModel.Event.StartClick) },
+ enabled = !state.isRunning
+ ) {
+ Text("Run Test")
+ }
+
+ Text(
+ text = state.log,
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainViewModel.kt
new file mode 100644
index 00000000..643dfb32
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/main/MainViewModel.kt
@@ -0,0 +1,70 @@
+package org.ooni.probe.ui.main
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
+import org.ooni.engine.Engine
+import org.ooni.engine.TaskSettings
+
+class MainViewModel(
+ private val engine: Engine
+) {
+ private val events = MutableSharedFlow(extraBufferCapacity = 1)
+
+ private val _state = MutableStateFlow(State())
+ val state = _state.asStateFlow()
+
+ init {
+ events
+ .flatMapLatest { event ->
+ when (event) {
+ Event.StartClick -> {
+ if (_state.value.isRunning) return@flatMapLatest emptyFlow()
+
+ _state.value = _state.value.copy(isRunning = true)
+
+ engine.startTask(TASK_SETTINGS)
+ .onEach { taskEvent ->
+ _state.update { state ->
+ state.copy(log = state.log + "\n" + taskEvent)
+ }
+ }
+ .onCompletion {
+ _state.update { it.copy(isRunning = false) }
+ }
+ }
+ }
+ }
+ .launchIn(CoroutineScope(Dispatchers.IO))
+ }
+
+ fun onEvent(event: Event) {
+ events.tryEmit(event)
+ }
+
+ data class State(
+ val isRunning: Boolean = false,
+ val log: String = ""
+ )
+
+ sealed interface Event {
+ data object StartClick : Event
+ }
+
+ companion object {
+ val TASK_SETTINGS = TaskSettings(
+ name = "web_connectivity",
+ inputs = listOf("https://ooni.org"),
+ logLevel = "DEBUG2"
+ )
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/platform/GoOONIProbeClientBridge.kt b/composeApp/src/commonMain/kotlin/platform/GoOONIProbeClientBridge.kt
deleted file mode 100644
index 44aa8290..00000000
--- a/composeApp/src/commonMain/kotlin/platform/GoOONIProbeClientBridge.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package platform
-
-expect class GoOONIProbeClientBridge {
-
- fun apiCall(funcName: String): String
- fun apiCallWithArgs(funcName: String, args : String): String
-
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/platform/MultiplatformSettings.kt b/composeApp/src/commonMain/kotlin/platform/MultiplatformSettings.kt
deleted file mode 100644
index 6b242261..00000000
--- a/composeApp/src/commonMain/kotlin/platform/MultiplatformSettings.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package platform
-
-import com.russhwolf.settings.Settings
-
-// This pattern is and related cross platform code is taken from:
-// https://github.com/joelkanyi/FocusBloom/blob/develop/shared/src/commonMain/kotlin/com/joelkanyi/focusbloom/platform/MultiplatformSettingsWrapper.kt
-expect class MultiplatformSettings {
- fun createSettings(): Settings
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/components/AppTab.kt b/composeApp/src/commonMain/kotlin/ui/components/AppTab.kt
deleted file mode 100644
index 58198114..00000000
--- a/composeApp/src/commonMain/kotlin/ui/components/AppTab.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package ui.components
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Home
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-
-import cafe.adriel.voyager.navigator.tab.Tab
-import cafe.adriel.voyager.navigator.tab.TabOptions
-
-import ui.screens.home.HomeScreen
-
-internal sealed class AppTab {
- internal object HomeTab : Tab {
- override val options: TabOptions
- @Composable
- get() {
- val title = "Home"
- val icon = rememberVectorPainter(image = Icons.Outlined.Home)
- return remember {
- TabOptions(
- index = 0u,
- title = title,
- icon = icon,
- )
- }
- }
-
- @Composable
- override fun Content() {
- HomeScreen()
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/screens/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ui/screens/home/HomeScreen.kt
deleted file mode 100644
index 9871019e..00000000
--- a/composeApp/src/commonMain/kotlin/ui/screens/home/HomeScreen.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package ui.screens.home
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import org.koin.compose.koinInject
-
-@Composable
-fun HomeScreen(
- screenModel: HomeScreenModel = koinInject(),
-) {
- val publicIP = screenModel.publicIP.collectAsState().value
- val httpResponse = screenModel.httpResponse.collectAsState().value
- HomeScreenContent(
- onClickReset={
- screenModel.clearSettings()
- },
- onClickDoRequest = {
- screenModel.doHTTPRequest()
- },
- onClickDoIPLookup = {
- screenModel.lookupIP()
- },
- publicIP=publicIP,
- httpResponse=httpResponse
- )
-}
-
-@Composable
-private fun HomeScreenContent(
- publicIP: String,
- httpResponse: String,
- onClickReset: () -> Unit,
- onClickDoIPLookup: () -> Unit,
- onClickDoRequest: () -> Unit
-) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxHeight()
- ) {
- Row(
- modifier = Modifier
- .padding(20.dp)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.Center
- ) {
- Column (
- horizontalAlignment = Alignment.CenterHorizontally,
- ){
- Text("This is home.",
- style = MaterialTheme.typography.headlineLarge,
- textAlign = TextAlign.Center
- )
-
- Spacer(modifier = Modifier.height(24.dp))
-
- TextField(
- value = publicIP,
- onValueChange = {},
- label = {Text("public ip")},
- readOnly = true
- )
- Button(
- onClick = onClickDoIPLookup,
- shape = MaterialTheme.shapes.medium
- ) {
- Text("DoIPLookup")
- }
-
- TextField(
- value = httpResponse,
- onValueChange = {},
- label = {Text("http response")},
- readOnly = true
- )
- Button(
- onClick = onClickDoRequest,
- shape = MaterialTheme.shapes.medium
- ) {
- Text("DoRequest")
- }
-
- Spacer(modifier = Modifier.height(78.dp))
-
- Button(
- onClick = onClickReset,
- shape = MaterialTheme.shapes.medium
- ) {
- Text("Reset app")
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/screens/home/HomeScreenModel.kt b/composeApp/src/commonMain/kotlin/ui/screens/home/HomeScreenModel.kt
deleted file mode 100644
index b5a46dad..00000000
--- a/composeApp/src/commonMain/kotlin/ui/screens/home/HomeScreenModel.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package ui.screens.home
-
-import cafe.adriel.voyager.core.model.ScreenModel
-import cafe.adriel.voyager.core.model.screenModelScope
-import core.probe.OONIProbeClient
-import core.settings.SettingsStore
-import io.github.aakira.napier.Napier
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-
-class HomeScreenModel(
- private val settingsStore: SettingsStore,
- private val ooniProbeClient: OONIProbeClient
-) : ScreenModel {
-
- private val _httpResponse = MutableStateFlow("")
- private val _publicIP = MutableStateFlow("")
-
- val publicIP = _publicIP.asStateFlow()
- val httpResponse = _httpResponse.asStateFlow()
-
- fun clearSettings() {
- settingsStore.clearAll()
- }
- fun doHTTPRequest() {
- screenModelScope.launch {
- try {
- val resp = ooniProbeClient.doHTTPRequest("https://google.com/humans.txt", 2)
- _httpResponse.value = resp.body
- } catch (e: Error) {
- _httpResponse.value = "error: ${e.message}"
- Napier.e("error fetching http ${e.message}")
- }
- }
- }
- fun lookupIP() {
- screenModelScope.launch {
- try {
- val ip = ooniProbeClient.getPublicIP()
- _publicIP.value = ip
- } catch (e: Error) {
- _publicIP.value = "error: ${e.message}"
- Napier.e("error looking up IP ${e.message}")
- }
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/screens/onboarding/OnboardingView.kt b/composeApp/src/commonMain/kotlin/ui/screens/onboarding/OnboardingView.kt
deleted file mode 100644
index 704189f4..00000000
--- a/composeApp/src/commonMain/kotlin/ui/screens/onboarding/OnboardingView.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package ui.screens.onboarding
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import cafe.adriel.voyager.core.screen.Screen
-import org.koin.compose.koinInject
-import org.koin.core.component.KoinComponent
-
-class OnboardingView(
-) : Screen, KoinComponent {
- @Composable
- override fun Content() {
- val screenModel = koinInject()
- OnboardingScreenContent(
- onClickDone = {
- screenModel.onboardingComplete()
- }
- )
- }
-}
-
-@Composable
-private fun OnboardingScreenContent(
- onClickDone: () -> Unit
-) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxHeight()
- ) {
- Row(
- modifier = Modifier
- .padding(20.dp)
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.Center
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text("Welcome human!",
- style = MaterialTheme.typography.headlineLarge,
- textAlign = TextAlign.Center
- )
-
- Button(
- onClick = onClickDone,
- shape = MaterialTheme.shapes.medium
- ) {
- Text("Done")
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/screens/onboarding/OnboardingViewModel.kt b/composeApp/src/commonMain/kotlin/ui/screens/onboarding/OnboardingViewModel.kt
deleted file mode 100644
index ff2aff51..00000000
--- a/composeApp/src/commonMain/kotlin/ui/screens/onboarding/OnboardingViewModel.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package ui.screens.onboarding
-
-import cafe.adriel.voyager.core.model.ScreenModel
-
-import core.settings.SettingsStore
-
-class OnboardingViewModel(
- private val settingsStore: SettingsStore,
-) : ScreenModel {
- fun onboardingComplete() {
- settingsStore.saveProbeCredentials("dummy value")
- }
-
-}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt b/composeApp/src/desktopMain/kotlin/Platform.jvm.kt
deleted file mode 100644
index f5e7e494..00000000
--- a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-class JVMPlatform: Platform {
- override val name: String = "Java ${System.getProperty("java.version")}"
-}
-
-actual fun getPlatform(): Platform = JVMPlatform()
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt
deleted file mode 100644
index 99559b98..00000000
--- a/composeApp/src/desktopMain/kotlin/main.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-import androidx.compose.ui.window.Window
-import androidx.compose.ui.window.application
-
-fun main() = application {
- Window(
- onCloseRequest = ::exitApplication,
- title = "OONI Probe",
- ) {
- App()
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/platform/MultiplatformSettings.jvm.kt b/composeApp/src/desktopMain/kotlin/platform/MultiplatformSettings.jvm.kt
deleted file mode 100644
index ac011760..00000000
--- a/composeApp/src/desktopMain/kotlin/platform/MultiplatformSettings.jvm.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package platform
-
-import com.russhwolf.settings.PreferencesSettings
-import com.russhwolf.settings.Settings
-import java.util.prefs.Preferences
-
-actual class MultiplatformSettings {
- actual fun createSettings(): Settings {
- val delegate: Preferences = Preferences.userRoot()
- return PreferencesSettings(delegate)
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/MainViewController.kt b/composeApp/src/iosMain/kotlin/MainViewController.kt
deleted file mode 100644
index fa143d45..00000000
--- a/composeApp/src/iosMain/kotlin/MainViewController.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-import androidx.compose.ui.window.ComposeUIViewController
-
-fun MainViewController() = ComposeUIViewController { App() }
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt
new file mode 100644
index 00000000..f9477233
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/MainViewController.kt
@@ -0,0 +1,9 @@
+package org.ooni.probe
+
+import androidx.compose.ui.window.ComposeUIViewController
+import org.ooni.engine.OonimkallBridge
+import org.ooni.probe.di.Dependencies
+import platform.Foundation.NSTemporaryDirectory
+
+fun MainViewController(bridge: OonimkallBridge) =
+ ComposeUIViewController { App(Dependencies(bridge, NSTemporaryDirectory())) }
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/platform/MultiplatformSettings.ios.kt b/composeApp/src/iosMain/kotlin/platform/MultiplatformSettings.ios.kt
deleted file mode 100644
index 9a218dd8..00000000
--- a/composeApp/src/iosMain/kotlin/platform/MultiplatformSettings.ios.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package platform
-
-import com.russhwolf.settings.NSUserDefaultsSettings
-import com.russhwolf.settings.Settings
-import platform.Foundation.NSUserDefaults
-
-actual class MultiplatformSettings {
- actual fun createSettings(): Settings {
- val delegate = NSUserDefaults.standardUserDefaults
- return NSUserDefaultsSettings(delegate)
- }
-}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4db653ed..f3a3514b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,36 +1,27 @@
[versions]
-agp = "8.2.0"
+agp = "8.3.2" # Max compatible version https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility
+
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
+
androidx-activityCompose = "1.9.0"
-androidx-appcompat = "1.6.1"
-androidx-constraintlayout = "2.1.4"
-androidx-core-ktx = "1.13.0"
-androidx-espresso-core = "3.5.1"
-androidx-material = "1.11.0"
-androidx-test-junit = "1.1.5"
-compose = "1.6.6"
-compose-plugin = "1.6.2"
-junit = "4.13.2"
-kotlin = "1.9.23"
+compose = "1.6.8"
+compose-plugin = "1.6.11"
+kotlin = "2.0.0"
[libraries]
-kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
-junit = { group = "junit", name = "junit", version.ref = "junit" }
-androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
-androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
-androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
-androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
-androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
-androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
+android-oonimkall = { module = "org.ooni:oonimkall", version = "2024.05.22-092559" }
+kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.7.1" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
-kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
\ No newline at end of file
+jetbrainsComposeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
\ No newline at end of file
diff --git a/iosApp/Podfile b/iosApp/Podfile
new file mode 100644
index 00000000..f3cd2a5a
--- /dev/null
+++ b/iosApp/Podfile
@@ -0,0 +1,16 @@
+platform :ios, '9.0'
+use_frameworks!
+
+target 'iosApp' do
+ pod 'composeApp', :path => '../composeApp'
+
+ ooni_version = "v3.22.0"
+ ooni_pods_location = "https://github.com/ooni/probe-cli/releases/download/#{ooni_version}"
+
+ pod "libcrypto", :podspec => "#{ooni_pods_location}/libcrypto.podspec"
+ pod "libevent", :podspec => "#{ooni_pods_location}/libevent.podspec"
+ pod "libssl", :podspec => "#{ooni_pods_location}/libssl.podspec"
+ pod "libtor", :podspec => "#{ooni_pods_location}/libtor.podspec"
+ pod "libz", :podspec => "#{ooni_pods_location}/libz.podspec"
+ pod "oonimkall", :podspec => "#{ooni_pods_location}/oonimkall.podspec"
+end
\ No newline at end of file
diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock
new file mode 100644
index 00000000..2c88a8c9
--- /dev/null
+++ b/iosApp/Podfile.lock
@@ -0,0 +1,46 @@
+PODS:
+ - composeApp (1.0)
+ - libcrypto (2024.05.22-093305)
+ - libevent (2024.05.22-093305)
+ - libssl (2024.05.22-093305)
+ - libtor (2024.05.22-093305)
+ - libz (2024.05.22-093305)
+ - oonimkall (2024.05.22-093305)
+
+DEPENDENCIES:
+ - composeApp (from `../composeApp`)
+ - libcrypto (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libcrypto.podspec`)
+ - libevent (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libevent.podspec`)
+ - libssl (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libssl.podspec`)
+ - libtor (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libtor.podspec`)
+ - libz (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/libz.podspec`)
+ - oonimkall (from `https://github.com/ooni/probe-cli/releases/download/v3.22.0/oonimkall.podspec`)
+
+EXTERNAL SOURCES:
+ composeApp:
+ :path: "../composeApp"
+ libcrypto:
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libcrypto.podspec
+ libevent:
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libevent.podspec
+ libssl:
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libssl.podspec
+ libtor:
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libtor.podspec
+ libz:
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/libz.podspec
+ oonimkall:
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.22.0/oonimkall.podspec
+
+SPEC CHECKSUMS:
+ composeApp: 3f1f4ca4e070c2c0aa528bb1eb838ffc3860f5c0
+ libcrypto: 1bb58600c586e28688f5578f4675f5ffa46c8eaf
+ libevent: 5c8502ca5cc38be31bb510ddade0f238bcc5f0dc
+ libssl: 170bebcaf567a0285e91a8850b9686137d07c3e1
+ libtor: c72b23da6a5d2e16173149784f11cf66156c35be
+ libz: 83658eb2a0db785623ffdf9ce13407e6b8b5c8f9
+ oonimkall: 9768ce9dad18265d45d2ea972c84fb0bd5237cc3
+
+PODFILE CHECKSUM: 9f3463701e9fb15f3dded022f7afabede102208e
+
+COCOAPODS: 1.15.2
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 48efd976..2562ec72 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -11,16 +11,22 @@
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
+ 90C537899E1A531A92327A33 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3690801A53F73052EF6710A7 /* Pods_iosApp.framework */; };
+ 93E977712C4FCCE3009CCABC /* IosOonimkallBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E977702C4FCCE3009CCABC /* IosOonimkallBridge.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
+ 3690801A53F73052EF6710A7 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 4C317D1F5DE6C99A874555F9 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
7555FF7B242A565900829871 /* OONI Probe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "OONI Probe.app"; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 93E977702C4FCCE3009CCABC /* IosOonimkallBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosOonimkallBridge.swift; sourceTree = ""; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
+ B24B7C9C049ADDADBB7C8DCE /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -28,6 +34,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 90C537899E1A531A92327A33 /* Pods_iosApp.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -42,9 +49,19 @@
path = "Preview Content";
sourceTree = "";
};
+ 20AA0919DAC14F9E057F989A /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ B24B7C9C049ADDADBB7C8DCE /* Pods-iosApp.debug.xcconfig */,
+ 4C317D1F5DE6C99A874555F9 /* Pods-iosApp.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
42799AB246E5F90AF97AA0EF /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 3690801A53F73052EF6710A7 /* Pods_iosApp.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -56,6 +73,7 @@
7555FF7D242A565900829871 /* iosApp */,
7555FF7C242A565900829871 /* Products */,
42799AB246E5F90AF97AA0EF /* Frameworks */,
+ 20AA0919DAC14F9E057F989A /* Pods */,
);
sourceTree = "";
};
@@ -75,10 +93,19 @@
7555FF8C242A565B00829871 /* Info.plist */,
2152FB032600AC8F00CF470E /* iOSApp.swift */,
058557D7273AAEEB004C7B11 /* Preview Content */,
+ 93E977722C4FCCF7009CCABC /* engine */,
);
path = iosApp;
sourceTree = "";
};
+ 93E977722C4FCCF7009CCABC /* engine */ = {
+ isa = PBXGroup;
+ children = (
+ 93E977702C4FCCE3009CCABC /* IosOonimkallBridge.swift */,
+ );
+ path = engine;
+ sourceTree = "";
+ };
AB1DB47929225F7C00F7AF9C /* Configuration */ = {
isa = PBXGroup;
children = (
@@ -94,18 +121,19 @@
isa = PBXNativeTarget;
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
+ 2221CAC0E44786DF6A5501B8 /* [CP] Check Pods Manifest.lock */,
F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,
7555FF77242A565900829871 /* Sources */,
B92378962B6B1156000C7307 /* Frameworks */,
7555FF79242A565900829871 /* Resources */,
+ 93E977732C4FE022009CCABC /* ShellScript */,
+ 3793390471A4D6FCCF24C27E /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = iosApp;
- packageProductDependencies = (
- );
productName = iosApp;
productReference = 7555FF7B242A565900829871 /* OONI Probe.app */;
productType = "com.apple.product-type.application";
@@ -134,8 +162,6 @@
Base,
);
mainGroup = 7555FF72242A565900829871;
- packageReferences = (
- );
productRefGroup = 7555FF7C242A565900829871 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -158,6 +184,62 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
+ 2221CAC0E44786DF6A5501B8 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3793390471A4D6FCCF24C27E /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 93E977732C4FE022009CCABC /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "cd \"$SRCROOT/..\"\n./gradlew embedAndSignAppleFrameworkForXcode\n";
+ };
F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -184,6 +266,7 @@
buildActionMask = 2147483647;
files = (
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
+ 93E977712C4FCCE3009CCABC /* IosOonimkallBridge.swift in Sources */,
7555FF83242A565900829871 /* ContentView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -311,6 +394,7 @@
};
7555FFA6242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = B24B7C9C049ADDADBB7C8DCE /* Pods-iosApp.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -319,7 +403,9 @@
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
- "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(inherited)",
);
INFOPLIST_FILE = iosApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
@@ -342,6 +428,7 @@
};
7555FFA7242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 4C317D1F5DE6C99A874555F9 /* Pods-iosApp.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -350,7 +437,9 @@
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
- "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(inherited)",
);
INFOPLIST_FILE = iosApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
@@ -395,4 +484,4 @@
/* End XCConfigurationList section */
};
rootObject = 7555FF73242A565900829871 /* Project object */;
-}
\ No newline at end of file
+}
diff --git a/iosApp/iosApp.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..c009e7d7
--- /dev/null
+++ b/iosApp/iosApp.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift
index 3cd5c325..52eb65b6 100644
--- a/iosApp/iosApp/ContentView.swift
+++ b/iosApp/iosApp/ContentView.swift
@@ -1,10 +1,10 @@
import UIKit
import SwiftUI
-import ComposeApp
+import composeApp
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
- MainViewControllerKt.MainViewController()
+ MainViewControllerKt.MainViewController(bridge: IosOonimkallBridge())
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
diff --git a/iosApp/iosApp/engine/IosOonimkallBridge.swift b/iosApp/iosApp/engine/IosOonimkallBridge.swift
new file mode 100644
index 00000000..fcad04fc
--- /dev/null
+++ b/iosApp/iosApp/engine/IosOonimkallBridge.swift
@@ -0,0 +1,19 @@
+import composeApp
+import Oonimkall
+
+class IosOonimkallBridge : OonimkallBridge {
+ func startTask(settingsSerialized: String) -> OonimkallBridgeTask {
+ var error: NSError?
+ let task = OonimkallStartTask(settingsSerialized, &error)!
+
+ class Task : OonimkallBridgeTask {
+ var task: OonimkallTask
+ init(task: OonimkallTask) { self.task = task }
+ func isDone() -> Bool { task.isDone() }
+ func interrupt() { task.interrupt() }
+ func waitForNextEvent() -> String { task.waitForNextEvent() }
+ }
+
+ return Task(task: task)
+ }
+}