chromium/components/webxr/android/java/src/org/chromium/components/webxr/XrSessionCoordinator.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.webxr;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.view.Surface;

import androidx.annotation.IntDef;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;

/**
 * Provides static methods called by the XrDelegateImpl as well as JNI methods to the C/C++ code
 * in order to interact with the various bits of the Java side of a session. This includes the
 * responsibility to standup/create any needed overlays/SurfaceViews and forwarding events both
 * from them and elsewhere within Chrome (forwarded/registered for via XrDelegate). This class is
 * also responsible for ensuring that there is only one active session at a time and answering
 * questions about that session; mainly via communication of its static members.
 */
@JNINamespace("webxr")
public class XrSessionCoordinator {
    private static final String TAG = "XrSessionCoordinator";
    private static final boolean DEBUG_LOGS = false;

    @IntDef({SessionType.NONE, SessionType.AR, SessionType.VR})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SessionType {
        int NONE = 0;
        int AR = 1;
        int VR = 2;
    }

    // ArDelegateImpl needs to know if there's an active immersive session so that it can handle
    // back button presses from ChromeActivity's onBackPressed(). It's only set while a session is
    // in progress, and reset to null on session end. The XrImmersiveOverlay member has a strong
    // reference to the ChromeActivity, and that shouldn't be retained beyond the duration of a
    // session.
    private static XrSessionCoordinator sActiveSessionInstance;

    /** Whether there is a non-null valid {@link #sActiveSessionInstance}. */
    private static XrSessionTypeSupplier sActiveSessionAvailableSupplier =
            new XrSessionTypeSupplier(SessionType.NONE);

    private long mNativeXrSessionCoordinator;

    // The native ArCoreDevice runtime creates a XrSessionCoordinator instance in its constructor,
    // and keeps a strong reference to it for the lifetime of the device. It creates and
    // owns an XrImmersiveOverlay for the duration of an immersive session, which in
    // turn contains a reference to XrSessionCoordinator for making JNI calls back to the device.
    private XrImmersiveOverlay mImmersiveOverlay;

    private @SessionType int mActiveSessionType = SessionType.NONE;

    // The WebContents that triggered the currently active session.
    private WebContents mWebContents;

    private WeakReference<Activity> mXrHostActivity;

    // Helper, obtains android Activity out of passed in WebContents instance.
    // Equivalent to ChromeActivity.fromWebContents(), but does not require that
    // the resulting instance is a ChromeActivity.
    @CalledByNative
    public static Activity getActivity(final WebContents webContents) {
        if (webContents == null) return null;
        WindowAndroid window = webContents.getTopLevelNativeWindow();
        if (window == null) return null;
        return window.getActivity().get();
    }

    @CalledByNative
    private static XrSessionCoordinator create(long nativeXrSessionCoordinator) {
        ThreadUtils.assertOnUiThread();
        return new XrSessionCoordinator(nativeXrSessionCoordinator);
    }

    /**
     * Gets the Activity of the currently active Session as a Context. Will return null if not
     * called between startSession and endSession.
     *
     * @return Context The current activity as a Context.
     */
    @CalledByNative
    private static Context getCurrentActivityContext() {
        if (sActiveSessionInstance == null || sActiveSessionInstance.mWebContents == null) {
            return null;
        }

        return getActivity(sActiveSessionInstance.mWebContents);
    }

    /**
     * Gets the current application context. May not correspond to an activity, and would not be
     * suitable for calls requiring such.
     *
     * @return Context The application context.
     */
    @CalledByNative
    private static Context getApplicationContext() {
        return ContextUtils.getApplicationContext();
    }

    private XrSessionCoordinator(long nativeXrSessionCoordinator) {
        if (DEBUG_LOGS) {
            Log.i(TAG, "constructor, nativeXrSessionCoordinator=" + nativeXrSessionCoordinator);
        }
        mNativeXrSessionCoordinator = nativeXrSessionCoordinator;
    }

    private void startSession(
            @SessionType int sessionType,
            XrImmersiveOverlay.Delegate overlayDelegate,
            final WebContents webContents) {
        assert (sActiveSessionInstance == null);
        assert (sessionType != SessionType.NONE);

        mImmersiveOverlay = new XrImmersiveOverlay();
        mImmersiveOverlay.show(overlayDelegate, webContents, this);

        mWebContents = webContents;
        mActiveSessionType = sessionType;
        sActiveSessionInstance = this;
        sActiveSessionAvailableSupplier.set(sessionType);
    }

