Skip to content

Setup Navigation

Set up your own location provider or use partner wrappers to enable indoor navigation in a plan.

Recommended: Prefer coroutine-based APIs. Callback APIs are available for compatibility (e.g., Java).


Overview

  • A plan consumes positions via IExpoFpLocationProvider.
  • You can implement your own provider or use ExpoFP wrappers (CrowdConnected, IndoorAtlas).
  • A provider may be attached to one presenter or shared as a global provider across screens.

Step 1. Add Dependencies (Gradle)

Use the 5.x line of ExpoFP artifacts. If you start from the first 5.x release, pin to 5.0.0 (or 5.0.+ to receive only patch updates). You can also use 5.+ to stay on the latest 5.x.

Kotlin DSL (build.gradle.kts):

dependencies {
    // Core ExpoFP SDK
    implementation("com.expofp:fplan:5.0.0") // or "5.0.+", or "5.+"

    // Partner wrappers (pick what you need)
    implementation("com.expofp:crowdconnected:5.0.0")             // CrowdConnected (foreground)
    implementation("com.expofp:crowdconnectedbackground:5.0.0")   // CrowdConnected (background-capable)
    implementation("com.expofp:indooratlas:5.0.0")                // IndoorAtlas
}

Version Catalogs (gradle/libs.versions.toml):

[versions]
expofp = "5.0.0"

[libraries]
expofp-fplan = { group = "com.expofp", name = "fplan", version.ref = "expofp" }
expofp-crowdconnected = { group = "com.expofp", name = "crowdconnected", version.ref = "expofp" }
expofp-crowdconnected-background = { group = "com.expofp", name = "crowdconnectedbackground", version.ref = "expofp" }
expofp-indooratlas = { group = "com.expofp", name = "indooratlas", version.ref = "expofp" }

Then use in build.gradle.kts:

dependencies {
    implementation(libs.expofp.fplan)
    implementation(libs.expofp.crowdconnected)             // optional
    implementation(libs.expofp.crowdconnected.background)  // optional
    implementation(libs.expofp.indooratlas)                // optional
}


Step 2. Manifest Setup

Permissions depend on the provider (GPS, BLE beacons, IPS SDK). Include only what you actually use.

<manifest>
    <!-- Network (map tiles, assets, etc.) -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <!-- Location -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    <!-- Background location (optional; only if needed) -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

    <!-- Bluetooth for IPS beacons (optional; Android 12+) -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

    <!-- Legacy Bluetooth (API < 31) -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

    <!-- Foreground service for location (if your provider runs a FGS) -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>

    <!-- Optional: BLE not strictly required -->
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>

    <application>
        ...
    </application>
</manifest>

Step 3. Request Runtime Permissions (example)

For production, prefer the Activity Result API. Below is a minimal example for clarity.

val permsModern = arrayOf(
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.BLUETOOTH_SCAN,
    Manifest.permission.BLUETOOTH_CONNECT
)

val permsLegacy = arrayOf(
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION
)

ActivityCompat.requestPermissions(
    this,
    if (Build.VERSION.SDK_INT >= 31) permsModern else permsLegacy,
    100
)

Step 4. Implement Your Own Provider

Implement IExpoFpLocationProvider and forward positions to the presenter through the delegate.

class YourLocationProvider(private val context: Context) : IExpoFpLocationProvider {
    override var expoFpLocationProviderDelegate: ExpoFpLocationProviderDelegate? = null

    private val fused by lazy { LocationServices.getFusedLocationProviderClient(context) }
    private var callback: LocationCallback? = null

    @MainThread
    override suspend fun startUpdatingLocation() {
        val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000L).build()
        callback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                val loc = result.lastLocation ?: return
                val position = ExpoFpPosition(lat = loc.latitude, lng = loc.longitude)
                expoFpLocationProviderDelegate?.positionDidChange(position)
            }
        }
        fused.requestLocationUpdates(request, callback as LocationCallback, Looper.getMainLooper())
    }

    override fun stopUpdatingLocation() {
        callback?.let { fused.removeLocationUpdates(it) }
        callback = null
    }
}

