> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Android SDK

> Kotlin SDK for integrating Gravity contextual ads into Android apps.

## Installation

Add the dependency to your module's `build.gradle.kts`:

```kotlin theme={null}
dependencies {
    implementation("ai.trygravity:gravity-sdk:1.0.0")
}
```

The SDK requires `INTERNET` permission (declared in its manifest — no action needed).

## How the pieces fit together

<Steps>
  <Step title="Initialize the client">
    Create a `Gravity` instance with your API key, a `Context`, and `production = true`.
  </Step>

  <Step title="Collect context & fetch ads">
    Call `gravity.getAds(...)` with the conversation messages, session ID, and placements. The SDK auto-collects device signals (UA, OS, screen, timezone). Run it in parallel with your LLM call using coroutines.
  </Step>

  <Step title="Render">
    Use the built-in `GravityAd` Compose composable, `GravityAdView` (Views), or render the ad data yourself. Impression tracking fires automatically.
  </Step>
</Steps>

## Quick start

```kotlin theme={null}
import ai.trygravity.sdk.Gravity
import ai.trygravity.sdk.models.*

// 1. Initialize (once, e.g. in your ViewModel or Application)
val gravity = Gravity(context, apiKey = "pub_...", production = true)

// 2. Fetch ads in parallel with your LLM call
val messages = listOf(
    Message("user", "What's the best way to deploy a Postgres DB?"),
    Message("assistant", "Here are some options..."),
)

viewModelScope.launch {
    val adDeferred = async { gravity.getAds(
        messages = messages,
        sessionId = "sess_abc123",
        userId = currentUser.id,
        placements = listOf(Placement.belowResponse()),
    ) }

    // Stream your LLM response...
    val result = adDeferred.await()

    // 3. Render
    result.ads.firstOrNull()?.let { ad ->
        // Use GravityAd composable or your own UI
    }
}
```

## Constructor

```kotlin theme={null}
Gravity(
    context: Context,
    apiKey: String = System.getenv("GRAVITY_API_KEY") ?: "",
    apiUrl: String = "https://server.trygravity.ai/api/v1/ad",
    timeoutSeconds: Long = 3L,
    production: Boolean = false,
    relevancy: Double = 0.2,
    excludedTopics: List<String>? = null,
)
```

| Parameter        | Type            | Default                   | Description                                  |
| ---------------- | --------------- | ------------------------- | -------------------------------------------- |
| `context`        | `Context`       | —                         | Android context for device signal collection |
| `apiKey`         | `String`        | `GRAVITY_API_KEY` env var | Your Gravity publisher API key               |
| `timeoutSeconds` | `Long`          | `3`                       | Abort after this many seconds                |
| `production`     | `Boolean`       | `false`                   | `false` returns test ads (no billing)        |
| `relevancy`      | `Double`        | `0.2`                     | Minimum relevancy threshold 0.0–1.0          |
| `excludedTopics` | `List<String>?` | `null`                    | Topics to exclude from matching              |

## `getAds()`

```kotlin theme={null}
suspend fun getAds(
    messages: List<Message>,
    sessionId: String,
    userId: String = "anonymous",
    placements: List<Placement>,
    userIP: String? = null,
    hashedIdentity: HashedIdentity? = null,
    extraUserFields: Map<String, String>? = null,
    extraDeviceFields: Map<String, Any>? = null,
    production: Boolean? = null,
    relevancy: Double? = null,
): AdResult
```

The SDK auto-collects device signals (User-Agent, OS, model, screen size, timezone, locale, device type) and sends them with the request. Your dashboard's Device and Geography breakdowns work automatically.

<Warning>
  `getAds()` never throws. On any failure, it returns `AdResult.EMPTY` (empty ads list). Safe to fire-and-forget.
</Warning>

## Placements

```kotlin theme={null}
Placement.belowResponse()           // "below_response"
Placement.inlineResponse()          // "inline_response"
Placement.aboveResponse()           // "above_response"

// Custom
Placement("right_response", "sidebar")
```

## Jetpack Compose — render ads

```kotlin theme={null}
import ai.trygravity.sdk.views.SpecAd

@Composable
fun ChatScreen(response: String, ads: List<AdResponse>, gravity: Gravity, sessionId: String) {
    Column {
        Text(response)
        ads.firstOrNull()?.let { ad ->
            SpecAd(
                ad = ad,
                gravity = gravity,
                sessionId = sessionId,
            )
        }
    }
}
```