    @CalledByNative
    private void startArSession(
            final ArCompositorDelegateProvider compositorDelegateProvider,
            final WebContents webContents,
            boolean useOverlay,
            boolean canRenderDomContent) {
        if (DEBUG_LOGS) Log.i(TAG, "startArSession");
        // The higher levels should have guaranteed that we're only called if there isn't any other
        // active session going on.
        assert (sActiveSessionInstance == null);

        XrImmersiveOverlay.Delegate overlayDelegate =
                ArClassProvider.getOverlayDelegate(
                        compositorDelegateProvider.create(webContents),
                        webContents,
                        useOverlay,
                        canRenderDomContent);
        startSession(SessionType.AR, overlayDelegate, webContents);
    }

    @CalledByNative
    private void startVrSession(
            final VrCompositorDelegateProvider compositorDelegateProvider,
            final WebContents webContents) {
        if (DEBUG_LOGS) Log.i(TAG, "startVrSession");
        // The higher levels should have guaranteed that we're only called if there isn't any other
        // active session going on.
        assert (sActiveSessionInstance == null);

        XrImmersiveOverlay.Delegate overlayDelegate =
                CardboardClassProvider.getOverlayDelegate(
                        compositorDelegateProvider.create(webContents), getActivity(webContents));
        startSession(SessionType.VR, overlayDelegate, webContents);
    }

    @CalledByNative
    private void startXrSession() {
        if (DEBUG_LOGS) Log.i(TAG, "startXrSession");
        // The higher levels should have guaranteed that we're only called if there isn't any other
        // active session going on.
        assert (sActiveSessionInstance == null);

        // The active session must be set before creating the host activity, since it will be
        // notified once the activity is ready.
        sActiveSessionInstance = this;
        mActiveSessionType = SessionType.VR;
        sActiveSessionAvailableSupplier.set(SessionType.VR);

        Intent intent = XrHostActivity.createIntent(getApplicationContext());
        getApplicationContext().startActivity(intent);
    }

    private void endSessionFromXrHost() {
        if (DEBUG_LOGS) Log.i(TAG, "endSessionFromXrHost");

        if (sActiveSessionInstance == null) return;
        assert (sActiveSessionInstance == this);

        // Since the XrHostActivity is removing us we don't need to clean it up, so null it out now.
        mXrHostActivity = null;
        endSession();
    }

    @CalledByNative
    private void endSession() {
        if (DEBUG_LOGS) Log.i(TAG, "endSession");

        if (sActiveSessionInstance == null) return;
        assert (sActiveSessionInstance == this);

        if (mImmersiveOverlay != null) {
            mImmersiveOverlay.cleanupAndExit();
            mImmersiveOverlay = null;
        } else {
            onJavaShutdown();
        }

        mActiveSessionType = SessionType.NONE;
        mWebContents = null;
        sActiveSessionInstance = null;
        sActiveSessionAvailableSupplier.set(SessionType.NONE);
        if (mXrHostActivity != null && mXrHostActivity.get() != null) {
            mXrHostActivity.get().finish();
            mXrHostActivity = null;
        }
    }

    // Called from XrDelegateImpl and XRHostActivity
    public static boolean endActiveSession() {
        if (DEBUG_LOGS) Log.i(TAG, "endActiveSession");
        // If there's an active immersive session shut it down and return true so that the caller
        // can take appropriate action, such as consuming a back gesture.
        if (sActiveSessionInstance != null) {
            sActiveSessionInstance.endSession();
            return true;
        }
        return false;
    }

    // Called from XrDelegateImpl and XRHostActivity
    public static boolean endActiveSessionFromXrHost() {
        if (DEBUG_LOGS) Log.i(TAG, "endActiveSessionFromXrHost");
        // If there's an active immersive session shut it down and return true so that the caller
        // can take appropriate action, such as consuming a back gesture.
        if (sActiveSessionInstance != null) {
            sActiveSessionInstance.endSessionFromXrHost();
            return true;
        }
        return false;
    }

    public static boolean hasActiveSession() {
        return sActiveSessionInstance != null;
    }

    public static boolean hasActiveArSession() {
        return sActiveSessionInstance.mActiveSessionType == SessionType.AR;
    }