You can also use partner wrappers (CrowdConnected, IndoorAtlas) via ExpoFP modules. See below.


Step 5. Attach to Presenter

val locationProvider = YourLocationProvider(context)
val presenter = ExpoFpPlan.createPlanPresenter(
    planLink = ExpoFpLinkType.ExpoKey("YourExpoKey"),
    locationProvider = locationProvider
)

After presenter creation

presenter.setLocationProvider(YourLocationProvider(context))

When the plan view appears, the presenter calls startUpdatingLocation() automatically.
When the plan view disappears, it calls stopUpdatingLocation() only if the provider is not global.

Manual control remains available:

locationProvider.startUpdatingLocation()
locationProvider.stopUpdatingLocation()


Step 6. Global Location Provider

Use one provider instance across multiple plans.

// Set once
val provider = YourLocationProvider(context)
ExpoFpPlan.globalLocationProvider.sharedProvider = provider

// Optional manual control
ExpoFpPlan.globalLocationProvider.startUpdatingLocation()
// ...
ExpoFpPlan.globalLocationProvider.stopUpdatingLocation()

Attach global provider to a presenter:

val presenter = ExpoFpPlan.createPlanPresenter(
    planLink = ExpoFpLinkType.ExpoKey("YourExpoKey"),
    locationProvider = ExpoFpPlan.globalLocationProvider
)
// or later:
presenter.setLocationProvider(ExpoFpPlan.globalLocationProvider)

For global providers the presenter does not stop updates on view disappearance — manage lifecycle yourself.


Step 7. Listen to Location Updates Manually

When a presenter uses a provider, it sets itself as the provider's delegate.
If you set your own delegate, forward updates to the plan manually via selectCurrentPosition(position, focus).

class YourLocationProviderDelegate(
    private val presenter: IExpoFpPlanPresenter
) : ExpoFpLocationProviderDelegate {

    @MainThread
    override fun positionDidChange(newPosition: ExpoFpPosition) {
        presenter.selectCurrentPosition(newPosition, focus = false)
    }
}

Attach the delegate:

val provider = YourLocationProvider(context)
provider.expoFpLocationProviderDelegate = YourLocationProviderDelegate(presenter)

// For global provider:
// ExpoFpPlan.globalLocationProvider.expoFpLocationProviderDelegate = YourLocationProviderDelegate(presenter)


CrowdConnected (ExpoFP Wrapper)

Settings

val ccSettings = ExpoFpCrowdConnectedLocationProviderSettings(
    appKey = "YOUR_APP_KEY",
    token = "YOUR_TOKEN",
    secret = "YOUR_SECRET",
    navigationType = ExpoFpCrowdConnectedNavigationType.IPS, // or GEO / ALL
    isAllowedInBackground = false,   // true -> include ACCESS_BACKGROUND_LOCATION in checks
    isHeadingEnabled = true,         // enable azimuth heading
    aliases = mapOf("visitorId" to "12345"),
    notificationText = "Indoor navigation is active", // foreground service notification
    serviceIcon = R.drawable.ic_navigation            // notification icon
)

Choose a Provider Class

// Foreground-only
val ccProvider = ExpoFpCrowdConnectedLocationProvider(applicationContext, ccSettings)

// Background-capable (adds BackgroundModule and background permission flow)
val ccBgProvider = ExpoFpCrowdConnectedBackgroundLocationProvider(applicationContext, ccSettings)

Start with Auto-Permissions

Activity vs RegistryOwner - Use activity for a simple Activity-based flow. - Use registryOwner when you integrate with ActivityResultRegistry (common inside Compose and custom components).
> You can pass both when available.

Coroutine + Activity

lifecycleScope.launch {
    ccProvider.startWithAutoPermissions(
        owner = this@YourActivity,   // LifecycleOwner
        activity = this@YourActivity // Activity
    )
}

Coroutine + ActivityResultRegistryOwner

