Develop
Develop
Select your platform

Hybrid apps overview

Updated: May 28, 2025

Getting started

In this guide, you’ll learn about hybrid apps, a type of Android app that can jump between 2D panel activities and immersive activities within the same app. This allows you to customize how users interact with your app.
Panel activities are built using 2D UI frameworks. They have the same capabilities as regular 2D activities and can run in the following contexts:
  • Home: the default home environment and landing point for the user when starting their device or exiting an immersive app.
  • Overlay: the menu that is brought up when pressing the Quest button.
  • Embedded: a panel activity can be embedded within another immersive activity.
Immersive activities are OpenXR-based activities. Both activities have access to Android and Horizon OS capabilities and APIs.
You can download a sample hybrid app built with Spatial SDK through this GitHub repo.

Interaction modes

Hybrid apps support two interaction models: exclusive and cooperative. Exclusive focuses on one activity at a time. Cooperative allows you to have multiple activities running concurrently.

Exclusive mode

In exclusive mode, users interact with one type of activity at a time. This is a good option if you’re focused on:
  • Mirroring content between panel and immersive activities.
  • Performing setup in a panel view before transferring to an immersive view.
  • Handling permission approval within a panel view before returning to an immersive view.
When moving between activities, your app should terminate any active activities after starting a new activity. For more details, see the Moving between activities section.

Cooperative mode

In cooperative mode, both panel and immersive activities for the same app can be simultaneously visible and interactable. To use cooperative mode, launch a panel activity as an overlay while within an immersive activity. For more details, see the Moving between activities section.

SDK setup

Apps must target Meta Horizon OS v69 or later to use hybrid functionality. Do this by adding the following metadata to your app’s AndroidManifest.xml file:
<manifest
    xmlns:horizonos="http://schemas.horizonos/sdk">

    <horizonos:uses-horizonos-sdk
        horizonos:minSdkVersion="69"
        horizonos:targetSdkVersion="76" />

Activity setup

Each activity within your app must include an identifier indicating if it renders in a panel or immersive view. This identifier is set within the activity’s intent filter, using com.oculus.intent.category.2D for panel activities and com.oculus.intent.category.VR for immersive activities.
// Panel Activity
<activity
    android:name=".PanelActivity"
    android:label="@string/panel_activity_name"
    // landscape is recommended, but portrait can be used if it better suits your experience
    android:screenOrientation="landscape"
    >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category
            android:name="android.intent.category.DEFAULT" />
        <category
            android:name="com.oculus.intent.category.2D" />
    </intent-filter>
</activity>

// Immersive Activity
<activity
    android:name=".VrActivity"
    android:label="@string/immersive_activity_name"
    android:screenOrientation="landscape"
    >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category
            android:name="android.intent.category.DEFAULT" />
        <category
            android:name="com.oculus.intent.category.VR" />
    </intent-filter>
</activity>

Set a default and minimum panel size

Include the layout element within the associated activity of your AndroidManifest.xml file to configure the panel size by setting a default and minimum size.
<activity>
    <layout
        defaultHeight="1080dp"
        defaultWidth="1920dp"
        minHeight="600dp"
        minWidth="800dp"
    />
</activity>

Customizing which activity to launch

When users launch an app, Meta Horizon OS selects the activity to launch using three flags in the AndroidManifest.xml file: one action and two category subelements. The final category subelement chooses which activity to launch.
For example, an app can choose to launch as an immersive activity when in the Home environment, and as a panel activity when launched inside another immersive app.

Selecting a default activity

By default, apps use the android.intent.category.LAUNCHER flag to indicate the starting activity if no other options are provided. This is required by Android and provides a fallback in case one or more launch options are overridden.
<activity>
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="com.oculus.intent.category.VR" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Launching an activity within Home

To specify the activity to launch when the user selects your app in the Home environment, add the com.oculus.intent.category.VR_HOME_LAUNCHER flag to that activity’s intent filter.
<activity>
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="com.oculus.intent.category.VR" />
        <category android:name="com.oculus.intent.category.VR_HOME_LAUNCHER" />
    </intent-filter>
</activity>

Launching an activity as an overlay

To specify the activity to launch when users launch your app within another immersive app, add the com.oculus.intent.category.OVERLAY_LAUNCHER flag to that activity’s intent filter.
<activity>
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="com.oculus.intent.category.2D" />
        <category android:name="com.oculus.intent.category.OVERLAY_LAUNCHER" />
    </intent-filter>
</activity>

Moving between activities

Navigating between activities involves launching intents to the desired activity in your app.

Transition from a panel to an immersive activity

Create an intent to specify the immersive activity to launch, passing in all relevant data from your panel activity. Call finishAndRemoveTask() after sending this intent to ensure your panel activity is no longer running.
fun launchImmersiveActivity() {
    Log.d(TAG, "Launching immersive Activity")
    val immersiveIntent =
        Intent(activity, VrActivity::class.java).apply {
            action = Intent.ACTION_MAIN
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }
    startActivity(immersiveIntent)
    activity?.finishAndRemoveTask()
}

Transition to a panel activity in Home

There are three intents that must be created to move from an immersive activity into a panel activity running in the user’s Home environment:
  • An intent to launch your panel activity.
  • A PendingIntent that wraps your panel activity intent.
  • An intent to launch the Home environment with the PendingIntent included as extra data.
Through this flow, your panel activity will be launched once the Home environment is opened.
fun launchPanelActivityInHome() {
    Log.d(TAG, "Setting up panel Activity pending intent")
    val context = context?.applicationContext ?: return

    // Create the intent used to launch the Panel Activity
    val panelIntent =
            Intent(context, PanelActivity::class.java).apply {
            action = Intent.ACTION_MAIN
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }

    // Wrap the Panel Intent inside of a PendingIntent object
    val pendingPanelIntent =
        PendingIntent.getActivity(
            context,
            0,
            panelIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

    // Create an Intent to launch the Home environment, providing the PendingIntent object as extra parameters
    Log.d(TAG, "Launching Home")
    val homeIntent =
            Intent(Intent.ACTION_MAIN)
            .addCategory(Intent.CATEGORY_HOME)
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            .putExtra("extra_launch_in_home_pending_intent", pendingPanelIntent)
    startActivity(homeIntent)
}

Launching panel activites as an overlay

If a user requests to open a panel activity while staying in the immersive activity, you can open the activity as an overlay within your immersive scene. This is handled by creating an intent to your panel activity and calling the startActivity() function.
Don’t call finishAndRemoveTask(), as it will close the immersive activity.
private fun startPanelActivityInOverlay() {
    // Start the panel activity in a new task
    val panelIntent = Intent(activity, PanelActivity::class.java).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }

    // After this call, the panel activity will be shown in overlay over the immersive activity running in the background.
    activity?.startActivity(panelIntent)
}

Launching adjacent panel activites

Starting with Meta Horizon OS v65, developers can use the FLAG_ACTIVITY_LAUNCH_ADJACENT flag to launch one or more adjacent panel activities within the Home environment.
fun openPanelInAdjacentWindow() {
    // Start the panel activity with the FLAG_ACTIVITY_LAUNCH_ADJACENT flag
    val panelIntent = Intent(activity, PanelActivity::class.java).apply {
        addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or Intent.FLAG_ACTIVITY_NEW_TASK)
    }

    // After this call, the panel activity will be launched next to the actively running activity from your app
    activity?.startActivity(panelIntent)
}
Did you find this page helpful?