Android Integration (Kotlin)
The native Android SDK supplies the AR engine, online and offline localization, occlusion, spatial content and navigation. It uses ARCore and SceneView/Filament, with ONNX Runtime and OpenCV for on-device photo positioning. Your app supplies its UI, authenticated tour/content requests and route data. There is no Unity runtime or MultiSet framework to embed.
VPSController supplies the host command/event API, and VPSSession owns the AR engine and its native controllers. They use one coordinate frame and one set of capabilities. Either integration works independently of the navigation app.
Install the compiled SDK
Obtain TreedisVPS-Android-0.3.0.zip and its SHA-256 checksum from Treedis. It contains the compiled AAR, inference models, consumer rules, Maven dependency metadata, and third-party licenses. Customers do not need the SDK implementation or a source checkout.
Requirements:
- JDK 17, Android compile SDK 36, min SDK 28. The checked build uses AGP 8.12.1, Gradle 8.13 and Kotlin 2.1.20.
- An ARCore-supported physical Android device and Google Play Services for AR for live AR. Emulator checks cover UI, protocol and photo inference; they do not validate camera localization or walking.
- Runtime camera permission before attaching the AR view. Add application metadata
com.google.ar.corewith valueoptionalif your host should offer QR/browsing on devices without ARCore. The SDK declares camera/internet permissions and an optional AR camera feature. - Host-provided VPS configuration. Keep keys out of URLs, logs, tour payloads and committed files. Requests to signed asset/media/upload URLs do not receive the VPS key.
Verify the supplied checksum and extract the archive into Vendor/TreedisVPS-Android-0.3.0 in your project. Keep the complete repository folder, including the POM and Gradle module metadata. In settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("Vendor/TreedisVPS-Android-0.3.0/repository")
content { includeGroup("com.treedis") }
}
}
}
In the app's build.gradle.kts:
dependencies { implementation("com.treedis:vps-android:0.3.0") }
android {
packaging { jniLibs.pickFirsts.add("**/libc++_shared.so") }
}
Import com.treedis.vps.*. Gradle consumes the compiled AAR and uses its metadata to resolve ARCore, SceneView, ONNX Runtime, OpenCV, and AndroidX dependencies from Google/Maven Central. The Treedis coordinate comes from the supplied local repository; it is not published to Maven Central. Copying only the AAR or using flatDir loses the dependency metadata.
Retain the supplied licenses and THIRD_PARTY_NOTICES.md. Pin the SDK version and checksum in your build process, and replace the complete distribution when upgrading. Tour/offline packs are separate downloads.
Native session
After camera permission is granted, create the view from a ComponentActivity and add it to your own hierarchy:
import com.treedis.vps.*
val session = VPSSession(context, VPSConfiguration(serverURL, apiKey))
val camera = session.createView(activity)
// Add camera to your host's view hierarchy.
session.setContent(listOf(
VPSContentItem("123", Vec3(2.0, 1.0, -4.0), "Reception")
))
session.start(VPSTarget.tour(tourID), VPSLocalizationSettings())
The host has already resolved the numeric tourID in its own environment. IDs can overlap between deployments: verify the map's environment, numeric tour ID, model and provider before starting. The SDK never resolves a slug through a hardcoded SaaS/CW API.
Retain one session per experience. Public controller and view calls belong on the main thread; inference and downloads run on worker dispatchers. Observe session.state, session.events, session.cameraPose, session.problem, and session.navigation.progress from lifecycle-aware tasks.
session.stop() pauses localization/ARCore while preserving the anchor/content for resume. Resume with session.start(target, settings). reset() clears the experience, route, content and hint. Call session.detach() before destroying a hosted AR view, then close() when the session owner is finished. A closed controller is not reusable. There is no Unity activity, message shim, native symbol or Unity lifecycle rule.
Platform adapters
Supply your own tour loader and route provider:
val sdk = VPSController(session)
sdk.loadTour = { slug, matterID ->
platform.experience(slug, matterID) // Your adapter returns VPSExperience.
}
session.navigation.routeProvider = { request ->
platform.route(request) // Your adapter returns VPSRoute.
}
sdk.listener = object : VPSControllerListener {
override fun onLocalizationStatusChanged(isLocalized: Boolean) { /* Update host UI. */ }
override fun onNavigationComplete() { /* Show arrival UI. */ }
override fun onEvent(name: String, args: org.json.JSONArray) { /* Handle other events. */ }
}
sdk.setTourInfo("your-tour-slug")
sdk.startLocalization()
sdk.start(camera)
platform above is a host-owned adapter, not another SDK dependency. VPSExperience carries target, settings, tags and objects. Its values, and route nodes, use the public tour coordinate frame.
Commands queue until start(view) attaches the AR view. The queue drains in order before onReady; asynchronous tour loading completes later through loading events. onReady is not a localization success. A late listener receives readiness again. stop() clears pending commands and cancels work; reset() resets the experience and cancels unfinished tour loads. Close the controller and session when ownership ends.
setEnvironment is forwarded to sdk.hostCommand: update your platform API/CDN clients and session.configuration there. Environment configuration completes before the next tour load. Also handle hosted UI, push, device integrity and account commands in that suspend callback. Missing adapters emit onError; a host handler must perform the operation or throw, rather than simply acknowledge it.
Navigation from A to B
The route provider receives VPSRouteRequest(start, destination, type). Its start is the live camera position; the destination has ID, title and position. Route types are SHORTEST, NO_ELEVATORS, and NO_STAIRS (wire values shortest, noElevators, noStairs). Call your server's route endpoint, then return ordered VPSPathNode values and the destination.
// Call from a coroutine after localization.
session.navigation.start(
VPSDestination("123", "Conference room", Vec3(2.0, 1.0, -4.0)),
VPSNavigationType.SHORTEST,
)
The SDK draws the path, measures remaining distance, derives turn direction, detects deviation, reroutes through the host provider, and emits arrival. recompute() preserves the previous usable route if its request fails. For precomputed nodes, use session.navigation.setPath(VPSRoute(nodes, destination)); this disables automatic rerouting. Route computation itself belongs to the host/server.
A floor-change entry node has VPSFloorTransition(kind, direction, name, exitPosition). Show confirmation on WAITING_FOR_FLOOR_CHANGE, call confirmTeleport() before the user physically moves between floors, then endTeleport() after arrival. Content hides during the move; the SDK applies a landing hint and requires a new localization before continuing. Do not send both commands back to back. The wire setPath provides a single-floor path without transition metadata.
Read navigation.progress for state, metres, instruction, bearing and pending transition. Pose events are emitted at most every 200 ms and distance events at most once per second while following a route.
Localization and strict offline mode
The default is VPSEngine.SERVER. A slow or failed request never silently selects the phone. Settings include confidence gating (default 0.4), background cadence (30 seconds or 10 metres), initial/background timeouts, position/floor/search-depth hints, query mode, multi-frame requests, GPS/geographic output, occlusion and mesh overlay.
session.update(session.settings.copy(
confidenceThreshold = 0.4,
relocalizeEverySeconds = 30.0,
relocalizeEveryMeters = 10.0,
searchDepth = 2,
))
session.setLocalizationHint(Vec3(2.0, 1.0, -4.0), radius = 10.0)
Changing engines starts a fresh scan. rescan() discards the anchor; relocalize() requests a fresh check. First anchors and inconsistent replacement anchors require agreement from separated viewpoints. Hints clear after an accepted localization. Server localization applies hints; the current offline engine searches its downloaded pack without hint filtering.
Offline use is explicit:
- Ask for download approval in your Settings UI.
session.offline.offer(target)reports size/version. - Call
session.offline.download(target, attachments = { yourHostFiles }, progress = { ... })from a coroutine. Attach tags, arSettings, verified identity and your navigation/content data as appropriate. - Persist the host's offline-enabled choice only after download completes. Downloading alone does not activate offline mode.
- Only while that choice is enabled, start with
settings.copy(engine = VPSEngine.ON_DEVICE). A complete pack for that exact server and tour must exist. - Read host data from
offline.attachment(name, target). A missing pack or file is an error, never an HTTP fallback. Delete packs and attachments when the host turns offline off.
Downloads verify SHA-256 and byte counts, install atomically and use app-private no-backup storage. Cancelled/failed downloads cannot replace the installed pack. Android currently supports individual tour packs offline; map-set offline localization is unavailable. Download/select a member tour instead.
New routes still need the host's server, cached route nodes or its own offline graph. SDK photo localization can run without an AR view through VPSPhotoLocalizer. Rich media requires local file URLs in strict offline mode. Bundled ONNX models total about 12.5 MB; each compact tour pack is downloaded separately.
session.server exposes map-set, map information, georeference and analytics methods. VPSTarget.mapSet(primaryTourId) localizes online in the primary tour's frame; member content can be transformed into that frame. GPS hints are host-supplied VPSGeoHint values; request location permission only if your host collects them.
Spatial objects and queries
setContent, setObjects, upsertObject, removeObject and setObjectVisible manage tags, threads, images, video, GLB/glTF models, static HTML and polygons. Objects support transform, range, visibility and thread completion state. Android does not load Apple's USDZ/Reality formats; use GLB/glTF for shared model assets. HTML has JavaScript disabled; interactive content belongs in host UI.
Native AR touches emit onARObjectClick. click is for a host overlay that intercepts touches; setWindowSize only scales those coordinates. raycastTag() selects a tag near the camera's forward ray. captureCameraFrame() returns a camera-only JPEG with matching intrinsics; the controller optionally PUTs it to an HTTPS upload URL without a VPS key.
Thread placement supports device/surface mode, confirm/cancel, nearest sweep, upsert, completion and removal. The SDK returns the placement; the host persists messages and business data.
Commands
Use typed methods, sdk.send(type, JSONObject) or sdk.receive(json). Wire data is a JSON object; optional requestId belongs inside it. Fields must have valid types: finite numbers, object IDs and booleans. Unknown commands or malformed fields produce onError.
| Wire commands | Kotlin / data |
|---|---|
setEnvironment |
setEnvironment(environment, customApiUrl?, customCdnUrl?); host adapter |
setTourInfo |
setTourInfo(slug, matterId?) |
startLocalization, relocalize, reset |
Methods with the same names |
setLocalizationHint, clearLocalizationHint |
setLocalizationHint(x,y,z,radiusMeters?,floorMinY?,floorMaxY?), clearLocalizationHint() |
showARObject, hideARObject |
String id, type; typed methods |
setMeshVisible |
setMeshVisible(visible) |
click, setWindowSize |
click(x,y), setWindowSize(width,height); top-left-origin view pixels |
startNavigation |
startNavigation(destinationName,x,y,z,navigationType = "shortest") |
stopNavigation, recomputePath |
Methods with the same names |
setPath |
setPath(destinationName, nodes: List<VPSPathNode>); wire nodes:[{x,y,z}] |
confirmTeleport, endTeleport |
confirmTeleport(), endTeleport() |
getPosition, raycastTag |
Optional requestId; getPosition(requestId), raycastTag(requestId) |
captureCameraFrame |
captureCameraFrame(requestId?, uploadUrl?) |
startThreadCreation, setThreadPlacementMode |
Typed methods with boolean useDevicePosition |
confirmThreadPlacement, cancelThreadCreation |
Methods with the same names |
spawnThread, updateThread |
spawnThread(thread), updateThread(thread); thread data has id, title, position fields and isCompleted |
removeThread, setThreadCompleted |
removeThread(threadId), setThreadCompleted(threadId,isCompleted) |
loadUI, unloadUI |
Host adapter; load data has url; openTreedisUI / closeTreedisUI helpers |
requestPushToken, requestDeviceIntegrity |
Host adapter |
reportSessionOrigin, signOut, log, logWebView |
Host adapter for account/web/diagnostic actions |
All commands have typed native methods and the same JSON names, including the host-owned operations. Object types are TAG, THREAD, VS_IMAGE, VS_VIDEO, VS_OBJECT, VS_HTML and POLYGON.
All public positions, including controller, wire and session hints, use the Treedis/Showcase Y-up metre frame. Hint radius defaults to 25 metres and is clamped to 1–100, including fractional values. Supply both floor bounds in ascending order. Convert any external coordinate format at the host boundary before calling the SDK. Position-query rotation is the camera forward vector; heading is degrees in [0,360).
Events
Set a VPSControllerListener or sdk.onEvent = { name, args -> ... }. Listener methods have no-op defaults. The catch-all receives every event, even when a typed callback also handles it. Native arguments are structured JSON values.
| Events | Arguments / typed callback |
|---|---|
onReady |
[]; onReady() |
onLoadingStarted, onLoadingComplete |
[]; typed callbacks |
onLoadingProgress |
[step, progress0to1]; onLoadingProgress |
onLocalizationStatusChanged |
[localized]; onLocalizationStatusChanged |
onLocalizationResult |
[success, confidence] per inference attempt; acceptance also requires the configured gates |
onARObjectClick |
[id,type]; onARObjectClick with string IDs |
onARObjectReady, onMeshReady |
[id,type], [true]; catch-all |
onNavigationDistance |
[metres]; onNavigationDistance |
onNavigationTeleport |
[direction,name,kind]; onNavigationTeleport |
onNavigationComplete |
[]; onNavigationComplete |
onPose |
[x,y,z,headingDegrees]; onPose |
onPosition |
[{requestId,localized,pose?,rotation?}]; onPosition(VPSPositionResult) |
onRaycast |
[{requestId,tagId,distance,confidence}]; absent tag ID is null |
onCameraFrame |
[{requestId,ok,mime?,width?,height?,image?,uploadUrl?,error?}]; image is JPEG base64 |
onThreadPlacementStarted |
["device" or "raycast"] |
onThreadPlaced |
[x,y,z,sweepID] |
onThreadPlacementCancelled |
[] |
onThreadPlacementFailed, onThreadCreationError |
[message] |
onMessage |
[{type,data}]; log, localization status/success/warning, tracking warning and info |
onError |
[{command?,objectId?,message}] |
onPushToken |
[token,platform]; host-reported |
onPushTap, onPushMessage |
[data]; host-reported |
onDeviceIntegrity |
[report]; host-reported |
onPosition.pose has x,y,z,confidence; its rotation has forward x,y,z. A result may be unlocalized. Soft localization warnings occur after prolonged low motion or failed background checks and are rate-limited; the host supplies its scanning prompt/animation.
Hosted UI and system services
Retain VPSWebBridge(webView, sdk, allowedOrigins) before loading a page. It installs TreedisVPS.send and named callbacks for explicitly allowed HTTPS origins and only accepts commands from the main frame. A current Android System WebView with web-message/document-start support is required. Disconnect before removing the page. Your app still controls navigation, cookies, camera/media permissions and account access.
Most events invoke matching global functions directly (onNavigationComplete, onPose, etc.). onMessage receives a structured {type,data} object, just as native event callbacks do.
Handle loadUI, unloadUI, environment updates, push, integrity policy, session diagnostics and sign-out in sdk.hostCommand. Report actual host results using:
sdk.reportHostEvent(VPSHostEvent.PushToken(token, "android"))
sdk.reportHostEvent(VPSHostEvent.PushTap(notificationData))
sdk.reportHostEvent(VPSHostEvent.PushMessage(notificationData))
sdk.reportHostEvent(VPSHostEvent.DeviceIntegrity(integrityReport))
notificationData is a Map<String,String>; integrityReport is a JSON object supplied by your host. Integrity uses the report fields such as platform, issues, flags and host-provided device/app metadata. The SDK does not manufacture push tokens or invent an integrity assessment.
QR and app links
Your host owns QR scanning, manual entry, and cold/warm app-link handling. Parse the tour and destination in the host, fetch the matching platform data, then pass SDK values or commands to the engine. You can implement navigation, Connected Workers, or another UI with the same compiled library.
App-link handling and release association are different steps. Configure your manifest/Activity to handle navigation URLs; verified public links require your website's assetlinks.json, the release package name and signing-certificate fingerprint. Store publication and website association are not performed by the SDK.
Android has no Apple App Clip target. Google retired Play Instant publishing and serving in December 2025; use your installed Android app's QR/app links. See Google's retirement notice. For Apple App Clips, see the iOS guide.
Independent host applications
Retain your VPSController and VPSSession, supply the host adapters described above, and close them when the owner is destroyed. Native commands, listener values and web events share the same contract. Independent apps consume the compiled SDK with their own UI and API client.
Verification scope
The Android checks cover coordinate conversion, URL/key scoping, route/floor-change state, request-ID/readiness behavior, host-service errors and the explicit offline boundary. Emulator UI tests cover QR/manual entry, Settings approval, category/search/preview and map-identity failure behavior. A private full-map reference photo localized in landscape and rotated portrait within 3.8 mm / 0.10°. Emulator CPU times ranged from 1.55 seconds initially to 8–17 seconds under concurrent build load. This is one reference photo in two orientations, not a held-out or live-device benchmark. Android hardware acceleration, physical QR invocation, ARCore mesh/media rendering and walking a published map still need a real-device acceptance run.

