diff --git a/README.md b/README.md index 90f1151..92d4bde 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ So `model="andy"` loads `models/andy.obj` on Android and `andy.usdz` on iOS. | Function | Returns | Description | |---|---|---| -| `initialize()` | `Promise` | On Android, checks ARCore availability without opening installation UI: `true` if supported and installed, `false` if unsupported, missing, or outdated; rejects on errors/timeouts. iOS currently resolves `true`. | +| `initialize()` | `Promise` | On Android, now prompts for ARCore install/update when needed and waits for the result: `true` when ready, `false` if unsupported or installation is cancelled/incomplete; rejects on errors. Call from a resumed Activity. iOS currently resolves `true`. | | `isDepthModeSupported()` | `boolean` | Whether the device supports depth. | | `isGeospatialModeSupported()` | `boolean` | Whether the device supports geospatial mode. | @@ -358,7 +358,7 @@ Status by platform. Items shared by both platforms (e.g. `initialize()`, geospat - [ ] Face-mesh overlay (`debugShowFaceMesh`) - [ ] Face-texture overlay (`faceTextureURI`) - [ ] `` `texture` prop -- [x] Android ARCore availability check in `initialize()` +- [x] Android ARCore availability check and install/update flow in `initialize()` - [ ] Blend shapes — not exposed by ARCore (iOS-only capability) ### iOS (ARKit / RealityKit) diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 0b4b9f2..e23cca1 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(${PACKAGE_NAME} SHARED ../cpp/Texture.cpp ../cpp/utils.cpp ../cpp/AndroidPlatformServices.cpp + ../cpp/ARCoreInitialization.cpp ../cpp/ARSessionManager.cpp ) diff --git a/android/src/main/cpp/cpp-adapter.cpp b/android/src/main/cpp/cpp-adapter.cpp index 0379873..07f6e41 100644 --- a/android/src/main/cpp/cpp-adapter.cpp +++ b/android/src/main/cpp/cpp-adapter.cpp @@ -2,10 +2,12 @@ #include #include "arcoreOnLoad.hpp" #include "ARApplicationWrapper.h" +#include "ARCoreAvailability.h" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { facebook::jni::initialize(vm, [] { ARApplicationWrapper::registerNatives(); + arcore::registerARCoreInitializationNatives(); }); return margelo::nitro::arcore::initialize(vm); } diff --git a/android/src/main/java/com/margelo/nitro/arcore/ARAppSystem.kt b/android/src/main/java/com/margelo/nitro/arcore/ARAppSystem.kt index 043e7ce..2cb3157 100644 --- a/android/src/main/java/com/margelo/nitro/arcore/ARAppSystem.kt +++ b/android/src/main/java/com/margelo/nitro/arcore/ARAppSystem.kt @@ -119,7 +119,7 @@ class ARAppSystem { if (isSessionInitialized()) { ARViewRegistry.emitSessionType(getActiveSessionType()) ARViewRegistry.emitSessionState("ready") - } else { + } else if (!ARCoreLifecycle.isInitializationPending()) { ARViewRegistry.emitSessionState("failed") } } diff --git a/android/src/main/java/com/margelo/nitro/arcore/ARCoreLifecycle.kt b/android/src/main/java/com/margelo/nitro/arcore/ARCoreLifecycle.kt new file mode 100644 index 0000000..6e8c261 --- /dev/null +++ b/android/src/main/java/com/margelo/nitro/arcore/ARCoreLifecycle.kt @@ -0,0 +1,95 @@ +package com.margelo.nitro.arcore + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.annotation.Keep +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.common.LifecycleState +import java.lang.ref.WeakReference +import java.util.WeakHashMap +import java.util.concurrent.atomic.AtomicLong + +@Keep +class ARCoreLifecycle private constructor(private val context: ReactApplicationContext) : + ReactContextBaseJavaModule(context), LifecycleEventListener { + private val runtimeId = nextRuntime.getAndIncrement() + private var host = WeakReference(null) + @Volatile private var invalidated = false + + init { + handler.post { + if (!invalidated) { + CurrentActivityTracker.register(context.applicationContext as Application) + registerRuntimeNative(runtimeId, context.applicationContext) + context.addLifecycleEventListener(this) + if (context.lifecycleState == LifecycleState.RESUMED) onHostResume() + } + } + } + + override fun getName() = "ARCoreLifecycle" + override fun getConstants(): Map = mapOf("runtimeId" to runtimeId.toDouble()) + + override fun onHostResume() { + if (invalidated) return + val activity = context.currentActivity ?: return + host = WeakReference(activity) + CurrentActivityTracker.observeHost(activity, runtimeId) + } + + override fun onHostPause() { + host.get()?.let { CurrentActivityTracker.pauseHost(it) } + } + + override fun onHostDestroy() { + host.get()?.let { CurrentActivityTracker.destroyHost(it) } + host.clear() + } + + override fun invalidate() { + invalidated = true + handler.post { + context.removeLifecycleEventListener(this) + removeRuntimeNative(runtimeId) + host.clear() + synchronized(modules) { + if (modules[context]?.get() === this) modules.remove(context) + } + } + super.invalidate() + } + + companion object { + private val handler = Handler(Looper.getMainLooper()) + private val nextRuntime = AtomicLong(1) + private val modules = WeakHashMap>() + + fun get(context: ReactApplicationContext): ARCoreLifecycle = synchronized(modules) { + modules[context]?.get()?.takeUnless { it.invalidated } + ?: ARCoreLifecycle(context).also { modules[context] = WeakReference(it) } + } + + @JvmStatic fun assertMainThread() { + check(Looper.myLooper() == Looper.getMainLooper()) + } + @JvmStatic fun post(action: Runnable, delayMs: Long) { + check(handler.postDelayed(action, delayMs)) { "Android main looper is unavailable" } + } + @JvmStatic fun cancel(action: Runnable) { handler.removeCallbacks(action) } + @JvmStatic fun notifyViews(taskId: Int, ready: Boolean, error: String) { + ARViewRegistry.onInitializationComplete(taskId, ready, error) + } + + @JvmStatic private external fun registerRuntimeNative(runtimeId: Long, context: Context) + @JvmStatic private external fun removeRuntimeNative(runtimeId: Long) + @JvmStatic external fun hostResumedNative(runtimeId: Long, taskId: Int, activity: Activity) + @JvmStatic external fun hostPausedNative(taskId: Int, activity: Activity) + @JvmStatic external fun hostDestroyedNative(taskId: Int, activity: Activity, changing: Boolean, finishing: Boolean) + @JvmStatic external fun isInitializationPending(): Boolean + } +} diff --git a/android/src/main/java/com/margelo/nitro/arcore/ARViewRegistry.kt b/android/src/main/java/com/margelo/nitro/arcore/ARViewRegistry.kt index 71b9d7f..ee98a9c 100644 --- a/android/src/main/java/com/margelo/nitro/arcore/ARViewRegistry.kt +++ b/android/src/main/java/com/margelo/nitro/arcore/ARViewRegistry.kt @@ -60,6 +60,11 @@ object ARViewRegistry { } /** Resumes GL rendering on every mounted view (app returning to foreground). */ + fun onInitializationComplete(taskId: Int, ready: Boolean, error: String) { + val snapshot: List = synchronized(this) { views.toList() } + snapshot.forEach { it.onInitializationComplete(taskId, ready, error) } + } + fun resumeGl() { val snapshot: List = synchronized(this) { views.toList() } snapshot.forEach { it.resumeGl() } diff --git a/android/src/main/java/com/margelo/nitro/arcore/ArCorePackage.kt b/android/src/main/java/com/margelo/nitro/arcore/ArCorePackage.kt index 8307a53..7083b54 100644 --- a/android/src/main/java/com/margelo/nitro/arcore/ArCorePackage.kt +++ b/android/src/main/java/com/margelo/nitro/arcore/ArCorePackage.kt @@ -9,7 +9,7 @@ import com.margelo.nitro.arcore.views.HybridARViewHybridManager class ArCorePackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { - return emptyList() + return listOf(ARCoreLifecycle.get(reactContext)) } override fun createViewManagers(reactContext: ReactApplicationContext): List> { diff --git a/android/src/main/java/com/margelo/nitro/arcore/CurrentActivityTracker.java b/android/src/main/java/com/margelo/nitro/arcore/CurrentActivityTracker.java index 69f913d..4a6a609 100644 --- a/android/src/main/java/com/margelo/nitro/arcore/CurrentActivityTracker.java +++ b/android/src/main/java/com/margelo/nitro/arcore/CurrentActivityTracker.java @@ -4,27 +4,51 @@ import android.app.Application; import android.os.Bundle; import java.lang.ref.WeakReference; +import java.util.WeakHashMap; public class CurrentActivityTracker implements Application.ActivityLifecycleCallbacks { - private static WeakReference mCurrentActivity = new WeakReference<>(null); + private static final CurrentActivityTracker INSTANCE = new CurrentActivityTracker(); + private static final WeakHashMap hosts = new WeakHashMap<>(); + private static WeakReference current = new WeakReference<>(null); + private static boolean registered; - public static void register(Application app) { - app.registerActivityLifecycleCallbacks(new CurrentActivityTracker()); + public static synchronized void register(Application app) { + if (registered) return; + app.registerActivityLifecycleCallbacks(INSTANCE); + registered = true; } - public static Activity getCurrentActivity() { - return mCurrentActivity.get(); + public static Activity getCurrentActivity() { return current.get(); } + + public static void observeHost(Activity activity, long runtimeId) { + ARCoreLifecycle.assertMainThread(); + hosts.put(activity, true); + current = new WeakReference<>(activity); + ARCoreLifecycle.hostResumedNative(runtimeId, activity.getTaskId(), activity); + } + + public static void pauseHost(Activity activity) { + ARCoreLifecycle.assertMainThread(); + if (!hosts.containsKey(activity)) return; + if (current.get() == activity) current.clear(); + ARCoreLifecycle.hostPausedNative(activity.getTaskId(), activity); } - @Override - public void onActivityResumed(Activity activity) { - mCurrentActivity = new WeakReference<>(activity); + public static void destroyHost(Activity activity) { + ARCoreLifecycle.assertMainThread(); + if (hosts.remove(activity) == null) return; + if (current.get() == activity) current.clear(); + ARCoreLifecycle.hostDestroyedNative(activity.getTaskId(), activity, + activity.isChangingConfigurations(), activity.isFinishing()); } - @Override public void onActivityPaused(Activity activity) {} - @Override public void onActivityCreated(Activity a, Bundle b) {} - @Override public void onActivityStarted(Activity a) {} - @Override public void onActivityStopped(Activity a) {} - @Override public void onActivitySaveInstanceState(Activity a, Bundle b) {} - @Override public void onActivityDestroyed(Activity a) {} + @Override public void onActivityResumed(Activity activity) { + if (hosts.containsKey(activity)) observeHost(activity, 0); + } + @Override public void onActivityPaused(Activity activity) { pauseHost(activity); } + @Override public void onActivityDestroyed(Activity activity) { destroyHost(activity); } + @Override public void onActivityCreated(Activity activity, Bundle state) {} + @Override public void onActivityStarted(Activity activity) {} + @Override public void onActivityStopped(Activity activity) {} + @Override public void onActivitySaveInstanceState(Activity activity, Bundle state) {} } diff --git a/android/src/main/java/com/margelo/nitro/arcore/HybridARView.kt b/android/src/main/java/com/margelo/nitro/arcore/HybridARView.kt index 4098bfb..9936024 100644 --- a/android/src/main/java/com/margelo/nitro/arcore/HybridARView.kt +++ b/android/src/main/java/com/margelo/nitro/arcore/HybridARView.kt @@ -33,7 +33,6 @@ class HybridARView(private val reactContext: com.facebook.react.uimanager.Themed private var viewAttached = false init { - CurrentActivityTracker.register(reactContext.applicationContext as android.app.Application) app.ensureInitialized(reactContext.applicationContext) registerAppLifecycleObserver(reactContext.applicationContext) } @@ -281,10 +280,26 @@ class HybridARView(private val reactContext: com.facebook.react.uimanager.Themed glSurfaceView.onResume() } + fun onInitializationComplete(taskId: Int, ready: Boolean, error: String) { + if (!viewAttached || !reactContext.hasActiveReactInstance()) return + val activity = reactContext.currentActivity ?: return + if (activity.taskId != taskId) return + if (ready) { + app.onResume(activity.applicationContext, activity) + glSurfaceView.onResume() + } else { + onSessionStateChange?.invoke("failed") + if (error.isNotEmpty()) onARCoreError?.invoke(ARError("initialization_failed", error)) + } + } + override fun cameraPermissionGranted() { - val activity = reactContext.currentActivity - ?: CurrentActivityTracker.getCurrentActivity() ?: return - app.onResume(activity.applicationContext, activity) + mainHandler.post { + if (!viewAttached) return@post + val activity = reactContext.currentActivity + ?: CurrentActivityTracker.getCurrentActivity() ?: return@post + app.onResume(activity.applicationContext, activity) + } } // Recreates the session when the augmentation type or camera facing changes at diff --git a/cpp/ARApplication.h b/cpp/ARApplication.h index ad7f7fc..fe66178 100644 --- a/cpp/ARApplication.h +++ b/cpp/ARApplication.h @@ -1066,7 +1066,6 @@ namespace arcore const ArFrame *frame); ArFrame *ar_frame_ = nullptr; - bool install_requested_ = false; bool calculate_uv_transform_ = false; int width_ = 1; int height_ = 1; diff --git a/cpp/ARCoreAvailability.h b/cpp/ARCoreAvailability.h index cd52e43..b138e94 100644 --- a/cpp/ARCoreAvailability.h +++ b/cpp/ARCoreAvailability.h @@ -1,5 +1,9 @@ #pragma once +#include +#include + namespace arcore { -bool checkARCoreAvailability(); +std::shared_ptr> initializeARCore(int64_t runtimeId); +void registerARCoreInitializationNatives(); } diff --git a/cpp/ARCoreInitialization.cpp b/cpp/ARCoreInitialization.cpp new file mode 100644 index 0000000..178b461 --- /dev/null +++ b/cpp/ARCoreInitialization.cpp @@ -0,0 +1,205 @@ +#include "ARCoreInitialization.h" +#include +#include +#include + +namespace arcore { +ARCoreInitialization::ARCoreInitialization( + IPlatformServices& platform, IInitializationScheduler& scheduler, + std::function notifyViews) + : platform_(platform), scheduler_(scheduler), notifyViews_(std::move(notifyViews)) {} + +void ARCoreInitialization::registerRuntime(RuntimeId runtime) { + scheduler_.assertMainThread(); + runtimes_.try_emplace(runtime, std::nullopt); +} + +void ARCoreInitialization::removeRuntime(RuntimeId runtime) { + scheduler_.assertMainThread(); + runtimes_.erase(runtime); + std::vector removed; + for (auto it = waiters_.begin(); it != waiters_.end();) { + if (it->runtime == runtime) { + removed.push_back(std::move(*it)); + it = waiters_.erase(it); + } else ++it; + } + for (auto& waiter : removed) + waiter.completion(false, "React runtime was destroyed during AR initialization"); + if (waiters_.empty() && viewTasks_.empty() && + (phase_ == Phase::Checking || phase_ == Phase::AwaitingHost)) + finish(false); +} + +void ARCoreInitialization::initialize(RuntimeId runtime, InitializationCompletion completion) { + scheduler_.assertMainThread(); + auto found = runtimes_.find(runtime); + if (found == runtimes_.end()) { + completion(false, "AR initialization runtime is unavailable"); + return; + } + if (pending()) { + waiters_.push_back({runtime, std::move(completion)}); + return; + } + if (!found->second || !platform_.hasResumedHost(*found->second)) { + completion(false, "AR initialization requires a resumed host Activity"); + return; + } + waiters_.push_back({runtime, std::move(completion)}); + begin(*found->second); +} + +bool ARCoreInitialization::ensureForSession(int taskId) { + scheduler_.assertMainThread(); + if (pending()) { + viewTasks_.insert(taskId); + return false; + } + if (!platform_.hasResumedHost(taskId)) return false; + if (readyTasks_.count(taskId)) return true; + if (attemptedTasks_.count(taskId)) return false; + begin(taskId); + if (pending()) viewTasks_.insert(taskId); + return readyTasks_.count(taskId) != 0; +} + +void ARCoreInitialization::hostResumed(RuntimeId runtime, int taskId) { + scheduler_.assertMainThread(); + if (auto found = runtimes_.find(runtime); found != runtimes_.end()) + found->second = taskId; + if (taskId != taskId_) return; + if (phase_ == Phase::Installing && paused_) { + phase_ = Phase::Continuing; + install(false); + } else if (phase_ == Phase::AwaitingHost) { + phase_ = Phase::Checking; + check(); + } +} + +void ARCoreInitialization::hostPaused(int taskId) { + scheduler_.assertMainThread(); + if (taskId == taskId_ && phase_ == Phase::Installing) paused_ = true; +} + +void ARCoreInitialization::hostDestroyed(int taskId, bool changingConfigurations, bool finishing) { + scheduler_.assertMainThread(); + if (taskId == taskId_ && pending()) { + if (changingConfigurations) hostPaused(taskId); + else if (finishing) finish(false); + } + if (finishing && !changingConfigurations) { + readyTasks_.erase(taskId); + attemptedTasks_.erase(taskId); + } +} + +bool ARCoreInitialization::pending() const { + scheduler_.assertMainThread(); + return phase_ != Phase::Idle; +} + +void ARCoreInitialization::begin(int taskId) { + taskId_ = taskId; + ++generation_; + deadline_.reset(); + paused_ = false; + attemptedTasks_.insert(taskId); + readyTasks_.erase(taskId); + phase_ = Phase::Checking; + check(); +} + +void ARCoreInitialization::check() { + try { + if (deadline_ && scheduler_.now() >= *deadline_) { + finish(false, "Timed out checking ARCore availability"); + return; + } + switch (platform_.checkAvailability()) { + case AR_AVAILABILITY_UNKNOWN_CHECKING: { + if (!deadline_) deadline_ = scheduler_.now() + 5000; + const auto generation = generation_; + timer_ = scheduler_.schedule(std::min(200, *deadline_ - scheduler_.now()), [this, generation] { + if (generation != generation_ || phase_ != Phase::Checking) return; + timer_ = 0; + check(); + }); + return; + } + case AR_AVAILABILITY_UNSUPPORTED_DEVICE_NOT_CAPABLE: + finish(false); + return; + case AR_AVAILABILITY_SUPPORTED_INSTALLED: + case AR_AVAILABILITY_SUPPORTED_NOT_INSTALLED: + case AR_AVAILABILITY_SUPPORTED_APK_TOO_OLD: + cancelTimer(); + deadline_.reset(); + if (!platform_.hasResumedHost(taskId_)) { + phase_ = Phase::AwaitingHost; + return; + } + phase_ = Phase::Installing; + install(true); + return; + case AR_AVAILABILITY_UNKNOWN_TIMED_OUT: + finish(false, "Timed out checking ARCore availability"); + return; + default: + finish(false, "Could not determine ARCore availability"); + } + } catch (const std::exception& error) { + finish(false, error.what()); + } +} + +void ARCoreInitialization::install(bool userRequested) { + try { + const auto result = platform_.requestInstall(taskId_, userRequested); + if (result.status == AR_UNAVAILABLE_USER_DECLINED_INSTALLATION || + result.status == AR_UNAVAILABLE_DEVICE_NOT_COMPATIBLE) { + finish(false); + } else if (result.status != AR_SUCCESS) { + finish(false, "ARCore installation failed (status " + std::to_string(result.status) + ")"); + } else if (result.installation == AR_INSTALL_STATUS_INSTALLED) { + finish(true); + } else if (!userRequested) { + finish(false); + } else if (result.installation != AR_INSTALL_STATUS_INSTALL_REQUESTED) { + finish(false, "Unexpected ARCore installation status"); + } + } catch (const std::exception& error) { + finish(false, error.what()); + } +} + +void ARCoreInitialization::cancelTimer() { + if (timer_) scheduler_.cancel(timer_); + timer_ = 0; +} + +void ARCoreInitialization::finish(bool ready, const std::string& error) { + cancelTimer(); + ++generation_; + deadline_.reset(); + phase_ = Phase::Idle; + paused_ = false; + auto waiters = std::move(waiters_); + waiters_.clear(); + auto views = std::move(viewTasks_); + viewTasks_.clear(); + if (ready) { + readyTasks_.insert(taskId_); + readyTasks_.insert(views.begin(), views.end()); + } + attemptedTasks_.insert(views.begin(), views.end()); + const auto generation = generation_; + for (auto& waiter : waiters) waiter.completion(ready, error); + for (int task : views) { + scheduler_.dispatch([this, generation, task, ready, error] { + if (generation == generation_) notifyViews_(task, ready, error); + }); + } +} +} diff --git a/cpp/ARCoreInitialization.h b/cpp/ARCoreInitialization.h new file mode 100644 index 0000000..864976c --- /dev/null +++ b/cpp/ARCoreInitialization.h @@ -0,0 +1,65 @@ +#pragma once + +#include "IPlatformServices.h" +#include +#include +#include +#include +#include +#include +#include + +namespace arcore { +using RuntimeId = int64_t; +using InitializationCompletion = std::function; + +class IInitializationScheduler { +public: + using Token = uint64_t; + virtual ~IInitializationScheduler() = default; + virtual void assertMainThread() const = 0; + virtual void dispatch(std::function action) = 0; + virtual Token schedule(int64_t delayMs, std::function action) = 0; + virtual void cancel(Token token) = 0; + virtual int64_t now() const = 0; +}; + +// All state and callbacks are confined to the scheduler's main thread. +class ARCoreInitialization { +public: + ARCoreInitialization(IPlatformServices& platform, IInitializationScheduler& scheduler, + std::function notifyViews); + void registerRuntime(RuntimeId runtime); + void removeRuntime(RuntimeId runtime); + void initialize(RuntimeId runtime, InitializationCompletion completion); + bool ensureForSession(int taskId); + void hostResumed(RuntimeId runtime, int taskId); + void hostPaused(int taskId); + void hostDestroyed(int taskId, bool changingConfigurations, bool finishing); + bool pending() const; + +private: + enum class Phase { Idle, Checking, AwaitingHost, Installing, Continuing }; + struct Waiter { RuntimeId runtime; InitializationCompletion completion; }; + void begin(int taskId); + void check(); + void install(bool userRequested); + void finish(bool ready, const std::string& error = {}); + void cancelTimer(); + + IPlatformServices& platform_; + IInitializationScheduler& scheduler_; + std::function notifyViews_; + std::map> runtimes_; + std::vector waiters_; + std::set viewTasks_; + std::set attemptedTasks_; + std::set readyTasks_; + Phase phase_ = Phase::Idle; + int taskId_ = -1; + bool paused_ = false; + uint64_t generation_ = 0; + IInitializationScheduler::Token timer_ = 0; + std::optional deadline_; +}; +} diff --git a/cpp/ARSessionManager.h b/cpp/ARSessionManager.h index d4eab07..6414b1e 100644 --- a/cpp/ARSessionManager.h +++ b/cpp/ARSessionManager.h @@ -57,18 +57,9 @@ class ArSessionManager { // Whether a session has been created. bool IsInitialized() const { return ar_session_ != nullptr; } - void SetInstallRequested(bool requested) { - install_requested_ = requested; - } - - bool IsInstallRequested() const { - return install_requested_; - } - private: ArSessionManager() = default; ~ArSessionManager() = default; ArSession* ar_session_ = nullptr; - bool install_requested_ = false; }; diff --git a/cpp/AndroidPlatformServices.cpp b/cpp/AndroidPlatformServices.cpp index 440520a..e614277 100644 --- a/cpp/AndroidPlatformServices.cpp +++ b/cpp/AndroidPlatformServices.cpp @@ -1,125 +1,190 @@ -#include +#include "AndroidPlatformServices.h" +#include "ARCoreAvailability.h" +#include "ARCoreInitialization.h" +#include #include +#include #include -#include -#include "AndroidPlatformServices.h" -#include "ARCoreAvailability.h" -#include "ARSessionManager.h" +using namespace facebook::jni; namespace { -facebook::jni::local_ref getApplicationContext() -{ - using namespace facebook::jni; - static const auto nitro = findClassStatic("com/margelo/nitro/NitroModules"); - static const auto getContext = nitro->getStaticMethod()>( - "getApplicationContext", "()Lcom/facebook/react/bridge/ReactApplicationContext;"); - auto context = getContext(nitro); - if (!context) - throw std::runtime_error("AR initialization context is unavailable"); - return context; -} -} - -bool arcore::checkARCoreAvailability() -{ - using namespace facebook::jni; - using namespace std::chrono_literals; - bool available = false; - ThreadScope::WithClassLoader([&] { - auto context = getApplicationContext(); - - auto* env = Environment::current(); - const auto deadline = std::chrono::steady_clock::now() + 5s; - while (true) - { - ArAvailability availability; - ArCoreApk_checkAvailability(env, context.get(), &availability); - throwPendingJniExceptionAsCppException(); - switch (availability) - { - case AR_AVAILABILITY_SUPPORTED_INSTALLED: - available = true; - return; - case AR_AVAILABILITY_UNSUPPORTED_DEVICE_NOT_CAPABLE: - case AR_AVAILABILITY_SUPPORTED_NOT_INSTALLED: - case AR_AVAILABILITY_SUPPORTED_APK_TOO_OLD: - return; - case AR_AVAILABILITY_UNKNOWN_CHECKING: - if (std::chrono::steady_clock::now() >= deadline) - throw std::runtime_error("Timed out checking ARCore availability"); - std::this_thread::sleep_for(200ms); - break; - case AR_AVAILABILITY_UNKNOWN_TIMED_OUT: - throw std::runtime_error("Timed out checking ARCore availability"); - default: - throw std::runtime_error("Could not determine ARCore availability"); - } - } - }); - return available; -} +struct Lifecycle : JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/arcore/ARCoreLifecycle;"; +}; -bool AndroidPlatformServices::isGooglePlayServicesAvailable() -{ - if (ArSessionManager::Instance().IsInitialized()) - { - return true; +class MainScheduler final : public arcore::IInitializationScheduler { +public: + void assertMainThread() const override { + static const auto method = Lifecycle::javaClassStatic()->getStaticMethod("assertMainThread"); + method(Lifecycle::javaClassStatic()); + } + void dispatch(std::function action) override { + ThreadScope::WithClassLoader([&] { + post(JNativeRunnable::newObjectCxxArgs(std::move(action)), 0); + }); + } + Token schedule(int64_t delayMs, std::function action) override { + assertMainThread(); + const auto token = ++nextToken_; + auto runnable = JNativeRunnable::newObjectCxxArgs([this, token, action = std::move(action)] { + if (!scheduled_.erase(token)) return; + action(); + }); + scheduled_.emplace(token, make_global(runnable)); + post(runnable, delayMs); + return token; } - using namespace facebook::jni; - try - { - auto context = getApplicationContext(); - static const auto tracker = findClassStatic("com/margelo/nitro/arcore/CurrentActivityTracker"); - static const auto getActivity = tracker->getStaticMethod()>( - "getCurrentActivity", "()Landroid/app/Activity;"); - auto activity = getActivity(tracker); - if (!activity) - return false; - bool available = checkARCoreInstallation(Environment::current(), context.get(), activity.get()); - throwPendingJniExceptionAsCppException(); - return available; + void cancel(Token token) override { + assertMainThread(); + auto found = scheduled_.find(token); + if (found == scheduled_.end()) return; + static const auto method = Lifecycle::javaClassStatic()->getStaticMethod)>("cancel"); + method(Lifecycle::javaClassStatic(), found->second); + scheduled_.erase(found); } - catch (const std::exception&) - { - return false; + int64_t now() const override { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); } +private: + static void post(alias_ref runnable, int64_t delayMs) { + static const auto method = Lifecycle::javaClassStatic()->getStaticMethod, jlong)>("post"); + method(Lifecycle::javaClassStatic(), runnable, delayMs); + } + Token nextToken_ = 0; + std::map> scheduled_; +}; + +MainScheduler& scheduler() { + static MainScheduler instance; + return instance; } -bool AndroidPlatformServices::checkARCoreInstallation(JNIEnv *env, jobject context, jobject activity) -{ - ArInstallStatus install_status; - bool user_requested_install = !ArSessionManager::Instance().IsInstallRequested(); - ArAvailability availability; +struct Host { + weak_ref activity; + bool resumed = false; +}; +std::map hosts; +global_ref applicationContext; - ArCoreApk_checkAvailability(env, context, &availability); - - switch (availability) - { - case AR_AVAILABILITY_SUPPORTED_INSTALLED: - case AR_AVAILABILITY_SUPPORTED_NOT_INSTALLED: - case AR_AVAILABILITY_SUPPORTED_APK_TOO_OLD: - break; - case AR_AVAILABILITY_UNKNOWN_CHECKING: - return false; - default: - return false; - } +void notifyViews(int task, bool ready, const std::string& error) { + static const auto method = Lifecycle::javaClassStatic()->getStaticMethod("notifyViews"); + method(Lifecycle::javaClassStatic(), task, ready, error); +} - ArStatus error = ArCoreApk_requestInstall(env, activity, user_requested_install, &install_status); - if (error != AR_SUCCESS) - { - return false; - } +arcore::ARCoreInitialization& coordinator() { + static AndroidPlatformServices platform; + static arcore::ARCoreInitialization instance(platform, scheduler(), notifyViews); + return instance; +} - switch (install_status) - { - case AR_INSTALL_STATUS_INSTALLED: - return true; - case AR_INSTALL_STATUS_INSTALL_REQUESTED: - ArSessionManager::Instance().SetInstallRequested(true); - return false; +bool isCurrentHost(int task, alias_ref activity) { + const auto found = hosts.find(task); + if (found == hosts.end()) return false; + auto current = found->second.activity.lockLocal(); + return current && Environment::current()->IsSameObject(current.get(), activity.get()); +} + +void registerRuntime(alias_ref, jlong runtime, alias_ref context) { + scheduler().assertMainThread(); + applicationContext = make_global(context); + coordinator().registerRuntime(runtime); +} +void removeRuntime(alias_ref, jlong runtime) { + coordinator().removeRuntime(runtime); +} +void hostResumed(alias_ref, jlong runtime, jint task, alias_ref activity) { + scheduler().assertMainThread(); + hosts[task] = {make_weak(activity), true}; + coordinator().hostResumed(runtime, task); +} +void hostPaused(alias_ref, jint task, alias_ref activity) { + scheduler().assertMainThread(); + if (!isCurrentHost(task, activity)) return; + hosts.at(task).resumed = false; + coordinator().hostPaused(task); +} +void hostDestroyed(alias_ref, jint task, alias_ref activity, + jboolean changing, jboolean finishing) { + scheduler().assertMainThread(); + if (!isCurrentHost(task, activity)) return; + hosts.erase(task); + coordinator().hostDestroyed(task, changing, finishing); +} +jboolean isPending(alias_ref) { return coordinator().pending(); } + +int taskId(alias_ref activity) { + const auto method = activity->getClass()->getMethod("getTaskId"); + return method(activity); +} +} + +namespace arcore { +void registerARCoreInitializationNatives() { + Lifecycle::javaClassStatic()->registerNatives({ + makeNativeMethod("registerRuntimeNative", "(JLandroid/content/Context;)V", registerRuntime), + makeNativeMethod("removeRuntimeNative", removeRuntime), + makeNativeMethod("hostResumedNative", "(JILandroid/app/Activity;)V", hostResumed), + makeNativeMethod("hostPausedNative", "(ILandroid/app/Activity;)V", hostPaused), + makeNativeMethod("hostDestroyedNative", "(ILandroid/app/Activity;ZZ)V", hostDestroyed), + makeNativeMethod("isInitializationPending", isPending), + }); +} + +std::shared_ptr> initializeARCore(int64_t runtimeId) { + auto promise = margelo::nitro::Promise::create(); + try { + scheduler().dispatch([runtimeId, promise] { + try { + coordinator().initialize(runtimeId, [promise](bool ready, const std::string& error) { + if (error.empty()) promise->resolve(ready); + else promise->reject(std::make_exception_ptr(std::runtime_error(error))); + }); + } catch (...) { + if (promise->isPending()) promise->reject(std::current_exception()); + } + }); + } catch (...) { + promise->reject(std::current_exception()); } + return promise; +} +} +ArAvailability AndroidPlatformServices::checkAvailability() { + scheduler().assertMainThread(); + if (!applicationContext) throw std::runtime_error("AR initialization context is unavailable"); + ArAvailability availability; + ArCoreApk_checkAvailability(Environment::current(), applicationContext.get(), &availability); + throwPendingJniExceptionAsCppException(); + return availability; +} + +ARCoreInstallResult AndroidPlatformServices::requestInstall(int task, bool userRequested) { + scheduler().assertMainThread(); + if (!hasResumedHost(task)) throw std::runtime_error("AR initialization requires a resumed host Activity"); + auto activity = hosts.at(task).activity.lockLocal(); + ARCoreInstallResult result{AR_ERROR_FATAL, AR_INSTALL_STATUS_INSTALL_REQUESTED}; + result.status = ArCoreApk_requestInstall(Environment::current(), activity.get(), userRequested, &result.installation); + throwPendingJniExceptionAsCppException(); + return result; +} + +bool AndroidPlatformServices::hasResumedHost(int task) { + scheduler().assertMainThread(); + auto found = hosts.find(task); + return found != hosts.end() && found->second.resumed && bool(found->second.activity.lockLocal()); +} + +bool AndroidPlatformServices::isGooglePlayServicesAvailable() { + scheduler().assertMainThread(); + for (const auto& [task, host] : hosts) + if (hasResumedHost(task)) return coordinator().ensureForSession(task); return false; -} \ No newline at end of file +} + +bool AndroidPlatformServices::checkARCoreInstallation(JNIEnv*, jobject, jobject activity) { + scheduler().assertMainThread(); + return coordinator().ensureForSession(taskId(wrap_alias(activity))); +} diff --git a/cpp/AndroidPlatformServices.h b/cpp/AndroidPlatformServices.h index 73c23b8..0972124 100644 --- a/cpp/AndroidPlatformServices.h +++ b/cpp/AndroidPlatformServices.h @@ -2,12 +2,12 @@ #include #include "IPlatformServices.h" -#include "arcore_c_api.h" class AndroidPlatformServices : public IPlatformServices { public: - + ArAvailability checkAvailability() override; + ARCoreInstallResult requestInstall(int taskId, bool userRequested) override; + bool hasResumedHost(int taskId) override; bool isGooglePlayServicesAvailable() override; bool checkARCoreInstallation(JNIEnv* env, jobject context, jobject activity); - -}; \ No newline at end of file +}; diff --git a/cpp/HybridCrossPlatformArCore.cpp b/cpp/HybridCrossPlatformArCore.cpp index 06b653a..b3580ed 100644 --- a/cpp/HybridCrossPlatformArCore.cpp +++ b/cpp/HybridCrossPlatformArCore.cpp @@ -8,13 +8,12 @@ namespace margelo::nitro::arcore { -std::shared_ptr> HybridCrossPlatformArCore::initialize() +std::shared_ptr> HybridCrossPlatformArCore::initialize(double runtimeId) { #ifdef __ANDROID__ - return Promise::async([]() { - return ::arcore::checkARCoreAvailability(); - }); + return ::arcore::initializeARCore(static_cast(runtimeId)); #else + (void)runtimeId; return Promise::async([]() { return true; }); diff --git a/cpp/HybridCrossPlatformArCore.hpp b/cpp/HybridCrossPlatformArCore.hpp index dffbfa3..3487d58 100644 --- a/cpp/HybridCrossPlatformArCore.hpp +++ b/cpp/HybridCrossPlatformArCore.hpp @@ -10,7 +10,7 @@ class HybridCrossPlatformArCore : public HybridCrossPlatformArCoreSpec public: HybridCrossPlatformArCore() : HybridObject(TAG) {} - std::shared_ptr> initialize() override; + std::shared_ptr> initialize(double runtimeId) override; bool isDepthModeSupported() override; bool isGeospatialModeSupported() override; }; diff --git a/cpp/IPlatformServices.h b/cpp/IPlatformServices.h index 1e6fa24..d685855 100644 --- a/cpp/IPlatformServices.h +++ b/cpp/IPlatformServices.h @@ -1,9 +1,17 @@ #pragma once +#include "arcore_c_api.h" + +struct ARCoreInstallResult { + ArStatus status; + ArInstallStatus installation; +}; + class IPlatformServices { public: virtual ~IPlatformServices() = default; - - // Pure virtual method - must be implemented by Android/iOS specific code + virtual ArAvailability checkAvailability() = 0; + virtual ARCoreInstallResult requestInstall(int taskId, bool userRequested) = 0; + virtual bool hasResumedHost(int taskId) = 0; virtual bool isGooglePlayServicesAvailable() = 0; -}; \ No newline at end of file +}; diff --git a/cpp/__tests__/ARCoreInitializationTest.cpp b/cpp/__tests__/ARCoreInitializationTest.cpp new file mode 100644 index 0000000..50271e4 --- /dev/null +++ b/cpp/__tests__/ARCoreInitializationTest.cpp @@ -0,0 +1,374 @@ +#include "ARCoreInitialization.h" +#include +#include +#include +#include + +#define CHECK(condition) do { if (!(condition)) throw std::runtime_error(#condition); } while (false) +using namespace arcore; + +struct Scheduler : IInitializationScheduler { + struct Task { int64_t at; std::function action; }; + int64_t time = 0; + Token next = 0; + bool main = true; + std::map tasks; + void assertMainThread() const override { if (!main) throw std::runtime_error("not main"); } + void dispatch(std::function action) override { schedule(0, std::move(action)); } + Token schedule(int64_t delay, std::function action) override { + auto token = ++next; + tasks.emplace(token, Task{time + delay, std::move(action)}); + return token; + } + void cancel(Token token) override { tasks.erase(token); } + int64_t now() const override { return time; } + void tick() { + CHECK(!tasks.empty()); + auto it = std::min_element(tasks.begin(), tasks.end(), [](auto& a, auto& b) { return a.second.at < b.second.at; }); + auto task = std::move(it->second); + tasks.erase(it); + time = std::max(time, task.at); + task.action(); + } +}; + +struct Platform : IPlatformServices { + struct Host { int activity; bool resumed; }; + std::map hosts; + ArAvailability availability = AR_AVAILABILITY_SUPPORTED_INSTALLED; + ARCoreInstallResult result{AR_SUCCESS, AR_INSTALL_STATUS_INSTALLED}; + std::vector> installs; + int checks = 0; + bool fail = false; + ArAvailability checkAvailability() override { + ++checks; + if (fail) throw std::runtime_error("native failure"); + return availability; + } + ARCoreInstallResult requestInstall(int task, bool requested) override { + CHECK(hasResumedHost(task)); + installs.emplace_back(task, hosts.at(task).activity, requested); + return result; + } + bool hasResumedHost(int task) override { return hosts.count(task) && hosts.at(task).resumed; } + bool isGooglePlayServicesAvailable() override { return false; } +}; + +struct Harness { + Scheduler scheduler; + Platform platform; + struct Result { int caller; bool ready; std::string error; }; + std::vector results; + std::vector views; + ARCoreInitialization coordinator{platform, scheduler, [this](int task, bool ready, const std::string& error) { + views.push_back({task, ready, error}); + }}; + Harness() { + coordinator.registerRuntime(1); + coordinator.registerRuntime(2); + resume(1, 10, 100); + } + void start(int caller = 1, RuntimeId runtime = 1) { + coordinator.initialize(runtime, [this, caller](bool ready, const std::string& error) { + results.push_back({caller, ready, error}); + }); + } + void resume(RuntimeId runtime = 1, int task = 10, int activity = 100) { + platform.hosts[task] = {activity, true}; + coordinator.hostResumed(runtime, task); + } + void pause(int task = 10) { + platform.hosts.at(task).resumed = false; + coordinator.hostPaused(task); + } + void destroy(bool changing = true, bool finishing = false) { + platform.hosts.erase(10); + coordinator.hostDestroyed(10, changing, finishing); + } + void installing() { platform.result = {AR_SUCCESS, AR_INSTALL_STATUS_INSTALL_REQUESTED}; } + void installed() { platform.result = {AR_SUCCESS, AR_INSTALL_STATUS_INSTALLED}; } +}; + +void installedAndUnsupported() { + Harness h; + h.start(); + CHECK(h.results.size() == 1 && h.results[0].ready); + CHECK(h.platform.installs.size() == 1 && std::get<2>(h.platform.installs[0])); + h.platform.availability = AR_AVAILABILITY_UNSUPPORTED_DEVICE_NOT_CAPABLE; + h.start(2); + CHECK(!h.results[1].ready && h.results[1].error.empty()); + CHECK(h.platform.installs.size() == 1); +} +void missingAndOutdated() { + for (auto availability : {AR_AVAILABILITY_SUPPORTED_NOT_INSTALLED, AR_AVAILABILITY_SUPPORTED_APK_TOO_OLD}) { + Harness h; + h.platform.availability = availability; + h.installing(); + h.start(); + CHECK(h.coordinator.pending() && h.results.empty()); + h.pause(); + h.installed(); + h.resume(); + CHECK(h.results.size() == 1 && h.results[0].ready); + CHECK(!std::get<2>(h.platform.installs[1])); + } +} +void pendingAvailabilityAndDeadline() { + Harness h; + h.platform.availability = AR_AVAILABILITY_UNKNOWN_CHECKING; + h.start(); + h.scheduler.tick(); + CHECK(h.scheduler.time == 200 && h.results.empty()); + h.platform.availability = AR_AVAILABILITY_SUPPORTED_INSTALLED; + h.scheduler.tick(); + CHECK(h.results[0].ready); + h.platform.availability = AR_AVAILABILITY_UNKNOWN_CHECKING; + h.start(2); + while (!h.scheduler.tasks.empty()) h.scheduler.tick(); + CHECK(h.scheduler.time == 5400); + CHECK(h.results[1].error.find("Timed out") != std::string::npos); +} +void installationHasNoDeadline() { + Harness h; + h.platform.availability = AR_AVAILABILITY_UNKNOWN_CHECKING; + h.start(); + auto stale = h.scheduler.tasks.begin()->second.action; + h.platform.availability = AR_AVAILABILITY_SUPPORTED_INSTALLED; + h.installing(); + h.scheduler.tick(); + h.pause(); + h.scheduler.time = 600000; + stale(); + CHECK(h.scheduler.tasks.empty() && h.results.empty()); + h.installed(); + h.resume(); + CHECK(h.results[0].ready); +} +void cancellationIncompleteAndErrors() { + for (auto status : {AR_UNAVAILABLE_USER_DECLINED_INSTALLATION, AR_UNAVAILABLE_DEVICE_NOT_COMPATIBLE, AR_SUCCESS, AR_ERROR_FATAL}) { + Harness h; + h.installing(); + h.start(); + h.pause(); + h.platform.result = {status, AR_INSTALL_STATUS_INSTALL_REQUESTED}; + h.resume(); + CHECK(h.results.size() == 1 && !h.results[0].ready); + CHECK(h.results[0].error.empty() == (status != AR_ERROR_FATAL)); + h.resume(); + CHECK(h.platform.installs.size() == 2 && !h.coordinator.pending()); + } +} +void statusIsIgnoredOnFailure() { + Harness h; + h.platform.result = {AR_ERROR_FATAL, AR_INSTALL_STATUS_INSTALLED}; + h.start(); + CHECK(!h.results[0].ready && !h.results[0].error.empty()); +} +void duplicateResumesAndUnrelatedTasks() { + Harness h; + h.installing(); + h.start(); + h.resume(); + CHECK(h.platform.installs.size() == 1); + h.pause(); + h.installed(); + h.resume(2, 20, 200); + CHECK(h.results.empty()); + h.resume(); + h.resume(0); + CHECK(h.results.size() == 1 && h.platform.installs.size() == 2); +} +void replacementHostContinues() { + Harness h; + h.installing(); + h.start(); + h.pause(); + h.destroy(); + CHECK(h.results.empty() && h.coordinator.pending()); + h.installed(); + h.resume(1, 10, 101); + CHECK(h.results[0].ready && std::get<1>(h.platform.installs[1]) == 101); +} +void finishingHostStopsOperation() { + Harness h; + h.installing(); + h.start(); + h.pause(); + h.destroy(false, true); + CHECK(h.results.size() == 1 && !h.results[0].ready && h.results[0].error.empty()); + CHECK(!h.coordinator.pending()); +} +void freshCallNeedsActivityButJoinDoesNot() { + Harness h; + h.start(1, 2); + CHECK(!h.results[0].error.empty()); + h.installing(); + h.start(2); + h.pause(); + h.start(3, 2); + CHECK(h.results.size() == 1); + h.installed(); + h.resume(); + CHECK(h.results.size() == 3 && h.results[1].ready && h.results[2].ready); +} +void runtimeTeardownIsIsolated() { + Harness h; + h.installing(); + h.start(1); + h.start(2, 2); + h.coordinator.removeRuntime(1); + CHECK(h.results.size() == 1 && h.results[0].caller == 1 && !h.results[0].error.empty()); + h.pause(); + h.installed(); + h.resume(2, 10, 101); + CHECK(h.results.size() == 2 && h.results[1].caller == 2 && h.results[1].ready); +} +void teardownRetainsInstallationGuard() { + Harness h; + h.installing(); + h.start(); + h.pause(); + h.coordinator.removeRuntime(1); + CHECK(h.coordinator.pending()); + h.start(2, 2); + CHECK(h.platform.installs.size() == 1); + h.installed(); + h.resume(2, 10, 101); + CHECK(h.results[1].ready && !std::get<2>(h.platform.installs[1])); +} +void teardownCancelsAvailabilityCallbacks() { + Harness h; + h.platform.availability = AR_AVAILABILITY_UNKNOWN_CHECKING; + h.start(); + auto stale = h.scheduler.tasks.begin()->second.action; + h.coordinator.removeRuntime(1); + CHECK(h.scheduler.tasks.empty() && !h.coordinator.pending()); + stale(); + CHECK(h.platform.checks == 1 && h.results.size() == 1); + h.start(2); + CHECK(!h.results[1].error.empty()); +} +void registrationIsIdempotent() { + Harness h; + h.coordinator.registerRuntime(1); + h.start(); + CHECK(h.results[0].ready); +} +void viewsJoinAndRetryOnlyOnSuccess() { + Harness h; + h.installing(); + h.start(); + CHECK(!h.coordinator.ensureForSession(10)); + CHECK(!h.coordinator.ensureForSession(10)); + CHECK(h.platform.installs.size() == 1); + h.pause(); + h.installed(); + h.resume(); + h.scheduler.tick(); + CHECK(h.views.size() == 1 && h.views[0].ready); + CHECK(h.coordinator.ensureForSession(10)); + CHECK(h.platform.installs.size() == 2); +} +void viewStartedInstallationCanBeJoined() { + Harness h; + h.installing(); + CHECK(!h.coordinator.ensureForSession(10)); + h.start(); + CHECK(h.platform.installs.size() == 1); + h.pause(); + h.installed(); + h.resume(); + h.scheduler.tick(); + CHECK(h.results[0].ready && h.views[0].ready); +} +void passiveFailureDoesNotReprompt() { + Harness h; + h.installing(); + CHECK(!h.coordinator.ensureForSession(10)); + h.pause(); + h.platform.result.status = AR_UNAVAILABLE_USER_DECLINED_INSTALLATION; + h.resume(); + CHECK(!h.coordinator.ensureForSession(10)); + h.scheduler.tick(); + CHECK(h.platform.installs.size() == 2 && !h.views[0].ready); + h.installed(); + h.start(); + CHECK(h.results[0].ready && std::get<2>(h.platform.installs[2])); +} +void availabilityErrorRejects() { + for (auto value : {AR_AVAILABILITY_UNKNOWN_TIMED_OUT, AR_AVAILABILITY_UNKNOWN_ERROR}) { + Harness h; + h.platform.availability = value; + h.start(); + CHECK(!h.results[0].error.empty()); + } + Harness h; + h.platform.fail = true; + h.start(); + CHECK(h.results[0].error == "native failure"); +} +void hostCanDisappearDuringAvailability() { + Harness h; + h.platform.availability = AR_AVAILABILITY_UNKNOWN_CHECKING; + h.start(); + h.pause(); + h.destroy(); + h.platform.availability = AR_AVAILABILITY_SUPPORTED_INSTALLED; + h.scheduler.tick(); + CHECK(h.results.empty() && h.platform.installs.empty()); + h.resume(1, 10, 101); + CHECK(h.results[0].ready); +} +void staleViewCompletionCannotAffectNewAttempt() { + Harness h; + h.installing(); + h.coordinator.ensureForSession(10); + h.pause(); + h.platform.result.status = AR_UNAVAILABLE_USER_DECLINED_INSTALLATION; + h.resume(); + h.installing(); + h.start(); + h.scheduler.tick(); + CHECK(h.views.empty() && h.results.empty() && h.coordinator.pending()); +} +void checkingContinuesForSurvivingRuntime() { + Harness h; + h.platform.availability = AR_AVAILABILITY_UNKNOWN_CHECKING; + h.start(); + h.start(2, 2); + h.coordinator.removeRuntime(1); + h.platform.availability = AR_AVAILABILITY_SUPPORTED_INSTALLED; + h.scheduler.tick(); + CHECK(h.results.size() == 2 && h.results[1].caller == 2 && h.results[1].ready); +} +void mainThreadIsRequired() { + Harness h; + h.scheduler.main = false; + bool rejected = false; + try { h.start(); } catch (const std::exception&) { rejected = true; } + CHECK(rejected); +} + +int main() { + const std::vector> tests = { + {"installed/unsupported", installedAndUnsupported}, {"missing/outdated", missingAndOutdated}, + {"availability deadline", pendingAvailabilityAndDeadline}, {"unlimited installation wait", installationHasNoDeadline}, + {"cancellation/incomplete/errors", cancellationIncompleteAndErrors}, {"invalid install status", statusIsIgnoredOnFailure}, + {"duplicate/unrelated resume", duplicateResumesAndUnrelatedTasks}, {"replacement Activity", replacementHostContinues}, + {"finishing task", finishingHostStopsOperation}, {"missing Activity/join", freshCallNeedsActivityButJoinDoesNot}, + {"runtime isolation", runtimeTeardownIsIsolated}, {"installation guard", teardownRetainsInstallationGuard}, + {"stale callbacks", teardownCancelsAvailabilityCallbacks}, {"registration", registrationIsIdempotent}, + {"view joins", viewsJoinAndRetryOnlyOnSuccess}, {"JS joins view", viewStartedInstallationCanBeJoined}, + {"passive failure/explicit retry", passiveFailureDoesNotReprompt}, {"availability errors", availabilityErrorRejects}, + {"host disappears during check", hostCanDisappearDuringAvailability}, {"main-thread confinement", mainThreadIsRequired}, + {"stale view notification", staleViewCompletionCannotAffectNewAttempt}, + {"surviving runtime during check", checkingContinuesForSurvivingRuntime}, + }; + for (const auto& [name, test] : tests) { + try { test(); } catch (const std::exception& error) { + std::cerr << name << ": " << error.what() << '\n'; + return 1; + } + } + std::cout << tests.size() << " ARCore initialization tests passed\n"; +} diff --git a/package.json b/package.json index 7b876e2..e92d019 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "lint": "eslint \"**/*.{js,ts,tsx}\"", "release": "release-it --only-version", "release:ci": "release-it --ci", - "test": "jest" + "test": "jest", + "test:android-initialization": "bash scripts/test-arcore-initialization.sh" }, "keywords": [ "react-native", diff --git a/scripts/test-arcore-initialization.sh b/scripts/test-arcore-initialization.sh new file mode 100644 index 0000000..94441cc --- /dev/null +++ b/scripts/test-arcore-initialization.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +test_binary=$(mktemp "${TMPDIR:-/tmp}/arcore-initialization.XXXXXX") +trap 'rm -f "$test_binary"' EXIT +"${CXX:-c++}" -std=c++20 -Wall -Wextra -Werror -Icpp -Ilibraries/include \ + cpp/ARCoreInitialization.cpp cpp/__tests__/ARCoreInitializationTest.cpp -o "$test_binary" +"$test_binary" diff --git a/src/ArCore.nitro.ts b/src/ArCore.nitro.ts index f5e83a9..a8fd137 100644 --- a/src/ArCore.nitro.ts +++ b/src/ArCore.nitro.ts @@ -2,7 +2,7 @@ import type { HybridObject } from 'react-native-nitro-modules'; export interface CrossPlatformArCore extends HybridObject<{ ios: 'c++'; android: 'c++' }> { - initialize(): Promise; + initialize(runtimeId: number): Promise; isDepthModeSupported(): boolean; isGeospatialModeSupported(): boolean; } diff --git a/src/session.ts b/src/session.ts index 9c73e9d..e50d08a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,3 +1,4 @@ +import { NativeModules, Platform } from 'react-native'; import { NitroModules } from 'react-native-nitro-modules'; import type { CrossPlatformArCore } from './ArCore.nitro'; @@ -6,7 +7,9 @@ const ArCoreModule = NitroModules.createHybridObject( ); export function initialize(): Promise { - return ArCoreModule.initialize(); + return ArCoreModule.initialize( + Platform.OS === 'android' ? NativeModules.ARCoreLifecycle.runtimeId : 0 + ); } export function isDepthModeSupported(): boolean {