diff --git a/Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings b/Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings new file mode 100644 index 0000000..46341dc --- /dev/null +++ b/Sources/SkipAndroidBridgeSamples/Resources/Localizable.xcstrings @@ -0,0 +1,20 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "literal" : { + + }, + "localized" : { + "comment" : "localized string", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Localized into English" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift b/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift index c4b313e..8d964e6 100644 --- a/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift +++ b/Sources/SkipAndroidBridgeSamples/SkipAndroidBridgeSamples.swift @@ -8,6 +8,8 @@ import SwiftJNI import AndroidNative #endif +let logger: Logger = Logger(subsystem: "SkipAndroidBridgeSamples", category: "Samples") + public let swiftStringConstant = "s" public func getStringValue(_ string: String?) -> String? { @@ -50,6 +52,10 @@ public func localizedStringResourceInterpolatedKey() -> String { return interpolation.key } +public func localizedStringValueNS() -> String { + NSLocalizedString("localized", bundle: Bundle.module, comment: "localized string") +} + public func mainActorAsyncValue() async -> String { await Task.detached { await MainActorClass().mainActorValue() @@ -72,3 +78,37 @@ public func nativeAndroidContextPackageName() throws -> String? { "MainActor!" } } + +public typealias MainActorCallback = @MainActor () async -> () + +public struct MainActorCallbacks: @unchecked Sendable { + let callbackMainActor: MainActorCallback + + public init(callbackMainActor: @escaping MainActorCallback) { + self.callbackMainActor = callbackMainActor + } +} + +// disabling causes a hang when running tests +/*@MainActor*/ public class MainActorCallbackModel { + public static let shared = MainActorCallbackModel() + var callbacks: MainActorCallbacks? + + public init(callbacks: MainActorCallbacks? = nil) { + self.callbacks = callbacks + } + + public func setCallbacks(_ callbacks: MainActorCallbacks) { + logger.log("setting callbacks on thread: \(Thread.current)") + self.callbacks = callbacks + logger.log("done setting callbacks: \(String(describing: callbacks.callbackMainActor))") + } + + public func doSomething() async { + logger.log("calling callbacks on thread: \(Thread.current)") + //try? await Task.sleep(for: .seconds(2)) + //logger.log("calling callbacks: done sleeping") + await callbacks?.callbackMainActor() + logger.log("calling callbacks: done") + } +} diff --git a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift index 17b10c7..61541c9 100644 --- a/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift +++ b/Tests/SkipAndroidBridgeSamplesTests/SkipAndroidBridgeSamplesTests.swift @@ -5,18 +5,41 @@ import SkipBridge import SkipAndroidBridge import SkipAndroidBridgeSamples import XCTest +#if SKIP +import androidx.test.platform.app.InstrumentationRegistry +#endif + +let logger: Logger = Logger(subsystem: "SkipAndroidBridgeSamplesTests", category: "Tests") final class SkipAndroidBridgeSamplesTests: XCTestCase { + static var wasSetupOnce = false + static var wasSetupOnMainThread: Bool? = nil + override func setUp() { + super.setUp() + #if SKIP + if Self.wasSetupOnce { + return // already set up once + } + Self.wasSetupOnce = true + loadPeerLibrary(packageName: "skip-android-bridge", moduleName: "SkipAndroidBridgeSamples") //try AndroidBridge.initBridge("SkipAndroidBridgeSamples") // doesn't work on Robolectric - let context = ProcessInfo.processInfo.androidContext - try AndroidBridgeBootstrap.initAndroidBridge(filesDir: context.getFilesDir().getAbsolutePath(), cacheDir: context.getCacheDir().getAbsolutePath()) + // we need to run this synchronously on the main thread in order for initAndroidBridge to setup the main looper properly + // androidx.test.runner.AndroidJUnitRunner,5,main] + InstrumentationRegistry.getInstrumentation().runOnMainSync { + logger.info("setting up tests on thread=\(java.lang.Thread.currentThread()) vs. mainLooper thread=\(android.os.Looper.getMainLooper().getThread())") + Self.wasSetupOnMainThread = java.lang.Thread.currentThread() == android.os.Looper.getMainLooper().getThread() + let context = ProcessInfo.processInfo.androidContext + try AndroidBridgeBootstrap.initAndroidBridge(filesDir: context.getFilesDir().getAbsolutePath(), cacheDir: context.getCacheDir().getAbsolutePath()) + } #endif + + logger.info("setup complete") } func testSimpleConstants() { @@ -39,7 +62,7 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { func testResourceURL() throws { if isRobolectric { // unwrap fails on Robolectric - throw XCTSkip("unknown error on Robolectric") + throw XCTSkip("bridged assets not working on Robolectric") } let url = try XCTUnwrap(getAssetURL(named: "sample.json")) @@ -87,6 +110,10 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { func testAndroidContext() throws { if !isAndroid { + throw XCTSkip("test only runs on Android") + } + + if isRobolectric { throw XCTSkip("no package name on Robolectric") } @@ -99,9 +126,49 @@ final class SkipAndroidBridgeSamplesTests: XCTestCase { XCTAssertEqual(localizedStringResourceInterpolatedKey(), "interpolated %lld!") } - // not working yet… -// func testMainActorAsync() async throws { -// let value = await mainActorAsyncValue() -// XCTAssertEqual("MainActor!", value) -// } + func testLocalizedStringNS() throws { + if isRobolectric { + throw XCTSkip("bridged localized strings not working on Robolectric") + } + + if !isJava && ProcessInfo.processInfo.environment["XCODE_SCHEME_NAME"] == nil { + // we guard for !isJava because on CI we are running using the OSS Swift toolchain, which doesn't process .xcstrings files + // this _will_ pass when running using Xcode's swift version, but AFAIK there isn't any way to check for that at runtime other than checking for an environment variable that is usually set by Xcode + throw XCTSkip("xcstrings not working on Swift OSS toolchain") + } + + XCTAssertEqual(localizedStringValueNS(), "Localized into English") + } + + func testMainActorAsync() async throws { + #if SKIP + XCTAssertEqual(true, Self.wasSetupOnMainThread, "test case was not initialized on the main thread") + + if isAndroid { + throw XCTSkip("test hangs on Android") + } + #endif + + // test hangs on Android emulator for some reason + let value = await mainActorAsyncValue() + XCTAssertEqual("MainActor!", value) + } + + func testMainActorCallback() async throws { + var counter = 0 + logger.log("setting callbacks") + let callbacks = MainActorCallbacks(callbackMainActor: { + try? await Task.sleep(nanoseconds: 1000) + counter += 1 + }) + MainActorCallbackModel.shared.setCallbacks(callbacks) + logger.log("setting callbacks: done") + + XCTAssertEqual(0, counter) + logger.log("calling callbacks") + await MainActorCallbackModel.shared.doSomething() + logger.log("calling callbacks: done") + _ = callbacks + XCTAssertEqual(1, counter) + } }