one 2.0.0-beta.148.1 → 2.0.0-beta.158.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/One.podspec +4 -4
- package/android/CMakeLists.txt +7 -2
- package/android/build.gradle +2 -0
- package/android/src/main/cpp/OneStorageDirectory.cpp +21 -0
- package/android/src/main/java/com/margelo/nitro/one/HybridOneAdaptive.kt +13 -6
- package/android/src/main/java/com/margelo/nitro/one/OneStorageDirectory.kt +19 -0
- package/android/src/main/java/dev/onejs/one/OneKotlinHostEvents.kt +41 -0
- package/android/src/main/java/dev/onejs/one/OneKotlinHostManager.kt +87 -0
- package/android/src/main/java/dev/onejs/one/OneKotlinHostView.kt +231 -0
- package/android/src/main/java/dev/onejs/one/OneNativeSourceViewDispatch.kt +13 -0
- package/android/src/main/java/dev/onejs/one/OneNativeSourceViewRegistry.kt +21 -0
- package/android/src/main/java/dev/onejs/one/OnePackage.kt +3 -0
- package/android/src/main/java/dev/onejs/one/sharetarget/OneShareDraftStore.kt +122 -0
- package/android/src/main/java/dev/onejs/one/sharetarget/OneShareIntake.kt +344 -0
- package/android/src/main/java/dev/onejs/one/sharetarget/OneShareModels.kt +36 -0
- package/android/src/main/java/dev/onejs/one/sharetarget/OneShareSessionModel.kt +207 -0
- package/android/src/main/java/dev/onejs/one/sharetarget/OneShareTargetActivity.kt +161 -0
- package/android/src/main/java/dev/onejs/one/sharetarget/OneShareTargetAdapter.kt +13 -0
- package/android/src/main/java/dev/onejs/onenative/OneNativeMenuTriggerManager.kt +102 -0
- package/cpp/HybridOneCrypto.cpp +37 -0
- package/cpp/HybridOneCrypto.hpp +19 -0
- package/cpp/HybridOneStorage.cpp +331 -0
- package/cpp/HybridOneStorage.hpp +23 -0
- package/cpp/OneStorageDirectory.hpp +11 -0
- package/dist/cjs/cli/prebuild.cjs +2 -2
- package/dist/cjs/cli/prebuild.native.cjs +4 -3
- package/dist/cjs/cli/prebuild.native.js +4 -3
- package/dist/cjs/cli/prebuild.native.js.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.native.cjs +2 -0
- package/dist/cjs/index.native.js +2 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/one.cjs +2 -1
- package/dist/cjs/one.native.cjs +2 -1
- package/dist/cjs/one.native.js +2 -1
- package/dist/cjs/one.native.js.map +1 -1
- package/dist/cjs/platform/AndroidMenu.cjs +14 -3
- package/dist/cjs/platform/AndroidMenu.native.cjs +14 -3
- package/dist/cjs/platform/AndroidMenu.native.js +14 -3
- package/dist/cjs/platform/AndroidMenu.native.js.map +1 -1
- package/dist/cjs/platform/Sheet.native.cjs +28 -2
- package/dist/cjs/platform/Sheet.native.js +28 -2
- package/dist/cjs/platform/Sheet.native.js.map +1 -1
- package/dist/cjs/platform/app-intents/index.cjs +29 -0
- package/dist/cjs/platform/app-intents/index.native.cjs +93 -0
- package/dist/cjs/platform/app-intents/index.native.js +95 -0
- package/dist/cjs/platform/app-intents/index.native.js.map +1 -0
- package/dist/cjs/platform/app-intents/validate.cjs +28 -0
- package/dist/cjs/platform/app-intents/validate.native.cjs +30 -0
- package/dist/cjs/platform/app-intents/validate.native.js +32 -0
- package/dist/cjs/platform/app-intents/validate.native.js.map +1 -0
- package/dist/cjs/platform/crypto/index.cjs +10 -6
- package/dist/cjs/platform/crypto/index.native.cjs +9 -13
- package/dist/cjs/platform/crypto/index.native.js +9 -13
- package/dist/cjs/platform/crypto/index.native.js.map +1 -1
- package/dist/cjs/platform/crypto/random.cjs +5 -8
- package/dist/cjs/platform/crypto/random.native.cjs +5 -8
- package/dist/cjs/platform/crypto/random.native.js +5 -8
- package/dist/cjs/platform/crypto/random.native.js.map +1 -1
- package/dist/cjs/platform/index.cjs +4 -2
- package/dist/cjs/platform/index.native.cjs +4 -2
- package/dist/cjs/platform/index.native.js +4 -2
- package/dist/cjs/platform/index.native.js.map +1 -1
- package/dist/cjs/platform/native-source/view.cjs +26 -0
- package/dist/cjs/platform/native-source/view.native.cjs +63 -0
- package/dist/cjs/platform/native-source/view.native.js +65 -0
- package/dist/cjs/platform/native-source/view.native.js.map +1 -0
- package/dist/cjs/platform/{specs/OnePreferences.nitro.cjs → native-source/viewTypes.cjs} +2 -2
- package/dist/cjs/platform/{specs/OnePreferences.nitro.native.cjs → native-source/viewTypes.native.cjs} +2 -2
- package/dist/cjs/platform/{specs/OnePreferences.nitro.native.js → native-source/viewTypes.native.js} +3 -3
- package/dist/cjs/platform/native-source/viewTypes.native.js.map +1 -0
- package/dist/cjs/platform/specs/OneAppIntents.nitro.cjs +16 -0
- package/dist/cjs/platform/specs/OneAppIntents.nitro.native.cjs +18 -0
- package/dist/cjs/platform/specs/OneAppIntents.nitro.native.js +20 -0
- package/dist/cjs/platform/specs/OneAppIntents.nitro.native.js.map +1 -0
- package/dist/cjs/platform/specs/OneKotlinHostNativeComponent.cjs +52 -0
- package/dist/cjs/platform/specs/OneKotlinHostNativeComponent.native.cjs +52 -0
- package/dist/cjs/platform/specs/OneKotlinHostNativeComponent.native.js +53 -0
- package/dist/cjs/platform/specs/OneKotlinHostNativeComponent.native.js.map +7 -0
- package/dist/cjs/platform/specs/OneNativeSheetNativeComponent.cjs +5 -0
- package/dist/cjs/platform/specs/OneNativeSheetNativeComponent.native.cjs +5 -0
- package/dist/cjs/platform/specs/OneNativeSheetNativeComponent.native.js +5 -0
- package/dist/cjs/platform/specs/OneNativeSheetNativeComponent.native.js.map +2 -2
- package/dist/cjs/platform/specs/OneStorage.nitro.cjs +16 -0
- package/dist/cjs/platform/specs/OneStorage.nitro.native.cjs +18 -0
- package/dist/cjs/platform/specs/OneStorage.nitro.native.js +20 -0
- package/dist/cjs/platform/specs/OneStorage.nitro.native.js.map +1 -0
- package/dist/cjs/platform/storage/index.cjs +52 -0
- package/dist/cjs/platform/storage/index.native.cjs +70 -0
- package/dist/cjs/platform/storage/index.native.js +72 -0
- package/dist/cjs/platform/storage/index.native.js.map +1 -0
- package/dist/cjs/platform/{preferences → storage}/validate.cjs +6 -10
- package/dist/cjs/platform/{preferences → storage}/validate.native.cjs +6 -10
- package/dist/cjs/platform/{preferences → storage}/validate.native.js +6 -10
- package/dist/cjs/platform/storage/validate.native.js.map +1 -0
- package/dist/cjs/tamagui.cjs +24 -0
- package/dist/cjs/tamagui.native.cjs +90 -0
- package/dist/cjs/tamagui.native.js +92 -0
- package/dist/cjs/tamagui.native.js.map +1 -0
- package/dist/cjs/useHeaderHeight.cjs +46 -0
- package/dist/cjs/useHeaderHeight.native.cjs +49 -0
- package/dist/cjs/useHeaderHeight.native.js +51 -0
- package/dist/cjs/useHeaderHeight.native.js.map +1 -0
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.cjs +4 -7
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.cjs +3 -6
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js +3 -6
- package/dist/cjs/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
- package/dist/cjs/vite/plugins/kotlinSourcePlugin.cjs +12 -4
- package/dist/cjs/vite/plugins/kotlinSourcePlugin.native.cjs +14 -4
- package/dist/cjs/vite/plugins/kotlinSourcePlugin.native.js +14 -4
- package/dist/cjs/vite/plugins/kotlinSourcePlugin.native.js.map +1 -1
- package/dist/esm/cli/prebuild.mjs +2 -2
- package/dist/esm/cli/prebuild.mjs.map +1 -1
- package/dist/esm/cli/prebuild.native.js +4 -3
- package/dist/esm/cli/prebuild.native.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.mjs +2 -1
- package/dist/esm/index.native.js +2 -1
- package/dist/esm/one.mjs +3 -2
- package/dist/esm/one.mjs.map +1 -1
- package/dist/esm/one.native.js +3 -2
- package/dist/esm/one.native.js.map +1 -1
- package/dist/esm/platform/AndroidMenu.mjs +15 -4
- package/dist/esm/platform/AndroidMenu.mjs.map +1 -1
- package/dist/esm/platform/AndroidMenu.native.js +15 -4
- package/dist/esm/platform/AndroidMenu.native.js.map +1 -1
- package/dist/esm/platform/Sheet.native.js +28 -2
- package/dist/esm/platform/Sheet.native.js.map +1 -1
- package/dist/esm/platform/app-intents/index.mjs +10 -0
- package/dist/esm/platform/app-intents/index.mjs.map +1 -0
- package/dist/esm/platform/app-intents/index.native.js +72 -0
- package/dist/esm/platform/app-intents/index.native.js.map +1 -0
- package/dist/esm/platform/app-intents/validate.mjs +8 -0
- package/dist/esm/platform/app-intents/validate.mjs.map +1 -0
- package/dist/esm/platform/app-intents/validate.native.js +8 -0
- package/dist/esm/platform/app-intents/validate.native.js.map +1 -0
- package/dist/esm/platform/crypto/index.mjs +11 -7
- package/dist/esm/platform/crypto/index.mjs.map +1 -1
- package/dist/esm/platform/crypto/index.native.js +10 -14
- package/dist/esm/platform/crypto/index.native.js.map +1 -1
- package/dist/esm/platform/crypto/random.mjs +6 -8
- package/dist/esm/platform/crypto/random.mjs.map +1 -1
- package/dist/esm/platform/crypto/random.native.js +6 -8
- package/dist/esm/platform/crypto/random.native.js.map +1 -1
- package/dist/esm/platform/index.mjs +3 -2
- package/dist/esm/platform/index.mjs.map +1 -1
- package/dist/esm/platform/index.native.js +3 -2
- package/dist/esm/platform/index.native.js.map +1 -1
- package/dist/esm/platform/native-source/view.mjs +6 -0
- package/dist/esm/platform/native-source/view.mjs.map +1 -0
- package/dist/esm/platform/native-source/view.native.js +36 -0
- package/dist/esm/platform/native-source/view.native.js.map +1 -0
- package/dist/esm/platform/specs/OneAppIntents.nitro.mjs +0 -0
- package/dist/esm/platform/specs/OneAppIntents.nitro.native.js +0 -0
- package/dist/esm/platform/specs/OneKotlinHostNativeComponent.mjs +34 -0
- package/dist/esm/platform/specs/OneKotlinHostNativeComponent.mjs.map +7 -0
- package/dist/esm/platform/specs/OneKotlinHostNativeComponent.native.js +34 -0
- package/dist/esm/platform/specs/OneKotlinHostNativeComponent.native.js.map +7 -0
- package/dist/esm/platform/specs/OneNativeSheetNativeComponent.mjs +5 -0
- package/dist/esm/platform/specs/OneNativeSheetNativeComponent.mjs.map +2 -2
- package/dist/esm/platform/specs/OneNativeSheetNativeComponent.native.js +5 -0
- package/dist/esm/platform/specs/OneNativeSheetNativeComponent.native.js.map +2 -2
- package/dist/esm/platform/specs/OneStorage.nitro.mjs +0 -0
- package/dist/esm/platform/specs/OneStorage.nitro.native.js +0 -0
- package/dist/esm/platform/storage/index.mjs +33 -0
- package/dist/esm/platform/storage/index.mjs.map +1 -0
- package/dist/esm/platform/storage/index.native.js +49 -0
- package/dist/esm/platform/storage/index.native.js.map +1 -0
- package/dist/esm/platform/storage/validate.mjs +9 -0
- package/dist/esm/platform/storage/validate.mjs.map +1 -0
- package/dist/esm/platform/storage/validate.native.js +9 -0
- package/dist/esm/platform/storage/validate.native.js.map +1 -0
- package/dist/esm/tamagui.mjs +4 -0
- package/dist/esm/tamagui.mjs.map +1 -0
- package/dist/esm/tamagui.native.js +69 -0
- package/dist/esm/tamagui.native.js.map +1 -0
- package/dist/esm/useHeaderHeight.mjs +27 -0
- package/dist/esm/useHeaderHeight.mjs.map +1 -0
- package/dist/esm/useHeaderHeight.native.js +28 -0
- package/dist/esm/useHeaderHeight.native.js.map +1 -0
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs +4 -7
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js +3 -6
- package/dist/esm/vite/plugins/fileSystemRouterPlugin.native.js.map +1 -1
- package/dist/esm/vite/plugins/kotlinSourcePlugin.mjs +12 -4
- package/dist/esm/vite/plugins/kotlinSourcePlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/kotlinSourcePlugin.native.js +14 -4
- package/dist/esm/vite/plugins/kotlinSourcePlugin.native.js.map +1 -1
- package/ios/Generated/OneNativeSheetContent.swift +2 -0
- package/ios/Nitro/HybridOneAppIntents.swift +148 -0
- package/ios/Nitro/OneAppIntentsBridge.h +14 -0
- package/ios/Nitro/OneAppIntentsBridge.m +17 -0
- package/ios/Nitro/OneBackgroundTasksBridge.m +2 -4
- package/ios/Nitro/OneHeadlessHost.h +8 -0
- package/ios/Nitro/OneHeadlessHost.m +10 -0
- package/ios/OneNativeComposition.swift +18 -14
- package/ios/OneNativeSheetComponentView.mm +7 -1
- package/ios/OneNativeSheetView.swift +28 -3
- package/ios/OneNativeSlot.swift +9 -0
- package/ios/OneStorageDirectory.mm +28 -0
- package/ios/OneSwiftHostView.swift +6 -1
- package/ios/ShareTarget/OneShareComposeViewController.swift +337 -0
- package/ios/ShareTarget/OneShareTargetAdapter.swift +128 -0
- package/ios/ShareTarget/OneShareTargetConfiguration.swift +38 -0
- package/ios/ShareTarget/OneShareTargetDraftStore.swift +118 -0
- package/ios/ShareTarget/OneShareTargetIntake.swift +344 -0
- package/ios/ShareTarget/OneShareTargetSubmissionCoordinator.swift +249 -0
- package/ios/ShareTarget/OneShareTargetViewController.swift +30 -0
- package/ios/Tests/ShareTarget/Package.swift +34 -0
- package/ios/Tests/ShareTarget/Tests/OneShareTargetHarnessTests/OneShareTargetDraftStoreTests.swift +75 -0
- package/ios/Tests/ShareTarget/Tests/OneShareTargetHarnessTests/OneShareTargetIntakeTests.swift +266 -0
- package/ios/Tests/ShareTarget/Tests/OneShareTargetHarnessTests/OneShareTargetSubmissionCoordinatorTests.swift +234 -0
- package/ios/Tests/ShareTarget/Tests/OneShareTargetHarnessTests/RuntimeRegressionTests.swift +285 -0
- package/ios/Tests/ShareTarget/UIHarness/HostApp/AppDelegate.swift +92 -0
- package/ios/Tests/ShareTarget/UIHarness/HostApp/HostApp.entitlements +10 -0
- package/ios/Tests/ShareTarget/UIHarness/HostApp/Info.plist +43 -0
- package/ios/Tests/ShareTarget/UIHarness/ShareExtension/GeneratedHarnessShareViewController.swift +18 -0
- package/ios/Tests/ShareTarget/UIHarness/ShareExtension/HarnessShareTargetAdapterExtensionOnly.swift +39 -0
- package/ios/Tests/ShareTarget/UIHarness/ShareExtension/Info.plist +45 -0
- package/ios/Tests/ShareTarget/UIHarness/ShareExtension/ShareExtension.entitlements +10 -0
- package/ios/Tests/ShareTarget/UIHarness/Shared/HarnessShareTargetAdapter.swift +82 -0
- package/ios/Tests/ShareTarget/UIHarness/build.sh +60 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/01-host-app.png +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/02-system-share-sheet-with-extension.jpg +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/after-tests.txt +99 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/baseline-tests.txt +109 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/before-caption-draft.json +12 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/before-caption.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/before-send-auto-dismiss.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/configuration-probe.txt +98 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-cancelled-disk.json +50 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-cancelled.ax.json +822 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-cancelled.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-delivered-disk.json +47 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-delivered.ax.json +375 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-delivered.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-destination-error.ax.json +1068 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-destination-error.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-destination-failed-disk.json +61 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-destination-recovered-disk.json +62 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-destination-recovered.ax.json +1068 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-destination-recovered.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-edited.ax.json +1338 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-edited.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-failed-mutations-blocked-disk.json +66 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-failed-send-disk.json +66 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-held-destinations-cancelled-disk.json +50 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-held-destinations-cancelled.ax.json +822 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-held-destinations-cancelled.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-held-destinations-disk.json +61 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-held-destinations.ax.json +1068 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-held-destinations.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-initial.ax.json +1092 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-initial.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-load-edit-preserved-disk.json +62 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-load-edit-preserved.ax.json +1044 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-load-edit-preserved.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-loading-disk.json +61 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-loading.ax.json +2256 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-loading.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-picker.ax.json +1166 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-picker.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-retried-disk.json +54 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-retry-ready.ax.json +1092 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-retry-ready.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-retry-verification.json +30 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-send-error.ax.json +1068 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-send-error.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-sending-disk.json +58 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-sending.ax.json +1215 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/final-sending.webp +0 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/principal-after-log.txt +74 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/share-receipt.json +15 -0
- package/ios/Tests/ShareTarget/UIHarness/evidence/strict-build.txt +8 -0
- package/ios/Tests/ShareTarget/UIHarness/interact.py +61 -0
- package/ios/Tests/ShareTarget/evidence.md +142 -0
- package/nitrogen/generated/android/One+autolinking.cmake +2 -2
- package/nitrogen/generated/android/OneOnLoad.cpp +15 -11
- package/nitrogen/generated/ios/One-Swift-Cxx-Bridge.cpp +25 -34
- package/nitrogen/generated/ios/One-Swift-Cxx-Bridge.hpp +80 -61
- package/nitrogen/generated/ios/One-Swift-Cxx-Umbrella.hpp +8 -10
- package/nitrogen/generated/ios/OneAutolinking.mm +19 -7
- package/nitrogen/generated/ios/OneAutolinking.swift +66 -78
- package/nitrogen/generated/ios/c++/{HybridOneCryptoSpecSwift.cpp → HybridOneAppIntentsSpecSwift.cpp} +2 -2
- package/nitrogen/generated/ios/c++/HybridOneAppIntentsSpecSwift.hpp +101 -0
- package/nitrogen/generated/ios/swift/AppIntentInvocation.swift +52 -0
- package/nitrogen/generated/ios/swift/Func_void_AppIntentInvocation.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridOneAppIntentsSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridOneAppIntentsSpec_cxx.swift +175 -0
- package/nitrogen/generated/shared/c++/AppIntentInvocation.hpp +92 -0
- package/nitrogen/generated/shared/c++/HybridOneAppIntentsSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridOneAppIntentsSpec.hpp +68 -0
- package/nitrogen/generated/shared/c++/HybridOneCryptoSpec.cpp +2 -1
- package/nitrogen/generated/shared/c++/HybridOneCryptoSpec.hpp +4 -2
- package/nitrogen/generated/shared/c++/HybridOneStorageSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridOneStorageSpec.hpp +67 -0
- package/package.json +26 -12
- package/schema.json +11 -0
- package/src/cli/prebuild.ts +4 -2
- package/src/index.ts +1 -0
- package/src/one.ts +7 -8
- package/src/platform/AndroidMenu.test.tsx +1 -0
- package/src/platform/AndroidMenu.tsx +35 -8
- package/src/platform/Sheet.native.tsx +29 -0
- package/src/platform/app-intents/index.native.ts +58 -0
- package/src/platform/app-intents/index.ts +10 -0
- package/src/platform/app-intents/validate.ts +5 -0
- package/src/platform/crypto/index.native.ts +14 -17
- package/src/platform/crypto/index.ts +12 -8
- package/src/platform/crypto/random.ts +14 -15
- package/src/platform/generated/sheetTypes.ts +4 -0
- package/src/platform/index.native.ts +3 -1
- package/src/platform/index.ts +3 -1
- package/src/platform/native-source/view.native.tsx +51 -0
- package/src/platform/native-source/view.tsx +8 -0
- package/src/platform/native-source/viewTypes.ts +9 -0
- package/src/platform/specs/OneAppIntents.nitro.ts +13 -0
- package/src/platform/specs/OneCrypto.nitro.ts +7 -6
- package/src/platform/specs/OneKotlinHostNativeComponent.ts +25 -0
- package/src/platform/specs/OneNativeSheetNativeComponent.ts +4 -0
- package/src/platform/specs/OneStorage.nitro.ts +11 -0
- package/src/platform/storage/index.native.ts +59 -0
- package/src/platform/storage/index.ts +32 -0
- package/src/platform/storage/validate.ts +10 -0
- package/src/polyfills-mobile.ts +1 -1
- package/src/tamagui.native.tsx +96 -0
- package/src/tamagui.ts +2 -0
- package/src/useHeaderHeight.ts +40 -0
- package/src/vite/plugins/fileSystemRouterPlugin.test.ts +55 -2
- package/src/vite/plugins/fileSystemRouterPlugin.tsx +8 -11
- package/src/vite/plugins/kotlinSourcePlugin.ts +14 -2
- package/types/cli/prebuild.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/one.d.ts +3 -2
- package/types/one.d.ts.map +1 -1
- package/types/platform/AndroidMenu.d.ts.map +1 -1
- package/types/platform/Sheet.native.d.ts +2 -2
- package/types/platform/Sheet.native.d.ts.map +1 -1
- package/types/platform/app-intents/index.d.ts +7 -0
- package/types/platform/app-intents/index.d.ts.map +1 -0
- package/types/platform/app-intents/index.native.d.ts +7 -0
- package/types/platform/app-intents/index.native.d.ts.map +1 -0
- package/types/platform/app-intents/validate.d.ts +2 -0
- package/types/platform/app-intents/validate.d.ts.map +1 -0
- package/types/platform/crypto/index.d.ts.map +1 -1
- package/types/platform/crypto/index.native.d.ts.map +1 -1
- package/types/platform/crypto/random.d.ts +6 -4
- package/types/platform/crypto/random.d.ts.map +1 -1
- package/types/platform/generated/sheetTypes.d.ts +4 -0
- package/types/platform/generated/sheetTypes.d.ts.map +1 -1
- package/types/platform/index.d.ts +3 -1
- package/types/platform/index.d.ts.map +1 -1
- package/types/platform/index.native.d.ts +3 -1
- package/types/platform/index.native.d.ts.map +1 -1
- package/types/platform/native-source/view.d.ts +4 -0
- package/types/platform/native-source/view.d.ts.map +1 -0
- package/types/platform/native-source/view.native.d.ts +4 -0
- package/types/platform/native-source/view.native.d.ts.map +1 -0
- package/types/platform/native-source/viewTypes.d.ts +7 -0
- package/types/platform/native-source/viewTypes.d.ts.map +1 -0
- package/types/platform/specs/OneAppIntents.nitro.d.ts +14 -0
- package/types/platform/specs/OneAppIntents.nitro.d.ts.map +1 -0
- package/types/platform/specs/OneCrypto.nitro.d.ts +4 -3
- package/types/platform/specs/OneCrypto.nitro.d.ts.map +1 -1
- package/types/platform/specs/OneKotlinHostNativeComponent.d.ts +24 -0
- package/types/platform/specs/OneKotlinHostNativeComponent.d.ts.map +1 -0
- package/types/platform/specs/OneNativeSheetNativeComponent.d.ts +5 -0
- package/types/platform/specs/OneNativeSheetNativeComponent.d.ts.map +1 -1
- package/types/platform/specs/OneStorage.nitro.d.ts +11 -0
- package/types/platform/specs/OneStorage.nitro.d.ts.map +1 -0
- package/types/platform/storage/index.d.ts +12 -0
- package/types/platform/storage/index.d.ts.map +1 -0
- package/types/platform/storage/index.native.d.ts +12 -0
- package/types/platform/storage/index.native.d.ts.map +1 -0
- package/types/platform/storage/validate.d.ts +3 -0
- package/types/platform/storage/validate.d.ts.map +1 -0
- package/types/tamagui.d.ts +2 -0
- package/types/tamagui.d.ts.map +1 -0
- package/types/tamagui.native.d.ts +2 -0
- package/types/tamagui.native.d.ts.map +1 -0
- package/types/useHeaderHeight.d.ts +6 -0
- package/types/useHeaderHeight.d.ts.map +1 -0
- package/types/vite/plugins/fileSystemRouterPlugin.d.ts.map +1 -1
- package/types/vite/plugins/kotlinSourcePlugin.d.ts.map +1 -1
- package/android/src/main/java/com/margelo/nitro/one/HybridOneCrypto.kt +0 -23
- package/dist/cjs/platform/preferences/index.cjs +0 -61
- package/dist/cjs/platform/preferences/index.native.cjs +0 -84
- package/dist/cjs/platform/preferences/index.native.js +0 -86
- package/dist/cjs/platform/preferences/index.native.js.map +0 -1
- package/dist/cjs/platform/preferences/validate.native.js.map +0 -1
- package/dist/cjs/platform/specs/OnePreferences.nitro.native.js.map +0 -1
- package/dist/esm/platform/preferences/index.mjs +0 -42
- package/dist/esm/platform/preferences/index.mjs.map +0 -1
- package/dist/esm/platform/preferences/index.native.js +0 -63
- package/dist/esm/platform/preferences/index.native.js.map +0 -1
- package/dist/esm/platform/preferences/validate.mjs +0 -13
- package/dist/esm/platform/preferences/validate.mjs.map +0 -1
- package/dist/esm/platform/preferences/validate.native.js +0 -13
- package/dist/esm/platform/preferences/validate.native.js.map +0 -1
- package/ios/Nitro/HybridOneCrypto.swift +0 -20
- package/ios/Nitro/HybridOnePreferences.swift +0 -64
- package/nitrogen/generated/android/c++/JHybridOneCryptoSpec.cpp +0 -54
- package/nitrogen/generated/android/c++/JHybridOneCryptoSpec.hpp +0 -63
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/HybridOneCryptoSpec.kt +0 -57
- package/nitrogen/generated/ios/c++/HybridOneCryptoSpecSwift.hpp +0 -84
- package/nitrogen/generated/ios/c++/HybridOnePreferencesSpecSwift.cpp +0 -11
- package/nitrogen/generated/ios/c++/HybridOnePreferencesSpecSwift.hpp +0 -120
- package/nitrogen/generated/ios/swift/HybridOneCryptoSpec.swift +0 -55
- package/nitrogen/generated/ios/swift/HybridOneCryptoSpec_cxx.swift +0 -138
- package/nitrogen/generated/ios/swift/HybridOnePreferencesSpec.swift +0 -60
- package/nitrogen/generated/ios/swift/HybridOnePreferencesSpec_cxx.swift +0 -229
- package/nitrogen/generated/shared/c++/HybridOnePreferencesSpec.cpp +0 -26
- package/nitrogen/generated/shared/c++/HybridOnePreferencesSpec.hpp +0 -69
- package/src/platform/preferences/index.native.ts +0 -66
- package/src/platform/preferences/index.ts +0 -46
- package/src/platform/preferences/validate.ts +0 -12
- package/src/platform/specs/OnePreferences.nitro.ts +0 -10
- package/types/platform/preferences/index.d.ts +0 -16
- package/types/platform/preferences/index.d.ts.map +0 -1
- package/types/platform/preferences/index.native.d.ts +0 -16
- package/types/platform/preferences/index.native.d.ts.map +0 -1
- package/types/platform/preferences/validate.d.ts +0 -3
- package/types/platform/preferences/validate.d.ts.map +0 -1
- package/types/platform/specs/OnePreferences.nitro.d.ts +0 -12
- package/types/platform/specs/OnePreferences.nitro.d.ts.map +0 -1
- /package/dist/esm/platform/{specs/OnePreferences.nitro.mjs → native-source/viewTypes.mjs} +0 -0
- /package/dist/esm/platform/{specs/OnePreferences.nitro.native.js → native-source/viewTypes.native.js} +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
package dev.onejs.one.sharetarget
|
|
2
|
+
|
|
3
|
+
import android.content.ContentResolver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.provider.OpenableColumns
|
|
8
|
+
import android.util.Patterns
|
|
9
|
+
import java.io.File
|
|
10
|
+
import kotlinx.coroutines.CancellationException
|
|
11
|
+
import kotlinx.coroutines.Dispatchers
|
|
12
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
13
|
+
import kotlinx.coroutines.ensureActive
|
|
14
|
+
import kotlinx.coroutines.withContext
|
|
15
|
+
|
|
16
|
+
// every field is required: a caller must make an explicit, bounded decision
|
|
17
|
+
// for each limit rather than inherit an unbounded default. acceptedFileMimePrefixes
|
|
18
|
+
// may still be null (any file type accepted), but that is now a choice the
|
|
19
|
+
// caller wrote down, not a fallback.
|
|
20
|
+
data class OneShareIntakeLimits(
|
|
21
|
+
val maxItems: Int,
|
|
22
|
+
val maxItemBytes: Long,
|
|
23
|
+
val maxTotalBytes: Long,
|
|
24
|
+
// null accepts any file mime type; entries may end in "/*" as a prefix match
|
|
25
|
+
val acceptedFileMimePrefixes: Set<String>?,
|
|
26
|
+
val acceptedText: Boolean,
|
|
27
|
+
val acceptedUrls: Boolean,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
enum class OneShareIntakeIssueReason {
|
|
31
|
+
UNACCEPTED_TYPE,
|
|
32
|
+
OVER_ITEM_LIMIT,
|
|
33
|
+
OVER_SIZE_LIMIT,
|
|
34
|
+
READ_ERROR,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
data class OneShareIntakeIssue(val label: String, val reason: OneShareIntakeIssueReason)
|
|
38
|
+
|
|
39
|
+
data class OneShareIntakeResult(
|
|
40
|
+
// EXTRA_TEXT verbatim, meant for the editable composer field; never also
|
|
41
|
+
// duplicated into items.
|
|
42
|
+
val text: String,
|
|
43
|
+
val items: List<ShareItem>,
|
|
44
|
+
val truncated: Boolean,
|
|
45
|
+
val issues: List<OneShareIntakeIssue>,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// thrown for a failure that isn't a single rejected/oversized/unreadable
|
|
49
|
+
// item (those become OneShareIntakeIssue entries and the loop continues).
|
|
50
|
+
// callers must handle this rather than let it crash the host activity.
|
|
51
|
+
class OneShareIntakeException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
|
52
|
+
|
|
53
|
+
// reads an incoming ACTION_SEND/ACTION_SEND_MULTIPLE intent into a bounded,
|
|
54
|
+
// private-storage-owned result. never reads a whole stream into memory and
|
|
55
|
+
// never decodes image bytes: every file is copied in fixed-size chunks
|
|
56
|
+
// straight to disk so size limits hold regardless of what the sender claims.
|
|
57
|
+
// text/url entries count against maxItems and their utf-8 byte length counts
|
|
58
|
+
// against maxTotalBytes exactly like files do.
|
|
59
|
+
internal object OneShareIntake {
|
|
60
|
+
private const val COPY_BUFFER_BYTES = 8192
|
|
61
|
+
|
|
62
|
+
// every resolver query, stream read and file write below is blocking
|
|
63
|
+
// disk/binder i/o; this always runs on Dispatchers.IO regardless of
|
|
64
|
+
// what dispatcher the caller is on, so a caller invoking this from a
|
|
65
|
+
// Main-dispatched coroutine (as the activity's LaunchedEffect does)
|
|
66
|
+
// never blocks the UI thread on it.
|
|
67
|
+
suspend fun intake(
|
|
68
|
+
context: Context,
|
|
69
|
+
intent: Intent,
|
|
70
|
+
destinationDir: File,
|
|
71
|
+
limits: OneShareIntakeLimits,
|
|
72
|
+
): OneShareIntakeResult =
|
|
73
|
+
withContext(Dispatchers.IO) {
|
|
74
|
+
try {
|
|
75
|
+
intakeOrThrow(context, intent, destinationDir, limits)
|
|
76
|
+
} catch (e: CancellationException) {
|
|
77
|
+
throw e
|
|
78
|
+
} catch (e: Exception) {
|
|
79
|
+
throw OneShareIntakeException("Failed to read shared content: ${e.message}", e)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// no-drop: a single rejected/oversized/unreadable entry anywhere in the
|
|
84
|
+
// intent (including EXTRA_TEXT itself) aborts the whole intake rather
|
|
85
|
+
// than silently continuing without it. any files already copied in this
|
|
86
|
+
// call are deleted before returning, so a caller never sees a partial
|
|
87
|
+
// item set it could accidentally send -- the caller is expected to
|
|
88
|
+
// surface the single issue and block sending, not offer a truncated
|
|
89
|
+
// result as success.
|
|
90
|
+
private suspend fun intakeOrThrow(
|
|
91
|
+
context: Context,
|
|
92
|
+
intent: Intent,
|
|
93
|
+
destinationDir: File,
|
|
94
|
+
limits: OneShareIntakeLimits,
|
|
95
|
+
): OneShareIntakeResult {
|
|
96
|
+
val resolver = context.contentResolver
|
|
97
|
+
// exact CharSequence bytes, never trimmed: whitespace the sender put
|
|
98
|
+
// in EXTRA_TEXT is part of what the user agreed to share.
|
|
99
|
+
val rawPrimaryText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString().orEmpty()
|
|
100
|
+
|
|
101
|
+
val seenTextValues = mutableSetOf<String>()
|
|
102
|
+
val items = mutableListOf<ShareItem>()
|
|
103
|
+
val copiedFiles = mutableListOf<File>()
|
|
104
|
+
var itemCount = 0
|
|
105
|
+
var totalBytes = 0L
|
|
106
|
+
var primaryText = ""
|
|
107
|
+
|
|
108
|
+
fun abort(issue: OneShareIntakeIssue): OneShareIntakeResult {
|
|
109
|
+
copiedFiles.forEach { it.delete() }
|
|
110
|
+
return OneShareIntakeResult(text = "", items = emptyList(), truncated = true, issues = listOf(issue))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (rawPrimaryText.isNotEmpty()) {
|
|
114
|
+
val isUrl = isUrl(rawPrimaryText)
|
|
115
|
+
if (isUrl && !limits.acceptedUrls) {
|
|
116
|
+
return abort(OneShareIntakeIssue(rawPrimaryText, OneShareIntakeIssueReason.UNACCEPTED_TYPE))
|
|
117
|
+
}
|
|
118
|
+
if (!isUrl && !limits.acceptedText) {
|
|
119
|
+
return abort(OneShareIntakeIssue(rawPrimaryText, OneShareIntakeIssueReason.UNACCEPTED_TYPE))
|
|
120
|
+
}
|
|
121
|
+
val bytes = rawPrimaryText.toByteArray(Charsets.UTF_8).size.toLong()
|
|
122
|
+
if (bytes > limits.maxItemBytes || bytes > limits.maxTotalBytes) {
|
|
123
|
+
return abort(OneShareIntakeIssue(rawPrimaryText, OneShareIntakeIssueReason.OVER_SIZE_LIMIT))
|
|
124
|
+
}
|
|
125
|
+
if (limits.maxItems < 1) return abort(OneShareIntakeIssue(rawPrimaryText, OneShareIntakeIssueReason.OVER_ITEM_LIMIT))
|
|
126
|
+
itemCount += 1
|
|
127
|
+
primaryText = rawPrimaryText
|
|
128
|
+
totalBytes += bytes
|
|
129
|
+
seenTextValues.add(rawPrimaryText)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (entry in collectEntries(intent)) {
|
|
133
|
+
currentCoroutineContext().ensureActive()
|
|
134
|
+
|
|
135
|
+
when (entry) {
|
|
136
|
+
is Entry.CharSequenceEntry -> {
|
|
137
|
+
val value = entry.value
|
|
138
|
+
if (!seenTextValues.add(value)) continue
|
|
139
|
+
|
|
140
|
+
val isUrl = isUrl(value)
|
|
141
|
+
if (isUrl && !limits.acceptedUrls) {
|
|
142
|
+
return abort(OneShareIntakeIssue(value, OneShareIntakeIssueReason.UNACCEPTED_TYPE))
|
|
143
|
+
}
|
|
144
|
+
if (!isUrl && !limits.acceptedText) {
|
|
145
|
+
return abort(OneShareIntakeIssue(value, OneShareIntakeIssueReason.UNACCEPTED_TYPE))
|
|
146
|
+
}
|
|
147
|
+
if (itemCount >= limits.maxItems) {
|
|
148
|
+
return abort(OneShareIntakeIssue(value, OneShareIntakeIssueReason.OVER_ITEM_LIMIT))
|
|
149
|
+
}
|
|
150
|
+
val bytes = value.toByteArray(Charsets.UTF_8).size.toLong()
|
|
151
|
+
if (bytes > limits.maxItemBytes || totalBytes + bytes > limits.maxTotalBytes) {
|
|
152
|
+
return abort(OneShareIntakeIssue(value, OneShareIntakeIssueReason.OVER_SIZE_LIMIT))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
items.add(if (isUrl) ShareItem.Url(value) else ShareItem.Text(value))
|
|
156
|
+
itemCount += 1
|
|
157
|
+
totalBytes += bytes
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
is Entry.UriEntry -> {
|
|
161
|
+
val uri = entry.uri
|
|
162
|
+
val label = uri.toString()
|
|
163
|
+
// incoming senders must use a permission-granted provider uri.
|
|
164
|
+
// a file uri would be opened under the receiver's own uid.
|
|
165
|
+
if (uri.scheme != ContentResolver.SCHEME_CONTENT) {
|
|
166
|
+
return abort(OneShareIntakeIssue(label, OneShareIntakeIssueReason.UNACCEPTED_TYPE))
|
|
167
|
+
}
|
|
168
|
+
val mimeType = resolver.getType(uri) ?: "application/octet-stream"
|
|
169
|
+
|
|
170
|
+
if (!isAcceptedMime(mimeType, limits.acceptedFileMimePrefixes)) {
|
|
171
|
+
return abort(OneShareIntakeIssue(label, OneShareIntakeIssueReason.UNACCEPTED_TYPE))
|
|
172
|
+
}
|
|
173
|
+
if (itemCount >= limits.maxItems) {
|
|
174
|
+
return abort(OneShareIntakeIssue(label, OneShareIntakeIssueReason.OVER_ITEM_LIMIT))
|
|
175
|
+
}
|
|
176
|
+
val remainingTotal = limits.maxTotalBytes - totalBytes
|
|
177
|
+
if (remainingTotal <= 0) {
|
|
178
|
+
return abort(OneShareIntakeIssue(label, OneShareIntakeIssueReason.OVER_SIZE_LIMIT))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
val (displayName, declaredSize) = queryMetadata(resolver, uri)
|
|
182
|
+
val cap = minOf(limits.maxItemBytes, remainingTotal)
|
|
183
|
+
if (declaredSize != null && declaredSize > cap) {
|
|
184
|
+
return abort(OneShareIntakeIssue(displayName ?: label, OneShareIntakeIssueReason.OVER_SIZE_LIMIT))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
val copied = boundedCopy(resolver, uri, destinationDir, displayName, cap)
|
|
188
|
+
if (copied == null) {
|
|
189
|
+
return abort(OneShareIntakeIssue(displayName ?: label, OneShareIntakeIssueReason.READ_ERROR))
|
|
190
|
+
}
|
|
191
|
+
if (copied.hitLimit) {
|
|
192
|
+
copied.file.delete()
|
|
193
|
+
return abort(OneShareIntakeIssue(displayName ?: label, OneShareIntakeIssueReason.OVER_SIZE_LIMIT))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
copiedFiles.add(copied.file)
|
|
197
|
+
items.add(
|
|
198
|
+
ShareItem.File(
|
|
199
|
+
uri = Uri.fromFile(copied.file),
|
|
200
|
+
name = displayName ?: copied.file.name,
|
|
201
|
+
mimeType = mimeType,
|
|
202
|
+
size = copied.bytesCopied,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
itemCount += 1
|
|
206
|
+
totalBytes += copied.bytesCopied
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return OneShareIntakeResult(
|
|
212
|
+
text = primaryText,
|
|
213
|
+
items = items,
|
|
214
|
+
truncated = false,
|
|
215
|
+
issues = emptyList(),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private sealed class Entry {
|
|
220
|
+
data class UriEntry(val uri: Uri) : Entry()
|
|
221
|
+
|
|
222
|
+
data class CharSequenceEntry(val value: String) : Entry()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// EXTRA_STREAM (single/multiple) and every ClipData item: a ClipData
|
|
226
|
+
// item carries either a content uri (file) or inline text/url, and a
|
|
227
|
+
// uri already seen via EXTRA_STREAM or an earlier ClipData item is
|
|
228
|
+
// never copied twice.
|
|
229
|
+
private fun collectEntries(intent: Intent): List<Entry> {
|
|
230
|
+
val seenUris = linkedSetOf<Uri>()
|
|
231
|
+
val entries = mutableListOf<Entry>()
|
|
232
|
+
|
|
233
|
+
fun addUri(uri: Uri) {
|
|
234
|
+
if (seenUris.add(uri)) entries.add(Entry.UriEntry(uri))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
when (intent.action) {
|
|
238
|
+
Intent.ACTION_SEND -> {
|
|
239
|
+
@Suppress("DEPRECATION")
|
|
240
|
+
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let(::addUri)
|
|
241
|
+
}
|
|
242
|
+
Intent.ACTION_SEND_MULTIPLE -> {
|
|
243
|
+
@Suppress("DEPRECATION")
|
|
244
|
+
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.forEach(::addUri)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
intent.clipData?.let { clipData ->
|
|
249
|
+
for (i in 0 until clipData.itemCount) {
|
|
250
|
+
val item = clipData.getItemAt(i)
|
|
251
|
+
val uri = item.uri
|
|
252
|
+
if (uri != null) {
|
|
253
|
+
addUri(uri)
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
val text = item.text?.toString()
|
|
257
|
+
if (!text.isNullOrEmpty()) entries.add(Entry.CharSequenceEntry(text))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return entries
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fun isUrl(value: String): Boolean = Patterns.WEB_URL.matcher(value).matches()
|
|
265
|
+
|
|
266
|
+
private fun isAcceptedMime(mimeType: String, prefixes: Set<String>?): Boolean {
|
|
267
|
+
if (prefixes == null) return true
|
|
268
|
+
return prefixes.any { prefix ->
|
|
269
|
+
if (prefix.endsWith("/*")) mimeType.startsWith(prefix.removeSuffix("*"))
|
|
270
|
+
else mimeType == prefix
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private fun queryMetadata(resolver: ContentResolver, uri: Uri): Pair<String?, Long?> =
|
|
275
|
+
try {
|
|
276
|
+
resolver
|
|
277
|
+
.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), null, null, null)
|
|
278
|
+
?.use { cursor ->
|
|
279
|
+
if (!cursor.moveToFirst()) return@use null to null
|
|
280
|
+
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
281
|
+
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
282
|
+
val name = if (nameIdx >= 0) cursor.getString(nameIdx) else null
|
|
283
|
+
val size = if (sizeIdx >= 0 && !cursor.isNull(sizeIdx)) cursor.getLong(sizeIdx) else null
|
|
284
|
+
name to size
|
|
285
|
+
} ?: (null to null)
|
|
286
|
+
} catch (_: Exception) {
|
|
287
|
+
null to null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private data class CopyResult(val file: File, val bytesCopied: Long, val hitLimit: Boolean)
|
|
291
|
+
|
|
292
|
+
// streams uri -> destinationDir in COPY_BUFFER_BYTES chunks, checking
|
|
293
|
+
// cancellation and the byte cap every chunk; a cancelled coroutine or a
|
|
294
|
+
// cap overrun both leave the partial file for the caller to delete.
|
|
295
|
+
private suspend fun boundedCopy(
|
|
296
|
+
resolver: ContentResolver,
|
|
297
|
+
uri: Uri,
|
|
298
|
+
destinationDir: File,
|
|
299
|
+
displayName: String?,
|
|
300
|
+
capBytes: Long,
|
|
301
|
+
): CopyResult? {
|
|
302
|
+
val input = try {
|
|
303
|
+
resolver.openInputStream(uri)
|
|
304
|
+
} catch (_: Exception) {
|
|
305
|
+
null
|
|
306
|
+
} ?: return null
|
|
307
|
+
|
|
308
|
+
destinationDir.mkdirs()
|
|
309
|
+
val file = File(destinationDir, uniqueFileName(displayName))
|
|
310
|
+
return try {
|
|
311
|
+
input.use { stream ->
|
|
312
|
+
file.outputStream().use { out ->
|
|
313
|
+
val buffer = ByteArray(COPY_BUFFER_BYTES)
|
|
314
|
+
var total = 0L
|
|
315
|
+
while (true) {
|
|
316
|
+
currentCoroutineContext().ensureActive()
|
|
317
|
+
val read = stream.read(buffer)
|
|
318
|
+
if (read < 0) break
|
|
319
|
+
total += read
|
|
320
|
+
if (total > capBytes) {
|
|
321
|
+
return@use CopyResult(file, total, true)
|
|
322
|
+
}
|
|
323
|
+
out.write(buffer, 0, read)
|
|
324
|
+
}
|
|
325
|
+
CopyResult(file, total, false)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (e: Exception) {
|
|
329
|
+
file.delete()
|
|
330
|
+
if (e is CancellationException) throw e
|
|
331
|
+
null
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private fun uniqueFileName(displayName: String?): String {
|
|
336
|
+
val sanitized = displayName
|
|
337
|
+
?.replace(Regex("[^A-Za-z0-9._-]"), "_")
|
|
338
|
+
?.trim('.', '_')
|
|
339
|
+
?.takeLast(120)
|
|
340
|
+
?.takeIf { it.isNotEmpty() }
|
|
341
|
+
?: "item"
|
|
342
|
+
return "${System.nanoTime()}-$sanitized"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package dev.onejs.one.sharetarget
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
|
|
5
|
+
// a place the user can send a share submission to (an account, a channel,
|
|
6
|
+
// a conversation...); the generated adapter decides what these mean.
|
|
7
|
+
data class ShareDestination(
|
|
8
|
+
val id: String,
|
|
9
|
+
val title: String,
|
|
10
|
+
val subtitle: String? = null,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
sealed class ShareItem {
|
|
14
|
+
data class Text(val value: String) : ShareItem()
|
|
15
|
+
|
|
16
|
+
data class Url(val value: String) : ShareItem()
|
|
17
|
+
|
|
18
|
+
// uri points at a file this process owns (copied into private storage
|
|
19
|
+
// by OneShareIntake), never at the original content:// uri the sender
|
|
20
|
+
// granted only for the lifetime of the share intent.
|
|
21
|
+
data class File(
|
|
22
|
+
val uri: Uri,
|
|
23
|
+
val name: String,
|
|
24
|
+
val mimeType: String,
|
|
25
|
+
val size: Long,
|
|
26
|
+
) : ShareItem()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// id is the stable draft id, stable across rotation/process recreation so
|
|
30
|
+
// the adapter and any retry logic can recognize a resubmission.
|
|
31
|
+
data class ShareSubmission(
|
|
32
|
+
val id: String,
|
|
33
|
+
val destinationId: String,
|
|
34
|
+
val text: String,
|
|
35
|
+
val items: List<ShareItem>,
|
|
36
|
+
)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
package dev.onejs.one.sharetarget
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import androidx.lifecycle.ViewModel
|
|
6
|
+
import java.util.UUID
|
|
7
|
+
import kotlinx.coroutines.CancellationException
|
|
8
|
+
import kotlinx.coroutines.CoroutineScope
|
|
9
|
+
import kotlinx.coroutines.Dispatchers
|
|
10
|
+
import kotlinx.coroutines.Job
|
|
11
|
+
import kotlinx.coroutines.SupervisorJob
|
|
12
|
+
import kotlinx.coroutines.cancel
|
|
13
|
+
import kotlinx.coroutines.cancelAndJoin
|
|
14
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
15
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
16
|
+
import kotlinx.coroutines.launch
|
|
17
|
+
import kotlinx.coroutines.sync.Mutex
|
|
18
|
+
import kotlinx.coroutines.sync.withLock
|
|
19
|
+
import kotlinx.coroutines.withContext
|
|
20
|
+
|
|
21
|
+
internal enum class SharePhase { LOADING, READY, FAILED, SENDING, CANCELLING, DELIVERED, CANCELLED }
|
|
22
|
+
|
|
23
|
+
internal data class ShareSessionState(
|
|
24
|
+
val id: String = "",
|
|
25
|
+
val phase: SharePhase = SharePhase.LOADING,
|
|
26
|
+
val text: String = "",
|
|
27
|
+
val items: List<ShareItem> = emptyList(),
|
|
28
|
+
val destinations: List<ShareDestination> = emptyList(),
|
|
29
|
+
val destinationId: String? = null,
|
|
30
|
+
val error: String? = null,
|
|
31
|
+
val pendingDelivery: Boolean = false,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// the activity only renders this retained owner. rotation never creates a
|
|
35
|
+
// second sender or loses the phase of a submission already in flight.
|
|
36
|
+
internal class OneShareSessionModel(
|
|
37
|
+
private val context: Context,
|
|
38
|
+
private val adapter: OneShareTargetAdapter,
|
|
39
|
+
private val limits: OneShareIntakeLimits,
|
|
40
|
+
) : ViewModel() {
|
|
41
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
|
42
|
+
private val store = OneShareDraftStore(context)
|
|
43
|
+
private val writes = Mutex()
|
|
44
|
+
private val mutableState = MutableStateFlow(ShareSessionState())
|
|
45
|
+
val state: StateFlow<ShareSessionState> = mutableState
|
|
46
|
+
private var loading: Job? = null
|
|
47
|
+
private val transitions = Mutex()
|
|
48
|
+
private var incoming: Intent? = null
|
|
49
|
+
|
|
50
|
+
fun open(intent: Intent, restoredId: String? = null) {
|
|
51
|
+
if (state.value.id.isNotEmpty()) return
|
|
52
|
+
start(intent, restoredId ?: UUID.randomUUID().toString())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun receive(intent: Intent) {
|
|
56
|
+
scope.launch { transitions.withLock {
|
|
57
|
+
val previous = state.value
|
|
58
|
+
if (previous.phase == SharePhase.LOADING || previous.phase == SharePhase.FAILED) {
|
|
59
|
+
loading?.cancelAndJoin()
|
|
60
|
+
// persist a completed intake; remove only an incomplete one.
|
|
61
|
+
writes.withLock {
|
|
62
|
+
withContext(Dispatchers.IO) {
|
|
63
|
+
if (store.load(previous.id) == null) store.delete(previous.id)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
start(intent, UUID.randomUUID().toString())
|
|
68
|
+
} }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun start(intent: Intent, id: String) {
|
|
72
|
+
incoming = intent
|
|
73
|
+
mutableState.value = ShareSessionState(id = id)
|
|
74
|
+
load()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fun retry() {
|
|
78
|
+
if (state.value.phase != SharePhase.FAILED) return
|
|
79
|
+
mutableState.value = state.value.copy(phase = SharePhase.LOADING, error = null)
|
|
80
|
+
load()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun load() {
|
|
84
|
+
val id = state.value.id
|
|
85
|
+
val intent = incoming
|
|
86
|
+
loading = scope.launch {
|
|
87
|
+
try {
|
|
88
|
+
val restored = withContext(Dispatchers.IO) { store.load(id) }
|
|
89
|
+
if (restored != null) {
|
|
90
|
+
mutableState.value = state.value.copy(text = restored.editedText, items = restored.items,
|
|
91
|
+
destinationId = restored.destinationId, pendingDelivery = restored.pendingDelivery)
|
|
92
|
+
} else {
|
|
93
|
+
val directory = withContext(Dispatchers.IO) {
|
|
94
|
+
store.clearItems(id)
|
|
95
|
+
store.itemsDirectory(id)
|
|
96
|
+
}
|
|
97
|
+
val result = OneShareIntake.intake(context, requireNotNull(intent), directory, limits)
|
|
98
|
+
require(result.issues.isEmpty()) { result.issues.joinToString { "${it.label}: ${it.reason}" } }
|
|
99
|
+
mutableState.value = state.value.copy(text = result.text, items = result.items)
|
|
100
|
+
persist(state.value)
|
|
101
|
+
}
|
|
102
|
+
val destinations = adapter.destinations()
|
|
103
|
+
val current = state.value
|
|
104
|
+
if (current.id != id) return@launch
|
|
105
|
+
require(!current.pendingDelivery || destinations.any { it.id == current.destinationId }) {
|
|
106
|
+
"The original destination is unavailable. Retry when it reconnects."
|
|
107
|
+
}
|
|
108
|
+
mutableState.value = current.copy(phase = SharePhase.READY, destinations = destinations,
|
|
109
|
+
destinationId = current.destinationId?.takeIf { selected -> destinations.any { it.id == selected } }
|
|
110
|
+
?: destinations.firstOrNull()?.id, error = if (current.pendingDelivery) "Earlier delivery may have been accepted. Retry sends the same submission." else null)
|
|
111
|
+
persist(state.value)
|
|
112
|
+
} catch (error: CancellationException) {
|
|
113
|
+
throw error
|
|
114
|
+
} catch (error: Exception) {
|
|
115
|
+
if (state.value.id == id) mutableState.value = state.value.copy(phase = SharePhase.FAILED, error = error.message)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun edit(text: String) {
|
|
121
|
+
if (state.value.phase != SharePhase.READY || state.value.pendingDelivery) return
|
|
122
|
+
mutableState.value = state.value.copy(text = text)
|
|
123
|
+
saveEdit()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fun select(id: String) {
|
|
127
|
+
if (state.value.phase != SharePhase.READY || state.value.pendingDelivery || state.value.destinations.none { it.id == id }) return
|
|
128
|
+
mutableState.value = state.value.copy(destinationId = id)
|
|
129
|
+
saveEdit()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun saveEdit() {
|
|
133
|
+
val snapshot = state.value
|
|
134
|
+
scope.launch {
|
|
135
|
+
try { persist(snapshot) }
|
|
136
|
+
catch (error: Exception) {
|
|
137
|
+
if (state.value.id == snapshot.id && state.value.phase == SharePhase.READY)
|
|
138
|
+
mutableState.value = state.value.copy(phase = SharePhase.FAILED, error = error.message)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private suspend fun persist(snapshot: ShareSessionState, sending: Boolean = false) = writes.withLock {
|
|
144
|
+
// a queued edit cannot write after the send or cancellation barrier.
|
|
145
|
+
val current = state.value
|
|
146
|
+
if (!sending && (current.id != snapshot.id || current.phase == SharePhase.SENDING ||
|
|
147
|
+
current.phase == SharePhase.CANCELLING || current.phase == SharePhase.DELIVERED || current.phase == SharePhase.CANCELLED)) return@withLock
|
|
148
|
+
withContext(Dispatchers.IO) {
|
|
149
|
+
store.save(StoredShareDraft(snapshot.id, snapshot.destinationId, snapshot.text, snapshot.items, sending))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fun canSend(snapshot: ShareSessionState = state.value): Boolean {
|
|
154
|
+
val bytes = snapshot.text.toByteArray(Charsets.UTF_8).size.toLong()
|
|
155
|
+
val itemBytes = snapshot.items.sumOf { when (it) {
|
|
156
|
+
is ShareItem.File -> it.size
|
|
157
|
+
is ShareItem.Text -> it.value.toByteArray(Charsets.UTF_8).size.toLong()
|
|
158
|
+
is ShareItem.Url -> it.value.toByteArray(Charsets.UTF_8).size.toLong()
|
|
159
|
+
} }
|
|
160
|
+
return snapshot.phase == SharePhase.READY && snapshot.destinationId != null &&
|
|
161
|
+
snapshot.destinations.any { it.id == snapshot.destinationId } &&
|
|
162
|
+
bytes <= limits.maxItemBytes && bytes + itemBytes <= limits.maxTotalBytes &&
|
|
163
|
+
snapshot.items.size + (if (snapshot.text.isEmpty()) 0 else 1) <= limits.maxItems &&
|
|
164
|
+
(snapshot.text.isNotEmpty() || snapshot.items.isNotEmpty())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun send() {
|
|
168
|
+
val snapshot = state.value
|
|
169
|
+
if (!canSend(snapshot)) return
|
|
170
|
+
mutableState.value = snapshot.copy(phase = SharePhase.SENDING, error = null)
|
|
171
|
+
scope.launch {
|
|
172
|
+
try {
|
|
173
|
+
// mark the attempt durably before transport; process recovery
|
|
174
|
+
// keeps this id so a retry uses the adapter's idempotency key.
|
|
175
|
+
persist(snapshot, sending = true)
|
|
176
|
+
if (state.value.id == snapshot.id) mutableState.value = state.value.copy(pendingDelivery = true)
|
|
177
|
+
adapter.send(ShareSubmission(snapshot.id, requireNotNull(snapshot.destinationId), snapshot.text, snapshot.items))
|
|
178
|
+
writes.withLock { withContext(Dispatchers.IO) { store.delete(snapshot.id) } }
|
|
179
|
+
if (state.value.id == snapshot.id) mutableState.value = state.value.copy(phase = SharePhase.DELIVERED)
|
|
180
|
+
} catch (error: CancellationException) {
|
|
181
|
+
throw error
|
|
182
|
+
} catch (error: Exception) {
|
|
183
|
+
if (state.value.id == snapshot.id) mutableState.value = state.value.copy(phase = SharePhase.READY, error = error.message)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fun discard() {
|
|
189
|
+
val snapshot = state.value
|
|
190
|
+
if (snapshot.pendingDelivery || snapshot.phase == SharePhase.SENDING || snapshot.phase == SharePhase.CANCELLING ||
|
|
191
|
+
snapshot.phase == SharePhase.DELIVERED || snapshot.phase == SharePhase.CANCELLED) return
|
|
192
|
+
mutableState.value = snapshot.copy(phase = SharePhase.CANCELLING)
|
|
193
|
+
scope.launch {
|
|
194
|
+
try {
|
|
195
|
+
loading?.cancelAndJoin()
|
|
196
|
+
writes.withLock { withContext(Dispatchers.IO) { store.delete(snapshot.id) } }
|
|
197
|
+
if (state.value.id == snapshot.id) mutableState.value = state.value.copy(phase = SharePhase.CANCELLED)
|
|
198
|
+
} catch (error: CancellationException) {
|
|
199
|
+
throw error
|
|
200
|
+
} catch (error: Exception) {
|
|
201
|
+
if (state.value.id == snapshot.id) mutableState.value = state.value.copy(phase = SharePhase.FAILED, error = error.message)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
override fun onCleared() { scope.cancel() }
|
|
207
|
+
}
|