lifecycleScope.launch {
    ccProvider.startWithAutoPermissions(
        owner = this@YourActivity,     // LifecycleOwner
        registryOwner = this@YourActivity // ActivityResultRegistryOwner
    )
}

Callback + Activity

ccProvider.startWithAutoPermissions(
    owner = this,
    activity = this,
    onStarted = { /* ready */ },
    onDenied = { denied, permanentlyDenied -> /* handle */ },
    onError = { e -> /* log */ }
)

Callback + ActivityResultRegistryOwner

ccProvider.startWithAutoPermissions(
    owner = this,
    registryOwner = this,
    onStarted = { /* ready */ },
    onDenied = { denied -> /* handle */ },
    onError = { e -> /* log */ }
)

Attach to Presenter

val presenter = ExpoFpPlan.createPlanPresenter(
    planLink = ExpoFpLinkType.RawLink("https://demo.expofp.com"),
    locationProvider = ccProvider // or ccBgProvider
)

Use in Compose

setContent {
    val context = LocalContext.current
    val activity = context as ComponentActivity
    val scope = rememberCoroutineScope()
    val expoView = remember { ExpoFpView(context).apply { attachPresenter(presenter) } }

    Column {
        Button(onClick = {
            scope.launch {
                ccProvider.startWithAutoPermissions(
                    owner = activity,
                    registryOwner = activity
                )
            }
        }) { Text("Start navigation") }

        AndroidView(factory = { expoView }, modifier = Modifier.fillMaxSize())
    }
}

Stop and Dispose

ccProvider.stopUpdatingLocation()
ccProvider.close()

IndoorAtlas (ExpoFP Wrapper)

Create Provider

val iaProvider = ExpoFpIndoorAtlasLocationProvider(
    appContext = applicationContext,
    apiKey = "YOUR_IA_API_KEY",
    apiSecret = "YOUR_IA_API_SECRET"
)

Start with Auto-Permissions

Activity vs RegistryOwner — same rationale as above.

Coroutine + Activity

lifecycleScope.launch {
    iaProvider.startWithAutoPermissions(
        owner = this@YourActivity,
        activity = this@YourActivity
    )
}

Coroutine + ActivityResultRegistryOwner

lifecycleScope.launch {
    iaProvider.startWithAutoPermissions(
        owner = this@YourActivity,
        registryOwner = this@YourActivity
    )
}

Callback + Activity

iaProvider.startWithAutoPermissions(
    owner = this,
    activity = this,
    onStarted = { /* ready */ },
    onDenied = { denied, permanentlyDenied -> /* handle */ },
    onError = { e -> /* log */ }
)

Callback + ActivityResultRegistryOwner

iaProvider.startWithAutoPermissions(
    owner = this,
    registryOwner = this,
    onStarted = { /* ready */ },
    onDenied = { denied -> /* handle */ },
    onError = { e -> /* log */ }
)

Attach to Presenter

val presenter = ExpoFpPlan.createPlanPresenter(
    planLink = ExpoFpLinkType.RawLink("https://demo.expofp.com"),
    locationProvider = iaProvider
)

Use in Compose

setContent {
    val context = LocalContext.current
    val activity = context as ComponentActivity
    val scope = rememberCoroutineScope()
    val expoView = remember { ExpoFpView(context).apply { attachPresenter(presenter) } }

    Column {
        Button(onClick = {
            scope.launch {
                iaProvider.startWithAutoPermissions(
                    owner = activity,
                    registryOwner = activity
                )
            }
        }) { Text("Start navigation") }

        AndroidView(factory = { expoView }, modifier = Modifier.fillMaxSize())
    }
}

Stop and Dispose

iaProvider.stopUpdatingLocation()
iaProvider.close()

Best Practices

  • Request only needed permissions: tailor to your provider (GPS/BLE/IPS).
  • Prefer coroutine APIs for lifecycle-aware control.
  • Explain background location to users and keep a persistent notification for foreground service.
  • Use a global provider only if several screens share the same location stream.
  • Stop updates when not needed to save battery.
  • Do not block the main thread inside provider callbacks; offload heavy work to coroutines.