`SpecAd` automatically handles the Gravity-managed placement design, safe fallback, impression and click tracking, required ad disclosure, images, and server-requested feedback controls.

| Parameter      | Type         | Description                                                                                                                                                                                                                                                                                                 |
| -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ad`           | `AdResponse` | The ad object from `getAds()`                                                                                                                                                                                                                                                                               |
| `gravity`      | `Gravity`    | Client instance for tracking                                                                                                                                                                                                                                                                                |
| `sessionId`    | `String`     | Must match the ad request                                                                                                                                                                                                                                                                                   |
| `showFeedback` | `Boolean`    | Allow feedback controls. Default `true`. The prompt renders only when this is `true` **and** the server marked the ad with `feedbackPrompt: true` (selective — borderline-relevance / tight-auction ads). One tap submits and swaps to "Thanks for the feedback"; feedback taps never trigger the ad click. |

## Android Views

For XML-based layouts, use `GravityAdView`:

```kotlin theme={null}
import ai.trygravity.sdk.views.GravityAdView

val adView = GravityAdView(
    context = this,
    ad = ad,
    gravity = gravity,
    sessionId = sessionId,
)
container.addView(adView)
```

## Custom rendering

If you render ads with your own UI, fire tracking manually:

```kotlin theme={null}
// Impression — fire once when the ad becomes visible
gravity.trackImpression(ad)

// Click — always use clickUrl, not url
ad.clickUrl?.let { url ->
    startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
```

## PII hashing

Hash email and phone for attribution without sending raw values:

```kotlin theme={null}
import ai.trygravity.sdk.hashPII

val hashed = hashPII(email = user.email, phone = user.phone)

val result = gravity.getAds(
    messages = messages,
    sessionId = sessionId,
    userId = user.id,
    placements = listOf(Placement.belowResponse()),
    hashedIdentity = hashed,
)
```

## Ad feedback

```kotlin theme={null}
// Thumbs up
gravity.sendFeedback(
    sessionId = sessionId,
    ad = ad,
    sentiment = Gravity.Sentiment.THUMBS_UP,
)

// Thumbs down with reasons
gravity.sendFeedback(
    sessionId = sessionId,
    ad = ad,
    sentiment = Gravity.Sentiment.THUMBS_DOWN,
    reasons = listOf("not_interested", "irrelevant"),
)
```

## Full example — parallel with LLM stream

```kotlin theme={null}
import ai.trygravity.sdk.Gravity
import ai.trygravity.sdk.models.*

class ChatViewModel(application: Application) : AndroidViewModel(application) {
    private val gravity = Gravity(application, apiKey = "pub_...", production = true)

    val ads = MutableStateFlow<List<AdResponse>>(emptyList())
    val response = MutableStateFlow("")

    fun send(messages: List<Message>, sessionId: String, userId: String) {
        viewModelScope.launch {
            // Fire ad request in parallel
            val adDeferred = async {
                gravity.getAds(
                    messages = messages,
                    sessionId = sessionId,
                    userId = userId,
                    placements = listOf(Placement.belowResponse()),
                )
            }

            // Stream LLM response
            streamLLM(messages).collect { chunk ->
                response.update { it + chunk }
            }

            // Append ads
            ads.value = adDeferred.await().ads
        }
    }
}
```

## FAQ

<AccordionGroup>
  <Accordion title="Does the ad request block my LLM response?">
    No. Use `async` to fire `getAds()` in parallel with your LLM stream. It never throws — failures return an empty list.
  </Accordion>

  <Accordion title="Does the SDK collect the Advertising ID (GAID)?">
    No. The SDK does not access Google Advertising ID. If you have user consent, pass it as `extraDeviceFields = mapOf("ifa" to gaid)`.
  </Accordion>

  <Accordion title="What Android versions are supported?">
    Android 7.0+ (API 24+).
  </Accordion>

  <Accordion title="What dependencies does the SDK add?">
    OkHttp for networking and Kotlin Coroutines. Compose dependencies are `compileOnly` — only pulled in if your app already uses Compose.
  </Accordion>
</AccordionGroup>