    public static XrSessionTypeSupplier getActiveSessionTypeSupplier() {
        return sActiveSessionAvailableSupplier;
    }

    public static void onActiveXrSessionButtonTouched() {
        sActiveSessionInstance.onXrSessionButtonTouched();
    }

    public void onDrawingSurfaceReady(
            Surface surface, WindowAndroid rootWindow, int rotation, int width, int height) {
        if (DEBUG_LOGS) Log.i(TAG, "onDrawingSurfaceReady");
        if (mNativeXrSessionCoordinator == 0) return;
        XrSessionCoordinatorJni.get()
                .onDrawingSurfaceReady(
                        mNativeXrSessionCoordinator,
                        XrSessionCoordinator.this,
                        surface,
                        rootWindow,
                        rotation,
                        width,
                        height);
    }

    public static XrSessionCoordinator getActiveInstanceForTesting() {
        return sActiveSessionInstance;
    }

    public void onDrawingSurfaceTouch(
            boolean isPrimary, boolean isTouching, int pointerId, float x, float y) {
        if (DEBUG_LOGS) Log.i(TAG, "onDrawingSurfaceTouch");
        if (mNativeXrSessionCoordinator == 0) return;
        XrSessionCoordinatorJni.get()
                .onDrawingSurfaceTouch(
                        mNativeXrSessionCoordinator,
                        XrSessionCoordinator.this,
                        isPrimary,
                        isTouching,
                        pointerId,
                        x,
                        y);
    }

    public void onDrawingSurfaceDestroyed() {
        if (DEBUG_LOGS) Log.i(TAG, "onDrawingSurfaceDestroyed");
        onJavaShutdown();
    }

    private void onJavaShutdown() {
        if (DEBUG_LOGS) Log.i(TAG, "onJavaShutdown");
        if (mNativeXrSessionCoordinator == 0) return;
        XrSessionCoordinatorJni.get()
                .onJavaShutdown(mNativeXrSessionCoordinator, XrSessionCoordinator.this);
    }

    public void onXrSessionButtonTouched() {
        if (DEBUG_LOGS) Log.i(TAG, "onXrSessionButtonTouched");
        if (mNativeXrSessionCoordinator == 0) return;
        XrSessionCoordinatorJni.get()
                .onXrSessionButtonTouched(mNativeXrSessionCoordinator, XrSessionCoordinator.this);
    }

    /**
     * Called when an XrHostActivity has started and is ready to be passed as an argument to
     * xrCreateInstance().
     *
     * @return True if an active session was notified that the activity is ready.
     */
    public static boolean onXrHostActivityReady(Activity activity) {
        if (DEBUG_LOGS) Log.i(TAG, "onXrHostActivityReady");
        if (sActiveSessionInstance != null) {
            sActiveSessionInstance.handleXrHostActivityReady(activity);
            return true;
        }
        return false;
    }

    private void handleXrHostActivityReady(Activity activity) {
        if (mNativeXrSessionCoordinator == 0) return;
        mXrHostActivity = new WeakReference(activity);
        XrSessionCoordinatorJni.get()
                .onXrHostActivityReady(
                        mNativeXrSessionCoordinator, XrSessionCoordinator.this, activity);
    }

    @CalledByNative
    private void onNativeDestroy() {
        // Native destructors should end sessions before destroying the native XrSessionCoordinator
        // object.
        assert sActiveSessionInstance != this : "unexpected active session in onNativeDestroy";

        mNativeXrSessionCoordinator = 0;
    }

    @NativeMethods
    interface Natives {
        void onDrawingSurfaceReady(
                long nativeXrSessionCoordinator,
                XrSessionCoordinator caller,
                Surface surface,
                WindowAndroid rootWindow,
                int rotation,
                int width,
                int height);

        void onDrawingSurfaceTouch(
                long nativeXrSessionCoordinator,
                XrSessionCoordinator caller,
                boolean primary,
                boolean touching,
                int pointerId,
                float x,
                float y);

        void onJavaShutdown(long nativeXrSessionCoordinator, XrSessionCoordinator caller);

        void onXrSessionButtonTouched(long nativeXrSessionCoordinator, XrSessionCoordinator caller);

        void onXrHostActivityReady(
                long nativeXrSessionCoordinator, XrSessionCoordinator caller, Activity activity);
    }
}