diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 25c3f3b7..34e71252 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -58,6 +58,22 @@
android:scheme="ooni" />
+
+
+
+
+
+
+
+
+
+
+
+
manageOoniRun(intent)
+ Intent.ACTION_SEND -> manageSend(intent)
+ else -> return
+ }
+ }
+
+ private fun manageSend(intent: Intent) {
+ val url = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return
+ deepLinkFlow.tryEmit(DeepLink.RunUrls(url))
+ }
+
+ private fun manageOoniRun(intent: Intent) {
val uri = intent.data ?: return
when (uri.host) {
"runv2",
@@ -61,6 +73,7 @@ class MainActivity : ComponentActivity() {
}
else -> {
+ deepLinkFlow.tryEmit(DeepLink.Error)
Logger.e { "Unknown deep link: $uri" }
}
}
diff --git a/composeApp/src/commonMain/composeResources/values/strings-common.xml b/composeApp/src/commonMain/composeResources/values/strings-common.xml
index b512b244..39dc3d3e 100644
--- a/composeApp/src/commonMain/composeResources/values/strings-common.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings-common.xml
@@ -290,4 +290,5 @@
Filter Logs
Only the last %1$d results are shown
Skip after this amount of results failed to upload
+ Unsupported URL
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
index 4b62ee59..a0df1626 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
@@ -19,6 +19,9 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import co.touchlab.kermit.Logger
+import ooniprobe.composeapp.generated.resources.AddDescriptor_Toasts_Unsupported_Url
+import ooniprobe.composeapp.generated.resources.Res
+import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.ooni.probe.data.models.DeepLink
import org.ooni.probe.di.Dependencies
@@ -111,6 +114,14 @@ fun App(
navController.navigate("add-descriptor/${deepLink.id}")
onDeeplinkHandled()
}
+ is DeepLink.RunUrls -> {
+ navController.navigate(Screen.ChooseWebsites(deepLink.url).route)
+ onDeeplinkHandled()
+ }
+ DeepLink.Error -> {
+ snackbarHostState.showSnackbar(getString(Res.string.AddDescriptor_Toasts_Unsupported_Url))
+ onDeeplinkHandled()
+ }
null -> Unit
}
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DeepLink.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DeepLink.kt
index 75aaa1f3..73f020fb 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DeepLink.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/DeepLink.kt
@@ -2,4 +2,8 @@ package org.ooni.probe.data.models
sealed class DeepLink {
data class AddDescriptor(val id: String) : DeepLink()
+
+ data class RunUrls(val url: String) : DeepLink()
+
+ data object Error : DeepLink()
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
index 86895b9e..6d99e0f8 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
@@ -390,10 +390,12 @@ class Dependencies(
fun chooseWebsitesViewModel(
onBack: () -> Unit,
goToDashboard: () -> Unit,
+ initialUrl: String?,
) = ChooseWebsitesViewModel(
onBack = onBack,
goToDashboard = goToDashboard,
startBackgroundRun = startSingleRunInner,
+ initialUrl = initialUrl,
)
fun dashboardViewModel(
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt
index 81d1f03c..88cc5369 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/choosewebsites/ChooseWebsitesViewModel.kt
@@ -19,10 +19,19 @@ class ChooseWebsitesViewModel(
onBack: () -> Unit,
goToDashboard: () -> Unit,
startBackgroundRun: (RunSpecification) -> Unit,
+ initialUrl: String? = null,
) : ViewModel() {
private val events = MutableSharedFlow(extraBufferCapacity = 1)
- private val _state = MutableStateFlow(State())
+ private val _state = MutableStateFlow(
+ State(
+ websites = listOf(
+ initialUrl?.let {
+ WebsiteItem(url = it, hasError = !it.isValidUrl())
+ } ?: WebsiteItem(),
+ ),
+ ),
+ )
val state = _state.asStateFlow()
init {
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt
index 1d9540b0..2256150d 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Navigation.kt
@@ -279,7 +279,7 @@ fun Navigation(
goToReviewDescriptorUpdates = {
navController.safeNavigate(Screen.ReviewUpdates)
},
- goToChooseWebsites = { navController.navigate(Screen.ChooseWebsites.route) },
+ goToChooseWebsites = { navController.safeNavigate(Screen.ChooseWebsites()) },
)
}
val state by viewModel.state.collectAsState()
@@ -296,10 +296,15 @@ fun Navigation(
ReviewUpdatesScreen(state, viewModel::onEvent)
}
- composable(route = Screen.ChooseWebsites.route) { entry ->
+ composable(
+ route = Screen.ChooseWebsites.NAV_ROUTE,
+ arguments = Screen.ChooseWebsites.ARGUMENTS,
+ ) { entry ->
+ val url = entry.arguments?.getString("url")
val viewModel = viewModel {
dependencies.chooseWebsitesViewModel(
onBack = { navController.goBack() },
+ initialUrl = url.decodeUrlFromBase64(),
goToDashboard = {
navController.goBackTo(Screen.Dashboard, inclusive = false)
},
@@ -342,7 +347,7 @@ private fun NavController.goBackAndNavigateToMain(screen: Screen) {
navigateToMainScreen(screen)
}
-private fun NavController.safeNavigate(screen: Screen) {
+fun NavController.safeNavigate(screen: Screen) {
if (!isResumed()) return
navigate(screen.route)
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt
index af4800be..a3d9bfba 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/ui/navigation/Screen.kt
@@ -81,6 +81,21 @@ sealed class Screen(
}
}
+ data class ChooseWebsites(
+ val url: String? = null,
+ ) : Screen("choose-websites?url=${url.encodeUrlToBase64()}") {
+ companion object {
+ const val NAV_ROUTE = "choose-websites?url={url}"
+ val ARGUMENTS = listOf(
+ navArgument("url") {
+ type = NavType.StringType
+ defaultValue = null
+ nullable = true
+ },
+ )
+ }
+ }
+
data class Descriptor(
val descriptorKey: String,
) : Screen("descriptors/$descriptorKey") {
@@ -91,6 +106,4 @@ sealed class Screen(
}
data object ReviewUpdates : Screen("review-updates")
-
- data object ChooseWebsites : Screen("choose-websites")
}
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index cce2d63a..7e9abdd3 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -11,6 +11,9 @@
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 */; };
+ 795E37782CD5053900086360 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795E37772CD5053900086360 /* ShareViewController.swift */; };
+ 795E377B2CD5053900086360 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 795E377A2CD5053900086360 /* Base */; };
+ 795E377F2CD5053900086360 /* share.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 795E37752CD5053900086360 /* share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
79B9D19A2C6552DE004DCEE6 /* IosNetworkTypeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B9D1992C6552DE004DCEE6 /* IosNetworkTypeFinder.swift */; };
79B9D19B2C6552DE004DCEE6 /* IosNetworkTypeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B9D1992C6552DE004DCEE6 /* IosNetworkTypeFinder.swift */; };
79FBD00D2C5A70AF004E041C /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
@@ -25,6 +28,30 @@
A63D4DAB2DEF80E322CD2A82 /* Pods_OONIProbe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD59ED7E2702C84FF0455021 /* Pods_OONIProbe.framework */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ 795E377D2CD5053900086360 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 7555FF73242A565900829871 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 795E37742CD5053900086360;
+ remoteInfo = share;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 795E37802CD5053900086360 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 795E377F2CD5053900086360 /* share.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase 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 = ""; };
@@ -36,6 +63,10 @@
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 = ""; };
+ 795E37752CD5053900086360 /* share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = share.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ 795E37772CD5053900086360 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; };
+ 795E377A2CD5053900086360 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; };
+ 795E377C2CD5053900086360 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
79A29BC42C9045A80052C9D0 /* OONIProbe.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OONIProbe.entitlements; sourceTree = ""; };
79B9D1992C6552DE004DCEE6 /* IosNetworkTypeFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosNetworkTypeFinder.swift; sourceTree = ""; };
79FBD01A2C5A70AF004E041C /* News Media Scan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "News Media Scan.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -51,6 +82,13 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 795E37722CD5053900086360 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
79FBD0102C5A70AF004E041C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -108,6 +146,7 @@
93D3BF622C6B70AD00E9A9B7 /* resources */,
AB1DB47929225F7C00F7AF9C /* Configuration */,
7555FF7D242A565900829871 /* iosApp */,
+ 795E37762CD5053900086360 /* share */,
7555FF7C242A565900829871 /* Products */,
20AA0919DAC14F9E057F989A /* Pods */,
79FBD01B2C5A70AF004E041C /* iosApp copy-Info.plist */,
@@ -120,6 +159,7 @@
children = (
7555FF7B242A565900829871 /* OONI Probe.app */,
79FBD01A2C5A70AF004E041C /* News Media Scan.app */,
+ 795E37752CD5053900086360 /* share.appex */,
);
name = Products;
sourceTree = "";
@@ -137,6 +177,16 @@
path = iosApp;
sourceTree = "";
};
+ 795E37762CD5053900086360 /* share */ = {
+ isa = PBXGroup;
+ children = (
+ 795E37772CD5053900086360 /* ShareViewController.swift */,
+ 795E37792CD5053900086360 /* MainInterface.storyboard */,
+ 795E377C2CD5053900086360 /* Info.plist */,
+ );
+ path = share;
+ sourceTree = "";
+ };
93D3BF5C2C6B684800E9A9B7 /* assets */ = {
isa = PBXGroup;
children = (
@@ -203,16 +253,35 @@
93E977732C4FE022009CCABC /* ShellScript */,
F57E468EACCE9B29FB4C68FC /* [CP] Copy Pods Resources */,
11F17E981064A71B74C323DA /* [CP] Embed Pods Frameworks */,
+ 795E37802CD5053900086360 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
+ 795E377E2CD5053900086360 /* PBXTargetDependency */,
);
name = OONIProbe;
productName = iosApp;
productReference = 7555FF7B242A565900829871 /* OONI Probe.app */;
productType = "com.apple.product-type.application";
};
+ 795E37742CD5053900086360 /* share */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 795E37832CD5053900086360 /* Build configuration list for PBXNativeTarget "share" */;
+ buildPhases = (
+ 795E37712CD5053900086360 /* Sources */,
+ 795E37722CD5053900086360 /* Frameworks */,
+ 795E37732CD5053900086360 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = share;
+ productName = share;
+ productReference = 795E37752CD5053900086360 /* share.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
79FBD0092C5A70AF004E041C /* NewsMediaScan */ = {
isa = PBXNativeTarget;
buildConfigurationList = 79FBD0172C5A70AF004E041C /* Build configuration list for PBXNativeTarget "NewsMediaScan" */;
@@ -241,13 +310,16 @@
7555FF73242A565900829871 /* Project object */ = {
isa = PBXProject;
attributes = {
- LastSwiftUpdateCheck = 1130;
+ LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1130;
ORGANIZATIONNAME = orgName;
TargetAttributes = {
7555FF7A242A565900829871 = {
CreatedOnToolsVersion = 11.3.1;
};
+ 795E37742CD5053900086360 = {
+ CreatedOnToolsVersion = 15.4;
+ };
};
};
buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
@@ -265,6 +337,7 @@
targets = (
7555FF7A242A565900829871 /* OONIProbe */,
79FBD0092C5A70AF004E041C /* NewsMediaScan */,
+ 795E37742CD5053900086360 /* share */,
);
};
/* End PBXProject section */
@@ -280,6 +353,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 795E37732CD5053900086360 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 795E377B2CD5053900086360 /* Base in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
79FBD0122C5A70AF004E041C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -489,6 +570,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 795E37712CD5053900086360 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 795E37782CD5053900086360 /* ShareViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
79FBD00C2C5A70AF004E041C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -502,6 +591,25 @@
};
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ 795E377E2CD5053900086360 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 795E37742CD5053900086360 /* share */;
+ targetProxy = 795E377D2CD5053900086360 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 795E37792CD5053900086360 /* MainInterface.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 795E377A2CD5053900086360 /* Base */,
+ );
+ name = MainInterface.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
/* Begin XCBuildConfiguration section */
7555FFA3242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
@@ -625,6 +733,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 205238D88AD064C8AF508845 /* Pods-OONIProbe.debug.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = OONIProbe;
CODE_SIGN_ENTITLEMENTS = OONIProbe.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -661,6 +770,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = B2A30B3FAFDB1355D98FFF9E /* Pods-OONIProbe.release.xcconfig */;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = OONIProbe;
CODE_SIGN_ENTITLEMENTS = OONIProbe.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -693,6 +803,72 @@
};
name = Release;
};
+ 795E37812CD5053900086360 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "${OONI_TEAM_ID}";
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = share/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = share;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 orgName. All rights reserved.";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "${OONI_PROBE_BUNDLE_ID}.debug.share";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 795E37822CD5053900086360 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "${OONI_TEAM_ID}";
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = share/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = share;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 orgName. All rights reserved.";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "${OONI_PROBE_BUNDLE_ID}.share";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
79FBD0182C5A70AF004E041C /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 506A0735B8488E63307B34DA /* Pods-NewsMediaScan.debug.xcconfig */;
@@ -784,6 +960,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 795E37832CD5053900086360 /* Build configuration list for PBXNativeTarget "share" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 795E37812CD5053900086360 /* Debug */,
+ 795E37822CD5053900086360 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
79FBD0172C5A70AF004E041C /* Build configuration list for PBXNativeTarget "NewsMediaScan" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift
index 41911610..c21e95a7 100644
--- a/iosApp/iosApp/iOSApp.swift
+++ b/iosApp/iosApp/iOSApp.swift
@@ -1,10 +1,18 @@
import SwiftUI
import composeApp
+extension URL {
+ subscript(queryParam:String) -> String? {
+ guard let url = URLComponents(string: self.absoluteString) else { return nil }
+ return url.queryItems?.first(where: { $0.name == queryParam })?.value
+ }
+}
+
@main
struct iOSApp: App {
@Environment(\.openURL) var openURL
+
@Environment(\.scenePhase)var scenePhase
let appDependencies = SetupDependencies(
@@ -39,11 +47,29 @@ struct iOSApp: App {
private func handleDeepLink(url: URL) {
- if let host = url.host, host == "runv2" || host == appDependencies.ooniRunDomain() {
+ guard let host = url.host else {
+ deepLinkFlow.emit(value: DeepLink.Error(), completionHandler: { error in
+ print(error ?? "none")
+ })
+ return
+ }
+
+ if host == "runv2" || host == appDependencies.ooniRunDomain() {
let id = url.lastPathComponent
deepLinkFlow.emit(value: DeepLink.AddDescriptor(id: id), completionHandler: {error in
print(error ?? "none")
})
+ } else if host == "nettest" {
+ if let webAddress = url["url"] {
+ deepLinkFlow.emit(value: DeepLink.RunUrls(url: webAddress), completionHandler: {error in
+ print(error ?? "none")
+ })
+ } else {
+
+ deepLinkFlow.emit(value: DeepLink.Error(), completionHandler: {error in
+ print(error ?? "none")
+ })
+ }
}
}
}
diff --git a/iosApp/share/Base.lproj/MainInterface.storyboard b/iosApp/share/Base.lproj/MainInterface.storyboard
new file mode 100644
index 00000000..286a5089
--- /dev/null
+++ b/iosApp/share/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/iosApp/share/Info.plist b/iosApp/share/Info.plist
new file mode 100644
index 00000000..4dd1bdfe
--- /dev/null
+++ b/iosApp/share/Info.plist
@@ -0,0 +1,21 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsWebURLWithMaxCount
+ 1
+
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.share-services
+
+
+
diff --git a/iosApp/share/ShareViewController.swift b/iosApp/share/ShareViewController.swift
new file mode 100644
index 00000000..166a0b0d
--- /dev/null
+++ b/iosApp/share/ShareViewController.swift
@@ -0,0 +1,102 @@
+import UIKit
+import Social
+import CoreServices
+
+/// Share controller handles action of receiving share action and
+/// launching the app with appropriate arguments ``incomingURL``.
+class ShareViewController: UIViewController {
+
+ private let typeText = String(kUTTypeText)
+ private let typeURL = String(kUTTypeURL)
+ private let appURL = "ooni://nettest"
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
+ let itemProvider = extensionItem.attachments?.first else {
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
+ return
+ }
+
+ if itemProvider.hasItemConformingToTypeIdentifier(typeText) {
+ handleIncomingText(itemProvider: itemProvider)
+ } else if itemProvider.hasItemConformingToTypeIdentifier(typeURL) {
+ handleIncomingURL(itemProvider: itemProvider)
+ } else {
+ NSLog("Error: No url or text found")
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
+ }
+ }
+
+ private func handleIncomingText(itemProvider: NSItemProvider) {
+ itemProvider.loadItem(forTypeIdentifier: typeText, options: nil) { (item, error) in
+ if let error = error { NSLog("Text-Error: \(error.localizedDescription)") }
+
+ if let text = item as? String {
+ do {
+ let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
+ let matches = detector.matches(
+ in: text,
+ options: [],
+ range: NSRange(location: 0, length: text.utf16.count)
+ )
+ if let firstMatch = matches.first, let range = Range(firstMatch.range, in: text) {
+ self.openMainApp(String(text[range]))
+ }
+ } catch let error {
+ NSLog("Do-Try Error: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ private func handleIncomingURL(itemProvider: NSItemProvider) {
+ itemProvider.loadItem(forTypeIdentifier: typeURL, options: nil) { (item, error) in
+ if let error = error { NSLog("URL-Error: \(error.localizedDescription)") }
+
+ if let url = item as? NSURL, let urlString = url.absoluteString {
+ self.openMainApp(urlString)
+ }
+ }
+ }
+
+
+ private func openMainApp(_ urlString: String) {
+
+ let dict: NSDictionary = [
+ "urls": [
+ urlString
+ ],
+ ]
+ do {
+ let data = try JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions.prettyPrinted)
+
+ let json = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
+ let queryItems = [
+ URLQueryItem(name: "url", value: urlString)
+ ]
+ var urlComps = URLComponents(string: appURL)!
+ urlComps.queryItems = queryItems
+ let result = urlComps.url!
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: { _ in
+ self.openURL(result)
+ })
+ } catch let error {
+ NSLog("something went wrong with parsing json",error.localizedDescription)
+ }
+ }
+
+ // Courtesy: https://stackoverflow.com/a/44499222/13363449 👇🏾
+ // Function must be named exactly like this so a selector can be found by the compiler!
+ // Anyway - it's another selector in another instance that would be "performed" instead.
+ @objc private func openURL(_ url: URL) -> Bool {
+ var responder: UIResponder? = self
+ while responder != nil {
+ if let application = responder as? UIApplication {
+ return application.perform(#selector(openURL(_:)), with: url) != nil
+ }
+ responder = responder?.next
+ }
+ return false
+ }
+}