Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ So `model="andy"` loads `models/andy.obj` on Android and `andy.usdz` on iOS.

| Function | Returns | Description |
|---|---|---|
| `initialize()` | `Promise<boolean>` | 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<boolean>` | 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. |

Expand Down Expand Up @@ -358,7 +358,7 @@ Status by platform. Items shared by both platforms (e.g. `initialize()`, geospat
- [ ] Face-mesh overlay (`debugShowFaceMesh`)
- [ ] Face-texture overlay (`faceTextureURI`)
- [ ] `<ARObject>` `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)
Expand Down
1 change: 1 addition & 0 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ add_library(${PACKAGE_NAME} SHARED
../cpp/Texture.cpp
../cpp/utils.cpp
../cpp/AndroidPlatformServices.cpp
../cpp/ARCoreInitialization.cpp
../cpp/ARSessionManager.cpp
)

Expand Down
2 changes: 2 additions & 0 deletions android/src/main/cpp/cpp-adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
#include <fbjni/fbjni.h>
#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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class ARAppSystem {
if (isSessionInitialized()) {
ARViewRegistry.emitSessionType(getActiveSessionType())
ARViewRegistry.emitSessionState("ready")
} else {
} else if (!ARCoreLifecycle.isInitializationPending()) {
ARViewRegistry.emitSessionState("failed")
}
}
Expand Down
95 changes: 95 additions & 0 deletions android/src/main/java/com/margelo/nitro/arcore/ARCoreLifecycle.kt
Original file line number Diff line number Diff line change
@@ -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<Activity>(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<String, Any> = 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<ReactApplicationContext, WeakReference<ARCoreLifecycle>>()

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<HybridARView> = synchronized(this) { views.toList() }
snapshot.forEach { it.onInitializationComplete(taskId, ready, error) }
}

fun resumeGl() {
val snapshot: List<HybridARView> = synchronized(this) { views.toList() }
snapshot.forEach { it.resumeGl() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.margelo.nitro.arcore.views.HybridARViewHybridManager

class ArCorePackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
return listOf(ARCoreLifecycle.get(reactContext))
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<out View, *>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Activity> mCurrentActivity = new WeakReference<>(null);
private static final CurrentActivityTracker INSTANCE = new CurrentActivityTracker();
private static final WeakHashMap<Activity, Boolean> hosts = new WeakHashMap<>();
private static WeakReference<Activity> 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) {}
}
23 changes: 19 additions & 4 deletions android/src/main/java/com/margelo/nitro/arcore/HybridARView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion cpp/ARApplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion cpp/ARCoreAvailability.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#pragma once

#include <NitroModules/Promise.hpp>
#include <cstdint>

namespace arcore {
bool checkARCoreAvailability();
std::shared_ptr<margelo::nitro::Promise<bool>> initializeARCore(int64_t runtimeId);
void registerARCoreInitializationNatives();
}
Loading
Loading