# VModal Android SDK technical introduction This guide covers installation, client setup, search, uploads, Android lifecycle integration, network behavior, the supported toolchain, and local development. For a product overview and a choice of runnable examples, return to the [Android SDK README](../README.md). ## Kotlin SDK reference Browse the generated [Kotlin SDK reference](https://v-modal.github.io/vmodal_sdk_android/) for public classes, constructors, properties, extension functions, and methods. KDoc beside the Kotlin declarations is the content authority. The published reference intentionally omits service hosts, endpoint paths, route tables, and implementation source; route synchronization is checked by a separate regression tool. Network diagnostics are disabled by default. For opt-in, SDK-sanitized request-start, response, failure, retry, timing, and signed-upload events—including a small Android Logcat binding—see the [redacted network diagnostics guide](network_diagnostics.md). There is no unredacted mode, and uploaded bytes, raw URLs, credentials, headers, bodies, and exception messages never reach the sink. ## Start in minutes For new content flows, the preferred API binds every upload, search, asset, index, and deletion call to one immutable project/collection/stream scope: ```kotlin import com.vmodal.sdk.VModal val content = VModal.configure( projectId = "food_app", apiKey = apiKeyLoadedByYourApp, ).scope( collectionName = "user_123", streamName = "uploads", ) val results = content.search("the cyclist crossing the bridge at sunset") ``` Use the lower-level `Client` API for authentication, administration, images, R2, and advanced operations not yet represented by the scoped facade. ### 1. Add the SDK The release coordinate is: ```kotlin dependencies { implementation("com.vmodal:vmodal-sdk-android:2.0.0") } ``` Keep `mavenCentral()` in `dependencyResolutionManagement`. Maven Central publication is still pending, so current adopters should clone the [public SDK repository](https://github.com/v-modal/vmodal_sdk_android) beside their app and include the source project: ```kotlin // settings.gradle.kts include(":vmodal-sdk-android") project(":vmodal-sdk-android").projectDir = file("../vmodal_sdk_android") ``` ```kotlin // app/build.gradle.kts dependencies { implementation(project(":vmodal-sdk-android")) } ``` The project uses Java 17. Your app also needs network permission: ```xml ``` ### 2. Connect with your runtime API key Load the key through your app's authenticated backend or secure, app-owned storage. Never bundle a real key in `BuildConfig`, resources, the manifest, or source control. ```kotlin import com.vmodal.sdk.Client import com.vmodal.sdk.MutableApiKeyProvider import com.vmodal.sdk.SdkConfig val keys = MutableApiKeyProvider(apiKeyLoadedByYourApp) val bootstrap = Client(SdkConfig(apiKeyProvider = keys)) val me = bootstrap.coroutines().auth.me() val vmodal = Client( bootstrap.cfg.copy( userId = requireNotNull(me.userId), tenantId = me.tenantId.orEmpty(), email = me.email.orEmpty(), ) ) ``` Keep `keys` and `vmodal` at application or authenticated-session scope so Activities, ViewModels, and workers share the same identity and key rotations. ## Search video from a ViewModel The coroutine facade is preferred for new Kotlin code. The ViewModel owns the scope; the SDK owns no lifecycle or UI dispatcher: ```kotlin viewModelScope.launch { val api = vmodal.coroutines() val groups = api.collections.listGroups("vid_file") val group = groups.findGroup("travel-diaries", "vid_file") ?: error("Collection is not available for this API key") val version = group.latestLancedbVersion ?: error("Collection has no searchable LanceDB version") val results = api.searches.searchVideo( queryText = "the cyclist crossing the bridge at sunset", groupName = group.groupName, streamName = "astream", limit = 20, versionLancedb = version, ) println("${results.cntActual} moments found") results.data.forEach(::println) } ``` Search collection names are scoped to the authenticated runtime key. Use a `vid_file` `GroupItem` returned by `listGroups()`, and send its `latestLancedbVersion`; omitting the advertised version can target the wrong index or an unavailable default. The response stays typed where the contract is stable and preserves `raw: Map` so new server fields remain available immediately. ## Upload from an Android picker Convert the selected `content://` URI into a reopenable `UploadSource` using the [`ContentResolver` adapter](../examples/01_starter/src/main/kotlin/com/vmodal/sdk/examples/ContentUriUploadSource.kt), then collect one signed upload: ```kotlin import com.vmodal.sdk.VideoUploadEvent val source = contentUriSource( context = applicationContext, uri = selectedVideoUri, fileName = "weekend-ride.mp4", ) vmodal.coroutines().collections.videoUploadEvents( source = source, collectionName = "travel-diaries", subCollectionName = "mobile-uploads", ).collect { event -> when (event) { is VideoUploadEvent.Progress -> println("Uploading ${event.progress.percent}%") is VideoUploadEvent.Completed -> println("Ready: ${event.response.destPath}") } } ``` The Flow is cold: every collection starts a new upload. Collect once in a caller-owned scope. If multiple consumers need one operation, share app state with `stateIn`, `shareIn`, or a repository `StateFlow`. Cancelling collection cancels the upload. The SDK streams the video instead of loading it into memory. Existing integrations may keep `videoUploadAsync()` and its `UploadHandle`, or the blocking `videoUpload()` on a worker thread, while migrating one operation at a time. Signed single upload is the production default for every file size. Multipart upload is experimental and must be enabled explicitly with `VideoUploadOptions(multipart = true)`; it fails with `FeatureDisabled` when the gateway does not expose the complete multipart route family. ## Made for Android lifecycles - Use `Client.coroutines()` from `viewModelScope` or `lifecycleScope` for new Kotlin search and collection operations. - Collect UI state with lifecycle awareness (`collectAsStateWithLifecycle` or `repeatOnLifecycle`). - Feed picker results through `ContentResolver` without copying the whole file into memory. - Collect `videoUploadEvents()` once for UI-driven uploads; collector cancellation reaches the upload handle. - Use `CoroutineWorker` for durable uploads. Never retry cancellation, and bound retries to transient failures. - Keep callback `videoUploadAsync()` and blocking `videoUpload()` for existing consumers during migration. - Keep the SDK UI-free: Jetpack Compose and classic Views are both first-class consumers. - Rotate a same-user credential without rebuilding the client: `keys.rotate(freshKey)`. - On logout or account switch, cancel work, clear upload persistence, call `keys.clear()`, and build a new client for the next identity. ## One client, focused resources ```text vmodal.auth identity and health vmodal.searches multimodal video search vmodal.collections upload and collection lifecycle vmodal.indexes create, inspect, and delete indexes vmodal.admin usage and cache statistics vmodal.r2 presigned object-storage operations vmodal.images image retrieval ``` All SDK failures derive from `SdkError`. Apps can handle `AuthError`, `ValidationFailed`, `ApiError`, `FeatureDisabled`, `TransportError`, `ResponseTooLarge`, and `MalformedResponse` separately. ## Security and network behavior Gateway mode is the default. It sends caller identity only through `Authorization: Bearer ` and ignores caller-supplied identity headers. `Client.unsafeDirect(...)` is reserved for trusted private networks whose downstream service independently authenticates identity. - `GET` and `HEAD` may retry recognized transient failures; mutations are sent once. - Authenticated calls require HTTPS, except literal loopback hosts used for development. - Redirects are not followed. - JSON/text responses are bounded to 8 MiB, errors to 1 MiB, and binary responses to 64 MiB. - Presigned uploads never receive the VModal bearer credential or identity headers. Releases use a **minimal release security** profile with one blocking security job: candidate-tree verified-secret detection. Normal SDK tests, route sync, authenticated live tests, clean consumers, version/license checks, and tested- artifact checksums remain blocking correctness gates. OSV/SBOM generation, full-history scanning, strict dependency-verification metadata, wrapper-JAR shell hashing, and compiled route-string scans are preserved but inactive. The profile therefore does not claim a complete dependency or supply-chain audit; see the [Maven Central release guide](maven_release.md) for residual risks. For the complete contract, read [SDK behavior and uploads](sdk_doc.md) and [runtime API-key management](manage_api_key.md). ## Android toolchain | Component | Reference configuration | |---|---:| | Kotlin | `1.9.24` | | Java / JVM target | `17` | | Gradle | `8.6` | | Android Gradle Plugin | `8.4.2` | | Reference app `minSdk` | API 24 / Android 7.0 | | Reference app `compileSdk` | API 34 | The core artifact deliberately avoids Android framework dependencies, which keeps it JVM-testable. The included Android reference app demonstrates Compose, `content://` uploads, lifecycle scopes, and source-project consumption. Gradle 8.6 is the supported build version and is pinned by the checked-in root wrapper. Use `./gradlew` for root builds and Android Studio imports; an installed system Gradle, including Gradle 9, is not part of the supported toolchain. ## Development ```bash git clone https://github.com/v-modal/vmodal_sdk_android.git cd vmodal_sdk_android ./gradlew --no-daemon help bash install.sh check bash test.sh ci bash test.sh all ``` Build the included Android app against the source checkout: ```bash cd examples/02_search ./gradlew --no-daemon :app:assembleDebug ``` `bash test.sh ci` reproduces the read-only pull-request gates with an isolated, checksummed Maven artifact, a clean standalone consumer, and both demo builds. No emulator or API credential is required. Maintainers can follow the [Maven Central release guide](maven_release.md).