Graine AI

Android SDK reference

Every public class, function, property, listener and error of ai.graine:inapp-android 0.8.0 (Kotlin and Java), grouped by what you use it for: signatures, parameters, what each returns, the errors it raises, and what to expect.

Every public declaration of the Android SDK, ai.graine:inapp-android 0.8.0 — Kotlin first, and callable from Java (@JvmStatic, @JvmOverloads, GraineCallback) — grouped by what you use it for. Each entry says what it is for, then gives its signature, its parameters, what it returns, the errors it can raise and an example; each type lists its fields with what to expect. The same API across every platform, topic by topic, is the SDK reference; installing and setting up is Android.

Requirements, install, permissions and initialisation

Artifactai.graine:inapp-android:0.10.3 on Maven Central
RequiresAndroid 7.0 (API 24) and up; Java 17 bytecode (Android Gradle Plugin 8 or later); built against compileSdk 36 with Kotlin 2.1.20. No AndroidX, OkHttp or Compose dependency: the only runtime dependency is kotlin-stdlib. Its R8/ProGuard rules ship inside it.
Calls needFirebase Cloud Messaging in your app (google-services.json, its Gradle plugin and the messaging library) for calls that ring — the SDK then registers for pushes itself. The SDK is compiled against Firebase and does not depend on it
WebRTC for callsOptional: ai.graine:inapp-android-rtc, at the same version (Adding WebRTC calls)
In the dashboardAdd app:// to the agent's Allowed domains (Agent → Embed & Widgets); without it every request is refused

Gradle (Kotlin DSL) — app/build.gradle.kts

dependencies {
    implementation("ai.graine:inapp-android:0.10.3")
    // For calls that ring: Firebase Cloud Messaging, as Firebase describes
    implementation(platform("com.google.firebase:firebase-bom:<latest>"))
    implementation("com.google.firebase:firebase-messaging")
}

What the library's manifest merges into your app

EntryWhy
android.permission.INTERNETThe voice engine and the conversation's connection
android.permission.RECORD_AUDIOThe agent hears the user (voice and calls). A runtime permission: the SDK asks
android.permission.MODIFY_AUDIO_SETTINGSA call routes its audio — earpiece, loudspeaker or a headset, Bluetooth included (no Bluetooth permission is needed)
android.permission.POST_NOTIFICATIONSThe ring, the ongoing call and a missed call are notifications. A runtime permission on Android 13 and up: your app asks
android.permission.USE_FULL_SCREEN_INTENTA ring that fills a locked screen. On Android 14 and up Google Play grants it by default only to apps whose main purpose is calling or alarms; without it the ring is a heads-up notification, still ringing
android.permission.FOREGROUND_SERVICEThe call runs in a foreground service
android.permission.FOREGROUND_SERVICE_MICROPHONEThat service keeps the microphone with the screen off (type microphone)
ai.graine.inapp.GraineCallActivityThe call screen: its own task (so ending a call returns the user to where they were), shown over the lock screen, not exported
ai.graine.inapp.GraineChatActivityThe chat on a screen of its own (GraineChatActivity.open), above the keyboard; not exported
ai.graine.inapp.GraineCallServiceThe call's foreground service, foregroundServiceType="microphone", not exported
ai.graine.inapp.GraineCallReceiverThe notifications' Decline and Hang up, not exported
ai.graine.inapp.GraineMessagingServiceWith Firebase Cloud Messaging in the app, receives the push token and Graine's call pushes, so the app needs no messaging service of its own. Priority -1: a messaging service your app declares outranks it. Not exported; without Firebase nothing starts it

What the SDK asks at run time: only RECORD_AUDIO — at voice.start() for the voice bar (forward your activity's onRequestPermissionsResult to voice.onRequestPermissionsResult), and from the call screen when a call is placed or after Answer (a refusal ends the call FAILED; no answer within 60 seconds too). Your app asks for notifications on Android 13 and up; without them, a ring with your app closed or in the background cannot show and ends MISSED. Your app offers the full-screen setting on Android 14 and up:

Android · Kotlin — onboarding

private val ask = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }
 
fun askForAgentPermissions() {
    val wanted = mutableListOf(Manifest.permission.RECORD_AUDIO)
    if (Build.VERSION.SDK_INT >= 33) wanted += Manifest.permission.POST_NOTIFICATIONS   // rings
    ask.launch(wanted.toTypedArray())
}
 
fun offerFullScreenRings() {
    val nm = getSystemService(NotificationManager::class.java)
    if (Build.VERSION.SDK_INT >= 34 && !nm.canUseFullScreenIntent()) {
        startActivity(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT, Uri.parse("package:$packageName")))
    }
}

Notification channels the SDK creates in calls.configure (visible in the phone's settings): Incoming calls (high importance, the ringtone, vibration, shown on the lock screen), Incoming calls (app ringtone) (used while the SDK plays the brand's Graine or Soft ring), Missed calls and Ongoing calls.

Not using calls? On Android 14 and up, Google Play asks apps that declare USE_FULL_SCREEN_INTENT or FOREGROUND_SERVICE_MICROPHONE to justify them. An app that never places or receives calls removes them in its own manifest:

XML — AndroidManifest.xml (an app without calls)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:node="remove" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" tools:node="remove" />
    <application>
        <service android:name="ai.graine.inapp.GraineCallService" tools:node="remove" />
    </application>
</manifest>

Adding WebRTC calls

The SDK's native calls — the ones the user places (startCall, a “Call” button) and the ones Graine rings and the user answers — can carry their audio over WebRTC instead of the WebSocket. It is one more module, opt-in per app build.

Gradle (Kotlin DSL) — app/build.gradle.kts

dependencies {
    implementation("ai.graine:inapp-android:0.10.3")
    implementation("ai.graine:inapp-android-rtc:0.10.3")   // WebRTC for calls: always the same version as the SDK
}

Nothing else changes — no code, no permission to ask for, no setting: the calls find the module by themselves, and the ring, the call screen, “Connecting…”, the listeners and the outcomes are as before. It is for native calls only; the voice bar's audio follows GraineConfig.transport and the dashboard's Call audio.

Moduleai.graine:inapp-android-rtc on Maven Central, at the same version as ai.graine:inapp-android
What it bringsA prebuilt WebRTC library — Google's WebRTC, BSD 3-Clause, its symbols renamed so it never clashes with another copy in the app — which your build fetches from Maven Central like any dependency; the SDK itself still has none. Its R8 rules ship inside the module.
Size, per phoneAn app bundle delivers one ABI: about 7.4 MB more installed on 64-bit ARM (about 2.5–3.3 MB more to download), 4.3 MB on 32-bit ARM, 8.2–8.4 MB on x86; and about 0.5 MB of classes.
PermissionIts manifest adds android.permission.ACCESS_NETWORK_STATE: a normal permission, granted at install — nothing is asked.
Licencesgraine-inapp-rtc/THIRD-PARTY-NOTICES.md (WebRTC, BSD 3-Clause): include it in your app's open-source licences screen.
In the dashboardThe agent's In-app call audio (agent → Calls & scheduling): WebRTC (recommended), or Standard, which puts every install's calls back on the WebSocket whatever the app chose — from each app's next start, with no release.
In the appGraineCallsConfig.callTransport: AUTO, the default — WebRTC when the module is in the app and the dashboard allows it; WEBSOCKET; or WEBRTC (GraineCallTransport).
Which one carried a callcalls.transport — "webrtc" or "websocket" — and, for a call that tried WebRTC and runs on the WebSocket, calls.transportFallbackReason (the reasons). Read them once the call is ACTIVE.

What the person notices. The same ring, the same call screen, the same “Connecting…”. On a weak or changing network the agent's voice holds up instead of stalling and catching up: a jitter buffer, loss concealment and the phone's own echo canceller. The call is prepared at the tap — or while the ring is showing — so it connects as fast as before. Over WebRTC the agent's voice arrives at telephone bandwidth today: steadier through a weak network, narrower than the WebSocket's on a perfect one.

When WebRTC cannot set up a call before any audio flows — a network that blocks it, a server that turns it down, no answer within the time allowed (about 12 seconds in all) — the call carries on over the WebSocket inside the same call: no second ring, no new call screen, no error; “Connecting…” shows a moment longer. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason, exactly as without the module. Once audio flows, a call never moves: a network drop is retried once, then the call ends as dropped (ENDED, FAILED, “The call's connection was lost.”).

// Application.onCreate — AUTO is the default; WEBRTC asks for WebRTC wherever it can carry the call
GraineInApp.get().calls.configure(GraineCallsConfig(displayName = "Acme", callTransport = GraineCallTransport.WEBRTC))
 
// Which one carried a call, once it is ACTIVE
GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onCallState(state: GraineCallState, call: GraineCall?) {
        if (state != GraineCallState.ACTIVE) return
        val calls = GraineInApp.get().calls
        Log.d("Graine", "audio over ${calls.transport}; fallback: ${calls.transportFallbackReason}")
    }
})

Initialise

Android · Kotlin — AcmeApp.kt

class AcmeApp : Application() {
    override fun onCreate() {
        super.onCreate()
        val graine = GraineInApp.init(this, GraineConfig(
            baseUrl = "https://www.graine.ai",
            publishableKey = "pk_live_…",                // Agent → Embed & Widgets → Install
            appVersion = BuildConfig.VERSION_NAME,
            debug = BuildConfig.DEBUG,
        ))
        graine.calls.configure(GraineCallsConfig(
            displayName = "Acme",
            smallIcon = R.drawable.ic_stat_call,         // monochrome; 0 = the platform's call icon
        ))
    }
}
 
class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)
        GraineInApp.get().voice.attach(this)             // the voice engine, idle: once; it follows the user between activities
        findViewById<FrameLayout>(R.id.root).addView(GraineAgentBarView(this),
            FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, Gravity.BOTTOM))
    }
 
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (!GraineInApp.get().voice.onRequestPermissionsResult(requestCode, grantResults))
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

Jetpack Compose hosts the views with AndroidView(factory = { GraineAgentBarView(it) }). Everything that touches a view runs on the main thread; nothing the SDK does blocks it. From Java, every entry point works with only the arguments you need (Notes for Java).

Install and the first answer

Configure the agent once, in Application.onCreate, then reach it anywhere with GraineInApp.get(). The SDK loads the agent's settings from Graine once per launch; until they arrive it uses the last ones kept on the device, so a call that wakes a closed app is drawn in the brand's design. Know they have arrived with GraineInApp.session or GraineListener.onSessionReady.

GraineInApp.init

Call it once per agent in Application.onCreate to start the SDK.

@JvmStatic @JvmOverloads
companion · fun init(application: Application, config: GraineConfig, name: String = DEFAULT): GraineInApp

It starts loading the agent's settings at once. Without a name it makes the default agent, which GraineInApp.get() returns; an app with several agents names the others (Several agents). Asking again for a name already set up gives you that same agent; the configuration passed the second time is ignored.

ParameterTypeDefaultMeaning
applicationApplicationrequiredYour Application.
configGraineConfigrequiredThe agent's publishable key, the base URL and the options (GraineConfig).
nameStringDEFAULTThe name to make the agent under; leave it out (or pass an empty name) for the default agent.

Returns — GraineInApp — the agent (GraineInApp).

Errors — none. A key or allowed-domain problem surfaces when the settings load, in GraineListener.onError.

GraineInApp.init(this, GraineConfig(baseUrl = "https://www.graine.ai", publishableKey = "pk_live_…"))

GraineInApp

Use it for everything the SDK does: it is the agent, and the way to its chat, its voice bar, calls and bookings.

class GraineInApp private constructor(…)

Made once with GraineInApp.init and reached anywhere with GraineInApp.get(). Who the user is, their memory choice, the screen and the call engine belong to the app and are shared by every agent; the rest is this agent's.

What to expect — Everything that touches a view runs on the main thread, and callbacks arrive on it with exactly one of value and error set; nothing touches the network on it. A request made before the agent's settings have loaded waits for them.

PropertyTypeMeaning
val config: GraineConfigGraineConfigRead it for the configuration this agent was made with.
val name: StringStringRead it to know which agent this is: DEFAULT unless init was given another name.
var session: SessionConfig?SessionConfig?Read it for the agent's settings once they have loaded this launch (SessionConfig); null before. Read-only.
val voice: GraineVoiceGraineVoiceUse it to drive the voice bar's call (GraineVoice).
val calls: GraineCallsGraineCallsUse it for native calls — the app's one call engine, whichever agent a call is with (GraineCalls).
val chat: GraineChatGraineChatUse it to drive this agent's chat (GraineChat).
var pendingWidget: JSONObject?JSONObject?Read it for the component the agent drew that is still waiting for an answer — kept because the view that draws it may not have been on screen when it arrived; null when there is none. Read-only.
val memoryEnabled: BooleanBooleanRead it to show the user's memory choice: their last choice on this install, else true.
val installId: StringStringRead it for this install's ID: a random UUID made on first launch and kept while the app is installed (not backed up, so a restored phone is a new install). Graine counts a user's devices by it; the agent never sees it.
val currentScreen: GraineScreen?GraineScreen?Read it for the screen as last reported, on any agent (GraineScreen).
val frictionRules: FrictionRulesFrictionRulesRead it for the stuck-user rules in force now — the defaults, the dashboard's and your app's (FrictionRules).
val design: ResolvedDesign?ResolvedDesign?Read it for the design being drawn: a preview when one is set, else this launch's, else the last one kept on the device; null from a server without designs (ResolvedDesign).
val schedulingRules: GraineSchedulingRulesGraineSchedulingRulesRead it for the booking rules and how Graine rings (GraineSchedulingRules) — this launch's, else the last ones kept, else the defaults with booking off.
val bookingAvailable: BooleanBooleanRead it to decide whether to offer booking (GraineInApp.bookingAvailable).
companion · const val DEFAULT = "default"StringUse it to name the default agent explicitly.
companion · val all: Map<String, GraineInApp>Map<String, GraineInApp>Read it to list every agent init made, by name, in the order they were made.

Its functions: init, get, find, identify, setMemory, memories, forgetMemory, setScreen, updateField, removeField, clearScreen, setVariables, currentVariables, track, tap, reportEvent, noteInteraction, configureFriction, registerAction, registerDeferredAction, checkActions, clearPendingWidget, answerWidget, presentBooking, scheduleCall, scheduledCalls, cancelScheduledCall, themeTokens, previewDesign, addListener, removeListener — each with its parameters, what it returns, its errors and an example.

GraineConfig

Pass it to GraineInApp.init to say which agent to start and how.

data class GraineConfig @JvmOverloads constructor(…)

What to expect — Made once, at launch. Optional fields have the defaults shown; nothing in it is sent to Graine except what the descriptions say.

FieldTypeDefaultMeaning
baseUrlStringrequiredWhere Graine is: "https://www.graine.ai" unless your workspace is self-hosted.
publishableKeyStringrequiredSelects the agent: its publishable key from Agent → Embed & Widgets. Not a secret, and it cannot place a call on its own — app:// must be in the agent's Allowed domains.
variablesMap<String, Any?>emptyMap()Prompt variables your app knows at build time. Values from identify() and setVariables() take precedence over these.
transportTransport?nullUse it only to force how the voice bar's audio travels (Transport); null lets the dashboard's Call audio setting decide.
appVersionString?nullUse it to tell builds apart: it lands on the call record (and the device registration). Left out, your app's version name.
endOnBackgroundBooleantrueEnds a live voice-bar call when the app goes to the background (after two seconds there). Keep it on.
timingTimingTiming()Load and connect timing for very slow networks (GraineConfig.Timing); the defaults suit almost every app.
debugBooleanfalseTurn it on in debug builds to log what the SDK and the voice engine say to logcat under the tag Graine — “the agent did not speak” is answered in one read of it.
frictionFrictionConfig?nullYour app's own stuck-user rules, over the dashboard's (FrictionConfig) — for screens only your app knows about.
fontsMap<String, Typeface>emptyMap()Fonts your app already bundles, by the family names the dashboard offers — mapOf("Inter" to Typeface.createFromAsset(assets, "fonts/Inter.ttf")). A family mapped here is never downloaded; every other family the design uses the SDK fetches and keeps on disk, using the system font until it arrives.
captureTapsBooleantrueLets the SDK notice taps on labelled controls (what TalkBack reads, a view's ID, a button's words) as tap:<name>, at most twice a second and only while a screen is reported, so the agent knows what the user just pressed. It only watches; every touch reaches your app as before.
chatSizeGraineChatSize?nullHow big the chat window opens in this app, over the dashboard's Window size (GraineChatSize); a size passed to chat.open(size) wins for that open.

GraineConfig.Timing

Pass it in GraineConfig.timing only for an app used on very slow networks, to give the voice engine longer to load.

data class Timing @JvmOverloads constructor(…)

The voice bar's engine loads in the background when you call voice.attach. The SDK watches that it keeps moving rather than giving it one flat timer, and reloads it when it stalls.

What to expect — Every value is in milliseconds. The defaults suit almost every app.

FieldTypeDefaultMeaning
stallMsLong15_000How long loading may go without anything arriving before the SDK reloads the engine (a dead connection, a captive portal).
bootMsLong15_000How long the engine may take to say it is ready once it has fully arrived, before the SDK reloads it.
hardCapMsLong120_000The longest one load may take in all, however slowly it keeps arriving.
retryDelaysMsList<Long>listOf(1_000, 2_000, 4_000, 8_000, 15_000, 15_000)How long to wait before each reload (each varied by up to a quarter, so phones coming back from the same outage do not reload together). After the last, loading stops: Connectivity.FAILED, and voice.retry() starts over.
endWatchdogMsLong4_000How long voice.end() waits for the call to confirm it hung up before the SDK ends it itself.
slowConnectMsLong4_000How long the user may wait on a connect before the voice bar says “Still connecting…” (GraineVoice.slowConnect).

SessionConfig

Read it to know what the dashboard says about the agent, once the settings have loaded.

data class SessionConfig(…)

GraineInApp.session, and the argument of GraineListener.onSessionReady.

What to expect — Loaded once per launch; null in session until then. A setting an older server does not send means its default: the user_id trait, 2 devices per user, booking off. Missing fields are empty, never a crash.

FieldTypeDefaultMeaning
agentIdStringrequiredUse it in logs and support requests: the agent's ID in Graine.
webcallAgentIdStringrequiredThe ID of the agent's voice conversations in the app (the same as agentId when the agent has no separate one) — for logs; the SDK uses it itself.
appearanceJSONObjectrequiredThe agent's appearance settings from the dashboard, as served — what the SDK's views draw from when Graine sends no design.
variablesMap<String, Any?>requiredThe agent's default prompt variables from the dashboard; your own variables win over them.
pageVersionString?requiredThe voice engine's current version: the SDK keeps the loaded engine until it changes; null when Graine does not say. Include it in support requests.
declaredActionsSet<String>requiredUse it to check your handlers: the actions declared on the agent. One declared and not registered is refused when the agent asks for it (GraineInApp.checkActions).
frictionJSONObject?nullThe dashboard's stuck-user rules, as served; your app's GraineConfig.friction applies over them.
designResolvedDesign?nullThe design the brand chose, as light and dark tokens (ResolvedDesign); null from a server that predates designs.
wsHostString?nullWhere the agent's conversations connect, when Graine says; null means the default.
identitySessionIdentity?nullWhich trait names the user, and how many devices they may use (SessionIdentity); null from a server that predates it.
memoryEnabledBoolean?nullRead it to know if the agent keeps memories of its users: true or false, or null when Graine did not say.
schedulingRulesGraineSchedulingRulesGraineSchedulingRules()The booking rules and how Graine rings (GraineSchedulingRules); the defaults, with booking off, from an older server.
PropertyTypeMeaning
val preferredTransport: TransportTransportRead it for the dashboard's Call audio setting: WEBRTC when it asks for high-quality audio, else WEBSOCKET.
val accent: String?String?Read it for the dashboard's accent colour (#RRGGBB), when one is set.

Identity, memory and conversations that continue

Tell the agent who the user is, so it can remember them, ring them, and continue their conversation. Where the agent remembers its users, a signed-in user who comes back finds the chat where they left it: it opens with the earlier messages (restored, oldest first; an earlier call's as one “Voice call · 6:05 PM” row under the day it happened) and the agent continues from them; the chat window's header then offers New conversation. Who the user is belongs to the app: identifying on any agent identifies them for every agent.

GraineInApp.identify

Call it after the user signs in, and with null when they sign out.

@JvmOverloads
fun identify(traits: Map<String, Any?>?, memory: Boolean? = null)

Traits become prompt variables and land on the call record, masked on the device. The trait the agent's Who is the user? setting names (user_id unless the dashboard chose email, phone or a custom trait) is also how Graine tells your users apart — what lets the agent remember a user and ring them — and it goes only to Graine, never into the prompt beyond the trait itself. A placeholder ("guest", "0", a masked value) identifies nobody.

Identity is taken at the start of each conversation, so a change takes effect from the next one. null signs out of every agent: this device is released for that user (it stops being rung for them and stops counting toward their devices), the chat they were having is cleared, and later conversations are anonymous. Identifying a different user does the same for the previous one.

ParameterTypeDefaultMeaning
traitsMap<String, Any?>?requiredWho the user is, by trait name — mapOf("user_id" to user.id, "name" to user.firstName). null signs out.
memoryBoolean?nullfalse: this user's conversations are neither remembered nor recalled; the choice is kept on this install for them. null keeps their last choice — on until they make one.

Returns — nothing.

Errors — none.

GraineInApp.get().identify(mapOf("user_id" to user.id, "name" to user.firstName, "plan" to user.plan), memory = user.allowsMemory)
// on sign-out
GraineInApp.get().identify(null)

GraineInApp.setMemory

Call it when the user changes their memory choice in your privacy settings.

fun setMemory(enabled: Boolean)

The same as identify with the same traits and memory = enabled, for every agent; kept on this install for this user.

ParameterTypeDefaultMeaning
enabledBooleanrequiredtrue to remember this user's conversations, false to stop.

Returns — nothing.

Errors — none.

GraineInApp.get().setMemory(false)

GraineInApp.memories

Call it to show the user what the agent remembers about them, on a privacy screen.

fun memories(done: GraineCallback<List<GraineMemory>>)

What the agent remembers about the signed-in user, newest first, at most 100, already redacted. Only from a device that user is using.

ParameterTypeDefaultMeaning
doneGraineCallback<List<GraineMemory>>requiredHears the list (GraineMemory), or the error, once, on the main thread.

Returns — nothing; the answer arrives in done. Example value (in done): [GraineMemory(id="m_31", text="Prefers a call after 6 PM", createdAt=Mon Oct 12 09:15:00 UTC 2026, updatedAt=null)].

Errors — in done's GraineError: IDENTITY_REQUIRED (“Nobody is signed in: identify() the user first.”) with nobody signed in; DEVICE_LIMIT from a device the user is not using the agent on; RATE_LIMITED, NETWORK, UNAVAILABLE, INVALID_KEY, ORIGIN_REFUSED.

GraineInApp.get().memories { list, error ->
    if (error != null) showMessage(error.message) else render(list.orEmpty().map { it.text })
}

GraineInApp.forgetMemory

Call it when the user asks the agent to forget them.

@JvmOverloads
fun forgetMemory(done: GraineCallback<Int>? = null)

Deletes everything the agent remembers about the signed-in user — only from a device that user is using. The conversation is part of what is forgotten: once it succeeds, the chat on screen is cleared too; a refusal leaves it as it was.

ParameterTypeDefaultMeaning
doneGraineCallback<Int>?nullHears how many memories were deleted, or the error, once, on the main thread.

Returns — nothing; the count arrives in done. Example value (in done): 3.

Errors — in done's GraineError: IDENTITY_REQUIRED (“Nobody is signed in: identify() the user first.”), DEVICE_LIMIT, RATE_LIMITED, NETWORK, UNAVAILABLE, INVALID_KEY, ORIGIN_REFUSED.

GraineInApp.get().forgetMemory { deleted, error ->
    if (error == null) showMessage("Forgot $deleted things.")
}

GraineChat.newConversation

Call it to give the user a fresh conversation instead of continuing the last one.

@JvmOverloads
fun newConversation(done: GraineCallback<Unit>? = null): Unit

The chat hangs up, Graine lets go of the conversation it kept for this agent and user (so the next chat does not continue it), and the lines on screen and any waiting to go are cleared; when the user was in the chat, a fresh chat opens with the welcome and the starters. Memories the agent keeps about the user stay as they are. With nobody signed in there is nothing kept to let go of, so only the screen is cleared. The SDK's chat window calls it from the New conversation button in its header, which it shows only while a conversation continued from before is on screen.

ParameterTypeDefaultMeaning
doneGraineCallback<Unit>?nullHears Unit once it is done, or the error, on the main thread.

Returns — nothing; the result arrives in done.

Errors — in done's GraineError: DEVICE_LIMIT, NETWORK, RATE_LIMITED, UNAVAILABLE, … — the chat is then as it was, connected again.

GraineInApp.get().chat.newConversation { _, error -> if (error != null) showMessage(error.message) }

GraineCallback

Pass one to a request to hear its answer: the value, or why there is none.

fun interface GraineCallback<T>

Every request the SDK makes to Graine on the user's behalf answers through one: memories, bookings, cancelling.

What to expect — Called once, on the main thread, with exactly one of value and error set. In Kotlin, a trailing lambda { value, error -> }; in Java, (value, error) -> { … }.

Its functions: onResult — each with its parameters, what it returns, its errors and an example.

GraineCallback.onResult

Implement it to receive a request's answer.

fun onResult(value: T?, error: GraineError?)
ParameterTypeDefaultMeaning
valueT?requiredThe answer, when the request succeeded; null otherwise.
errorGraineError?requiredWhy there is no answer (GraineError); null when it succeeded.

Returns — nothing.

Errors — none.

val done = GraineCallback<Int> { deleted, error ->
    if (error != null) showMessage(error.message) else showMessage("Deleted $deleted")
}

GraineMemory

Read it to show one thing the agent remembers about the user.

data class GraineMemory @JvmOverloads constructor(val id: String, val text: String, val createdAt: java.util.Date? = null, val updatedAt: java.util.Date? = null)

What to expect — From memories(): newest first, at most 100, already redacted. Only for the signed-in user, from one of their devices. The dates are moments; show them in the device's time zone.

FieldTypeDefaultMeaning
idStringrequiredUse it as a stable key when you list memories.
textStringrequiredShow it: what is remembered, in a sentence, already redacted.
createdAtjava.util.Date?nullWhen it was first remembered; null when Graine did not say.
updatedAtjava.util.Date?nullWhen it last changed; null when it never did or Graine did not say.

SessionIdentity

Read it to know which trait names the user, and on how many devices they may use the agent.

data class SessionIdentity(val field: String, val devicesPerUser: Int)

SessionConfig.identity.

What to expect — From an older server that does not say: the user_id trait and 2 devices.

FieldTypeDefaultMeaning
fieldStringrequiredThe trait identify must carry for memory and booking to work: user_id, email, phone or a custom one such as member_id.
devicesPerUserIntrequiredHow many devices one user may use the agent on, 1–5; a device beyond it gets DEVICE_LIMIT.

Chat

The typed conversation with the agent: GraineInApp.get().chat, or GraineInApp.get("support").chat for another agent. The voice bar switches to it when the dashboard offers chat (Appearance → mode: voice, chat or both); GraineChatView and GraineChatActivity show it as a whole screen. Everything the agent can do on a call works here too — it sees the screen you report, runs your actions and draws its components — and each of its replies streams into a single bubble. A line typed while the chat is connecting — or reconnecting after a drop, which it does by itself while the user is in it — is sent as soon as it can be, never lost. One conversation at a time across every agent: opening the chat ends a voice-bar call, starting a call ends the chat, and a native call holds the chat until it is over.

Where Graine rings this app, the chat window shows the user's booked calls first in the conversation, with Book a call where booking is available (Booking with the SDK's sheet).

GraineChat

Use it to open, send to and follow one agent's chat.

class GraineChat internal constructor(…) : ChatSource

GraineInApp.get().chat. What the voice bar's chat window and GraineChatView draw.

What to expect — Main thread. Changes reach GraineListener.onChatState and onChatMessage, and every change listener. messages empties on sign-out, when another user signs in, after a successful forgetMemory() and on newConversation().

PropertyTypeMeaning
var connected: BooleanBooleanRead it to know the conversation is up: lines go out as they are typed. Read-only.
var connecting: BooleanBooleanRead it to show “Connecting…”: it is being opened, or reopened after a drop. Read-only.
val reconnecting: BooleanBooleanRead it to say “Reconnecting…”, not “Offline”: the conversation dropped — or a first try to open it failed — while the user is in the chat, and it is being made again; true until it is connected, gives up (failed) or is closed. connecting is true with it.
override var failed: BooleanBooleanRead it to show that nothing could connect it, after every retry; a send or connect tries again. Read-only.
override val messages: List<GraineChatTurn>List<GraineChatTurn>Read it to draw the conversation, oldest first (GraineChatTurn).
override val online: BooleanBooleanRead it to show “Online”: the chat is connected.
override val typing: BooleanBooleanRead it to draw a typing indicator: from the user's message until the agent's first words or component, the chat closing, or 12 seconds.
var upcomingCalls: List<GraineScheduledCall>List<GraineScheduledCall>Read it for the user's calls still to come, soonest first, at most three — the chat window's Upcoming calls card. Fetched when the chat opens and after the agent finishes a reply, only where Graine rings this app. Read-only.
var openedSize: GraineChatSize?GraineChatSize?Read it for the size the last open asked for, until close; null follows GraineConfig.chatSize, then the dashboard's. Read-only.
companion · const val CLOSE_UNSEEN_MS = 15_000LLongHow long a conversation no window shows is kept for the next screen to take before it is closed — 15 seconds.
companion · const val UPCOMING_EVERY_MS = 10_000LLongHow often, at most, the upcoming calls are fetched again unless asked — 10 seconds.

Its functions: open, connect, send, close, clear, newConversation, shown, addChangeListener, removeChangeListener, refreshUpcomingCalls, cancelUpcomingCall — each with its parameters, what it returns, its errors and an example.

GraineChat.open

Call it from your own “Chat with us” button to open the chat.

@JvmOverloads
fun open(size: GraineChatSize? = null): Unit

Connects the chat. A GraineAgentBarView on screen moves to its chat, where the agent offers one, and draws it at size — this open's own, until close(); null follows GraineConfig.chatSize, then the dashboard's Window size.

ParameterTypeDefaultMeaning
sizeGraineChatSize?nullHow big the chat opens this time: COMPACT, LARGE or FULL (GraineChatSize).

Returns — nothing.

Errors — none.

GraineInApp.get().chat.open(GraineChatSize.FULL)

GraineChat.connect

Call it to connect the chat yourself — for a chat screen of your own that is not a GraineChatView.

fun connect(): Unit

Opens the conversation if it is not open or opening, and fetches the upcoming calls. A chat window on screen does this by itself.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.connect()

GraineChat.send

Call it to send the user's message from a chat screen of your own.

override fun send(text: String)

On screen at once; sent now when the chat is up, or as soon as it is (a message typed while not connected is SENDING). Typing is the user asking again: a chat that gave up connects again. Blank lines are ignored.

ParameterTypeDefaultMeaning
textStringrequiredWhat the user typed.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.send("Where is my order?")

GraineChat.close

Call it when the user leaves your chat screen.

fun close(): Unit

Ends the conversation: the agent is told it is over and no reconnect follows. What was said stays visible; a later connect or send begins a fresh conversation below it.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.close()

GraineChat.clear

Call it to clear the lines on screen — when your app starts a new topic, for example.

fun clear(): Unit

Only the screen clears; the conversation itself goes on — connected stays connected, a message still waiting for the connection is still sent, and nothing Graine keeps is touched: a conversation that continues comes back with the next history, and the agent's memories stay. To start a fresh conversation, use newConversation; to forget, forgetMemory.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.clear()

GraineChat.shown

Call it from a chat screen of your own when it comes on screen (true) and leaves it (false).

override fun shown(visible: Boolean): Unit

The chat connects while it is on screen, and is kept for 15 seconds after it leaves, for the next screen to take, before it closes. GraineChatView does this by itself.

ParameterTypeDefaultMeaning
visibleBooleanrequiredtrue when your chat screen came on screen, false when it left.

Returns — nothing.

Errors — none.

override fun onStart() { super.onStart(); GraineInApp.get().chat.shown(true) }

GraineChat.addChangeListener

Call it to redraw a chat screen of your own whenever anything the chat shows changes.

override fun addChangeListener(listener: Runnable)

The listener runs on the main thread after every change — a line arriving or growing, the connection coming up or dropping, the upcoming calls.

ParameterTypeDefaultMeaning
listenerRunnablerequiredWhat to run on each change.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.addChangeListener(redraw)

GraineChat.removeChangeListener

Call it when your chat screen goes away, to stop hearing changes.

override fun removeChangeListener(listener: Runnable)
ParameterTypeDefaultMeaning
listenerRunnablerequiredThe listener you added.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.removeChangeListener(redraw)

ChatSource

Implement it to show a conversation your app runs itself in the SDK's chat views.

interface ChatSource

What a chat window draws from — GraineAgentBarView.chat and GraineChatView.source. The SDK's own is GraineChat, the default everywhere.

What to expect — Called on the main thread. typing and failed default to false, and shown does nothing unless you override it.

PropertyTypeMeaning
val messages: List<GraineChatTurn>List<GraineChatTurn>Provide the conversation, oldest first.
val online: BooleanBooleanProvide true while connected: the window says “Online”, otherwise “Connecting…”.
val typing: BooleanBooleanProvide true while the agent is working on a reply: the window draws its typing indicator.
val failed: BooleanBooleanProvide true when nothing could connect the conversation: the window says so, and a send tries again.

Its functions: send, shown, addChangeListener, removeChangeListener — each with its parameters, what it returns, its errors and an example.

ChatSource.send

Implement it to send the line the user typed in the chat window.

fun send(text: String)
ParameterTypeDefaultMeaning
textStringrequiredWhat the user typed.

Returns — nothing.

Errors — none.

override fun send(text: String) { myConversation.post(text) }

ChatSource.shown

Implement it to open your conversation when a chat window shows it, and let it go when none does.

fun shown(visible: Boolean)

Called with true when a chat window showing this conversation comes on screen, false when it leaves. Does nothing unless overridden.

ParameterTypeDefaultMeaning
visibleBooleanrequiredWhether a window now shows it.

Returns — nothing.

Errors — none.

override fun shown(visible: Boolean) { if (visible) myConversation.connect() }

ChatSource.addChangeListener

Implement it so the chat window hears every change to your conversation.

fun addChangeListener(listener: Runnable)
ParameterTypeDefaultMeaning
listenerRunnablerequiredRun it, on the main thread, after every change.

Returns — nothing.

Errors — none.

override fun addChangeListener(listener: Runnable) { listeners += listener }

ChatSource.removeChangeListener

Implement it so a chat window can stop hearing changes.

fun removeChangeListener(listener: Runnable)
ParameterTypeDefaultMeaning
listenerRunnablerequiredThe listener to stop running.

Returns — nothing.

Errors — none.

override fun removeChangeListener(listener: Runnable) { listeners -= listener }

GraineChatTurn

Read it to draw one message of the chat.

data class GraineChatTurn @JvmOverloads constructor(…)

What to expect — One per message, oldest first. The agent's message updates in place (same id) while live, then stops. A user's message typed while the chat is not connected is SENDING and goes as soon as it is; FAILED means every reconnect was spent — the next message, or a retry, sends it. Restored messages arrive once, when the chat opens, for a signed-in user on an agent with memory on, on one of their devices: at most 50, oldest first, delivered, never live, with their original times; the welcome is not sent then. Restored lines never reach onChatMessage.

FieldTypeDefaultMeaning
idStringrequiredUse it as a stable key: t1, t2, … for the life of the conversation on screen.
roleRolerequiredWho said it: Role.CUSTOMER (the user) or Role.AGENT (Role).
textStringrequiredWhat was said; an agent's message may be only a component.
atjava.util.DaterequiredWhen the message began; show it in the device's time zone.
deliveryDeliveryDelivery.SENTWhere a user's message is (GraineChatTurn.Delivery); an agent's is always SENT.
proactiveBooleanfalseUse it to badge a closed chat: the agent said this without being asked.
componentGraineComponent?nullThe component the agent drew with this message (GraineComponent is GraineWidget), when it drew one.
liveBooleanfalseDraw the words as provisional while true: the agent's message is still arriving.
restoredBooleanfalsetrue for a message from an earlier conversation, restored when the chat opened: draw it as history, with its own time.
channelChannelChannel.CHATWhere it was said (GraineChatTurn.Channel): a chat window draws a run of voice messages as one “Voice call” row, never their words.

GraineChatTurn.Delivery

Read it to show where one of the user's messages is.

enum class Delivery { SENDING, SENT, FAILED }

What to expect — Only meaningful on the user's messages: SENDING → SENT; FAILED once every reconnect was spent.

CaseMeaning
SENDINGThe user typed it while the chat was offline; it goes out as soon as the chat connects. Show it as pending.
SENTOn its way to the agent.
FAILEDNot sent, because the chat could not reconnect. Offer a retry: a retry, or the user's next message, makes the chat try again.

GraineChatTurn.Channel

Read it to tell a typed message from one said on a call.

enum class Channel { CHAT, VOICE }

What to expect — VOICE only on a restored message.

CaseMeaning
CHATTyped, in the chat.
VOICESaid on a call with the agent — draw a run of them as one “Voice call” row (ChatRows lays them out).

Role

Read it to know who said a line.

enum class Role { AGENT, CUSTOMER }

CaseMeaning
AGENTThe agent.
CUSTOMERThe person using your app.

ChatRows

Use it to lay out a chat of your own the way the SDK's chat window draws it: a bubble per message, one row for an earlier call, and a line where the day changes.

object ChatRows

From GraineInApp.get().chat.messages. A conversation all from today is a bubble per message. A conversation that continues from before gets a day line — Today, Yesterday or a date — before its first row and wherever the phone's day changes between rows, and a run of an earlier call's messages on one day becomes one “Voice call · 6:05 PM” row, never the call's transcript. The SDK's own chat window draws exactly these rows. Every function is static, so Java calls ChatRows.build(messages) as Kotlin does.

What to expect — Pure: no state, no network, any thread. Days are the phone's, in its time zone unless you pass another.

PropertyTypeMeaning
const val TODAY = "today"StringA day line's day for today.
const val YESTERDAY = "yesterday"StringA day line's day for yesterday.
const val DATE = "date"StringA day line's day for an older day, which its date names.

Its functions: build, dayLabel, callLabel, callSpoken, offersNewConversation, offersStarters — each with its parameters, what it returns, its errors and an example.

ChatRows.Row

Read it for one row of a chat window: a day line, an earlier call, or a message.

sealed class Row

From ChatRows.build, oldest first. Branch on its three kinds with when.

CaseFieldsMeaning
Dayday: String, date: String, at: java.util.DateA day line: day is TODAY, YESTERDAY or DATE; date the phone's local date, YYYY-MM-DD; at the first message's time on it. Show ChatRows.dayLabel(row).
Voiceid: String, at: java.util.Date, first: Int, last: IntAn earlier call, one row for a run of its messages on one day: id a stable key (voice- and its first message's ID), at its first message's time, first and last the positions in the messages of the first and last it stands for. Show ChatRows.callLabel with the time.
Turnturn: GraineChatTurnOne message: draw turn as a bubble (GraineChatTurn).
PropertyTypeMeaning
Day.key: StringStringA day line's word as Graine writes it — "today", "yesterday" or the local date: a stable key for the row.

ChatRows.build

Call it for the rows to draw in a chat of your own, in order.

@JvmStatic @JvmOverloads
fun build(turns: List<GraineChatTurn>, now: java.util.Date = java.util.Date(), zone: java.util.TimeZone = java.util.TimeZone.getDefault()): List<Row>

A day line goes before the first row when it is not from today, and wherever the day changes between rows; a run of an earlier call's messages on one day is one Voice row (a day line never sits inside one); every other message is a Turn. Build again whenever the chat changes (addChangeListener).

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe conversation, oldest first — chat.messages.
nowjava.util.Datejava.util.Date()What counts as today and yesterday; now when left out.
zonejava.util.TimeZonethe phone'sWhose days.

Returns — List<ChatRows.Row> (ChatRows.Row). Example value: [Day(day=yesterday, date=2026-10-14, …), Voice(id=voice-t1, …, first=0, last=5), Day(day=today, …), Turn(turn=GraineChatTurn(id=t7, …))].

Errors — none.

val clock = android.text.format.DateFormat.getTimeFormat(this)            // the phone's short time
for (row in ChatRows.build(GraineInApp.get().chat.messages)) when (row) {
    is ChatRows.Row.Day -> addDayLine(ChatRows.dayLabel(row))                // Today, Yesterday, Tue 14 Oct
    is ChatRows.Row.Voice -> clock.format(row.at).let { addCallRow(ChatRows.callLabel(it), ChatRows.callSpoken(it)) }
    is ChatRows.Row.Turn -> addBubble(row.turn)
}

ChatRows.dayLabel

Call it for the words on a day line.

@JvmStatic @JvmOverloads
fun dayLabel(day: Row.Day, now: java.util.Date = java.util.Date(), zone: java.util.TimeZone = java.util.TimeZone.getDefault(), locale: java.util.Locale = java.util.Locale.getDefault()): String

“Today”, “Yesterday”, else the date in the phone's own language and order — “Tue 14 Oct” — with the year when it is not this year.

ParameterTypeDefaultMeaning
dayChatRows.Row.DayrequiredThe day line.
nowjava.util.Datejava.util.Date()What counts as this year.
zonejava.util.TimeZonethe phone'sWhose calendar.
localejava.util.Localethe phone'sThe language and order of the date.

Returns — String. Example value: "Tue 14 Oct".

Errors — none.

val dayLines = ChatRows.build(GraineInApp.get().chat.messages).filterIsInstance<ChatRows.Row.Day>().map { ChatRows.dayLabel(it) }

ChatRows.callLabel

Call it for the words on an earlier call's row.

@JvmStatic
fun callLabel(clock: String): String
ParameterTypeDefaultMeaning
clockStringrequiredThe call's time (Voice.at) in the phone's short time — 6:05 PM or 18:05.

Returns — String — “Voice call · ” and the time. Example value: "Voice call · 6:05 PM".

Errors — none.

val label = ChatRows.callLabel("6:05 PM")   // "Voice call · 6:05 PM"

ChatRows.callSpoken

Call it for what a screen reader says for an earlier call's row.

@JvmStatic
fun callSpoken(clock: String): String
ParameterTypeDefaultMeaning
clockStringrequiredThe same time as callLabel's.

Returns — String. Example value: "Voice call at 6:05 PM".

Errors — none.

val spoken = ChatRows.callSpoken("6:05 PM")   // the row's contentDescription

ChatRows.offersNewConversation

Call it to decide whether your chat's header offers New conversation.

@JvmStatic
fun offersNewConversation(turns: List<GraineChatTurn>): Boolean

New conversation belongs to a conversation that continues from before: it is offered while restored messages are on screen, and calls chat.newConversation().

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe messages on screen.

Returns — Boolean — true when any of them is restored. Example value: true.

Errors — none.

val offerNew = ChatRows.offersNewConversation(GraineInApp.get().chat.messages)

ChatRows.offersStarters

Call it to decide whether your chat shows the brand's starters.

@JvmStatic
fun offersStarters(turns: List<GraineChatTurn>): Boolean

The starters belong to a new chat: not once the user has said something, nor on a conversation that continues from before.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe messages on screen.

Returns — Boolean — true while none of them is the user's and none is restored. Example value: false.

Errors — none.

val showStarters = ChatRows.offersStarters(GraineInApp.get().chat.messages)

GraineChatSize

Pass it to choose how big the chat window opens.

enum class GraineChatSize(val wire: String)

What to expect — Which size wins: the size passed to chat.open(size), then GraineConfig.chatSize, then the agent's Window size in the dashboard, else compact. An open's size lasts until the chat closes. On a phone, large is a sheet over most of the screen (the app dimmed behind it, a handle to drag it closed, Back too) and full the whole screen; on a tablet both open a panel, 420 dp wide, in the corner. A word from a newer design that the SDK does not know is passed over.

PropertyTypeMeaning
val wire: StringStringThe design's word for it — compact, large or full.
CaseMeaning
COMPACTThe window above the voice bar.
LARGEOn a phone, a sheet covering most of the screen; on a tablet, a panel in the corner.
FULLThe whole screen.

Its functions: from — each with its parameters, what it returns, its errors and an example.

GraineChatSize.from

Call it to read a size word — from your own settings, for example — into a GraineChatSize.

@JvmStatic
companion · fun from(wire: Any?): GraineChatSize?

Case and surrounding spaces do not matter.

ParameterTypeDefaultMeaning
wireAny?requiredThe word: "compact", "large" or "full".

Returns — GraineChatSize? — null for anything else (absent, or a word newer than this SDK). Example value: GraineChatSize.LARGE.

Errors — none.

val size = GraineChatSize.from("large")

GraineChatView

Use it to show the chat as a whole screen inside your own layout.

class GraineChatView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs), GraineListener

The text conversation, drawn by the SDK in the brand's design — header, bubbles, composer, suggestions — with the agent's components right under the line that asked for them, and the booked calls first in the conversation. The conversation opens when the view appears and closes shortly after the view leaves the screen. Built from standard Android views, so it needs neither AndroidX nor Compose.

What to expect — Main thread. Add it to any layout, or open GraineChatActivity to show it as a screen of its own.

Constructor parameterTypeDefaultMeaning
contextContextrequiredThe activity it is shown in.
attrsAttributeSet?nullFrom a layout file; null in code.
PropertyTypeMeaning
var source: ChatSourceChatSourceSet it to choose the conversation drawn: the default agent's chat unless set — another agent's (GraineInApp.get("support").chat), drawn in that agent's design, or one your app runs itself.
var scheme: String?String?Set it to force "light", "dark" or "auto" (the device); null follows the design.
var onClose: (() -> Unit)?(() -> Unit)?Set it to hear the close button — finish your screen there. Unset, the button does nothing.
val widgetView: GraineWidgetViewGraineWidgetViewThe components the agent draws in the chat, between the messages and the composer (GraineWidgetView).
setContentView(GraineChatView(this).apply { onClose = { finish() } })
// Another of the app's agents:
GraineChatView(this).apply { source = GraineInApp.get("support").chat }

GraineChatActivity

Use it to open the chat on a screen of its own, with no layout of yours.

class GraineChatActivity : Activity()

GraineChatView full screen, in the brand's design, above the keyboard, with the system bars in the chat's colours; closed by its close button or Back. Declared in the SDK's manifest; nothing for your app to add.

What to expect — Open it with GraineChatActivity.open; it shows the agent you name, or the default.

Its functions: open — each with its parameters, what it returns, its errors and an example.

GraineChatActivity.open

Call it from any “Chat with us” entry point to open the chat screen.

@JvmStatic @JvmOverloads
companion · fun open(context: Context, agent: String = GraineInApp.DEFAULT)

From anything with a context; a non-activity context starts it in a new task. A name no agent was made under opens the default agent's chat, with a warning in the log.

ParameterTypeDefaultMeaning
contextContextrequiredAn activity, or any context of your app.
agentStringGraineInApp.DEFAULTWhich agent's chat, by the name init was given; the default agent when left out.

Returns — nothing.

Errors — none.

GraineChatActivity.open(this, agent = "support")

The voice bar

The agent talking inside your app, beside the screen it talks about. Attach the voice engine once with voice.attach(activity) — it loads idle in the background, so a tap costs only the call — and place a GraineAgentBarView (or a GraineLauncherView) in your layout: the bar places and ends calls by itself, shows captions, the connection's state and the chat beside the call, all in the agent's design. GraineVoice's state and the listener's events are for an app that draws its own controls. The bar says the network's state out loud — “Weak connection”, “No connection”, “Reconnecting…” — and, while a call comes up or the agent works out its answer, shows the dashboard's loader in the brand's words.

GraineVoice

Use it to place and end the voice bar's call from controls of your own, and to read its state.

class GraineVoice internal constructor(…)

GraineInApp.get().voice. Attach the engine once with attach(activity); the bar starts and ends calls itself.

What to expect — Main thread. The state below changes as the call moves, and each change reaches GraineListener. One conversation at a time: starting a call closes an open chat, and a native call holds the line.

PropertyTypeMeaning
var connected: BooleanBooleanRead it to show the call as live. Read-only.
var connecting: BooleanBooleanRead it to show “Connecting…”: a call is being placed. Read-only.
var muted: BooleanBooleanRead it to show the mute control as on. Read-only.
var ready: BooleanBooleanRead it to know the engine has loaded and a call can start at once (a start before is held until it has). Read-only.
var quality: ConnectionQualityConnectionQualityRead it to show the connection in one word (ConnectionQuality). Read-only.
var online: Boolean?Boolean?Read it to know whether the device is online, as the engine sees it; null before it knows. Read-only.
var reconnecting: BooleanBooleanRead it to show “Reconnecting…”: the network interrupted the call and it is being re-established. Read-only.
var connectivity: ConnectivityConnectivityRead it to know whether the engine loaded (Connectivity). Read-only.
var error: String?String?Read it to tell your team something must be fixed before calls can work — a key, or an agent not set up for calls; null otherwise. Also sent to onError; cleared when the next call is placed. Never show it to the user. Read-only.
val connectStage: ConnectStage?ConnectStage?Read it to say where a call being placed is — registering, dialling, retrying, switching (ConnectStage); null outside a connect.
val slowConnect: BooleanBooleanRead it to say “Still connecting…” honestly: the user has waited past GraineConfig.Timing.slowConnectMs (4 s) on a call they placed, or on a bar open while the engine loads.
val callFailed: BooleanBooleanRead it to say “Couldn’t connect · tap to retry”: the last start could not be connected on either audio path. start() tries again; cleared by the next start or a call that connects.
val phase: CallPhaseCallPhaseRead it for what the call is doing in one word (CallPhase) — the bar says “Thinking…” from it.
var captions: List<GraineCaption>List<GraineCaption>Read it for what has been said on the call, oldest first — the agent's turns and the user's (GraineCaption). A live line is replaced as its words arrive, not appended; the last 12 are kept, and a new call starts empty. onCaption hears each line as it comes. Read-only.
var captionsOn: BooleanBooleanRead it to draw your captions (CC) control: the switch, on by default. While it is off the voice bar hides the line being said; captions keeps what was said either way. Read-only; set it with setCaptionsOn.
val attached: BooleanBooleanRead it to know the engine is mounted: attach was called, and detach not since.
companion · const val MIC_REQUEST = 0x6E7AIntThe request code the SDK uses for the microphone prompt — what onRequestPermissionsResult recognises.

Its functions: attach, detach, start, end, setMuted, setCaptionsOn, clearCaptions, retry, onRequestPermissionsResult, viewOpened, viewClosed, answerWidget, reportEvent — each with its parameters, what it returns, its errors and an example.

GraineVoice.attach

Call it once, from your first activity, to load the voice engine so a call can start at once.

fun attach(activity: Activity)

The engine loads idle — nothing is dialled and the microphone is not used — so a start() later costs only the call. Once is enough: from then on the engine follows the user, moving to whichever of your activities is in front, and a call in progress carries on across them. Calling it again from another activity is the same move, never a reload.

ParameterTypeDefaultMeaning
activityActivityrequiredThe activity in front.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.attach(this)

GraineVoice.detach

Call it only when your app no longer wants the voice bar at all — a sign-out that removes the agent, for example.

fun detach()

Ends a call in progress and unloads the engine. attach loads it again.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.detach()

GraineVoice.start

Call it from your own “Talk” control to place the voice bar's call.

fun start()

Asks for the microphone first if the app does not hold it — forward your activity's onRequestPermissionsResult to onRequestPermissionsResult so the call goes ahead when it is granted. Before the engine is ready the start is held and placed when it is. It ends the chat (its lines stay) and another agent's call.

Parameters — none.

Returns — nothing.

Errors — none as an exception: before attach it reports “GraineVoice.attach(activity) must be called before start().” to onError; while a native call is live, “A call is already in progress.”. A refused microphone is onMicDenied("NotAllowedError").

GraineInApp.get().voice.start()

GraineVoice.end

Call it from your own hang-up control.

fun end()

Hangs up. If the call does not confirm it within GraineConfig.Timing.endWatchdogMs (4 s), the SDK ends it itself.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.end()

GraineVoice.setMuted

Call it from your own mute control.

fun setMuted(value: Boolean)

Stops sending the user's voice without ending the call.

ParameterTypeDefaultMeaning
valueBooleanrequiredtrue to stop sending the user's voice, false to send it again.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.setMuted(true)

GraineVoice.setCaptionsOn

Call it from your own captions (CC) control to show or hide the line being said.

fun setCaptionsOn(on: Boolean)

The voice bar shows the line being said only while the switch is on. The call goes on either way, and captions keeps what was said. On by default; your choice holds for the rest of the launch.

ParameterTypeDefaultMeaning
onBooleanrequiredtrue to show the captions, false to hide them.

Returns — nothing.

Errors — none.

val voice = GraineInApp.get().voice
voice.setCaptionsOn(!voice.captionsOn)

GraineVoice.clearCaptions

Call it to forget what has been said on the call — from a “Clear” control on a transcript of yours, say.

fun clearCaptions()

Empties captions without ending the call; the voice bar's caption clears too. A new call starts empty by itself.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.clearCaptions()

GraineVoice.retry

Call it from a “Try again” control when the engine could not load (connectivity is FAILED).

fun retry()

Reloads the engine and starts the reload schedule over (GraineConfig.Timing.retryDelaysMs).

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.retry()

GraineVoice.onRequestPermissionsResult

Call it from your activity's onRequestPermissionsResult, so a call waiting for the microphone can go ahead.

fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray): Boolean

When the user refuses, the waiting call is dropped and onMicDenied("NotAllowedError") fires.

ParameterTypeDefaultMeaning
requestCodeIntrequiredAs Android passes it to your activity.
grantResultsIntArrayrequiredAs Android passes them to your activity.

Returns — Boolean — true when the request was the SDK's (MIC_REQUEST) and is handled; pass anything else on. Example value: true.

Errors — none.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    if (!GraineInApp.get().voice.onRequestPermissionsResult(requestCode, grantResults))
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

GraineVoice.viewOpened

Call it when a voice interface of your own — anything but a GraineAgentBarView, which says so itself — comes on screen.

fun viewOpened()

While one of your views is open, the user is waiting on the voice: an engine still loading behind it is their wait, reported as slowConnect (“Still connecting…”) past Timing.slowConnectMs. Call it in pairs with viewClosed; several views of yours count as one while any is open. Main thread.

Parameters — none.

Returns — nothing.

Errors — none.

override fun onStart() { super.onStart(); GraineInApp.get().voice.viewOpened() }
override fun onStop() { super.onStop(); GraineInApp.get().voice.viewClosed() }

GraineVoice.viewClosed

Call it when the voice interface of yours from viewOpened leaves the screen.

fun viewClosed()

When it was the last one, and no GraineAgentBarView is on screen either, a failed connect and its stage are no longer news: callFailed, connectStage and slowConnect clear. A close with nothing open is ignored.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.viewClosed()

GraineCaption

Read it for one line of what was said on the voice bar's call — to draw a transcript of your own.

data class GraineCaption(val role: Role, val text: String, val live: Boolean)

GraineVoice.captions, oldest first.

What to expect — A live line is replaced as its words arrive, not appended; the last 12 lines are kept, and a new call starts empty. The captions switch (captionsOn) hides the bar's caption, never this list.

FieldTypeDefaultMeaning
roleRolerequiredWho said it: Role.AGENT or Role.CUSTOMER (Role).
textStringrequiredThe words so far.
liveBooleanrequiredtrue while the words are still arriving: draw them as provisional.

GraineAgentBarView

Place it in your layout to give the user the voice bar.

class GraineAgentBarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs), GraineListener

What the user sees during a call, drawn exactly as the dashboard's Garage previews it: the avatar (the design's glyph, the brand's logo whole on its plate, or the first letter), the heading and a status line with a meter that moves at the design's pace, the microphone, and the end button. Above it, the line being said on a card of its own (while voice.captionsOn), and above that the components the agent draws (widgetView), so a card the agent shows moves with the bar. Where the dashboard offers chat, the bar switches between the call and the chat. Plain platform widgets: no AndroidX, Compose or Material.

What to expect — The design is the source for everything; the properties below override one thing each. From a server without designs, the bar is Graine Classic in the dashboard's accent and corners.

Constructor parameterTypeDefaultMeaning
contextContextrequiredThe activity it is shown in.
attrsAttributeSet?nullFrom a layout file; null in code.
PropertyTypeMeaning
var accent: String?String?Set it (#RRGGBB) to colour the avatar, the microphone and the meter; null uses the design's accent.
var name: String?String?Set it to change the name beside the status; null uses the dashboard's heading, else “Assistant”.
var avatarUrl: String?String?Set it to a picture of your own for the avatar, filling its circle (yours to crop); null uses the design's glyph, else the logo, else the first letter.
var scheme: String?String?Set it to force "light", "dark" or "auto" (the device); null follows the design.
var radius: Int?Int?Set it (dp) to round the caption card (the bar takes it plus 8, at most 26); null uses the design's.
var onClose: (() -> Unit)?(() -> Unit)?Set it to hide the bar (or show your launcher again) when the user presses end with no call to end, or once a call has hung up and its goodbye has played. Unset, the bar stays.
var draggable: BooleanBooleanSet it to false to keep the bar where you put it; true (the default) lets the user drag it up and down, and a tap still taps.
var placementOffset: IntIntRead it for where the user left the bar, in dp above its anchor; set it to restore a position you kept (clamped to the screen once laid out).
var onPlacementChange: ((bottomOffset: Int) -> Unit)?((bottomOffset: Int) -> Unit)?Set it to keep the user's chosen position: called when they let go somewhere new — save the value and give it back to placementOffset later. The SDK stores nothing.
var keyboardAware: BooleanBooleanLeave it true so the bar rises above the soft keyboard; turn it off only if your layout already moves it.
val widgetView: GraineWidgetViewGraineWidgetViewThe components the agent draws, mounted above the caption so they move with the bar (GraineWidgetView).
var graine: GraineInAppGraineInAppSet it to talk to another of your agents — bar.graine = GraineInApp.get("sales") (attach that agent's voice once too); the default agent unless set. Set it as the screen opens; changing it never moves a call that is already live to the new agent.
var chat: ChatSource?ChatSource?Set it to choose the chat the bar opens beside the call: the agent's own GraineChat by default, a conversation your app runs itself, or null for the call alone whatever the dashboard offers.

Its functions: resetPlacement — each with its parameters, what it returns, its errors and an example.

GraineAgentBarView.resetPlacement

Call it to put the bar back where you placed it — from a “Reset position” setting, for example.

fun resetPlacement()

Moves the bar back to its anchor and tells onPlacementChange (with 0).

Parameters — none.

Returns — nothing.

Errors — none.

bar.resetPlacement()

GraineLauncherView

Place it in your layout to give the user a floating button that starts the call — or opens what you choose.

class GraineLauncherView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs), GraineListener

The floating button, drawn the way the brand designed it: 56 dp, in the accent, with the design's glyph, the brand's logo (whole, on its plate) or the first letter of the name, in the design's shape — circle, squircle, rounded or plain square, or a pill as wide as its label — with the design's depth. A tap starts the call, and ends one that is live; setOnClickListener replaces that. Place it yourself, like any view (bottom-end over the content is usual); it measures its own size, shadow room included.

What to expect — Main thread. Without a design (an older server) it is a circle in the dashboard's accent with its logo.

Constructor parameterTypeDefaultMeaning
contextContextrequiredThe activity it is shown in.
attrsAttributeSet?nullFrom a layout file; null in code.
PropertyTypeMeaning
var label: String?String?Set it to change the pill's words and what TalkBack reads; null uses the dashboard's launcher label, else “Talk to us”.
var accent: String?String?Set it (#RRGGBB) to change the fill; null uses the design's accent, else the dashboard's.
var scheme: String?String?Set it to force "light", "dark" or "auto"; null follows the design.
var graine: GraineInApp?GraineInApp?Set it to call another of your agents, in its design — GraineInApp.get("sales"); the default agent unless set.

Its functions: setOnClickListener — each with its parameters, what it returns, its errors and an example.

GraineLauncherView.setOnClickListener

Call it to make the launcher open something of yours — the voice bar, the chat — instead of starting the call.

override fun setOnClickListener(l: OnClickListener?)

Replaces the default tap (start or end the call) with your own listener.

ParameterTypeDefaultMeaning
lOnClickListener?requiredWhat a tap does now.

Returns — nothing.

Errors — none.

launcher.setOnClickListener { bar.visibility = View.VISIBLE }

GraineLoaderView

Use it to draw the brand's loader in a waiting state of your own.

class GraineLoaderView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs)

How waiting looks, from the dashboard's Appearance → loader: dots, pulse, ring, bars, or the brand's logo breathing (“mark”, which is the pulse when no logo is uploaded), at the chosen size and speed. Drawn on a canvas, with no resources or animation library. The bar and the components use it themselves.

What to expect — Call configure with a look from Loaders.resolve.

Constructor parameterTypeDefaultMeaning
contextContextrequiredThe activity it is shown in.
attrsAttributeSet?nullFrom a layout file; null in code.

Its functions: configure — each with its parameters, what it returns, its errors and an example.

GraineLoaderView.configure

Call it to set what the loader draws — the kind, colour, size and speed.

fun configure(look: LoaderLook, color: Int, track: Int, sizeDp: Int? = null, logoUrl: String? = null)
ParameterTypeDefaultMeaning
lookLoaderLookrequiredThe loader to draw (LoaderLook) — usually Loaders.resolve(session.appearance.opt("loader")).
colorIntrequiredThe colour to draw in, as an ARGB colour int: the look's own color, else your accent.
trackIntrequiredThe ring's unlit part, as an ARGB colour int.
sizeDpInt?nullA size of your own, in dp, over the look's — for a small loader in a status line.
logoUrlString?nullThe brand's logo, for the mark kind.

Returns — nothing.

Errors — none.

val look = Loaders.resolve(GraineInApp.get().session?.appearance?.opt("loader"))
val loaderView = GraineLoaderView(this)
loaderView.configure(look, Color.parseColor(look.color ?: "#318CE7"), 0x332C2E33, sizeDp = 16)

LoaderLook

Read it to draw the dashboard's loader yourself, with every value worked out.

data class LoaderLook(val kind: String, val color: String?, val px: Int, val periodMs: Long, val text: LoaderText, val skeleton: Boolean)

From Loaders.resolve.

FieldTypeDefaultMeaning
kindStringrequiredWhat to draw: dots, pulse, ring, bars or mark.
colorString?requiredThe colour chosen (#RRGGBB); null means use the accent.
pxIntrequiredThe loader's size, in dp: 14, 20 or 28.
periodMsLongrequiredHow long one animation cycle takes: 1600, 1100 or 700 ms.
textLoaderTextrequiredThe words for each waiting moment, every one filled in (LoaderText).
skeletonBooleanrequiredtrue to draw a component's placeholder shape while it loads.

LoaderStyle

Read it for the loader exactly as chosen in the dashboard, before defaults are applied.

data class LoaderStyle(val kind: String? = null, val color: String? = null, val size: String? = null, val speed: String? = null, val text: LoaderText? = null, val skeleton: Boolean? = null)

From Loaders.normalise; only known words survive. null means “use the default”.

FieldTypeDefaultMeaning
kindString?nulldots, pulse, ring, bars or mark.
colorString?null"accent" or a #RRGGBB colour.
sizeString?nullsmall, medium or large.
speedString?nullcalm, normal or quick.
textLoaderText?nullThe brand's own words for the waiting moments.
skeletonBoolean?nullWhether components show a placeholder shape while they load.

LoaderText

Read it for the words shown in each moment the user waits.

data class LoaderText(val connecting: String? = null, val reconnecting: String? = null, val thinking: String? = null, val component: String? = null)

What to expect — null means the default: “Connecting…”, “Reconnecting…”, “Thinking…”, “Loading…”. A word the brand chose is at most 40 characters, often not English — show it as it is.

FieldTypeDefaultMeaning
connectingString?nullWhile a call comes up.
reconnectingString?nullWhile a dropped call is re-established, or a connect retries.
thinkingString?nullWhile the agent works out its answer.
componentString?nullWhile a component the agent announced loads.

Its functions: forMoment — each with its parameters, what it returns, its errors and an example.

LoaderText.forMoment

Call it to get the words for one waiting moment by name.

fun forMoment(moment: String): String?
ParameterTypeDefaultMeaning
momentStringrequired"connecting", "reconnecting", "thinking" or "component".

Returns — String? — the words for that moment, or null (a moment with no words of its own, or an unknown name). Example value: "Thinking…".

Errors — none.

val look = Loaders.resolve(GraineInApp.get().session?.appearance?.opt("loader"))
val words = look.text.forMoment("thinking")

Loaders

Use it to turn the dashboard's loader setting into something you can draw.

object Loaders

PropertyTypeMeaning
val KINDS: List<String>List<String>The loader kinds the SDK draws: dots, pulse, ring, bars, mark.
val SIZES: List<String>List<String>The sizes: small, medium, large (14, 20 and 28 dp).
val SPEEDS: List<String>List<String>The speeds: calm, normal, quick (1600, 1100 and 700 ms a cycle).
val MOMENTS: List<String>List<String>The waiting moments that have words: connecting, reconnecting, thinking, component.
const val DEFAULT_KIND = "dots"StringThe kind drawn when none is chosen.
const val TEXT_MAX = 40IntThe longest a moment's words may be, in characters; longer words are cut.
val DEFAULT_TEXT: LoaderTextLoaderTextThe words used where the brand chose none: “Connecting…”, “Reconnecting…”, “Thinking…”, “Loading…”.

Its functions: normalise, resolve — each with its parameters, what it returns, its errors and an example.

Loaders.normalise

Call it to read the dashboard's loader setting, keeping only the words the SDK knows.

fun normalise(raw: Any?): LoaderStyle
ParameterTypeDefaultMeaning
rawAny?requiredThe appearance's loader value, as served (a JSONObject).

Returns — LoaderStyle (LoaderStyle) — anything unknown or malformed is dropped. Example value: LoaderStyle(kind=ring, color=accent, size=null, speed=quick, text=null, skeleton=null).

Errors — none.

val style = Loaders.normalise(GraineInApp.get().session?.appearance?.opt("loader"))

Loaders.resolve

Call it to get the loader to draw, with every default applied.

fun resolve(raw: Any?): LoaderLook
fun resolve(s: LoaderStyle): LoaderLook

Takes the raw setting (anything — junk resolves to the default dots) or a LoaderStyle already read.

ParameterTypeDefaultMeaning
rawAny?requiredThe appearance's loader value, as served.
sLoaderStylerequiredA style from Loaders.normalise (the second form).

Returns — LoaderLook (LoaderLook). Example value: LoaderLook(kind=dots, color=null, px=20, periodMs=1100, text=LoaderText(…), skeleton=false).

Errors — none.

val look = Loaders.resolve(GraineInApp.get().session?.appearance?.opt("loader"))

Connectivity

Read it to know whether the voice engine has loaded.

enum class Connectivity { LOADING, READY, RETRYING, FAILED }

What to expect — LOADING → READY; a stalled load goes RETRYING (the SDK reloads it on its own schedule) and, after the last reload, FAILED.

CaseMeaning
LOADINGThe engine is loading; a start now is placed as soon as it is ready.
READYLoaded: a call can start at once.
RETRYINGLoading stalled and the SDK is trying again.
FAILEDEvery reload was spent: offer “Try again” (voice.retry()).

ConnectionQuality

Read it to tell the user when the network is the problem.

enum class ConnectionQuality { EXCELLENT, GOOD, POOR, LOST, UNKNOWN }

What to expect — Reported as it changes (onQuality). The same thresholds on every Graine SDK and the website.

CaseMeaning
EXCELLENTUnder 150 ms round trip, 30 ms jitter and 2 % loss.
GOODUsable: at or over one of those, under the poor marks.
POOR400 ms round trip, 80 ms jitter or 8 % loss, or worse — say “Weak connection”.
LOSTNothing is getting through.
UNKNOWNNo measurement yet.

Its functions: classify — each with its parameters, what it returns, its errors and an example.

ConnectionQuality.classify

Call it to rate a connection you measured yourself the way the SDK does.

companion · fun classify(rttMs: Double?, jitterMs: Double?, loss: Double?): ConnectionQuality

The worst of the three decides: any one alone can ruin a call.

ParameterTypeDefaultMeaning
rttMsDouble?requiredThe round-trip time, in milliseconds.
jitterMsDouble?requiredThe jitter, in milliseconds.
lossDouble?requiredThe packet loss, from 0 to 1.

Returns — ConnectionQuality — UNKNOWN when all three are null. Example value: ConnectionQuality.GOOD.

Errors — none.

val quality = ConnectionQuality.classify(180.0, 12.0, 0.01)   // GOOD

ConnectStage

Read it to say where a call being placed is, so the user waits instead of giving up.

enum class ConnectStage(val wire: String)

What to expect — Only during a connect; null outside one. A stage newer than this SDK is ignored. Nothing could carry the call at all is not a stage: it is GraineVoice.callFailed.

PropertyTypeMeaning
val wire: StringStringThe engine's word for the stage.
CaseMeaning
REGISTERINGSetting up the line (a cold start).
DIALLINGThe call is on its way to the agent.
RETRYINGThe first attempt went unanswered and a fresh connection is being made — on mobile data, usually a connection the phone dropped while idle.
SWITCHINGThe call is moving to the other audio path.

Its functions: from — each with its parameters, what it returns, its errors and an example.

ConnectStage.from

Call it to read a stage word into a ConnectStage.

@JvmStatic
companion · fun from(s: String?): ConnectStage?
ParameterTypeDefaultMeaning
sString?requiredThe word: registering, dialling, retrying or switching.

Returns — ConnectStage? — null for anything else (failed, or a stage newer than this SDK). Example value: ConnectStage.DIALLING.

Errors — none.

val stage = ConnectStage.from("retrying")

CallPhase

Read it for what the voice bar's call is doing, in one word — to draw your own status line.

enum class CallPhase { IDLE, CONNECTING, RECONNECTING, THINKING, SPEAKING, LISTENING }

CaseMeaning
IDLENo call; ready to start.
CONNECTINGThe engine is loading, or a call is being placed.
RECONNECTINGThe engine failed and is being reloaded, or the network dropped the call.
THINKINGThe user finished a sentence and the agent has not answered yet.
SPEAKINGThe agent is talking.
LISTENINGThe agent is waiting for the user.

Its functions: of — each with its parameters, what it returns, its errors and an example.

CallPhase.of

Call it to work out the phase from state you track yourself, the way the bar does.

@JvmStatic
companion · fun of(reconnecting: Boolean, connectivity: Connectivity, connecting: Boolean, ready: Boolean, connected: Boolean, agentSpeaking: Boolean, awaitingAgent: Boolean): CallPhase

First match wins: reconnecting (or the engine retrying or failed) → RECONNECTING; connecting or not ready → CONNECTING; not connected → IDLE; the agent speaking → SPEAKING; waiting for the agent → THINKING; else LISTENING.

ParameterTypeDefaultMeaning
reconnectingBooleanrequiredThe call is being re-established after a drop.
connectivityConnectivityrequiredWhether the engine loaded.
connectingBooleanrequiredA call is being placed.
readyBooleanrequiredThe engine is ready.
connectedBooleanrequiredThe call is live.
agentSpeakingBooleanrequiredThe agent's line is arriving.
awaitingAgentBooleanrequiredThe user finished a line and the agent has not begun its answer.

Returns — CallPhase. Example value: CallPhase.LISTENING.

Errors — none.

val phase = CallPhase.of(false, Connectivity.READY, false, true, true, false, false)   // LISTENING

Transport

Pass it in GraineConfig.transport only to force how the voice bar's audio travels.

enum class Transport(val wire: String)

What to expect — Leave transport null and the dashboard's Call audio setting decides. onTransport says which path carries a call, and why it is not the one asked for.

PropertyTypeMeaning
val wire: StringStringThe setting's word: ws or rtc.
CaseMeaning
WEBSOCKETThe standard path: the audio on the conversation's own connection. It works on any network; on one that drops packets, the audio drops out too.
WEBRTCHigh-quality audio, with a jitter buffer and the device's echo canceller; falls back to WEBSOCKET.

Its functions: from — each with its parameters, what it returns, its errors and an example.

Transport.from

Call it to read a transport word into a Transport.

companion · fun from(s: String?): Transport
ParameterTypeDefaultMeaning
sString?requiredThe word: "rtc" or "ws".

Returns — Transport — WEBRTC for "rtc", WEBSOCKET for anything else. Example value: Transport.WEBRTC.

Errors — none.

val transport = Transport.from("rtc")

Several agents in one app

A support agent in Help, a sales agent on Plans — each with its own publishable key, made under its own name with GraineInApp.init(application, config, name = "sales") and reached with GraineInApp.get("sales"). Each agent has its own settings, chat, voice bar, actions, and scheduling and memory requests, sent with its own key. The install ID, who the user is (identify on one is for all; identify(null) signs out of all), the memory choice, the screen and the call engine are the app's. One conversation at a time across every agent. Views take the agent: GraineAgentBarView.graine, GraineChatView.source = GraineInApp.get("sales").chat, GraineChatActivity.open(context, agent), GraineWidgetView.graine, GraineLauncherView.graine; calls.startCall(activity, reason, variables, agent) calls a named one, and a ring Graine sends rings with the agent it is for. An app with one agent names none.

GraineInApp.init(this, GraineConfig("https://www.graine.ai", "pk_live_support…"))                 // the default agent
GraineInApp.init(this, GraineConfig("https://www.graine.ai", "pk_live_sales…"), name = "sales")
GraineInApp.get("sales").chat.open()

GraineInApp.get

Call it anywhere to reach an agent you made with init.

@JvmStatic @JvmOverloads
companion · fun get(name: String = DEFAULT): GraineInApp
ParameterTypeDefaultMeaning
nameStringDEFAULTThe name the agent was made under; the default agent when left out.

Returns — GraineInApp — the agent.

Errors — IllegalStateException when there is none: “GraineInApp.init(application, config) has not been called” for the default agent, or “No agent is named "sales": GraineInApp.init(application, config, name = "sales") first”. Use find when it may not exist.

val sales = GraineInApp.get("sales")

GraineInApp.find

Call it to reach an agent that may not have been made.

@JvmStatic
companion · fun find(name: String): GraineInApp?
ParameterTypeDefaultMeaning
nameStringrequiredThe name the agent was made under.

Returns — GraineInApp? — the agent, or null when init made none under that name. Example value: null.

Errors — none.

GraineInApp.find("support")?.chat?.open()

Screen context, actions and components

Tell the agent what the user is looking at, and let it act in your app through handlers you register. The screen is the app's: reported on any agent, every agent has it and sends it on while it has a live conversation. Values are masked on the device before anything leaves it — card and account numbers, government ID numbers, phone numbers and email addresses, plus your own rules — so report real values. The agent's components (product lists, plan comparisons, choices, time slots, information cards) are drawn by GraineWidgetView, which the voice bar and the chat already hold; a view of your own answers them with answerWidget.

GraineInApp.setScreen

Call it on every meaningful change of what the user can see, so the agent talks about the right thing.

fun setScreen(next: GraineScreen?)

An unchanged screen is not resent. The first change goes at once and later ones at most every 400 ms, the latest always last — a screen updated on every keystroke is neither flooded nor stalled. After a pause, the agent names the field that is INVALID or EMPTY instead of asking “need a hand?”. null clears it.

ParameterTypeDefaultMeaning
nextGraineScreen?requiredWhat the user can see now (GraineScreen), or null.

Returns — nothing.

Errors — none.

GraineInApp.get().setScreen(GraineScreen(
    screen = "checkout_address", title = "Delivery address",
    journey = GraineScreen.Journey("Checkout", step = 2, of = 3, next = "Payment"),
    fields = listOf(GraineField(name = "postal_code", label = "Postal code", value = "400001",
        status = GraineField.Status.INVALID, error = "We don't deliver there yet")),
))

GraineInApp.updateField

Call it when one field changes, so a screen described once need not be described again on every keystroke.

fun updateField(field: GraineField)

The field replaces the one with the same name on the current screen — or is added, if the screen has none — and the agent gets the update like any other screen change. Before any screen has been reported it does nothing.

ParameterTypeDefaultMeaning
fieldGraineFieldrequiredThe field as it is now (GraineField).

Returns — nothing.

Errors — none.

GraineInApp.get().updateField(GraineField("postal_code", value = "400001", status = GraineField.Status.FILLED))

GraineInApp.removeField

Call it when a field leaves the screen.

fun removeField(name: String)

Nothing happens when it was never reported.

ParameterTypeDefaultMeaning
nameStringrequiredThe field's name.

Returns — nothing.

Errors — none.

GraineInApp.get().removeField("coupon")

GraineInApp.clearScreen

Call it when the user leaves a screen — in the screen's onStop or onDestroy.

@JvmOverloads
fun clearScreen(screenId: String? = null)

With screenId, only if that screen is still the one reported — a screen closing late must not erase the one that replaced it. null clears whatever is there.

ParameterTypeDefaultMeaning
screenIdString?nullThe screen's screen ID, to clear only that one.

Returns — nothing.

Errors — none.

override fun onStop() { super.onStop(); GraineInApp.get().clearScreen("checkout_address") }

GraineInApp.setVariables

Call it when your app learns something the agent's prompt uses — a plan, a cart total.

fun setVariables(values: Map<String, Any?>?)

Merged over what is there, not replaced: null under a key removes it, null for the whole map clears them all. Numbers and booleans are written out as text; nested maps and lists are dropped. Masked on the device. Reaches the next call, and a live voice-bar call picks it up. A conversation carries up to 12 variables, about 1 KB — past that, the biggest are left off first, who_called and call_reason last.

ParameterTypeDefaultMeaning
valuesMap<String, Any?>?requiredPrompt variables by name.

Returns — nothing.

Errors — none.

GraineInApp.get().setVariables(mapOf("plan" to "gold", "cart_total" to 42))

GraineInApp.currentVariables

Call it to check, while you build, exactly which prompt variables the agent will receive.

fun currentVariables(): Map<String, String>

In order of precedence, each later one winning: the dashboard's defaults, GraineConfig.variables, setVariables, then the signed-in user's traits (without the trait that identifies them).

Parameters — none.

Returns — Map<String, String>. Example value: {plan=gold, cart_total=42, name=Asha}.

Errors — none.

Log.d("Graine", GraineInApp.get().currentVariables().toString())

GraineInApp.track

Call it to tell the agent about something the user did that informs without interrupting — a coupon applied, a filter changed.

@JvmOverloads
fun track(name: String, data: Map<String, Any?> = emptyMap(), coalesce: Boolean = false)

It rides along with the screen, among the user's recent events (the last ten are kept), for every agent. Masked on the device.

ParameterTypeDefaultMeaning
nameStringrequiredThe event's name, such as "coupon_applied".
dataMap<String, Any?>emptyMap()Details of the event, by name.
coalesceBooleanfalsetrue for context rather than news: it waits for the next screen update instead of sending one of its own (taps work this way).

Returns — nothing.

Errors — none.

GraineInApp.get().track("coupon_applied", mapOf("code_length" to 8))

GraineInApp.tap

Call it to tell the agent the user tapped a control the SDK cannot name by itself — a custom-drawn button, say.

fun tap(name: String)

Reported as tap:<name> among the screen's recent events, at most twice a second and only while a screen is reported — a tap means something only against the screen it was on. Labelled controls are noticed by themselves (GraineConfig.captureTaps).

ParameterTypeDefaultMeaning
nameStringrequiredThe control's name as the agent should read it: one line, at most 40 characters kept.

Returns — nothing.

Errors — none.

GraineInApp.get().tap("Apply coupon")

GraineInApp.reportEvent

Call it when something happens that the agent may want to speak up about — a payment failing, a form being rejected.

@JvmOverloads
fun reportEvent(name: String, detail: String? = null, data: Map<String, Any?> = emptyMap())

It reaches whichever conversation is live — the voice-bar call or the chat — with the current screen sent first, so the agent reads the event against the right one. When no conversation is live there is no one to tell, and the event is discarded.

ParameterTypeDefaultMeaning
nameStringrequiredWhat happened, in a word, such as "payment_failed".
detailString?nullA sentence about it for the agent.
dataMap<String, Any?>emptyMap()Details, by name.

Returns — nothing.

Errors — none.

GraineInApp.get().reportEvent("payment_failed", "The card was declined.")

GraineVoice.reportEvent

Call it to tell the voice-bar call about something the agent may want to speak up about.

@JvmOverloads
fun reportEvent(name: String, detail: String? = null, data: Map<String, Any?> = emptyMap())

Only during a voice-bar call; prefer GraineInApp.reportEvent, which reaches whichever conversation is live.

ParameterTypeDefaultMeaning
nameStringrequiredWhat happened, in a word.
detailString?nullA sentence about it.
dataMap<String, Any?>emptyMap()Details, by name.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.reportEvent("upload_failed", "The file was too large.")

GraineInApp.noteInteraction

Call it when the user does something that is not a field edit — scrolls a list, opens a picker — so the SDK does not think they are stuck.

fun noteInteraction()

Restarts the clock that notices a user stuck on a step. Field edits do not count: someone typing one character every forty seconds is exactly who is stuck.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().noteInteraction()

GraineInApp.configureFriction

Call it to change your app's own stuck-user rules while it runs.

fun configureFriction(override: FrictionConfig?)

Replaces your app's layer of rules — the same layer as GraineConfig.friction — over the dashboard's. Safe at any time; null leaves the dashboard's and the defaults.

ParameterTypeDefaultMeaning
overrideFrictionConfig?requiredYour app's rules (FrictionConfig), or null.

Returns — nothing.

Errors — none.

GraineInApp.get().configureFriction(FrictionConfig(screens = mapOf("terms" to FrictionConfig.Screen(ignore = true))))

GraineInApp.registerAction

Call it to let the agent do one thing in your app — open a screen, apply a filter, change a setting.

fun registerAction(name: String, handler: (JSONObject) -> ActionResult): () -> Unit

Declare the action on the agent in the dashboard too: a handler with no declaration is never offered, and a declared action with no handler is refused (“This app build does not implement "…".”). The handler runs on the main thread (it will usually touch your UI) with the agent's arguments, and must answer within six seconds (a little sooner on a native call), or the agent is told it was not done (“The app did not answer within 6 seconds.”). A handler that throws answers error with its message. Defining an action again under the same name replaces its handler. Your app's own screen list (GraineScreen.availableActions) narrows what is offered on a screen.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, exactly as it is declared on the agent (Embed & Widgets → Actions).
handler(JSONObject) -> ActionResultrequiredRuns the action with the agent's arguments and says how it went (ActionResult). Tell the truth: the agent speaks about exactly this.

Returns — () -> Unit — call it to unregister this handler.

Errors — none.

val unregister = GraineInApp.get().registerAction("set_language") { args ->
    val language = args.optString("language")
    if (language.isEmpty()) ActionResult.refused("No language was given.")
    else { settings.setLanguage(language); ActionResult.ok("The app is now in $language.") }
}

GraineInApp.registerDeferredAction

Call it instead of registerAction when the answer is not ready when the handler returns — a network call, a confirmation the user taps.

fun registerDeferredAction(name: String, handler: (args: JSONObject, answer: (ActionResult) -> Unit) -> Unit): () -> Unit

Your handler is called on the main thread; call answer exactly once, from whichever thread finishes the work. You still have six seconds: after that the agent has been told the action did not complete, so a late answer is ignored, and so is any answer after the first.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as declared on the agent.
handler(args: JSONObject, answer: (ActionResult) -> Unit) -> UnitrequiredStarts the action and calls answer with how it went.

Returns — () -> Unit — call it to unregister this handler.

Errors — none.

GraineInApp.get().registerDeferredAction("confirm_address") { args, answer ->
    showConfirmDialog(args.optString("address")) { confirmed ->
        answer(if (confirmed) ActionResult.ok("Confirmed.") else ActionResult.refused("The user said no."))
    }
}

GraineInApp.checkActions

Call it once at start-up, while you build, to see which of your actions the agent can actually call.

fun checkActions(): Map<String, List<String>>

Compares your handlers with the actions declared on the agent. Read it after the settings load (onSessionReady); before, nothing is declared yet. The SDK also logs a warning for declared actions with no handler.

Parameters — none.

Returns — Map<String, List<String>> with three keys — usable (registered and declared), undeclared (registered, never offered), unhandled (declared, refused when asked). Example value: {usable=[set_language], undeclared=[], unhandled=[open_screen]}.

Errors — none.

Log.d("Graine", GraineInApp.get().checkActions().toString())

GraineInApp.clearPendingWidget

Call it to forget a component the agent drew that you no longer want shown — answered elsewhere, or from a call that is over.

fun clearPendingWidget()

Clears pendingWidget. Answering or dismissing a component, and a new call starting, do it by themselves.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().clearPendingWidget()

GraineScreen

Pass it to setScreen to describe what the user can see.

data class GraineScreen @JvmOverloads constructor(…)

What to expect — Values are masked on the device before they leave it — report real ones; a screen that pre-redacts only blinds the agent. An unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms.

FieldTypeDefaultMeaning
screenStringrequiredA stable ID for the screen, such as "checkout_address"; keep it constant — friction rules and clearScreen match on it.
titleString?nullThe screen's title as the user sees it — what the agent calls the screen.
journeyJourney?nullWhere the screen sits in a flow (GraineScreen.Journey), so the agent can say “step 2 of 3”.
fieldsList<GraineField>emptyList()The inputs on the screen (GraineField): what lets the agent name the field that is wrong.
errorString?nullA screen-level error banner, as shown — distinct from a field's own.
dataMap<String, Any?>emptyMap()Anything else the agent should know about the screen, by name.
availableActionsList<String>?nullWhich of your registered actions work on this screen: null offers every registered action, an empty list none.

GraineScreen.Journey

Pass it in GraineScreen.journey to say where a screen sits in a flow.

data class Journey @JvmOverloads constructor(val name: String, val step: Int? = null, val of: Int? = null, val next: String? = null)

FieldTypeDefaultMeaning
nameStringrequiredThe flow's name, such as "Checkout".
stepInt?nullWhich step this screen is, counting from 1.
ofInt?nullHow many steps the flow has.
nextString?nullWhat comes next, such as "Payment" — so the agent can say what is left.

GraineField

Pass it in GraineScreen.fields (or to updateField) to describe one input.

data class GraineField @JvmOverloads constructor(val name: String, val label: String? = null, val value: String? = null, val status: Status? = null, val error: String? = null, val optional: Boolean = false)

What to expect — Its status — empty, filled, invalid, pending or locked — is what lets the agent name the field that is wrong.

FieldTypeDefaultMeaning
nameStringrequiredA stable ID for the field, by which updateField and removeField find it.
labelString?nullThe label the user sees — what the agent calls the field.
valueString?nullThe real value; it is masked on the device on its way out. Never pre-redact it.
statusStatus?nullWhere the field stands (GraineField.Status).
errorString?nullThe validation message actually shown — what the agent can offer to fix.
optionalBooleanfalsetrue when leaving it blank is fine, so the agent does not count it as work left.

GraineField.Status

Use it to say where a field stands.

enum class Status(val wire: String) { EMPTY("empty"), FILLED("filled"), INVALID("invalid"), PENDING("pending"), LOCKED("locked") }

What to expect — INVALID and EMPTY (when not optional) are what make the agent speak up about a field.

PropertyTypeMeaning
val wire: StringStringThe word sent to the agent.
CaseMeaning
EMPTYNothing entered yet.
FILLEDEntered and accepted.
INVALIDEntered and refused — set error to the message shown.
PENDINGBeing checked — a code being verified, an upload in progress.
LOCKEDNot the user's to change; the agent does not ask them to.

ActionResult

Return it from an action handler to tell the agent what happened.

data class ActionResult(val status: Status, val message: String = "", val data: Map<String, Any?> = emptyMap())

What to expect — The agent speaks about exactly this, so tell the truth: ok only when it really happened. refused with a reason is more useful than a false success.

FieldTypeDefaultMeaning
statusStatusrequiredHow it went (ActionResult.Status).
messageString""What the agent is told — not shown to the user; the agent decides how to say it.
dataMap<String, Any?>emptyMap()Structured details the agent can use in its reply.

Its functions: ok, refused, error — each with its parameters, what it returns, its errors and an example.

ActionResult.Status

Read it to know how an action went.

enum class Status(val wire: String) { OK("ok"), REFUSED("refused"), ERROR("error") }

PropertyTypeMeaning
val wire: StringStringThe word sent to the agent.
CaseMeaning
OKIt happened; the agent tells the user it is done.
REFUSEDThe action will not be done, with the reason — often the most helpful thing to tell the agent.
ERRORIt was tried and failed.

ActionResult.ok

Return it when the action happened.

@JvmStatic @JvmOverloads
companion · fun ok(message: String = "", data: Map<String, Any?> = emptyMap()): ActionResult
ParameterTypeDefaultMeaning
messageString""What the agent is told happened.
dataMap<String, Any?>emptyMap()A structured result for the agent.

Returns — ActionResult with status OK.

Errors — none.

ActionResult.ok("The app is now in Spanish.")

ActionResult.refused

Return it when the action will not happen, and say why.

@JvmStatic
companion · fun refused(message: String): ActionResult
ParameterTypeDefaultMeaning
messageStringrequiredWhy not, for the agent to explain.

Returns — ActionResult with status REFUSED.

Errors — none.

ActionResult.refused("There is no order with that number.")

ActionResult.error

Return it when the action was tried and failed.

@JvmStatic
companion · fun error(message: String): ActionResult
ParameterTypeDefaultMeaning
messageStringrequiredWhat went wrong, for the agent.

Returns — ActionResult with status ERROR.

Errors — none.

ActionResult.error("The network is unavailable.")

Masking

Use it to mask a value shape only your app knows, before anything leaves the device.

object Masking

Applied to screens, variables, traits and events before they leave the device: card and account numbers, government ID numbers, phone numbers and email addresses are replaced with [CARD], [PAN], [AADHAAR], [PHONE] and [EMAIL]; rules you add apply after them.

What to expect — One set of rules for the whole app. Graine masks again on arrival; masking here means the value is never sent at all.

Its functions: mask, addRule, clearCustomRules, deep — each with its parameters, what it returns, its errors and an example.

Masking.mask

Call it to see how a text will look once the SDK masks it — in a test, for example.

fun mask(input: String): String
ParameterTypeDefaultMeaning
inputStringrequiredThe text to mask.

Returns — String — the text with every match replaced. Example value: "Call me on [PHONE]".

Errors — none.

val shown = Masking.mask("Call me on 9876543210")

Masking.addRule

Call it once at start-up to mask a value shape only your app knows — an account number, say.

fun addRule(pattern: Regex, token: String = "[REDACTED]")
ParameterTypeDefaultMeaning
patternRegexrequiredWhat to mask.
tokenString"[REDACTED]"What replaces each match.

Returns — nothing.

Errors — none.

Masking.addRule(Regex("ACC-[0-9]+"), "[ACCOUNT]")

Masking.clearCustomRules

Call it to remove every rule you added (the built-in ones stay).

fun clearCustomRules()

Parameters — none.

Returns — nothing.

Errors — none.

Masking.clearCustomRules()

Masking.deep

Call it to mask every text inside a JSON value the way the SDK does.

fun deep(value: Any?): Any?

Keys are kept; every string value, at any depth of a JSONObject or JSONArray, is masked. Numbers, booleans and null pass through.

ParameterTypeDefaultMeaning
valueAny?requiredA String, JSONObject, JSONArray or anything else.

Returns — Any? — a masked copy of the same shape. Example value: {"note":"Reach me at [EMAIL]"}.

Errors — none.

val safe = Masking.deep(JSONObject().put("note", "Reach me at asha@example.com"))

FrictionConfig

Pass it in GraineConfig.friction (or to configureFriction) to tune when the SDK decides a user is stuck.

data class FrictionConfig @JvmOverloads constructor(…)

Your app's layer of stuck-user rules, over the dashboard's (Embed & Widgets → friction), over the built-in defaults. When a user sits on one screen with a field outstanding through a stretch of a live conversation, the SDK tells the agent — which can offer help — and your app hears GraineListener.onFriction. Most apps set nothing here; use it for screens only your app knows about — a terms page nobody is stuck on, a code-entry step where twenty seconds is already long.

What to expect — Every field is optional; null inherits the layer below.

FieldTypeDefaultMeaning
enabledBoolean?nullSet it to false to switch stuck-user detection off for the whole app.
idleAfterMsLong?nullHow long on one screen counts as stuck — 45 seconds by default; 0 or less switches the rule off.
cooldownMsLong?nullThe least time between two signals on the same screen — 60 seconds by default.
cooldownSecondsInt?nullHow long the agent waits between offers of help it makes unasked — 20 seconds by default.
maxPerSessionInt?nullHow many unasked offers of help the agent may make in one conversation — 3 by default.
openLauncherBoolean?nullPassed on in FrictionSignal.openLauncher for an app that opens its own launcher when the user looks stuck.
messageString?nullWhat the agent is told instead of the built-in sentence; {title}, {screen}, {seconds}, {field} and {error} are filled in.
requireOutstandingFieldBoolean?nulltrue by default: count only while a field is outstanding (invalid, or empty and not optional), from when it became so. false counts time on the screen at all.
screensMap<String, Screen>emptyMap()Rules for single screens, by the screen ID your app reports (FrictionConfig.Screen).

Its functions: fromJson — each with its parameters, what it returns, its errors and an example.

FrictionConfig.Screen

Use it in FrictionConfig.screens to change the rules on one screen.

data class Screen(val ignore: Boolean? = null, val idleAfterMs: Long? = null, val message: String? = null)

FieldTypeDefaultMeaning
ignoreBoolean?nulltrue turns stuck-user detection off on this screen — someone reading terms is not stuck.
idleAfterMsLong?nullHow long counts as stuck on this screen; 0 or less switches the rule off here.
messageString?nullWhat the agent is told on this screen instead of the sentence above it.

FrictionConfig.fromJson

Call it to read a friction document — the dashboard's format — into a FrictionConfig.

companion · fun fromJson(o: JSONObject?): FrictionConfig?

Reads enabled, idle (false, or { afterMs }), cooldownMs, nudge (cooldownSeconds, maxPerSession, openLauncher), message, requireOutstandingField and screens; unknown keys are dropped, and a number may be written as text.

ParameterTypeDefaultMeaning
oJSONObject?requiredThe document, such as SessionConfig.friction.

Returns — FrictionConfig? — null for null. Example value: FrictionConfig(enabled=true, idleAfterMs=30000, …).

Errors — none.

val dashboardRules = FrictionConfig.fromJson(GraineInApp.get().session?.friction)

FrictionRules

Read it to see the stuck-user rules actually in force — the defaults, the dashboard's and your app's together.

data class FrictionRules(…)

GraineInApp.frictionRules.

FieldTypeDefaultMeaning
enabledBooleantrueWhether stuck-user detection is on.
idleAfterMsLong?45_000How long on one screen counts as stuck; null when the rule is off.
cooldownMsLong60_000The least time between two signals on one screen.
cooldownSecondsInt20How long the agent waits between unasked offers of help.
maxPerSessionInt3How many unasked offers of help the agent may make in one conversation.
openLauncherBooleanfalsePassed on in each FrictionSignal.
messageString""The sentence the agent is told; empty uses the built-in one.
requireOutstandingFieldBooleantrueWhether only time with a field outstanding counts.
screensMap<String, FrictionConfig.Screen>emptyMap()The rules for single screens.

Its functions: forScreen — each with its parameters, what it returns, its errors and an example.

FrictionRules.forScreen

Call it to see the rules on one screen, with that screen's own rules applied.

fun forScreen(id: String?): ForScreen
ParameterTypeDefaultMeaning
idString?requiredThe screen's screen ID.

Returns — FrictionRules.ForScreen (FrictionRules.ForScreen). Example value: ForScreen(ignore=false, idleAfterMs=45000, message=).

Errors — none.

val onTerms = GraineInApp.get().frictionRules.forScreen("terms")

FrictionRules.ForScreen (type)

Read it for the rules on one screen.

data class ForScreen(val ignore: Boolean, val idleAfterMs: Long?, val message: String)

FieldTypeDefaultMeaning
ignoreBooleanrequiredtrue when nothing is detected on this screen.
idleAfterMsLong?requiredHow long counts as stuck here; null when the rule is off.
messageStringrequiredThe sentence the agent is told here; empty uses the built-in one.

FrictionSignal

Read it in onFriction to react when the user looks stuck — badge a control, count it.

data class FrictionSignal(val name: String, val screen: String, val detail: String, val data: Map<String, Any?>, val cooldownSeconds: Int, val maxPerSession: Int, val openLauncher: Boolean)

What to expect — At most once per visit to a screen, and never twice on one screen within the cooldown. The agent has already been told.

FieldTypeDefaultMeaning
nameStringrequiredWhat kind of signal: stalled_on_step.
screenStringrequiredThe screen ID the user is stuck on.
detailStringrequiredThe sentence the agent was given, such as “The customer has not touched the "Delivery address" screen for 45s. The "Postal code" field is showing: We don't deliver there yet.”.
dataMap<String, Any?>requiredseconds, field and error: how long, which field, and its error or status.
cooldownSecondsIntrequiredThe pacing for the agent's unasked offers of help.
maxPerSessionIntrequiredHow many unasked offers the agent may make in one conversation.
openLauncherBooleanrequiredYour rules' openLauncher: open your launcher when true, if you want.

GraineWidgetView

Place it in your layout to draw the agent's components outside the voice bar and the chat.

class GraineWidgetView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs), GraineListener

The component the agent put on screen, drawn by the SDK: a product list in five looks (gallery, cards, list, spotlight, compact), a plan comparison, an information card, chips for choices and quick actions, calendar slots — and, for a kind this build cannot draw, the agent's own fallback sentence. Where it goes follows one rule: a product list is the screen, a choice to finish slides up as a sheet, a fact sits inline as a card. A tap answers the agent with what was chosen and the sentence that is the user's turn; a component that names an action of yours runs it first. Closing is not answering: the agent is told quietly, and reopen() puts it back. GraineAgentBarView and GraineChatView hold one already.

What to expect — Main thread. Everything visual comes from the agent's design; a component's own style still wins over it, and the properties below override one thing each.

Constructor parameterTypeDefaultMeaning
contextContextrequiredThe activity it is shown in.
attrsAttributeSet?nullFrom a layout file; null in code.
PropertyTypeMeaning
var accent: String?String?Set it (#RRGGBB) to colour buttons and badges; null uses the component's own, then the design's.
var radius: Int?Int?Set it (dp) to round the corners; null uses the component's own, then its platform's, then the design's.
var scheme: String?String?Set it to force "light", "dark" or "auto"; null follows the design.
var theme: Theme?Theme?Set it to override single colours (GraineWidgetView.Theme); null uses the design.
var onChange: (() -> Unit)?(() -> Unit)?Set it to hear every change of what is drawn — to show your own “Show that again”, say.
val widget: GraineWidget?GraineWidget?Read it for the component on screen, if any (GraineWidget).
val answered: GraineAnswer?GraineAnswer?Read it for what the user last chose, kept as a line until the next component or a dismiss (GraineAnswer).
val closedWidget: GraineWidget?GraineWidget?Read it for the last component closed without an answer — reopen() puts it back.
val isLoading: BooleanBooleanRead it to know a component is on its way and its loader is showing.
var graine: GraineInApp?GraineInApp?Set it to draw another agent's components (GraineInApp.get("support")); the default agent unless set.

Its functions: present, presentPending, dismiss, reopen — each with its parameters, what it returns, its errors and an example.

GraineWidgetView.Theme

Pass it in GraineWidgetView.theme to override single colours of the components.

data class Theme(val surface: String? = null, val border: String? = null, val text: String? = null, val muted: String? = null, val onAccent: String? = null)

What to expect — Each a #RRGGBB colour; null keeps the design's.

FieldTypeDefaultMeaning
surfaceString?nullThe component's background.
borderString?nullIts outlines and separators.
textString?nullIts main text.
mutedString?nullIts secondary text.
onAccentString?nullText and icons drawn on the accent.

GraineWidgetView.present

Call it to draw a component you received some other way — a preview, a test — exactly as if the agent sent it.

fun present(frame: JSONObject)
ParameterTypeDefaultMeaning
frameJSONObjectrequiredThe component as the agent sends it: widget_id, kind, data, fallback_text, style, on_select.

Returns — nothing.

Errors — none.

widgetView.present(JSONObject("""{"widget_id":"w1","kind":"choices","data":{"options":["Morning","Evening"]}}"""))

GraineWidgetView.presentPending

Call it to show the loader for a component on its way — in a preview of your own.

fun presentPending(hint: JSONObject)
ParameterTypeDefaultMeaning
hintJSONObjectrequiredWhat is coming, as the agent announces it (its kind, for the loader's shape).

Returns — nothing.

Errors — none.

widgetView.presentPending(JSONObject().put("kind", "product_list"))

GraineWidgetView.dismiss

Call it to close the component without choosing — from your own close control.

fun dismiss()

The agent is told quietly, so it does not go on describing options nobody can see and does not speak up because someone closed a card. reopen() puts it back.

Parameters — none.

Returns — nothing.

Errors — none.

widgetView.dismiss()

GraineWidgetView.reopen

Call it from your own “Show that again” control.

fun reopen()

Puts back the last component the user closed, exactly as it was, without asking the agent again.

Parameters — none.

Returns — nothing.

Errors — none.

if (widgetView.closedWidget != null) widgetView.reopen()

GraineInApp.answerWidget

Call it from a component view of your own when the user picks something, to answer the agent on whichever conversation is live.

fun answerWidget(widgetId: String, data: Map<String, Any?>, summary: String)

It answers the voice-bar call while one is live, otherwise the chat while it is connected — the structured answer, then summary as the user's turn, exactly as the SDK's own component views send them. summary is the sentence the agent hears as the user's turn (“The gold plan, please”); without it the answer lands and the conversation does not move. data goes to your webhook. It clears pendingWidget. Main thread. GraineWidgetView answers for itself.

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe component's id (GraineWidget.id).
dataMap<String, Any?>requiredWhat was chosen, structured — for your webhooks and the agent.
summaryStringrequiredThe sentence that is the user's turn.

Returns — nothing.

Errors — none.

val graine = GraineInApp.get()
val widget = GraineWidget.adopt(graine.pendingWidget, graine.config.baseUrl) ?: return
graine.answerWidget(widget.id, mapOf("plan" to "gold"), "The gold plan, please")

GraineVoice.answerWidget

Call it from a component view of your own when the user picks something, to answer the agent.

fun answerWidget(widgetId: String, data: Map<String, Any?>, summary: String)

Only the voice-bar call; prefer GraineInApp.answerWidget, which answers whichever conversation is live — the call or the chat. summary is the sentence the agent hears as the user's turn (“Chose the gold plan”); without it the tap lands and the conversation does not move. GraineWidgetView does this itself.

ParameterTypeDefaultMeaning
widgetIdStringrequiredThe component's id (GraineWidget.id).
dataMap<String, Any?>requiredWhat was chosen, structured — for your webhooks and the agent.
summaryStringrequiredThe sentence that is the user's turn.

Returns — nothing.

Errors — none.

GraineInApp.get().voice.answerWidget(widget.id, mapOf("plan" to "gold"), "Chose the gold plan")

GraineWidget

Read it to draw a component the agent sent, in a view of your own.

data class GraineWidget(val id: String, val kind: String, val data: JSONObject, val fallbackText: String, val style: WidgetStyle, val onSelect: WidgetSelectAction)

Carried by GraineWidgetView.widget and GraineChatTurn.component (where it is called GraineComponent).

What to expect — Pictures given as root-relative addresses are already made absolute. A kind you do not know is a card with fallbackText.

FieldTypeDefaultMeaning
idStringrequiredThe component's ID: what an answer refers to.
kindStringrequiredWhat it is: product_list, plan_comparison, choices, quick_actions, calendar_slots, info_card — or a kind the agent declares.
dataJSONObjectrequiredWhat to draw: the items, choices or slots.
fallbackTextStringrequiredWhat the agent would have said instead — draw it for a kind you cannot.
styleWidgetStylerequiredHow the dashboard asked for it to look (WidgetStyle), already checked.
onSelectWidgetSelectActionrequiredWhat your app does on a tap, beyond telling the agent (WidgetSelectAction).

Its functions: adopt — each with its parameters, what it returns, its errors and an example.

GraineWidget.adopt

Call it to turn the component JSON from onWidget into a GraineWidget.

@JvmStatic
companion · fun adopt(raw: JSONObject?, baseUrl: String): GraineWidget?
ParameterTypeDefaultMeaning
rawJSONObject?requiredThe component as GraineListener.onWidget hands it over.
baseUrlStringrequiredYour GraineConfig.baseUrl, to make root-relative pictures absolute.

Returns — GraineWidget? — null when it names no widget_id (a component that cannot be answered is not one to draw). Example value: GraineWidget(id=w1, kind=choices, …).

Errors — none.

val widget = GraineWidget.adopt(json, GraineInApp.get().config.baseUrl) ?: return

GraineAnswer

Read it for what the user chose on a component.

data class GraineAnswer(val widget: GraineWidget, val data: Map<String, Any?>, val summary: String)

GraineWidgetView.answered.

FieldTypeDefaultMeaning
widgetGraineWidgetrequiredThe component that was answered.
dataMap<String, Any?>requiredWhat was chosen, structured — for your webhooks.
summaryStringrequiredThe sentence the agent heard as the user's turn.

WidgetAnswer

Read it for an answer to a component: what was chosen and the sentence the agent hears.

data class WidgetAnswer(val data: Map<String, Any?>, val summary: String)

FieldTypeDefaultMeaning
dataMap<String, Any?>requiredWhat was chosen, structured.
summaryStringrequiredThe sentence the agent hears as the user's turn.

WidgetStyle

Read it to draw a component in the look the dashboard chose for it.

data class WidgetStyle(…)

Chosen on the component in the dashboard and carried with it. Only known words survive; null means the default. The words are the dashboard's own, so a view of your own compares against the same words.

FieldTypeDefaultMeaning
presetString?nullThe overall look: gallery, cards, list, spotlight or compact (cards when none is chosen).
imageSizeString?nullHow big pictures are: small, medium or large.
densityString?nullHow much room rows get: comfortable or compact.
buttonString?nullHow a row's button looks: filled, outline or row (the whole row is the button).
factsBoolean?nullWhether a row's extra facts are shown.
columnsInt?null1 or 2 columns, for the gallery and cards looks.
surfaceString?nullWhere it goes: auto, screen, sheet or card.
radiusInt?nullIts corner rounding, 0–24 dp.
accentString?nullIts own accent colour, #RRGGBB.
platformString?nullWhich phone's shapes to use: auto, ios, material or oneui.
selectString?nullWhether the user picks one or many.
maxSelectInt?nullHow many may be picked with many, 2–8.

WidgetSelectAction

Read it to know what your app should do when the user taps a component's item.

data class WidgetSelectAction(val path: String? = null, val action: String? = null, val args: Map<String, String>? = null, val say: String? = null)

Beyond telling the agent: one of your registered actions, or a screen path for your open_screen action, with arguments filled from the tapped item ({id}, {title}, …).

FieldTypeDefaultMeaning
pathString?nullA screen in your app to open, run through your open_screen action.
actionString?nullThe registered action to run.
argsMap<String, String>?nullIts arguments; {name} placeholders are filled from the tapped item.
sayString?nullWhat the agent hears about the tap.
PropertyTypeMeaning
val isEmpty: BooleanBooleanRead it to know the component names no action and no path: a tap only answers the agent.

Calls the user places

Native calls, like WhatsApp: the user calls the agent with startCall, or the agent rings the user (Calls Graine rings). The call runs in your app's own process, with the platform's microphone and echo canceller; it rings with a call notification and a full-screen call screen over the lock screen, keeps going with the screen off in a microphone foreground service, and reports what became of it with the call's ID for your server. One call at a time, and never alongside a voice-bar call: starting one ends the other.

What a call does.

  • Its audio travels over the WebSocket — or over WebRTC, prepared at the tap, when the app includes the WebRTC module (Adding WebRTC calls); a call WebRTC cannot set up carries on over the WebSocket inside the same call. The timings below are the WebSocket's; over WebRTC, a network drop is retried once, then the call ends ENDED with FAILED and “The call's connection was lost.”.
  • It starts at once on the agent's settings already kept on the device (this launch's, or the last ones), and refreshes them behind the call instead of waiting for the network first.
  • Opening the call has 15 seconds: then it fails — GraineCallOutcome FAILED, error “The call could not connect within 15 seconds.”, and the call screen says “Call failed”.
  • Once connected, nothing at all arriving for 50 seconds — two 25-second keep-alive periods, which a live connection always answers — ends the call: ENDED with endReason FAILED and error “Nothing arrived for 50 seconds: the connection was lost.”; the call screen says “Call failed”.
  • Sending never holds the call up on a slow network: at most 300 ms of the user's audio waits to be sent, the oldest dropped first when the line falls behind; nothing else is ever dropped.
  • Hanging up lets go of the microphone at once, and a close the network cannot carry is forced within about a second.
  • The prompt's variables travel inside the call's one-time connection pass, not in the connection's address; a direct connection, used when no pass can be had, still carries them in its address.
  • A microphone that stops mid-call is rebuilt once; if it cannot be, the call ends ENDED with FAILED and “The microphone stopped working.”. If the phone takes the call's audio (a phone call ringing, another app's call or player), the call holds — nothing is sent or played — until it comes back; answering a phone call during the hold ends the agent's call (ENDED, LOCAL).

GraineCalls

Use it for native calls: configure them, place one, control it, and read its state.

class GraineCalls internal constructor(…)

GraineInApp.get().calls — the app's one call engine, whichever agent it is reached through: startCall names the agent a call is with, and a ring Graine sends rings with the agent it is for, in its design. Configure it once in Application.onCreate; hand Firebase's token and messages to the companion functions; place calls from an activity in front.

What to expect — Main thread, except handlePush. Listeners are called on the main thread.

PropertyTypeMeaning
var config: GraineCallsConfigGraineCallsConfigRead it for how calls look and ring now (GraineCallsConfig). Read-only; set it with configure.
var state: GraineCallStateGraineCallStateRead it to know where the call is (GraineCallState); changes reach onCallState. Read-only.
var call: GraineCall?GraineCall?Read it for the call in progress (GraineCall), or null. Read-only.
var muted: BooleanBooleanRead it to show the mute control: the call goes on, nothing the user says is sent. Read-only.
var speaker: BooleanBooleanRead it to show the loudspeaker control: on, or the call is on a headset (if one is connected) or the earpiece. Read-only.
var agentSpeaking: BooleanBooleanRead it to animate the agent's avatar while its voice is heard. Read-only.
var caption: Triple<Role, String, Boolean>?Triple<Role, String, Boolean>?Read it for the last line on the call — who, the words, still arriving — for a screen that opens mid-call. Read-only.
var connectedAt: LongLongRead it to show the call's length: when it connected, in SystemClock.elapsedRealtime() milliseconds (which keep counting in deep sleep); 0 before. Read-only.
val callAgent: GraineInApp?GraineInApp?Read it for the agent the call in progress is with — for a call screen of your own to draw in its design; null with no call.
val transport: String?String?Read it for how the call in progress carries its audio: "webrtc" or "websocket"; null with no call. Decided when the call starts — at the tap, or when its ring arrives — and final once it is ACTIVE: read it then. Calls in both directions (Adding WebRTC calls).
val transportFallbackReason: String?String?Read it for why a call that would have used WebRTC runs on the WebSocket instead: mint_failed, register_failed, no_response, no_answer, ice_timeout, error, dashboard or unavailable (each explained); null for every other call, and with no call. Final once the call is ACTIVE.
companion · const val ACTION_ANSWERString"ai.graine.inapp.action.ANSWER_CALL": your own call screen was opened to answer (the notification's Answer).
companion · const val ACTION_SHOWString"ai.graine.inapp.action.SHOW_CALL": your call screen was opened to show the call.
companion · const val ACTION_DECLINEString"ai.graine.inapp.action.DECLINE_CALL": the ring notification's Decline, handled by the SDK.
companion · const val ACTION_HANGUPString"ai.graine.inapp.action.HANG_UP": the ongoing-call notification's Hang up, handled by the SDK.
companion · const val ACTION_MISSEDString"ai.graine.inapp.action.SHOW_MISSED_CALL": your call screen was opened on a missed call.
companion · const val ACTION_CALL_BACKString"ai.graine.inapp.action.CALL_BACK": your call screen was opened to call the agent back after a missed call.
companion · const val EXTRA_CALL_IDString"ai.graine.inapp.extra.CALL_ID": which call an intent is about.
companion · const val EXTRA_AGENTString"ai.graine.inapp.extra.AGENT": which agent (its init name) that call is with.
companion · const val EXTRA_CALLER_NAMEString"ai.graine.inapp.extra.CALLER_NAME": who a missed call was from, for the missed screen.
companion · const val EXTRA_REASONString"ai.graine.inapp.extra.REASON": why a missed call was placed, carried into the call back.
companion · const val MISSED_REASON = "missed_call"StringWhat a call back tells the agent (as call_reason) when the missed call had no reason of its own.
companion · const val MIC_REQUEST = 0x6E7BIntThe request code the SDK uses for the call's microphone prompt — what onRequestPermissionsResult recognises.
val callPermissions: GraineCallPermissionsGraineCallPermissionsRead it for the call permissions the phone holds now (GraineCalls.callPermissions).
val callSetupProblems: List<GraineCallSetupProblem>List<GraineCallSetupProblem>Read it for what the app's setup still lacks for calls, each with its fix (GraineCalls.callSetupProblems).

Its functions: configure, startCall, end, setMuted, setSpeaker, onRequestPermissionsResult, onNewToken, handlePush, answer, decline, callBack, addListener, removeListener, requestCallPermissions, openCallSettings — each with its parameters, what it returns, its errors and an example.

GraineCalls.configure

Call it once in Application.onCreate, after GraineInApp.init, to set how calls ring and what they show.

fun configure(config: GraineCallsConfig)

Creates the notification channels (Incoming calls, Incoming calls (app ringtone), Missed calls, Ongoing calls). Configure before a push can arrive.

ParameterTypeDefaultMeaning
configGraineCallsConfigrequiredThe name, icon, ringtone, call screen, ring time and how calls carry their audio (GraineCallsConfig).

Returns — nothing.

Errors — none.

GraineInApp.get().calls.configure(GraineCallsConfig(displayName = "Acme", smallIcon = R.drawable.ic_stat_call))

GraineCallsConfig

Pass it to calls.configure to set how calls ring and what they show.

data class GraineCallsConfig @JvmOverloads constructor(…)

What to expect — Set once, at launch. The ring's look and words otherwise come from the agent's design (Garage → Call screen).

FieldTypeDefaultMeaning
displayNameString"Assistant"The name on the ring and the call screen when the push names no caller (a call Graine rang uses the agent's call name).
smallIconInt0A monochrome drawable for the notifications' status-bar icon; 0 uses the platform's call icon.
ringtoneUri?nullYour own ring; it wins over the design's ring. null uses the design's (the phone's ringtone, or the brand's Graine or Soft tone).
callActivityClass<out Activity>?nullYour own call screen; null uses the SDK's GraineCallActivity. Yours draws calls.state and calls the same functions, and forwards its permission answers to calls.onRequestPermissionsResult.
ringTimeoutMsLong30_000How long a call from your own server rings before it is missed. A call Graine rang rings for the agent's own ring time (GraineSchedulingRules.ringTimeoutSeconds).
callTransportGraineCallTransportGraineCallTransport.AUTOHow calls carry their audio, in both directions (GraineCallTransport): AUTO, WebRTC when the app includes ai.graine:inapp-android-rtc and the dashboard allows it, else the WebSocket; WEBSOCKET; or WEBRTC. A call WebRTC cannot set up carries on over the WebSocket within the same call.

GraineCallTransport

Pass it in GraineCallsConfig.callTransport to choose how native calls carry their audio.

enum class GraineCallTransport { AUTO, WEBSOCKET, WEBRTC }

For calls in both directions — the ones the user places with startCall, and the ones Graine rings and the user answers. WebRTC needs the module ai.graine:inapp-android-rtc in the app (Adding WebRTC calls); without it every call uses the WebSocket, as before.

What to expect — Decided when a call starts — at the tap, or when its ring arrives — and the call is prepared there. The agent's In-app call audio set to Standard in the dashboard puts calls on the WebSocket whatever you chose. A call WebRTC cannot set up before any audio flows carries on over the WebSocket inside the same call — no second ring, no error; once audio flows it never moves. calls.transport says which one carried the call, and calls.transportFallbackReason why it is not WebRTC.

CaseMeaning
AUTOThe default: WebRTC when the module is in the app and the dashboard allows it, else the WebSocket.
WEBSOCKETAlways the WebSocket, even with the module in the app.
WEBRTCWebRTC wherever it can carry the call; the WebSocket only when the module is missing or WebRTC cannot set up this call.

Why a call is on the WebSocket — calls.transportFallbackReason, set only for a call that would have used WebRTC and runs on the WebSocket; null for every other call. Read it once the call is ACTIVE.

ValueMeaning
mint_failedThe call's WebRTC session could not be set up: refused, rate-limited, or unreachable.
register_failedGraine's media service did not accept the connection, or never answered it.
no_responseThe call got no reply at all.
no_answerReplies came; the answer did not.
ice_timeoutAnswered, but the audio path never connected — a network that blocks WebRTC.
errorAnything else before the audio flowed.
dashboardThe agent's In-app call audio is Standard.
unavailableThe app asked for WEBRTC, and this build has no WebRTC module.

GraineCalls.startCall

Call it from a “Call us” button to let the user call the agent.

@JvmOverloads
fun startCall(activity: Activity, reason: String? = null, variables: Map<String, Any?> = emptyMap(), agent: String = GraineInApp.DEFAULT): String?

The call screen opens and the call connects — at once, on the settings already kept on the device. reason becomes the prompt's call_reason (and who_called is customer); variables are this call's own prompt variables, over the SDK's, masked on the device. It ends any voice-bar call first, and asks for the microphone from the call screen if the app does not hold it (no answer within 60 seconds fails the call).

ParameterTypeDefaultMeaning
activityActivityrequiredThe activity in front — the microphone service may only start from there.
reasonString?nullWhy the user is calling — the agent hears it as call_reason. Blank is none.
variablesMap<String, Any?>emptyMap()Prompt variables for this call only.
agentStringGraineInApp.DEFAULTWhich agent to call, by the name init was given.

Returns — String? — the call's ID (call_ and a UUID), which every outcome carries; null when a call is already live or no agent has that name (the log says so). Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — IllegalStateException when called off the main thread. A call that cannot connect is a FAILED outcome, not an exception.

val callId = GraineInApp.get().calls.startCall(this, reason = "billing_help", agent = "support")

GraineCalls.end

Call it from your own hang-up control.

fun end()

Hangs up (ENDED, LOCAL) — or, while the call rings, declines it. The microphone is released at once.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().calls.end()

GraineCalls.setMuted

Call it from your own mute control.

fun setMuted(value: Boolean)

Stops sending the user's voice without ending the call; listeners hear onCallControls.

ParameterTypeDefaultMeaning
valueBooleanrequiredtrue to stop sending the user's voice, false to send it again.

Returns — nothing.

Errors — none.

GraineInApp.get().calls.setMuted(true)

GraineCalls.setSpeaker

Call it from your own loudspeaker control.

fun setSpeaker(on: Boolean)

Off, the call goes back to a headset if one is connected (Bluetooth first), else the earpiece; the choice is made again whenever a headset is connected or removed.

ParameterTypeDefaultMeaning
onBooleanrequiredtrue for the loudspeaker.

Returns — nothing.

Errors — none.

GraineInApp.get().calls.setSpeaker(true)

GraineCalls.onRequestPermissionsResult

Call it from your own call screen's onRequestPermissionsResult, so a call waiting for the microphone can go ahead.

fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray): Boolean

The SDK asks for the microphone from the call screen when it does not have it; the SDK's GraineCallActivity forwards by itself. A refusal fails the call (“The microphone permission was denied.”).

ParameterTypeDefaultMeaning
requestCodeIntrequiredAs Android passes it to your activity.
grantResultsIntArrayrequiredAs Android passes them to your activity.

Returns — Boolean — true when the request was the SDK's (MIC_REQUEST). Example value: true.

Errors — none.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    if (!GraineInApp.get().calls.onRequestPermissionsResult(requestCode, grantResults))
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

GraineCall

Read it for the call in progress: who placed it, why, and its ID.

data class GraineCall @JvmOverloads constructor(val callId: String, val direction: Direction, val callerName: String?, val reason: String?, val variables: Map<String, String>, val hosted: Boolean = false)

calls.call, and the argument of onIncomingCall and onCallState.

What to expect — It carries only what the ring shows: nothing personal.

FieldTypeDefaultMeaning
callIdStringrequiredThe ID every outcome carries: from the push for an incoming call; made by startCall for one the user places.
directionDirectionrequiredWho placed it (GraineCall.Direction).
callerNameString?requiredRead it for the name on the ring: the push's caller_name when it has one, otherwise GraineCallsConfig.displayName.
reasonString?requiredRead it for why the call was placed: the push's reason, or the one passed to startCall — the prompt sees it as call_reason.
variablesMap<String, String>requiredThe call's own prompt variables: who_called, call_reason and the push's variables.
hostedBooleanfalsetrue when Graine rang it — a call booked by the agent, the user or your team; the ring says “Scheduled call”, and Graine hears the outcome too.

GraineCall.Direction

Read it to know who placed the call.

enum class Direction { INCOMING, OUTGOING }

CaseMeaning
INCOMINGThe agent rang the user (a push).
OUTGOINGThe user called the agent (startCall).

GraineCallState

Read it to know where the one native call is.

enum class GraineCallState { IDLE, RINGING, CONNECTING, ACTIVE, ENDED }

What to expect — IDLE → RINGING (an incoming call) or CONNECTING (a call placed) → ACTIVE → ENDED, then IDLE again once reported. One call at a time: a second ring while a call is live is reported MISSED (busy).

CaseMeaning
IDLENo call.
RINGINGThe agent is calling the user (incoming only); show Answer and Decline.
CONNECTINGThe call was answered, or placed by the user, and is connecting.
ACTIVEThe agent and the user can hear each other.
ENDEDOver — reported once, then IDLE.

GraineCallOutcome

Read it to send what became of a call to your server.

data class GraineCallOutcome @JvmOverloads constructor(…)

Reported once per call, with the call's ID, to GraineCallListener.onCallOutcome.

What to expect — ANSWERED, then ENDED with who ended it (and durationSeconds) or FAILED with why; or DECLINED, MISSED (busy when another call was live) or FAILED alone. hosted means Graine rang it, and the SDK has already told Graine. Do not rely on its order relative to the call's state changes.

FieldTypeDefaultMeaning
kindKindrequiredWhat became of the call (GraineCallOutcome.Kind).
endReasonEndReason?nullWho ended a call that connected (GraineCallOutcome.EndReason) — with ENDED; null otherwise.
errorString?nullWhy, in a sentence: why it never connected (with FAILED) or why the connection broke (with ENDED and FAILED) — for example “The call could not connect within 15 seconds.” or “Nothing arrived for 50 seconds: the connection was lost.”; null otherwise.
callIdString""The call's ID — the same one onCallOutcome carries beside it.
durationSecondsInt?nullHow long a call that connected lasted, in whole seconds — with ENDED; null otherwise.
hostedBooleanfalsetrue when Graine rang it (a call booked with the agent): Graine hears this outcome too.
busyBooleanfalsetrue for a MISSED ring refused because another call was live.
PropertyTypeMeaning
val wire: StringStringOne word for your server: answered, declined, missed, ended or failed.
val outcome: StringStringThe same word as wire.
val reason: String?String?Why, in one word or sentence: who ended a call (local, remote, failed) with ENDED, the error with FAILED, busy for a refused ring; null otherwise.
companion · val ANSWERED: GraineCallOutcomeGraineCallOutcomeAn ANSWERED outcome, for tests and your own call screen.
companion · val DECLINED: GraineCallOutcomeGraineCallOutcomeA DECLINED outcome.
companion · val MISSED: GraineCallOutcomeGraineCallOutcomeA MISSED outcome.

Its functions: ended, failed — each with its parameters, what it returns, its errors and an example.

GraineCallOutcome.Kind

Read it to branch on what became of a call.

enum class Kind(val wire: String) { ANSWERED("answered"), DECLINED("declined"), MISSED("missed"), ENDED("ended"), FAILED("failed") }

PropertyTypeMeaning
val wire: StringStringThe word for your server.
CaseMeaning
ANSWEREDThe user picked up an incoming call; ENDED or FAILED follows.
DECLINEDThe user declined it.
MISSEDNobody answered within the ring time — or another call was live (busy).
ENDEDThe call connected and has now finished; endReason tells you which side ended it.
FAILEDThe call never connected: error says why.

GraineCallOutcome.EndReason

Read it to know who ended a call that connected.

enum class EndReason(val wire: String) { LOCAL("local"), REMOTE("remote"), FAILED("failed") }

PropertyTypeMeaning
val wire: StringStringThe word for your server.
CaseMeaning
LOCALThis app hung up — the user, end(), or a phone call answered during a hold.
REMOTEThe agent hung up.
FAILEDThe connection broke mid-call, nothing arrived for 50 seconds, or the microphone stopped; error says which.

GraineCallOutcome.ended

Call it to make an ENDED outcome — in a test, or for a call screen of your own.

@JvmStatic @JvmOverloads
companion · fun ended(reason: EndReason, error: String? = null): GraineCallOutcome
ParameterTypeDefaultMeaning
reasonEndReasonrequiredWho ended it.
errorString?nullWhy, for EndReason.FAILED.

Returns — GraineCallOutcome with kind ENDED. Example value: GraineCallOutcome(kind=ENDED, endReason=REMOTE, error=null, …).

Errors — none.

val outcome = GraineCallOutcome.ended(GraineCallOutcome.EndReason.REMOTE)

GraineCallOutcome.failed

Call it to make a FAILED outcome — in a test, or for a call screen of your own.

@JvmStatic
companion · fun failed(error: String): GraineCallOutcome
ParameterTypeDefaultMeaning
errorStringrequiredWhy the call never connected.

Returns — GraineCallOutcome with kind FAILED. Example value: GraineCallOutcome(kind=FAILED, error=The call could not connect within 15 seconds., …).

Errors — none.

val outcome = GraineCallOutcome.failed("The call could not connect within 15 seconds.")

GraineCallActivity

Leave it as your call screen, or name your own in GraineCallsConfig.callActivity.

class GraineCallActivity : Activity(), GraineCallListener

The SDK's call screen — over the lock screen, for the ring and the call — drawn in the brand's design from the agent's call settings (Garage → Call screen), in six moments: a call Graine rang ringing (“Scheduled call”); any other call ringing, with Decline and Answer and a halo growing from the avatar; connecting; on the call, with the timer (or Listening / Speaking), the caption and the design's controls in its order; ended (“Call ended”, or “Call failed”, “Declined”), closing in a second; and missed, with Close and Call back, closing in 30 seconds. The last design is kept on the device, so a push that wakes a closed app draws the same screen.

What to expect — Declared in the library's manifest; your app does not start it. Closing it never ends the call — the ongoing notification brings it back.

GraineCallService

Nothing to call: it is what keeps a call going with the screen off.

class GraineCallService : Service()

The call's foreground service — a microphone foreground service — so the call keeps its microphone and its process with the screen off and no activity open, with the ongoing-call notification (and Hang up) as the way back to it. Started from the call screen, and stopped with the call.

What to expect — Declared in the library's manifest and started by the SDK; your app does not start or call it. An app without calls removes it (Requirements).

GraineCallReceiver

Nothing to call: it handles the notifications' Decline and Hang up.

class GraineCallReceiver : BroadcastReceiver()

What to expect — Declared in the library's manifest; your app does not call it.

Permissions and the setup check

What a call needs from the phone, and from your app's setup, in one place. The SDK never asks on its own at launch: it asks for the microphone on the call screen when a call starts without it, and requestCallPermissions asks earlier, when your app chooses — from a transparent screen of the SDK's own, so no activity forwards a permission result. callSetupProblems says what your setup still lacks; each problem is also logged once in logcat (Calls setup (code): …) from configure on. A problem is never an exception: calls still work where they can.

GraineCalls.callPermissions

Read it to see which call permissions the phone holds now.

val callPermissions: GraineCallPermissions

The microphone; the ring's notifications (Android 13+ asks for POST_NOTIFICATIONS; before 13 they are on unless the customer turned them off in Settings); and the full-screen ring (Android 14+: NotificationManager.canUseFullScreenIntent(), Settings only; before 14 granted with the app; before 10 not needed). What each ask came to is kept on the device, so a permission refused for good reads BLOCKED. Any thread.

Returns — a GraineCallPermissions. Read it again in onResume: the customer may have changed Settings.

Errors — none.

val p = GraineInApp.get().calls.callPermissions
if (p.notifications != GraineCallPermissionStatus.GRANTED) showRingsOffBanner()

GraineCalls.requestCallPermissions

Call it at a moment you choose — onboarding, a "Call me" screen — to ask for what a call needs.

@JvmOverloads
fun requestCallPermissions(activity: Activity? = null, done: GraineCallPermissionsCallback)

Asks for the call permissions the system's dialog can still ask for — the microphone, and notifications on Android 13+ — from a transparent screen of the SDK's own over activity (by default the app's screen in front), then answers them as they stand after, on the main thread. Nothing to ask, or no screen in front: the answer comes at once. One ask at a time — a second request waits for the first one's answer. The full-screen ring has no dialog: offer openCallSettings(GraineCallPermission.FULL_SCREEN). The SDK never asks on its own at launch.

ParameterTypeDefaultMeaning
activityActivity?nullThe screen to ask over; null, the app's screen in front.
doneGraineCallPermissionsCallbackrequiredGets the permissions after the customer answers.

Returns — nothing; done gets a GraineCallPermissions.

Errors — none.

GraineInApp.get().calls.requestCallPermissions(this) { p ->
    if (p.fullScreen == GraineCallPermissionStatus.BLOCKED) GraineInApp.get().calls.openCallSettings(GraineCallPermission.FULL_SCREEN)
}

GraineCalls.openCallSettings

Call it to send the customer to the Settings page that changes a call permission.

@JvmOverloads
fun openCallSettings(item: GraineCallPermission, activity: Activity? = null): Boolean

Opens the full-screen ring's own page on Android 14+ (Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT), the app's notification settings for notifications, or the app's page (Permissions → Microphone) for the microphone — and the app's page whenever the first cannot open.

ParameterTypeDefaultMeaning
itemGraineCallPermissionrequiredThe permission the customer should change.
activityActivity?nullWhere to open it from; null, the app's screen in front (else a new task).

Returns — Boolean: true when a page opened. Example value: true.

Errors — none.

GraineInApp.get().calls.openCallSettings(GraineCallPermission.FULL_SCREEN)

GraineCalls.callSetupProblems

Read it to learn what your app's setup still lacks for calls, in plain words.

val callSetupProblems: List<GraineCallSetupProblem>

What the app's setup still lacks for calls, read now — each a stable code and a message with the exact fix; empty when nothing is missing. Any thread.

CodeWhen
manifest_entry_missingThe merged manifest lacks a call entry — a permission (RECORD_AUDIO, POST_NOTIFICATIONS, USE_FULL_SCREEN_INTENT, FOREGROUND_SERVICE_MICROPHONE, …), the call screen, GraineCallPermissionActivity, GraineCallService (type microphone) or GraineCallReceiver. The message names them.
no_push_tokenNo Firebase token reached the SDK 15 seconds after configure, on this launch or an earlier one: calls can't ring. The message says why — Firebase Cloud Messaging not in the app, Firebase not set up (google-services.json, the Google services plugin), its automatic token creation turned off, or the reason Firebase gave — or, for an app with its own messaging service, that no token was handed to GraineCalls.onNewToken.
notifications_not_allowedNotifications are not allowed (or turned off): a call can't ring while the app is closed or in the background.
full_screen_not_allowedThe full-screen ring is not allowed: a locked phone rings with a heads-up notification instead.
microphone_blockedThe microphone is refused for good: only Settings can let calls hear the customer.
hosted_ringing_offA session is known and no configured agent's workspace has push credentials for Android: Graine can't ring this phone.

Returns — List<GraineCallSetupProblem>.

Errors — none.

GraineInApp.get().calls.callSetupProblems.forEach { Log.w("Calls", "${it.code}: ${it.message}") }

GraineCallPermission

Use it to name a call permission.

enum class GraineCallPermission(val wire: String) — a permission a WhatsApp-style call needs. wire is the word every Graine SDK uses.

What to expect — Three permissions, the same on every SDK.

CaseMeaning
MICROPHONE"microphone" — the customer's voice.
NOTIFICATIONS"notifications" — a call that rings while the app is closed or in the background.
FULL_SCREEN"fullScreen" — a ring that fills a locked screen.
PropertyTypeMeaning
val wire: StringStringThe word every Graine SDK uses for it.
companion object { @JvmStatic fun of(word: String?): GraineCallPermission? }

GraineCallPermission.of(word) reads one by its word — microphone, notifications, fullScreen (full_screen and any case too); null for any other.

GraineCallPermissionStatus

Read it to tell whether a call permission is allowed, can still be asked for, or only Settings can change it.

enum class GraineCallPermissionStatus(val wire: String) — where a call permission stands.

What to expect — GRANTED and NOT_NEEDED need nothing; DENIED means requestCallPermissions can ask; BLOCKED means only Settings can change it — offer openCallSettings.

CaseMeaning
GRANTED"granted" — allowed.
DENIED"denied" — not allowed yet: asking may change it.
BLOCKED"blocked" — refused for good, or only allowed in Settings (the full-screen ring on Android 14+, notifications turned off).
NOT_NEEDED"notNeeded" — this phone needs no such permission (the full-screen ring before Android 10).
PropertyTypeMeaning
val wire: StringStringThe word every Graine SDK uses for it.
val isSatisfied: BooleanBooleanGranted, or nothing to grant.

GraineCallPermissions

Read it to see every call permission at once.

data class GraineCallPermissions(val microphone: GraineCallPermissionStatus, val notifications: GraineCallPermissionStatus, val fullScreen: GraineCallPermissionStatus)

What to expect — A snapshot: read callPermissions again for the latest. allGranted is the one check before offering calls.

PropertyTypeMeaning
val microphoneGraineCallPermissionStatusThe microphone.
val notificationsGraineCallPermissionStatusThe ring's notifications.
val fullScreenGraineCallPermissionStatusThe full-screen ring.
val allGranted: BooleanBooleanEvery permission is granted or not needed: calls can ring, fill the locked screen and connect.

permissions[GraineCallPermission.MICROPHONE] reads one by name (operator fun get).

GraineCallPermissionsCallback

Implement it (or pass a lambda) to hear the permissions once asking is over.

fun interface GraineCallPermissionsCallback { fun onResult(permissions: GraineCallPermissions) }

onResult runs once, on the main thread, with the permissions as they stand after the customer answered. From Java: calls.requestCallPermissions(activity, permissions -> { … }).

GraineCallSetupProblem

Read it to show or log what your app's setup still lacks.

data class GraineCallSetupProblem(val code: String, val message: String) — one thing the app's setup still lacks for calls, from callSetupProblems.

What to expect — The code is stable and the same on every SDK (the list is under GraineCalls.callSetupProblems); the message is a sentence for the developer, with the exact fix.

PropertyTypeMeaning
val code: StringStringStable: act on it, or count it.
val message: StringStringWhat is missing and how to fix it, in plain words.

Calls Graine rings

The agent rings the user: a call the agent booked in a conversation, one the user booked (Booking with the SDK's sheet), one your team booked — or one your own server sends. With your push credentials uploaded in Workspace settings → In-app calling and Firebase Cloud Messaging in the app, nothing else is needed — the SDK's own messaging service (ai.graine.inapp.GraineMessagingService, merged from its manifest) receives every new token and every message, and the SDK reads the current token when it starts: the SDK registers the device with every agent's key (on every new token, when what it says changes — another user, a new time zone, a new app version — and otherwise at most once a day), rings each call Graine sends exactly as your own pushes ring, and reports what became of it. The ring is a call-style notification that keeps ringing until answered, declined or timed out, with a full-screen call screen; with your app in front, the call screen opens at once. A ring nobody answered leaves a Missed calls notification and the call screen on “Missed call”, both with Call back. With the WebRTC module in the app (Adding WebRTC calls), a call Graine rang carries its audio over WebRTC as a call the user places does — prepared while the phone rings — and calls.transport and calls.transportFallbackReason say how it went.

The push your own server sends (an FCM data message, nothing personal): type: "agent_call", call_id (required — every outcome carries it back), reason (the prompt's call_reason), caller_name (what the ring shows), variables (a JSON object as a string: more prompt variables), and agent_id (which of the app's agents the call is with; with several agents and none named, the default agent rings, and the log says so).

Already have your own FirebaseMessagingService? Android delivers each message to one service, and yours outranks the SDK's (declared at priority -1): hand Graine's over, two lines. A debug build logs it when your service receives the messages instead of the SDK's.

class Messaging : FirebaseMessagingService() {
    override fun onNewToken(token: String) = GraineCalls.onNewToken(this, token)
    override fun onMessageReceived(message: RemoteMessage) {
        if (GraineCalls.handlePush(this, message.data)) return
        // …your own messages
    }
}

GraineCalls.onNewToken

Call it from a FirebaseMessagingService of your own, in onNewToken — one line — so Graine can ring this install. Without one, the SDK's own messaging service receives each new token, and the SDK reads the current one when it starts.

@JvmStatic
companion · fun onNewToken(context: Context, token: String)

The token is kept on the device and registered with Graine — by every agent the app runs, each with its own key — when the workspace has Graine ring its users (Workspace settings → In-app calling); with no push credentials uploaded there, nothing is sent. Safe before GraineInApp.init.

ParameterTypeDefaultMeaning
contextContextrequiredThe messaging service (any context of your app).
tokenStringrequiredThe new Firebase Cloud Messaging token; a blank one is ignored.

Returns — nothing.

Errors — none. A registration that fails is tried again later; the log says why.

override fun onNewToken(token: String) = GraineCalls.onNewToken(this, token)

GraineCalls.handlePush

Call it first thing in the onMessageReceived of a FirebaseMessagingService of your own, so a call rings. Without one, the SDK's own messaging service calls it for every message.

@JvmStatic
companion · fun handlePush(context: Context, data: Map<String, String>): Boolean
fun handlePush(data: Map<String, String>): Boolean

When the message is a call — Graine's own ring, or one your server sent — the SDK rings it (or reports it MISSED as busy when another call is live; a repeat of the call in progress is ignored) and the answer is true; your own messages answer false. Safe on the messaging service's thread. The companion form works before the SDK is made (it rings once GraineInApp.init has run in Application.onCreate); the instance form is GraineInApp.get().calls.handlePush(data).

ParameterTypeDefaultMeaning
contextContextrequiredThe messaging service (the companion form only).
dataMap<String, String>requiredThe message's data, exactly as the messaging service received it.

Returns — Boolean — true when it was a call (ringing, refused as busy, or malformed) and you should do nothing more with it. Example value: true.

Errors — none. A malformed call (no call_id) is answered true and logged.

override fun onMessageReceived(message: RemoteMessage) {
    if (GraineCalls.handlePush(this, message.data)) return
}

GraineCalls.answer

Call it from a call screen of your own to pick up the ringing call.

fun answer()

Reported ANSWERED; the call connects, after asking for the microphone if needed. The SDK's call screen and the notification's Answer do this themselves.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().calls.answer()

GraineCalls.decline

Call it from a call screen of your own to decline the ringing call.

fun decline()

Reported DECLINED. For a call Graine rang, nothing rings that user again for 6 hours.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().calls.decline()

GraineCalls.callBack

Call it from a missed-call screen's “Call back”, so the user calls the agent back.

@JvmOverloads
fun callBack(activity: Activity, missedCallId: String?, reason: String? = null): String?

Places a call with the missed call's agent. The agent hears reason — else missed_call (a call Graine rang keeps its own reason with Graine). Clears the missed-call notification. The SDK's missed screen and notification do this themselves.

ParameterTypeDefaultMeaning
activityActivityrequiredThe activity in front.
missedCallIdString?requiredThe missed call's ID, so the call back goes to the same agent; null uses the default agent.
reasonString?nullWhy the user calls back; missed_call when left out.

Returns — String? — the new call's ID, or null when another call is live. Example value: "call_8a20d3c1-5b6e-4f7a-9c2d-1e0f3a4b5c6d".

Errors — none.

val callId = GraineInApp.get().calls.callBack(this, intent.getStringExtra(GraineCalls.EXTRA_CALL_ID))

Booking with the SDK's sheet

“Call me at…” with no screen of your own: presentBooking shows the SDK's booking sheet over an activity, in the agent's design, and Graine rings this phone at the time the user picks. Offer it from your own button, or let the user find it in the chat.

What the sheet shows. A header, “Book a call”, with “{call name} will call you” under it — the agent's call name (Calls & scheduling), else its chat title, else its heading — and a close button. Day: a chip for each day from today to the furthest day the agent allows — “Today”, “Tomorrow”, then the weekday and date in the phone's words, such as “Thu 16 Oct” — scrolling sideways; a day with no time left is not offered. Time: Android's own time picker in spinner mode, inside the SDK's own dialog (no Material), in the phone's 12- or 24-hour clock, its minute wheel in 5-minute steps (00, 05 … 55; on a phone whose picker has no such wheel, the minute picked is rounded to 5), held to the times the chosen day offers — between earliest(now), rounded up to the next 5 minutes, and latest(now); a time the day does not have moves to the nearest it has. Under it, the phone's time zone and the chosen time, such as “Asia/Kolkata · Tue 14 Oct, 6:00 PM”. What's it about? (optional): one line, at most 120 characters. Then Book call, with a spinner while it books. Once booked: “Booked — {call name} will call you on {day}, {time}” and Done.

A plain platform dialog — the SDK has no dependencies: a sheet up from the bottom on a phone, a centred panel on a tablet (the shorter side 600 dp or more), the app dimmed behind it. Back, a tap outside or the close button closes it — not while a booking is on its way. If the agent's settings arrive while it is open, the offer follows them.

A refusal stays in the sheet as one line under the button, in these words:

CodeThe line
TOO_SOONPick a time at least N minutes from now
TOO_FARPick a time within N days
OUTSIDE_WINDOWThat time is outside the hours we can call — pick another
TOO_MANY_PENDINGYou already have N calls booked — cancel one first
SCHEDULING_OFFBooking a call isn't available right now
IDENTITY_REQUIREDSign in to book a call
anything elseCouldn't book just now — try again

N is the agent's own number, singular for one (“1 minute”, “1 day”, “1 call”). After TOO_SOON the offer moves on to what is bookable now. An agent that cannot be booked now opens a sheet that shows the SCHEDULING_OFF or IDENTITY_REQUIRED line, with Book call disabled — so check bookingAvailable before offering it.

In the chat. Where booking is available, the chat window — the voice bar's, GraineChatView, GraineChatActivity — shows Book a call under the booked calls in its Upcoming calls card, or on its own in the card's place when none are booked; never when the design hides the booked-calls card (Garage → Chat window → Booked calls). It opens this sheet, and a booking — from the sheet or from scheduleCall — refreshes the card.

GraineInApp.bookingAvailable

Read it to decide whether to show your own “Call me” button.

val bookingAvailable: Boolean

true when all hold: Graine rings this phone (the workspace's hosted calling is on, and its platforms are empty or name Android), the agent lets users book (Calls & scheduling), and the user is identified with the trait the agent's Who is the user? setting names — otherwise a booking would be refused with IDENTITY_REQUIRED. It is what the SDK's own “Book a call” follows. Each agent answers for itself. Read it again after identify and once the settings have loaded.

Parameters — none.

Returns — Boolean. Example value: true.

Errors — none.

callMeButton.visibility = if (GraineInApp.get().bookingAvailable) View.VISIBLE else View.GONE

GraineInApp.presentBooking

Call it from your own “Call me” button to let the user book a call in the SDK's sheet.

@JvmOverloads
fun presentBooking(activity: Activity, reason: String? = null, done: GraineCallback<GraineScheduledCall?>)

Shows the booking sheet (above) over activity, on the main thread. One sheet at a time: while one is open, a second call answers null at once. An agent that cannot be booked opens a sheet that says why, and the log warns you.

ParameterTypeDefaultMeaning
activityActivityrequiredThe activity to show the sheet over.
reasonString?nullPre-fills What's it about?, which the user can edit. The agent gets it with the call; it is never included in the push that rings the phone.
doneGraineCallback<GraineScheduledCall?>requiredCalled once the sheet closes: with the booked call, or with null when the user closed it without booking.

Returns — nothing; the call arrives in done. Example value (in done): GraineScheduledCall(id=6f1c2a9e-…, at=Thu Oct 16 18:00:00 GMT+05:30 2026, status=scheduled, source=sdk, reason=Talk through my order).

Errors — none in done: a refusal stays in the sheet as its line. done hears null at once when the activity is finishing or another sheet is open.

if (GraineInApp.get().bookingAvailable) {
    GraineInApp.get().presentBooking(this, reason = "Talk through my order") { call, _ ->
        if (call != null) showMessage("We'll call you then.")
    }
}

GraineBookingDialog

Use it to show the booking sheet your own way — with another agent, or from code that manages its own dialogs.

class GraineBookingDialog @JvmOverloads constructor(activity: Activity, graine: GraineInApp = GraineInApp.get(), reason: String? = null, done: GraineCallback<GraineScheduledCall?>? = null) : Dialog(activity)

The booking sheet itself, as a plain platform Dialog — what presentBooking shows. Make it, then show() it; it places itself (a sheet on a phone, a centred panel on a tablet) and draws itself in the agent's design, following the design and who is signed in while it is open.

What to expect — Main thread. done hears once, when it closes: the booked call, or null. Closed while a booking is on its way, it waits for that booking's answer. Unlike presentBooking, it does not stop you showing two at once.

Its functions: constructor — each with its parameters, what it returns, its errors and an example.

GraineBookingDialog (constructor)

Use it to make a booking sheet you show yourself.

@JvmOverloads
GraineBookingDialog(activity: Activity, graine: GraineInApp = GraineInApp.get(), reason: String? = null, done: GraineCallback<GraineScheduledCall?>? = null)
ParameterTypeDefaultMeaning
activityActivityrequiredThe activity to show the sheet over.
graineGraineInAppGraineInApp.get()The agent the call is booked with, in its design and with its rules.
reasonString?nullFills What's it about?.
doneGraineCallback<GraineScheduledCall?>?nullHears the booked call, or null, once the sheet closes.

Returns — a GraineBookingDialog; call show() to show it.

Errors — IllegalStateException when graine is left out and no default agent was made.

GraineBookingDialog(this, GraineInApp.get("sales"), reason = "Upgrade") { call, _ ->
    if (call != null) showMessage("Booked.")
}.show()

The scheduled-calls list

Book calls from your own interface, list the user's upcoming calls and cancel one — the same calls the sheet and the chat's Upcoming calls card show. Graine rings this phone at the booked time.

GraineSchedulingRules

Read it to build your own booking screen — which times the agent accepts, and whether users may book at all.

data class GraineSchedulingRules @JvmOverloads constructor(…)

GraineInApp.schedulingRules (and SessionConfig.schedulingRules): the rules for booking calls, and for the calls Graine rings.

What to expect — A property: no request. This launch's rules, else the last ones kept on the device (a push that woke the app), else the defaults — no booking, no hosted ringing. scheduleCall refuses on the device first — SCHEDULING_OFF, TOO_SOON (before earliest(now) or not in the future), TOO_FAR (after latest(now)) — and Graine then checks the calling hours and the calls already waiting.

FieldTypeDefaultMeaning
usersCanBookBooleanfalseRead it to know whether users may book calls from the app at all (Calls & scheduling).
minLeadMinutesInt5The least notice a booking needs, in minutes — use it with earliest().
maxDaysAheadInt14How far ahead a booking may be, in days — use it with latest().
maxPendingPerUserInt3How many upcoming calls a user may have at once; beyond it, TOO_MANY_PENDING.
ringTimeoutSecondsInt45How long a call Graine rings keeps ringing before it is missed, 20–60 seconds.
callNameString?nullThe caller name the dashboard gives this agent's calls, when one is set.
hostedRingingBooleanfalseRead it to know whether Graine rings this app's users itself (the workspace uploaded its push credentials).
platformsList<String>emptyList()The platforms Graine rings: "ios", "android"; empty means every one.
PropertyTypeMeaning
val ringsAndroid: BooleanBooleanRead it to know whether Graine rings this app on Android: hosted ringing on, and platforms empty or naming Android.
companion · const val RING_SECONDS_MIN = 20IntThe shortest ring the dashboard allows, in seconds.
companion · const val RING_SECONDS_MAX = 60IntThe longest ring the dashboard allows, in seconds.

Its functions: earliest, latest, parse — each with its parameters, what it returns, its errors and an example.

GraineSchedulingRules.earliest

Call it for the soonest time a booking is accepted, to start your time picker there.

@JvmOverloads
fun earliest(now: java.util.Date = java.util.Date()): java.util.Date
ParameterTypeDefaultMeaning
nowjava.util.Datejava.util.Date()The moment to count from; now when left out.

Returns — java.util.Date — now plus minLeadMinutes. Example value: Thu Oct 15 18:05:00 GMT+05:30 2026.

Errors — none.

val first = GraineInApp.get().schedulingRules.earliest()

GraineSchedulingRules.latest

Call it for the latest time a booking is accepted, to end your time picker there.

@JvmOverloads
fun latest(now: java.util.Date = java.util.Date()): java.util.Date
ParameterTypeDefaultMeaning
nowjava.util.Datejava.util.Date()The moment to count from; now when left out.

Returns — java.util.Date — now plus maxDaysAhead days. Example value: Thu Oct 29 18:00:00 GMT+05:30 2026.

Errors — none.

val last = GraineInApp.get().schedulingRules.latest()

GraineSchedulingRules.parse

Call it to read the rules from the agent's settings document yourself — in a test, or on your server's copy.

@JvmStatic
companion · fun parse(raw: Any?): GraineSchedulingRules

Reads the settings' calls object: hosted, platforms, ringTimeoutSeconds (held to 20–60), callName and scheduling (usersCanBook, minLeadMinutes, maxDaysAhead, maxPendingPerUser). Anything missing keeps its default.

ParameterTypeDefaultMeaning
rawAny?requiredThe calls object (a JSONObject), or anything else for the defaults.

Returns — GraineSchedulingRules — the defaults (no booking, no hosted ringing) when there is nothing to read. Example value: GraineSchedulingRules(usersCanBook=true, minLeadMinutes=5, maxDaysAhead=14, maxPendingPerUser=3, ringTimeoutSeconds=45, callName=Acme, hostedRinging=true, platforms=[android]).

Errors — none.

val rules = GraineSchedulingRules.parse(JSONObject("""{"hosted":true,"scheduling":{"usersCanBook":true}}"""))

GraineInApp.scheduleCall

Call it to book a call from your own booking screen.

@JvmOverloads
fun scheduleCall(at: java.util.Date, reason: String? = null, done: GraineCallback<GraineScheduledCall>? = null)

Books a call from the agent for the signed-in user (or, with nobody signed in, this install) at at — “call me tomorrow at 6”. It waits for the agent's settings, then refuses on the device — before anything is sent — what the rules already settle, in the booking sheet's words: SCHEDULING_OFF, “Booking a call isn't available right now”; TOO_SOON, “Pick a time at least N minutes from now” (“1 minute” for one); TOO_FAR, “Pick a time within N days”. Graine then checks what only it knows: the calling hours, in the user's time zone, and the calls already waiting. The time is sent with the phone's offset and time zone. reason (up to 120 characters) is stored with the call for the agent and is never included in the push that rings the phone. An open chat's Upcoming calls card shows the booking at once.

ParameterTypeDefaultMeaning
atjava.util.DaterequiredWhen the call should ring: between schedulingRules.earliest(now) and latest(now).
reasonString?nullWhat the call is about, at most 120 characters — the agent hears it when it rings.
doneGraineCallback<GraineScheduledCall>?nullHears the booked call, or why not, once, on the main thread.

Returns — nothing; the call arrives in done (GraineScheduledCall). Example value (in done): GraineScheduledCall(id=6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44, at=Thu Oct 16 18:00:00 GMT+05:30 2026, status=scheduled, source=sdk, reason=Talk through my order).

Errors — in done's GraineError — on the device: SCHEDULING_OFF, TOO_SOON, TOO_FAR; from Graine: OUTSIDE_WINDOW, TOO_MANY_PENDING, IDENTITY_REQUIRED, DEVICE_LIMIT, INVALID_REQUEST, RATE_LIMITED, NETWORK, UNAVAILABLE (“The booking was accepted but not described.” when Graine booked it without describing it).

GraineInApp.get().scheduleCall(java.util.Date(System.currentTimeMillis() + 3_600_000), reason = "Talk through my order") { call, error ->
    if (error != null) showMessage(error.message) else showMessage("Booked for ${call?.at}")
}

GraineInApp.scheduledCalls

Call it to list the user's upcoming calls in your own interface.

fun scheduledCalls(done: GraineCallback<List<GraineScheduledCall>>)

Lists the upcoming calls booked for the signed-in user — or for this install when nobody is signed in — with the soonest first.

ParameterTypeDefaultMeaning
doneGraineCallback<List<GraineScheduledCall>>requiredHears the list, or the error, once, on the main thread.

Returns — nothing; the list arrives in done. Example value (in done): [GraineScheduledCall(id=6f1c2a9e-…, at=Thu Oct 16 18:00:00 GMT+05:30 2026, status=scheduled, source=agent, reason=null)].

Errors — in done's GraineError: NETWORK, RATE_LIMITED, UNAVAILABLE, INVALID_KEY, ORIGIN_REFUSED.

GraineInApp.get().scheduledCalls { calls, error -> if (error == null) render(calls.orEmpty()) }

GraineInApp.cancelScheduledCall

Call it when the user calls off one of their booked calls.

@JvmOverloads
fun cancelScheduledCall(id: String, done: GraineCallback<GraineCallStatus>? = null)

Only a call booked for this user or install; any other is NOT_FOUND. An open chat's card drops it too once it is gone.

ParameterTypeDefaultMeaning
idStringrequiredThe call's id, from scheduleCall, scheduledCalls or presentBooking.
doneGraineCallback<GraineCallStatus>?nullHears the call's status afterwards, or the error.

Returns — nothing; the status arrives in done — CANCELLED when Graine does not say otherwise. Example value (in done): GraineCallStatus.CANCELLED.

Errors — in done's GraineError: INVALID_REQUEST (“No call id.”) for a blank id, NOT_FOUND, NETWORK, RATE_LIMITED, UNAVAILABLE.

GraineInApp.get().cancelScheduledCall("6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44")

GraineChat.refreshUpcomingCalls

Call it after you book or cancel a call somewhere the chat does not see, so its Upcoming calls card shows it.

fun refreshUpcomingCalls(): Unit

Fetches the upcoming calls again now. The card refreshes itself when the chat opens, after the agent replies (at most every 10 seconds), and after a booking made with scheduleCall or the sheet. Only where Graine rings this app.

Parameters — none.

Returns — nothing.

Errors — none.

GraineInApp.get().chat.refreshUpcomingCalls()

GraineChat.cancelUpcomingCall

Call it from a chat screen of your own to cancel one of the card's calls.

fun cancelUpcomingCall(id: String): Unit

The call's row says “Cancelling…” until it is done, then goes; a failure says “Couldn’t cancel · tap to retry”. The SDK's chat window does this from its Cancel button.

ParameterTypeDefaultMeaning
idStringrequiredThe call's id, from chat.upcomingCalls.

Returns — nothing.

Errors — none.

val chat = GraineInApp.get().chat
chat.upcomingCalls.firstOrNull { it.canCancel }?.let { chat.cancelUpcomingCall(it.id) }

GraineScheduledCall

Read it to show a booked call: when it rings, where it is, and whether it can still be cancelled.

data class GraineScheduledCall @JvmOverloads constructor(…)

A call the agent will make to this user, booked for later: Graine rings it at at, through the app's call screen.

What to expect — From scheduleCall (status SCHEDULED), scheduledCalls(), presentBooking and the chat's upcoming calls. at is a moment: show it in the device's time zone. A status or source newer than the SDK arrives as Unknown, carrying its word.

FieldTypeDefaultMeaning
idStringrequiredUse it to cancel the call — Graine's ID for it, a UUID.
atjava.util.Date?requiredWhen it rings; null only when Graine's time could not be read.
statusGraineCallStatusGraineCallStatus.SCHEDULEDWhere it is (GraineCallStatus).
sourceGraineCallSource?nullWho booked it (GraineCallSource); null when Graine does not say.
reasonString?nullWhy, as it was booked; null when none was given (or Graine keeps it to itself).
PropertyTypeMeaning
val isUpcoming: BooleanBooleanUse it to keep a call in an upcoming list: still to come — scheduled, or ringing now.
val canCancel: BooleanBooleanUse it to show a Cancel control: it can still be called off — it has not rung yet.

GraineCallStatus

Read it to show where a booked call is.

sealed class GraineCallStatus(val wire: String)

What to expect — SCHEDULED → RINGING → ANSWERED → COMPLETED; or DECLINED, MISSED, FAILED, CANCELLED, EXPIRED, NO_DEVICE. A status newer than this SDK arrives as Unknown(raw), carrying its word: give every when an else.

PropertyTypeMeaning
val wire: StringStringGraine's word for the status — scheduled, no_device, … — for logs and your server.
companion · val KNOWN: List<GraineCallStatus>List<GraineCallStatus>Every status this build knows — for a settings screen or a test.
CaseMeaning
SCHEDULEDBooked, waiting for its time; it can still be cancelled.
RINGINGRinging the user now.
ANSWEREDThe user picked up.
COMPLETEDThe call took place and is over.
DECLINEDThe user declined it; nothing rings them again for 6 hours.
MISSEDIt rang out unanswered.
FAILEDIt could not be placed.
CANCELLEDCalled off before it rang.
EXPIREDIts time passed while it could not ring — more than 10 minutes late, or no opening left in the calling hours.
NO_DEVICEGraine had no device of this user's to ring — none seen in the last 30 days.
Unknown(raw)A status newer than this SDK, carrying Graine's word in wire.

Its functions: from — each with its parameters, what it returns, its errors and an example.

GraineCallSource

Read it to show who booked a call.

sealed class GraineCallSource(val wire: String)

What to expect — A source newer than this SDK arrives as Unknown(raw), carrying its word.

PropertyTypeMeaning
val wire: StringStringGraine's word for the source — sdk for APP.
CaseMeaning
AGENTThe agent booked it in a conversation.
APPYour app booked it (scheduleCall, the booking sheet).
DASHBOARDYour team booked it in the dashboard.
APIYour server booked it through the API.
Unknown(raw)A source newer than this SDK, carrying Graine's word.

Its functions: from — each with its parameters, what it returns, its errors and an example.

GraineCallStatus.from

Call it to read a status word from your server (or Graine's webhooks) into a GraineCallStatus.

@JvmStatic
companion · fun from(wire: String): GraineCallStatus

Case and surrounding spaces do not matter.

ParameterTypeDefaultMeaning
wireStringrequiredGraine's word, such as "no_device".

Returns — GraineCallStatus — a known status, or Unknown(wire) for a word newer than this SDK. Example value: GraineCallStatus.NO_DEVICE.

Errors — none.

val status = GraineCallStatus.from("completed")

GraineCallSource.from

Call it to read a source word into a GraineCallSource.

@JvmStatic
companion · fun from(wire: String): GraineCallSource

sdk and app are APP; case and surrounding spaces do not matter.

ParameterTypeDefaultMeaning
wireStringrequiredGraine's word, such as "dashboard".

Returns — GraineCallSource — a known source, or Unknown(wire). Example value: GraineCallSource.DASHBOARD.

Errors — none.

val source = GraineCallSource.from("sdk")   // APP

Design

The brand chooses a design in the Garage — a look and a few knobs — and the agent's settings carry it already worked out, as two complete sets of tokens, light and dark. The SDK's views draw what the tokens say; a look added in the dashboard later reaches your app without a release. Nothing from the network is trusted: a value that is missing or malformed takes Graine Classic's (the look every Graine surface had before designs), and reading never throws. Use these types to draw your own interface in the brand's design, or to preview a design.

GraineInApp.themeTokens

Call it to draw your own interface in the brand's design, in the right scheme.

@JvmOverloads
fun themeTokens(context: Context? = null, scheme: String? = null): ThemeTokens?

The brand's fixed light or dark when it chose one, else whichever the phone is in — context's own, so an activity that forces night mode gets dark. Read it again when the phone switches.

ParameterTypeDefaultMeaning
contextContext?nullWhose light or dark setting to follow; the application's when left out.
schemeString?nullYour own choice — "light", "dark" or "auto" — which wins over the design's.

Returns — ThemeTokens? (ThemeTokens) — null without a design (an older server). Example value: ThemeTokens(v=1, preset=graine-classic, scheme=light, …).

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return

GraineInApp.previewDesign

Call it to show a design before the brand saves it — a preview screen, a screenshot, a test.

fun previewDesign(design: ResolvedDesign?)

Every SDK view redraws in design instead of the session's; null goes back to the session's.

ParameterTypeDefaultMeaning
designResolvedDesign?requiredThe design to draw (ResolvedDesign), or null.

Returns — nothing.

Errors — none.

GraineInApp.get().previewDesign(ResolvedDesign.parse(designJson))

ResolvedDesign

Read it for the brand's design: the knobs it turned, and the light and dark tokens to draw.

data class ResolvedDesign(val config: DesignConfig, val light: ThemeTokens, val dark: ThemeTokens)

GraineInApp.design and SessionConfig.design.

What to expect — When Graine sends only one of light and dark, the other uses the same tokens.

FieldTypeDefaultMeaning
configDesignConfigrequiredThe knobs the brand turned (DesignConfig).
lightThemeTokensrequiredThe tokens to draw in light (ThemeTokens).
darkThemeTokensrequiredThe tokens to draw in dark.

Its functions: pick, toJson, parse — each with its parameters, what it returns, its errors and an example.

ResolvedDesign.pick

Call it to choose which tokens to draw, light or dark.

@JvmOverloads
fun pick(deviceDark: Boolean?, override: String? = null): ThemeTokens

The brand's fixed scheme when it chose one; under “auto” (or none) the phone's — light when the phone's is unknown. Pick again whenever the phone switches.

ParameterTypeDefaultMeaning
deviceDarkBoolean?requiredWhether the phone is in dark mode; null when unknown.
overrideString?nullYour own choice — "light", "dark" or "auto" — which wins.

Returns — ThemeTokens. Example value: ThemeTokens(…, scheme=dark, …).

Errors — none.

val design = GraineInApp.get().design ?: return
val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val tokens = design.pick(deviceDark = night)

ResolvedDesign.toJson

Call it to log the design, or hand it to another part of your app.

fun toJson(): JSONObject

Parameters — none.

Returns — JSONObject — { config, light, dark }, as the settings spell them.

Errors — none.

GraineInApp.get().design?.let { Log.d("Graine", it.toJson().toString()) }

ResolvedDesign.parse

Call it to read a design document — from your own preview tool, say.

@JvmStatic
companion · fun parse(raw: Any?): ResolvedDesign?

Reads a JSONObject, or its JSON text. A value missing or malformed takes Graine Classic's.

ParameterTypeDefaultMeaning
rawAny?requiredThe design: { config, light, dark }.

Returns — ResolvedDesign? — null when it is absent or is not a design. Example value: ResolvedDesign(config=DesignConfig(preset=graine-classic, …), light=…, dark=…).

Errors — none: it never throws.

val design = ResolvedDesign.parse(designJson) ?: return

DesignConfig

Read it to show which choices the brand made in the Garage.

data class DesignConfig @JvmOverloads constructor(…)

The knobs the brand turned. Only scheme changes what the SDK draws (which token set); the rest are already inside the tokens and are kept for an app that wants to show them.

FieldTypeDefaultMeaning
presetStringDesign.DEFAULT_PRESETThe design the brand started from, such as graine-classic.
accentString?nullThe accent colour the brand chose.
schemeString?null"light", "dark" or "auto"; absent means auto.
roundnessDouble?nullHow rounded corners are, as the brand set it.
fontString?nullThe body typeface the brand chose.
headingFontString?nullThe heading typeface.
densityString?nullHow tight the spacing is.
shadowString?nullHow depth is drawn.
motionString?nullHow much things move.
neutralString?nullThe neutral palette the greys come from.
glassBoolean?nullWhether surfaces are translucent.
bubblesString?nullHow chat bubbles look.
launcherIconString?nullThe launcher's glyph.
launcherShapeString?nullThe launcher's shape.
iconStyleString?nullThe style of the SDK's icons.
chromeString?null"none" or "window", when the brand turned the knob; ThemeTokens.chrome is what is drawn.
chatMap<String, Any?>?nullThe chat window's knobs as the brand set them (Garage → Chat window); ThemeTokens.chat is what is drawn.
callMap<String, Any?>?nullThe call screen's knobs as the brand set them (Garage → Call screen); ThemeTokens.call is what is drawn.

Its functions: toJson — each with its parameters, what it returns, its errors and an example.

DesignConfig.toJson

Call it to log the brand's choices, or hand them on.

fun toJson(): JSONObject

Parameters — none.

Returns — JSONObject — the knobs, as the settings spell them; absent ones are left out.

Errors — none.

GraineInApp.get().design?.let { Log.d("Graine", it.config.toJson().toString()) }

ThemeTokens

Read it for everything you need to draw in the brand's design, in one scheme.

data class ThemeTokens @JvmOverloads constructor(…)

Every value already worked out: flat numbers (dp, sp, ms) and #RRGGBB colours. From themeTokens(), or ResolvedDesign.light and dark.

What to expect — Resolved from the agent's design when the settings load; a missing or malformed value takes Graine Classic's. Reading them never throws.

FieldTypeDefaultMeaning
vIntrequiredThe tokens' format version, for logs.
presetStringrequiredThe design the tokens come from, such as graine-classic.
schemeStringrequired"light" or "dark".
colorsThemeColorsrequiredEvery colour (ThemeColors).
fontThemeFontrequiredThe typefaces, sizes, weights, spacing and line heights (ThemeFont).
shapeThemeShaperequiredCorner radii, outline width and the launcher's shape (ThemeShape).
spacingThemeSpacingrequiredThe spacing unit, card padding and gaps (ThemeSpacing).
elevationThemeElevationrequiredHow depth is drawn: shadows, or a hard offset (ThemeElevation).
motionThemeMotionrequiredHow long and how things move (ThemeMotion).
iconsThemeIconsrequiredThe icons' style and stroke, and the launcher's glyph (ThemeIcons).
glassThemeGlass?requiredTranslucent surfaces (ThemeGlass); null for opaque ones.
bubblesStringrequiredHow chat bubbles look: filled, outline or minimal.
chromeString"none""none" or "window": with window the chat window, the caption card and every component card open as desktop windows, with a title bar, three coloured dots and a monospaced title.
chatChatTokens?nullThe chat window's values (ChatTokens); null from a server that predates them, when the SDK works them out from colors.
callCallTokens?nullThe call screen's values (CallTokens); null from an older server, when the SDK works them out.
PropertyTypeMeaning
val isDark: BooleanBooleanRead it to know these are the dark tokens (scheme == "dark").

Its functions: toJson — each with its parameters, what it returns, its errors and an example.

ThemeTokens.toJson

Call it to log the tokens, or hand them to another part of your app.

fun toJson(): JSONObject

Parameters — none.

Returns — JSONObject — the tokens as the settings spell them.

Errors — none.

GraineInApp.get().themeTokens(this)?.let { Log.d("Graine", it.toJson().toString(2)) }

ThemeColors

Read it for every colour the brand's surfaces are drawn in.

data class ThemeColors(…)

What to expect — Each a #RRGGBB colour.

FieldTypeDefaultMeaning
backgroundStringrequiredBehind everything — a full-screen chat's page.
surfaceStringrequiredCards, the voice bar and sheets.
surfaceRaisedStringrequiredSurfaces above a surface: a component card, a selected chip's backing.
textStringrequiredMain text.
textMutedStringrequiredSecondary text: captions, labels, times.
borderStringrequiredOutlines and separators.
accentStringrequiredThe brand colour: buttons, the launcher, the microphone.
onAccentStringrequiredText and icons drawn on the accent.
accentSoftStringrequiredA pale tint of the accent, for chips, selected rows and the focus ring.
successStringrequiredGood news: online dots, confirmations.
warningStringrequiredCautions: a weak connection.
dangerStringrequiredErrors, the end-call button, a refusal line.
userBubbleStringrequiredThe user's chat bubbles.
userBubbleTextStringrequiredText in the user's bubbles.
agentBubbleStringrequiredThe agent's chat bubbles.
agentBubbleTextStringrequiredText in the agent's bubbles.
scrimStringrequiredThe dimming behind a sheet or dialog (drawn at 40 %).

ThemeFont

Read it for the brand's typefaces and type scale.

data class ThemeFont(val body: String, val heading: String, val scale: FontScale, val weights: FontWeights, val letterSpacing: FontPair, val lineHeight: FontPair)

FieldTypeDefaultMeaning
bodyStringrequiredThe body typeface's family: "system" is the platform's own font; any other is a family your app may bundle (GraineConfig.fonts) or the SDK fetches.
headingStringrequiredThe headings' typeface's family, the same way.
scaleFontScalerequiredType sizes (FontScale).
weightsFontWeightsrequiredWeights (FontWeights).
letterSpacingFontPairrequiredLetter spacing for body and titles, in em (FontPair).
lineHeightFontPairrequiredLine height for body and titles, as a multiple of the size.

FontScale

Read it for the brand's type sizes.

data class FontScale(val caption: Double, val body: Double, val title: Double, val display: Double)

What to expect — In sp.

FieldTypeDefaultMeaning
captionDoublerequiredCaptions, labels and times.
bodyDoublerequiredBody text and messages.
titleDoublerequiredTitles and headers.
displayDoublerequiredThe largest text, such as the call screen's name.

FontWeights

Read it for the brand's type weights.

data class FontWeights(val body: Int, val strong: Int, val title: Int)

What to expect — CSS weights, 100–900.

FieldTypeDefaultMeaning
bodyIntrequiredBody text.
strongIntrequiredEmphasis, buttons and labels.
titleIntrequiredTitles.

FontPair

Read it for one number for body text and one for titles.

data class FontPair(val body: Double, val title: Double)

What to expect — Letter spacing in em, or line height as a multiple of the size, as its field says.

FieldTypeDefaultMeaning
bodyDoublerequiredFor body text.
titleDoublerequiredFor titles.

ThemeShape

Read it for the brand's corners, outlines and launcher shape.

data class ThemeShape(val radius: ThemeRadius, val borderWidth: Double, val launcher: String)

FieldTypeDefaultMeaning
radiusThemeRadiusrequiredThe corner radii (ThemeRadius).
borderWidthDoublerequiredThe width of every outline, in dp.
launcherStringrequiredThe launcher's shape: circle, squircle, rounded-square, square or pill.

ThemeRadius

Read it for the brand's corner radii.

data class ThemeRadius(val sm: Double, val md: Double, val lg: Double, val xl: Double)

What to expect — In dp.

FieldTypeDefaultMeaning
smDoublerequiredSmall corners: chips, a plain-square launcher.
mdDoublerequiredFields and buttons.
lgDoublerequiredCards and panels.
xlDoublerequiredSheets and the voice bar.

ThemeSpacing

Read it for the brand's spacing.

data class ThemeSpacing(val unit: Double, val cardPadding: Double, val gap: Double)

What to expect — In dp.

FieldTypeDefaultMeaning
unitDoublerequiredThe base spacing step.
cardPaddingDoublerequiredThe padding inside a card.
gapDoublerequiredThe gap between items in a card.

ThemeElevation

Read it for how the brand draws depth.

data class ThemeElevation(val card: ThemeShadow?, val floating: ThemeShadow?, val hardOffset: ThemeOffset?)

FieldTypeDefaultMeaning
cardThemeShadow?requiredA card's shadow (ThemeShadow); null for none.
floatingThemeShadow?requiredThe shadow of what floats — the voice bar, the launcher, sheets; null for none.
hardOffsetThemeOffset?requiredA solid offset drawn instead of shadows (ThemeOffset); null unless the design draws depth that way.

ThemeShadow

Read it to draw one of the brand's soft shadows.

data class ThemeShadow(val y: Double, val blur: Double, val spread: Double, val opacity: Double, val color: String)

Design.androidShadow turns one into what Android can draw.

FieldTypeDefaultMeaning
yDoublerequiredHow far down the shadow falls, in dp.
blurDoublerequiredHow soft it is, in dp.
spreadDoublerequiredHow far it spreads, in dp.
opacityDoublerequiredHow strong it is, 0–1, applied to color.
colorStringrequiredIts colour, #RRGGBB.

ThemeOffset

Read it to draw the brand's hard offset — a solid shadow in the text colour.

data class ThemeOffset(val x: Double, val y: Double)

What to expect — In dp; drawn in colors.text, with an outline to match.

FieldTypeDefaultMeaning
xDoublerequiredHow far right.
yDoublerequiredHow far down.

ThemeMotion

Read it to animate in the brand's way — or not at all.

data class ThemeMotion(val durationMs: Int, val easing: String, val bounce: Double)

What to expect — The phone's own “Remove animations” wins over any number here (Design.motionMs).

FieldTypeDefaultMeaning
durationMsIntrequiredHow long a transition runs; 0 means none at all.
easingStringrequiredHow it moves: standard, emphasized, spring, linear or none.
bounceDoublerequiredHow much a spring overshoots, 0–1.

ThemeIcons

Read it to draw icons in the brand's style.

data class ThemeIcons(val style: String, val stroke: Double, val launcher: LauncherGlyph?)

FieldTypeDefaultMeaning
styleStringrequiredoutline, filled, duotone or sharp.
strokeDoublerequiredThe stroke width of outline icons, 0.5–4.
launcherLauncherGlyph?requiredThe launcher's glyph (LauncherGlyph); null shows the brand's logo.

LauncherGlyph

Read it to draw the launcher's glyph yourself.

data class LauncherGlyph(val id: String, val path: String, val stroke: Boolean)

The voice bar draws it from its path; the launcher fetches it as an image (Design.glyphUrl).

FieldTypeDefaultMeaning
idStringrequiredThe glyph's name, such as waveform — what Design.glyphUrl asks for.
pathStringrequiredIts drawing: SVG path data on a 24 × 24 grid.
strokeBooleanrequiredtrue to stroke the path, false to fill it.

ThemeGlass

Read it to draw the brand's translucent surfaces.

data class ThemeGlass(val blur: Double, val surfaceOpacity: Double)

What to expect — Surfaces are colors.surface at surfaceOpacity, with what is behind blurred where the platform can.

FieldTypeDefaultMeaning
blurDoublerequiredHow much what is behind is blurred, in dp.
surfaceOpacityDoublerequiredHow opaque the surface is, 0–1.

ChatTokens

Read it to draw a chat window of your own in the brand's chat design.

data class ChatTokens(…)

ThemeTokens.chat: the chat window's values in one scheme, as the brand set them in the Garage → Chat window. Colours are #RRGGBB.

FieldTypeDefaultMeaning
titleStringrequiredThe header's title; empty uses the agent's heading.
subtitleStringrequiredThe header's second line while online; empty says “Online”.
avatarStringrequiredThe header's mark: logo, glyph or none.
placeholderStringrequiredThe message field's placeholder, such as “Message…”.
showTimestampsBooleanrequiredWhether each message shows its time.
suggestionsList<String>requiredUp to four conversation starters, shown before the first message; a tap sends one.
backgroundStringrequiredBehind the messages.
patternStringrequirednone, or dots over the messages in patternColor.
patternColorStringrequiredThe pattern's colour.
headerBackgroundStringrequiredThe header's fill (and the status bar's, on the chat screen).
headerTextStringrequiredThe header's title colour.
headerSubtextStringrequiredThe header's second line colour.
composerBackgroundStringrequiredThe message field's fill.
composerTextStringrequiredThe message field's text.
composerPlaceholderStringrequiredThe message field's placeholder colour.
composerBorderStringrequiredThe message field's outline.
sizeGraineChatSizeGraineChatSize.COMPACTHow big the window opens (Window size), unless the app or an open says otherwise.
showUpcomingCallsBooleantrueWhether the card of the user's booked calls — and Book a call — shows at the top of the chat (Booked calls).
upcomingTitleString""That card's title; empty says “Upcoming call” or “Upcoming calls”.
showHistoryBooleantrueWhether the earlier conversation is shown (Earlier messages); off, the agent still has it.
newConversationBooleantrueWhether the header offers New conversation; off, chat.newConversation() still works.

CallTokens

Read it to draw a call screen of your own in the brand's call design.

data class CallTokens(…)

ThemeTokens.call: the call screen's values in one scheme (Garage → Call screen). Colours are #RRGGBB. Before any session — a call that wakes a closed app on a first launch — the SDK draws Graine Classic's dark call screen.

FieldTypeDefaultMeaning
backgroundStringrequiredThe screen's background (the gradient's top).
backgroundToString?requiredThe gradient's bottom; null for a solid background.
textStringrequiredThe name and main text.
textMutedStringrequiredThe status line and labels.
avatarStringrequiredWhat the avatar shows: logo, glyph or initials.
avatarShapeStringrequiredcircle, squircle or rounded-square.
avatarFillStringrequiredThe avatar's fill.
avatarTextStringrequiredThe avatar's initials or glyph colour.
buttonFillStringrequiredA control's fill (mute, loudspeaker).
buttonIconStringrequiredA control's icon.
buttonActiveFillStringrequiredA control's fill while on (muted, the loudspeaker).
buttonActiveIconStringrequiredA control's icon while on.
endFillStringrequiredThe end-call button's fill.
endIconStringrequiredThe end-call button's icon.
endShapeStringrequiredThe end-call button: round or pill.
acceptFillStringrequiredThe Answer button's fill.
acceptIconStringrequiredThe Answer button's icon.
declineFillStringrequiredThe Decline button's fill.
declineIconStringrequiredThe Decline button's icon.
callbackFillStringrequiredThe Call back button's fill, on a missed call.
callbackIconStringrequiredThe Call back button's icon.
captionsBooleanrequiredWhether the screen shows what is being said.
timerBooleanrequiredWhether it shows how long the call has lasted.
controlsList<String>requiredThe in-call controls, in order: mute, speaker, chat, end (an end button is always drawn).
ringtoneStringrequiredThe ring: system (the phone's), graine or soft.
statusTextCallStatusTextrequiredThe screen's words (CallStatusText).

CallStatusText

Read it for the call screen's words, as the brand wrote them.

data class CallStatusText @JvmOverloads constructor(val ringing: String, val connecting: String, val ended: String, val scheduled: String? = null, val missed: String? = null)

What to expect — Absent from an older server, scheduled and missed read “Scheduled call” and “Missed call” through scheduledText and missedText.

FieldTypeDefaultMeaning
ringingStringrequiredWhile a call rings, such as “Incoming call”.
connectingStringrequiredWhile it connects, such as “Connecting…”.
endedStringrequiredOnce a call that went well is over, such as “Call ended”.
scheduledString?nullWhile a call Graine rang at a booked time rings; read scheduledText.
missedString?nullFor a ring nobody answered; read missedText.
PropertyTypeMeaning
val scheduledText: StringStringShow it while a call Graine rang rings (booked by the agent, the user or your team).
val missedText: StringStringShow it for a ring nobody answered — on the screen and in the missed-call notification.
companion · const val DEFAULT_SCHEDULED = "Scheduled call"StringThe words used when the brand wrote none for scheduled.
companion · const val DEFAULT_MISSED = "Missed call"StringThe words used when the brand wrote none for missed.

Design (object)

Use it for the design's arithmetic — parsing colours, the launcher's corners, the glyph's address, motion.

object Design

PropertyTypeMeaning
const val DEFAULT_PRESET = "graine-classic"StringThe design used for everything Graine leaves out: Graine Classic.
const val GLYPH_ASSET_VERSION = 2IntThe launcher-glyph image version in glyphUrl; a changed drawing gets a new number.
const val LAUNCHER_SIZE = 56.0DoubleThe launcher's height (and width, unless it is a pill), in dp.
const val LAUNCHER_GLYPH = 0.46DoubleThe launcher's glyph or logo, as a share of its height.
val LAUNCHER_SHAPES: List<String>List<String>The launcher's shapes: circle, squircle, rounded-square, square, pill.
val BUBBLES: List<String>List<String>The chat bubble styles: filled, outline, minimal.
val EASINGS: List<String>List<String>The motion curves: standard, emphasized, spring, linear, none.
val ICON_STYLES: List<String>List<String>The icon styles: outline, filled, duotone, sharp.
val CHROMES: List<String>List<String>How a surface that holds content is framed: none (plain) or window (a desktop window).
val CLASSIC_LIGHT: ThemeTokensThemeTokensGraine Classic in light — what the SDK draws when there is no design.
val CLASSIC_DARK: ThemeTokensThemeTokensGraine Classic in dark.

Its functions: normaliseHex, glyphUrl, launcherCorner, motionMs, androidShadow, fontFamily, fontFor — each with its parameters, what it returns, its errors and an example.

Design.normaliseHex

Call it to read any hex colour into the #RRGGBB form the tokens use.

@JvmStatic
fun normaliseHex(value: Any?): String?
ParameterTypeDefaultMeaning
valueAny?requiredA colour such as "#abc", "AABBCC" or "#aabbcc".

Returns — String? — "#RRGGBB" in capitals, or null for anything that is not a hex colour. Example value: "#AABBCC".

Errors — none.

val hex = Design.normaliseHex("#abc")   // "#AABBCC"

Design.glyphUrl

Call it for the address of the launcher's glyph as an image, to show it in a view of your own.

@JvmStatic @JvmOverloads
fun glyphUrl(origin: String?, id: String?, color: String?, sizePx: Double, stroke: Double = 2.0): String

The ID is lower-cased, the colour written as capital hex without “#”, the size held to 16–512 physical pixels (dp × density, so it is sharp on every screen), and the stroke held to 0.5–4 in quarter steps.

ParameterTypeDefaultMeaning
originString?requiredYour GraineConfig.baseUrl; Graine's when null.
idString?requiredThe glyph's LauncherGlyph.id.
colorString?requiredThe colour to draw it in (#RRGGBB); white when not a colour.
sizePxDoublerequiredIts size in physical pixels; 0 asks for 96.
strokeDouble2.0The stroke width, for a stroked glyph.

Returns — String — the image's address. Example value: "https://www.graine.ai/api/embed/glyph?id=waveform&c=FFFFFF&s=72&w=2&v=2".

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val url = Design.glyphUrl(GraineInApp.get().config.baseUrl, tokens.icons.launcher?.id, tokens.colors.onAccent, 26.0 * resources.displayMetrics.density)

Design.launcherCorner

Call it for the launcher's corner radius at a height, to draw a launcher of your own in the brand's shape.

@JvmStatic
fun launcherCorner(shape: String?, height: Double, radius: ThemeRadius): Double

circle and pill are fully round; squircle is 30 % of the height; rounded-square the design's large radius; square its small one — never more than half the height.

ParameterTypeDefaultMeaning
shapeString?requiredThe launcher's shape (ThemeShape.launcher).
heightDoublerequiredThe launcher's height, in dp.
radiusThemeRadiusrequiredThe design's radii (ThemeShape.radius).

Returns — Double — the corner radius, in dp. Example value: 16.8 (a 56 dp squircle).

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val corner = Design.launcherCorner(tokens.shape.launcher, 56.0, tokens.shape.radius)

Design.motionMs

Call it for how long a transition of yours should run, honouring both the design and the phone.

@JvmStatic
fun motionMs(motion: ThemeMotion?, animatorScale: Float): Long

The design's duration — or 0 (none) when the design says none, when there is no design, or when the phone has animations off.

ParameterTypeDefaultMeaning
motionThemeMotion?requiredThe design's motion (ThemeTokens.motion).
animatorScaleFloatrequiredThe system's animator duration scale (Settings.Global.ANIMATOR_DURATION_SCALE); 0 is “Remove animations”.

Returns — Long — milliseconds. Example value: 200.

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val ms = Design.motionMs(tokens.motion, Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f))

Design.androidShadow

Call it to draw one of the brand's shadows the way Android can.

@JvmStatic
fun androidShadow(s: ThemeShadow?): Pair<Double, Double>

Android draws depth as an elevation and a shadow colour's alpha (which it multiplies by the theme's own); this works out both so the shadow lands where the design meant.

ParameterTypeDefaultMeaning
sThemeShadow?requiredThe shadow (ThemeElevation.card or floating).

Returns — Pair<Double, Double> — the elevation in dp and the shadow colour's alpha, 0–1; (0, 0) for no shadow. Example value: (8.0, 0.84).

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val (elevation, alpha) = Design.androidShadow(tokens.elevation.floating)

Design.fontFamily

Call it to know which typeface family to ask your app for.

@JvmStatic
fun fontFamily(name: String?): String?
ParameterTypeDefaultMeaning
nameString?requiredA design family (ThemeFont.body or heading).

Returns — String? — the family, or null for the platform's own font ("system" or empty). Example value: "Inter".

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val family = Design.fontFamily(tokens.font.body)

Design.fontFor

Call it to find your bundled typeface for a design family.

@JvmStatic
fun <T> fontFor(fonts: Map<String, T>, family: String?): T?

Matched without regard to case.

ParameterTypeDefaultMeaning
fontsMap<String, T>requiredYour fonts by family name — usually GraineConfig.fonts.
familyString?requiredThe design family.

Returns — T? — your font, or null when you have none for it. Example value: Typeface@3e1a….

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val face = Design.fontFor(GraineInApp.get().config.fonts, tokens.font.body)

NativeAppearance

Read it for the dashboard's appearance settings, worked out for a phone — when Graine sends no design.

data class NativeAppearance @JvmOverloads constructor(val accent: String, val heading: String, val logoUrl: String?, val radius: Double, val logoPlate: LogoPlate = LogoPlate.ACCENT)

From Appearance.resolve.

FieldTypeDefaultMeaning
accentStringrequiredThe accent, always a valid #rgb or #rrggbb.
headingStringrequiredThe panel's heading (such as “Talk to our AI assistant”) — not the assistant's name.
logoUrlString?requiredThe brand's logo, as an absolute address; null when none is set.
radiusDoublerequiredCorner rounding, held to the largest your view can draw (maxRadius).
logoPlateLogoPlateLogoPlate.ACCENTWhat the logo sits on where it stands in for the launcher glyph (LogoPlate).

Appearance

Use it to read the dashboard's appearance settings and colours when Graine sends no design.

object Appearance

PropertyTypeMeaning
const val DEFAULT_ACCENT = "#318CE7"StringThe accent used when none is set.
const val DEFAULT_ORIGIN = "https://www.graine.ai"StringWhere logo addresses point when no origin is given.

Its functions: resolve, isDark, palette, luminance, argb, tint, encodeURIComponent — each with its parameters, what it returns, its errors and an example.

Appearance.resolve

Call it to work out the dashboard's appearance for a view of your own, with your values as the fallback.

fun resolve(appearance: JSONObject?, accent: String, heading: String, radius: Double? = null, maxRadius: Double? = null, origin: String? = null): NativeAppearance

The dashboard decides; your values fill only what it leaves out. The accent may arrive without its “#”.

ParameterTypeDefaultMeaning
appearanceJSONObject?requiredSessionConfig.appearance.
accentStringrequiredYour accent, used when the dashboard sets none.
headingStringrequiredYour heading, used when the dashboard sets none.
radiusDouble?nullYour corner radius, used when the dashboard sets none (else 18).
maxRadiusDouble?nullThe largest radius your view can draw (at most 48).
originString?nullYour GraineConfig.baseUrl, for the logo's address.

Returns — NativeAppearance (NativeAppearance). Example value: NativeAppearance(accent=#318CE7, heading=Talk to our AI assistant, logoUrl=null, radius=18.0, logoPlate=ACCENT).

Errors — none.

val graine = GraineInApp.get()
val look = Appearance.resolve(graine.session?.appearance, Appearance.DEFAULT_ACCENT, "Assistant", origin = graine.config.baseUrl)

Appearance.isDark

Call it to decide light or dark for an appearance setting.

fun isDark(theme: String?, deviceDark: Boolean?): Boolean

"auto" (or none) follows the phone, not a default of the dashboard's.

ParameterTypeDefaultMeaning
themeString?required"light", "dark" or "auto".
deviceDarkBoolean?requiredWhether the phone is in dark mode; null when unknown (light).

Returns — Boolean — true for dark. Example value: true.

Errors — none.

val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val dark = Appearance.isDark("auto", night)

Appearance.palette

Call it for the colours to draw a component in, from the appearance settings.

fun palette(appearance: JSONObject?, deviceDark: Boolean?, accent: String?): WidgetPalette
ParameterTypeDefaultMeaning
appearanceJSONObject?requiredSessionConfig.appearance.
deviceDarkBoolean?requiredWhether the phone is in dark mode.
accentString?requiredThe accent, to choose a readable colour on it.

Returns — WidgetPalette (WidgetPalette). Example value: WidgetPalette(surface=#FFFFFF, border=#E5E4E0, text=#101318, muted=#5F6672, onAccent=#FFFFFF, mode=light).

Errors — none.

val session = GraineInApp.get().session ?: return
val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val palette = Appearance.palette(session.appearance, night, session.accent)

Appearance.luminance

Call it to know how light a colour is — to pick readable text on it.

fun luminance(hex: String): Double
ParameterTypeDefaultMeaning
hexStringrequiredA #rrggbb colour.

Returns — Double — its relative luminance (WCAG), 0 for black to 1 for white; 0 for anything that is not a colour. Example value: 0.2126.

Errors — none.

val light = Appearance.luminance("#318CE7") > 0.45

Appearance.argb

Call it to turn a hex colour into an Android colour int.

fun argb(hex: String?): Int?
ParameterTypeDefaultMeaning
hexString?required#rgb, #rrggbb or #aarrggbb, with or without the “#”.

Returns — Int? — the ARGB colour, or null for anything else. Example value: -13530905 (0xFF318CE7).

Errors — none.

val color = Appearance.argb("#318CE7") ?: Color.BLUE

Appearance.tint

Call it for a colour at partial strength — a badge or a selected row's fill.

fun tint(hex: String?, alpha: Double): Int
ParameterTypeDefaultMeaning
hexString?requiredA #rrggbb colour; a slate grey for anything else.
alphaDoublerequiredHow strong, 0–1.

Returns — Int — the ARGB colour. Example value: 0x1F318CE7.

Errors — none.

val fill = Appearance.tint("#318CE7", 0.12)

Appearance.encodeURIComponent

Call it to put a value into a web address the way a browser would.

fun encodeURIComponent(s: String): String

Everything but letters, digits and - _ . ! ~ * ' ( ) is escaped, as UTF-8.

ParameterTypeDefaultMeaning
sStringrequiredThe text to encode.

Returns — String. Example value: "a%20b%2Fc".

Errors — none.

val part = Appearance.encodeURIComponent("a b/c")

WidgetPalette

Read it for the colours to draw a component in.

data class WidgetPalette(val surface: String, val border: String, val text: String, val muted: String, val onAccent: String, val mode: String)

From Appearance.palette. Colours are #RRGGBB.

FieldTypeDefaultMeaning
surfaceStringrequiredThe component's background.
borderStringrequiredIts outlines.
textStringrequiredIts main text.
mutedStringrequiredIts secondary text.
onAccentStringrequiredText on the accent — chosen by how light the accent is, so a yellow accent still has a readable label.
modeStringrequired"light" or "dark", after resolving "auto" against the phone.

LogoPlate

Read it to know what the brand's logo sits on where it stands in for the launcher glyph.

enum class LogoPlate(val wire: String)

The dashboard picks it from the logo's pixels when the logo is uploaded, and the brand can change it (Appearance → Logo).

PropertyTypeMeaning
val wire: StringStringThe setting's word: accent or white.
CaseMeaning
ACCENTThe shape's own accent fill — for a white or light logo made for a coloured background. The default.
WHITEWhite with a hairline in the border colour — for a coloured or dark logo, or an opaque image.

Its functions: from — each with its parameters, what it returns, its errors and an example.

LogoPlate.from

Call it to read the logo-plate setting.

@JvmStatic
companion · fun from(value: Any?): LogoPlate

Exactly "white" is WHITE; anything else — "WHITE", a number, nothing — is ACCENT.

ParameterTypeDefaultMeaning
valueAny?requiredThe appearance's logoPlate value.

Returns — LogoPlate. Example value: LogoPlate.WHITE.

Errors — none.

val plate = LogoPlate.from(GraineInApp.get().session?.appearance?.opt("logoPlate"))

Use it to draw the brand's logo whole, on its plate — in a launcher or avatar of your own.

object Logo

The logo is fitted — whole, proportions kept, centred — into the largest square its shape holds, never cropped to fill it.

PropertyTypeMeaning
const val PLATE_WHITE = "#FFFFFF"StringA white plate's fill.
const val ON_WHITE = "#101318"StringA pill launcher's label colour on the white plate.
const val CLASSIC_HAIRLINE = "#E5E7EB"StringThe hairline under a white plate when there is no design.

Its functions: box, plate — each with its parameters, what it returns, its errors and an example.

Logo.box

Call it for the size of the square the logo is fitted into, inside a shape.

@JvmStatic
fun box(shape: String?, size: Double): Int

80 % of the shape for rounded-square and square; 70 % for the rest (the largest square a circle holds; a squircle is treated as one; a pill by its height) — rounded down, so its corners never poke out.

ParameterTypeDefaultMeaning
shapeString?requiredThe shape: circle, squircle, rounded-square, square or pill.
sizeDoublerequiredThe shape's size across (a pill: its height), in your units.

Returns — Int — the square's side, in the same units, at least 1. Example value: 39 (a 56 dp circle).

Errors — none.

val side = Logo.box("circle", 56.0)

Logo.plate (function)

Call it for what to draw under the logo: the fill, and an outline or none.

@JvmStatic @JvmOverloads
fun plate(plate: LogoPlate, accent: String, border: String, borderWidth: Double = 1.0): Plate

WHITE is white with an outline of at least 1 in border; ACCENT is the accent with no outline. Use ACCENT for anything other than the logo image: a glyph or a letter always sits on the accent colour.

ParameterTypeDefaultMeaning
plateLogoPlaterequiredThe logo's plate.
accentStringrequiredThe accent colour, #RRGGBB.
borderStringrequiredThe outline colour for a white plate.
borderWidthDouble1.0The outline's width.

Returns — Logo.Plate (Logo.Plate). Example value: Plate(fill=#FFFFFF, outline=Stroke(width=1.0, color=#E5E4E0)).

Errors — none.

val tokens = GraineInApp.get().themeTokens(this) ?: return
val plate = Logo.plate(LogoPlate.WHITE, tokens.colors.accent, tokens.colors.border)

Logo.Stroke (type)

Read it for an outline: its width and colour.

data class Stroke(val width: Double, val color: String)

FieldTypeDefaultMeaning
widthDoublerequiredIn the shape's units.
colorStringrequired#RRGGBB.

Logo.Plate (type)

Read it for what to draw under a logo.

data class Plate(val fill: String, val outline: Stroke?)

FieldTypeDefaultMeaning
fillStringrequiredThe plate's colour, #RRGGBB.
outlineStroke?requiredIts outline; null for none.

Going live

  • Debug logging. GraineConfig(debug = BuildConfig.DEBUG): in debug builds the SDK logs to logcat under the tag Graine — what the voice engine says, the call's steps — so “the agent did not speak” is answered in one read of it. Leave it off in release builds.
  • Allowed domains. app:// must be in the agent's Allowed domains (Agent → Embed & Widgets) for the app's requests to be accepted.
  • Google Play. On Android 14 and up, Play asks apps that declare USE_FULL_SCREEN_INTENT or the microphone foreground service to justify them; calls need both. An app that never uses calls removes them (Requirements). Ask for POST_NOTIFICATIONS during onboarding — without it the phone cannot ring.
  • R8 / ProGuard. The library's rules ship inside it; nothing to add.
  • Calls Graine rings need your push credentials uploaded in Workspace settings → In-app calling, and Firebase Cloud Messaging in the release build — the SDK registers for pushes itself; an app with its own messaging service keeps the two lines in it (Calls Graine rings).
  • WebRTC for calls. Keep ai.graine:inapp-android-rtc at the SDK's version, put its notices in your open-source licences screen, and watch calls.transportFallbackReason on calls that ran on the WebSocket — a network that blocks WebRTC shows as ice_timeout (Adding WebRTC calls).
  • The version. GraineVersion.NAME says which SDK build the app runs; it is also sent with the device registration and lands on the call record. Include it when you ask for support.

GraineVersion

Read it to know which version of the SDK your app runs — include it when you ask for support.

object GraineVersion

PropertyTypeMeaning
const val NAME: StringStringThe SDK's version: "0.10.3" in this release. It lands on the call record to tell builds apart; nothing secret.

Events and listeners

What the SDK tells your app as it happens. GraineListener (on an agent) carries the voice bar's call, the chat, actions, friction and errors; GraineCallListener (on calls) carries native calls. Every method has a default that does nothing, so you override only what you use — in Kotlin or Java. All are called on the main thread.

GraineInApp.addListener

Call it to start hearing an agent's events.

fun addListener(l: GraineListener)

Kept until removed. The SDK's own views add themselves; this is for your app.

ParameterTypeDefaultMeaning
lGraineListenerrequiredThe listener (GraineListener).

Returns — nothing.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onError(message: String) { Log.w("Graine", message) }
})

GraineInApp.removeListener

Call it to stop hearing an agent's events — when the screen that listens goes away.

fun removeListener(l: GraineListener)
ParameterTypeDefaultMeaning
lGraineListenerrequiredThe listener you added.

Returns — nothing.

Errors — none.

GraineInApp.get().removeListener(listener)

GraineListener

Implement it to hear an agent's events — the voice bar's call, the chat, actions, friction and errors.

interface GraineListener

What to expect — Implement only the methods you need; the others do nothing. Called on the main thread.

Its functions: onSessionReady, onCallState, onCaption, onWidget, onWidgetPending, onEnded, onMicDenied, onQuality, onNetwork, onReconnecting, onConnectProgress, onTransport, onConnectivity, onActionRequested, onActionCompleted, onFriction, onError, onChatState, onChatMessage — each with its parameters, what it returns, its errors and an example.

GraineListener.onSessionReady

Implement it to know the agent's settings have loaded — to offer what depends on them, such as booking.

fun onSessionReady(session: SessionConfig)

Once per launch, when the settings arrive (GraineInApp.session from then on).

ParameterTypeDefaultMeaning
sessionSessionConfigrequiredThe agent's settings (SessionConfig).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onSessionReady(session: SessionConfig) {
        callMeButton.visibility = if (GraineInApp.get().bookingAvailable) View.VISIBLE else View.GONE
    }
})

GraineListener.onCallState

Implement it to follow the voice bar's call in controls of your own.

fun onCallState(connected: Boolean, connecting: Boolean)

Fires when either changes.

ParameterTypeDefaultMeaning
connectedBooleanrequiredThe call is live.
connectingBooleanrequiredA call is being placed.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onCallState(connected: Boolean, connecting: Boolean) {
        talkButton.isEnabled = !connecting
    }
})

GraineListener.onCaption

Implement it to show what is being said on the voice bar's call.

fun onCaption(role: Role, text: String, live: Boolean)

A line being said, again as its words arrive (live) and once more when it is final. voice.captions keeps the last 12.

ParameterTypeDefaultMeaning
roleRolerequiredWho is speaking: AGENT or CUSTOMER.
textStringrequiredThe words so far.
liveBooleanrequiredtrue while the words are still arriving.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onCaption(role: Role, text: String, live: Boolean) {
        captionView.text = text
    }
})

GraineListener.onWidget

Implement it to draw the agent's components yourself, instead of with GraineWidgetView.

fun onWidget(widget: JSONObject)

The agent drew a component — on the voice-bar call or in the chat. Read it with GraineWidget.adopt, and answer it with GraineInApp.answerWidget, which answers whichever conversation is live.

ParameterTypeDefaultMeaning
widgetJSONObjectrequiredThe component as the agent sent it.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onWidget(widget: JSONObject) {
        GraineWidget.adopt(widget, GraineInApp.get().config.baseUrl)?.let { draw(it) }
    }
})

GraineListener.onWidgetPending

Implement it to show your own loader while a component is on its way.

fun onWidgetPending(hint: JSONObject)

A component is coming and its content is loading.

ParameterTypeDefaultMeaning
hintJSONObjectrequiredWhat is coming (its kind, for the loader's shape).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onWidgetPending(hint: JSONObject) {
        showComponentLoader()
    }
})

GraineListener.onEnded

Implement it to know the voice bar's call is over.

fun onEnded()

Once per call, however it ended.

Parameters — none.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onEnded() {
        hideCallControls()
    }
})

GraineListener.onMicDenied

Implement it to explain how to allow the microphone when the user refused it.

fun onMicDenied(reason: String)

The microphone was refused, or there is no usable one; the call does not start.

ParameterTypeDefaultMeaning
reasonStringrequiredNotAllowedError (refused) or NotFoundError (no usable microphone).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onMicDenied(reason: String) {
        showMicrophoneHelp()
    }
})

GraineListener.onQuality

Implement it to show the user when the network is the problem.

fun onQuality(quality: ConnectionQuality)

One word for the connection; fires only when the word changes.

ParameterTypeDefaultMeaning
qualityConnectionQualityrequiredThe connection now (ConnectionQuality).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onQuality(quality: ConnectionQuality) {
        weakBadge.visibility = if (quality == ConnectionQuality.POOR) View.VISIBLE else View.GONE
    }
})

GraineListener.onNetwork

Implement it to know the device went offline or came back.

fun onNetwork(online: Boolean)

As the voice engine sees it.

ParameterTypeDefaultMeaning
onlineBooleanrequiredtrue when back online.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onNetwork(online: Boolean) {
        offlineBanner.visibility = if (online) View.GONE else View.VISIBLE
    }
})

GraineListener.onReconnecting

Implement it to show “Reconnecting…” while a dropped call is re-established.

fun onReconnecting(active: Boolean)

The network interrupted the call and it is being re-established; not an end.

ParameterTypeDefaultMeaning
activeBooleanrequiredtrue while it is reconnecting.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onReconnecting(active: Boolean) {
        statusView.text = if (active) "Reconnecting…" else ""
    }
})

GraineListener.onConnectProgress

Implement it to say where a call being placed is, and when it is slow or could not connect.

fun onConnectProgress(stage: ConnectStage?, slow: Boolean, failed: Boolean)

Fires when any of the three changes; the same as voice.connectStage, slowConnect and callFailed.

ParameterTypeDefaultMeaning
stageConnectStage?requiredWhere the connect is (ConnectStage); null outside one.
slowBooleanrequiredThe user has waited past Timing.slowConnectMs: say “Still connecting…”.
failedBooleanrequiredThe call could not be connected at all: offer to try again.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onConnectProgress(stage: ConnectStage?, slow: Boolean, failed: Boolean) {
        if (failed) statusView.text = "Couldn’t connect · tap to retry"
    }
})

GraineListener.onTransport

Implement it to know which audio path carries the voice-bar call, and why it is not the one asked for.

fun onTransport(transport: Transport, requested: Transport, reason: String?, message: String?)

Fires when a call's path is settled.

ParameterTypeDefaultMeaning
transportTransportrequiredThe path carrying the call.
requestedTransportrequiredThe path asked for (your config, else the dashboard's).
reasonString?requiredWhy they differ, in a word — for example not_configured (high-quality audio is not enabled for the workspace) or unreachable; null when they do not.
messageString?requiredA sentence about it, for your logs.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onTransport(transport: Transport, requested: Transport, reason: String?, message: String?) {
        Log.d("Graine", "audio on ${transport.wire} ($reason)")
    }
})

GraineListener.onConnectivity

Implement it to follow the voice engine loading — to show a retry when it fails.

fun onConnectivity(state: Connectivity, attempt: Int, reason: String?)

Fires on every change. When every reload is spent, onError also says “The assistant could not load. Check your connection and try again.”

ParameterTypeDefaultMeaning
stateConnectivityrequiredLoading, ready, retrying or failed (Connectivity).
attemptIntrequiredHow many reloads have been made.
reasonString?requiredWhy it is retrying or failed, in a word — stalled, boot_timeout, load_cap, http-<status>, load-error:<code>, or session (the agent's settings could not load); null otherwise.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onConnectivity(state: Connectivity, attempt: Int, reason: String?) {
        retryButton.visibility = if (state == Connectivity.FAILED) View.VISIBLE else View.GONE
    }
})

GraineListener.onActionRequested

Implement it to know the agent asked for one of your actions — to log it, or show progress.

fun onActionRequested(name: String)

Before your handler runs.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onActionRequested(name: String) {
        Log.d("Graine", "action $name")
    }
})

GraineListener.onActionCompleted

Implement it to know what an action answered the agent.

fun onActionCompleted(name: String, result: ActionResult)

Your handler's answer, or the SDK's when it was late, threw or was not registered.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name.
resultActionResultrequiredWhat the agent was told (ActionResult).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onActionCompleted(name: String, result: ActionResult) {
        if (result.status != ActionResult.Status.OK) Log.w("Graine", result.message)
    }
})

GraineListener.onFriction

Implement it to react when the user looks stuck — badge a control, count it.

fun onFriction(signal: FrictionSignal)

On one screen, with a field outstanding, through a stretch of a live conversation. The agent has already been told.

ParameterTypeDefaultMeaning
signalFrictionSignalrequiredWhat was noticed (FrictionSignal).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onFriction(signal: FrictionSignal) {
        analytics.log("stuck", signal.screen)
    }
})

GraineListener.onError

Implement it to see misconfiguration — a key, an allowed domain — in your logs; never show it to the user.

fun onError(message: String)

Loud for you, invisible to the user. The reasons are listed under Errors.

ParameterTypeDefaultMeaning
messageStringrequiredWhat is wrong, in a sentence.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onError(message: String) {
        Log.w("Graine", message)
    }
})

GraineListener.onChatState

Implement it to follow the chat's connection in a chat screen of your own.

fun onChatState(connected: Boolean, connecting: Boolean)

The text conversation connected, is connecting or reconnecting, or is off; chat.reconnecting tells a reconnect from a first connect.

ParameterTypeDefaultMeaning
connectedBooleanrequiredThe chat is up.
connectingBooleanrequiredIt is being opened, or reopened after a drop.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onChatState(connected: Boolean, connecting: Boolean) {
        statusView.text = if (connected) "Online" else if (connecting) "Connecting…" else ""
    }
})

GraineListener.onChatMessage

Implement it to hear the chat's lines as they come — to badge a closed chat, or log them.

fun onChatMessage(role: Role, text: String, live: Boolean)

The agent's turn as it streams in (live, the words so far), then once more when it is complete; the user's line when they send it. Restored lines are not repeated here.

ParameterTypeDefaultMeaning
roleRolerequiredWho said it.
textStringrequiredThe words so far.
liveBooleanrequiredtrue while the agent's words are still arriving.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().addListener(object : GraineListener {
    override fun onChatMessage(role: Role, text: String, live: Boolean) {
        if (role == Role.AGENT && !live && !chatOpen) badgeChat()
    }
})

GraineCalls.addListener

Call it to start hearing native calls' events.

fun addListener(l: GraineCallListener)

Kept until removed. One engine for the whole app: you hear every call, whichever agent it is with.

ParameterTypeDefaultMeaning
lGraineCallListenerrequiredThe listener (GraineCallListener).

Returns — nothing.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onCallOutcome(callId: String, outcome: GraineCallOutcome) { server.report(callId, outcome.wire, outcome.reason) }
})

GraineCalls.removeListener

Call it to stop hearing native calls' events.

fun removeListener(l: GraineCallListener)
ParameterTypeDefaultMeaning
lGraineCallListenerrequiredThe listener you added.

Returns — nothing.

Errors — none.

GraineInApp.get().calls.removeListener(callListener)

GraineCallListener

Implement it to hear native calls: rings, state, outcomes, captions and controls.

interface GraineCallListener

What to expect — Implement only the methods you need; the others do nothing. Called on the main thread.

Its functions: onIncomingCall, onCallState, onCallOutcome, onCallCaption, onAgentSpeaking, onCallControls — each with its parameters, what it returns, its errors and an example.

GraineCallListener.onIncomingCall

Implement it to know an agent's call started ringing — for your own interface or analytics.

fun onIncomingCall(call: GraineCall)

The SDK shows the ring itself.

ParameterTypeDefaultMeaning
callGraineCallrequiredThe ringing call (GraineCall).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onIncomingCall(call: GraineCall) {
        analytics.log("ring", call.callId)
    }
})

GraineCallListener.onCallState

Implement it to drive a call screen of your own.

fun onCallState(state: GraineCallState, call: GraineCall?)

The call moved.

ParameterTypeDefaultMeaning
stateGraineCallStaterequiredWhere the call is now (GraineCallState).
callGraineCall?requiredThe call; null once it is back to IDLE.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onCallState(state: GraineCallState, call: GraineCall?) {
        render(state, call)
    }
})

GraineCallListener.onCallOutcome

Implement it to send what became of each call to your server.

fun onCallOutcome(callId: String, outcome: GraineCallOutcome)

Once per call (an answered call also reports how it ended).

ParameterTypeDefaultMeaning
callIdStringrequiredThe call's ID: your server's call_id, Graine's for a ring it sent, or the one startCall returned.
outcomeGraineCallOutcomerequiredWhat became of it (GraineCallOutcome).

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onCallOutcome(callId: String, outcome: GraineCallOutcome) {
        server.report(callId, outcome.wire, outcome.reason, outcome.durationSeconds)
    }
})

GraineCallListener.onCallCaption

Implement it to show what is being said on the call.

fun onCallCaption(role: Role, text: String, live: Boolean)

The agent's words as they are said (live), then the line once its turn is complete; the user's likewise.

ParameterTypeDefaultMeaning
roleRolerequiredWho is speaking.
textStringrequiredThe words so far.
liveBooleanrequiredtrue while the line is still arriving.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onCallCaption(role: Role, text: String, live: Boolean) {
        captionView.text = text
    }
})

GraineCallListener.onAgentSpeaking

Implement it to animate the agent's avatar while its voice is heard.

fun onAgentSpeaking(speaking: Boolean)

Called each time the agent starts or stops being heard.

ParameterTypeDefaultMeaning
speakingBooleanrequiredtrue while it is heard.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onAgentSpeaking(speaking: Boolean) {
        avatar.animate().scaleX(if (speaking) 1.06f else 1f)
    }
})

GraineCallListener.onCallControls

Implement it to keep your own mute and loudspeaker controls in step.

fun onCallControls(muted: Boolean, speaker: Boolean)

Muted or the loudspeaker changed — from your app, or from the call screen.

ParameterTypeDefaultMeaning
mutedBooleanrequiredThe user's voice is not being sent.
speakerBooleanrequiredThe loudspeaker is on.

Returns — your implementation returns nothing; the SDK calls it on the main thread when it happens.

Errors — none.

GraineInApp.get().calls.addListener(object : GraineCallListener {
    override fun onCallControls(muted: Boolean, speaker: Boolean) {
        muteButton.isSelected = muted
    }
})

Errors

A request that asks Graine on the user's behalf answers its callback with a GraineError carrying a code. Branch on the code; show the message, which is written for the user and never carries the identifier, traits or a push token. Misconfiguration of the voice bar and chat arrives in GraineListener.onError instead, and calling the SDK before init throws IllegalStateException — a mistake in the app, not a refusal.

GraineError

Read it in a callback to decide what to do about a refused request.

data class GraineError @JvmOverloads constructor(val code: GraineErrorCode, val message: String, val retryAfterSeconds: Int? = null)

What to expect — Passed by every request that asks Graine. A refusal made on the device before anything was sent carries the same codes.

FieldTypeDefaultMeaning
codeGraineErrorCoderequiredBranch on it: the reason in one stable word (GraineErrorCode).
messageStringrequiredShow it: Graine's sentence (or the SDK's), fit for the user.
retryAfterSecondsInt?nullUse it to wait before trying again: how long Graine asked, with RATE_LIMITED; null when it did not say.

GraineErrorCode

Read it to branch on why a request was refused.

sealed class GraineErrorCode(val wire: String)

What to expect — A code newer than this SDK arrives as Unknown(raw), carrying its word: give every when an else.

PropertyTypeMeaning
val wire: StringStringGraine's word for the code — too_soon, network, … — for logs.
companion · val KNOWN: List<GraineErrorCode>List<GraineErrorCode>Every code this build knows.
CaseMeaning
INVALID_KEYThe publishable key is not one, or belongs to no agent.
ORIGIN_REFUSEDThe agent does not allow apps yet: add app:// to its Allowed domains.
RATE_LIMITEDToo many requests: wait retryAfterSeconds when Graine said how long.
INVALID_REQUESTA field is missing or malformed — a blank call ID, say.
IDENTITY_REQUIREDThe request needs a signed-in user: identify() with the trait the agent's Who is the user? setting names.
DEVICE_LIMITThe user has already reached the agent's limit of devices.
SCHEDULING_OFFBooking is switched off for this agent (Calls & scheduling).
TOO_SOONEarlier than schedulingRules.earliest(now).
TOO_FARLater than schedulingRules.latest(now).
OUTSIDE_WINDOWOutside the agent's calling hours.
TOO_MANY_PENDINGThe user already has the most calls waiting that the agent allows.
NOT_FOUNDNo such booking for this user or install.
UNAVAILABLEGraine could not answer just now. Worth trying again later.
NETWORKNothing came back: the phone is offline, or Graine could not be reached. Worth trying again.
Unknown(raw)A code this build does not know, as Graine sent it.

Its functions: from — each with its parameters, what it returns, its errors and an example.

GraineErrorCode.from

Call it to read an error word — from your server, say — into a GraineErrorCode.

@JvmStatic
companion · fun from(wire: String): GraineErrorCode

Case and surrounding spaces do not matter.

ParameterTypeDefaultMeaning
wireStringrequiredThe word, such as "too_soon".

Returns — GraineErrorCode — a known code, or Unknown(wire). Example value: GraineErrorCode.TOO_SOON.

Errors — none.

val code = GraineErrorCode.from("rate_limited")

GraineCallsHostException

Catch it only if you build a React Native or Flutter bridge yourself: it names a wrong call option.

class GraineCallsHostException(val code: String, message: String) : IllegalArgumentException(message)

Thrown by GraineCallsHost.configure when an option is wrong, with a sentence that names it (“displayName is required”, “ringTimeoutMs must be a number”). A native Android app never sees it.

PropertyTypeMeaning
val code: StringStringThe platform error's code: invalid_options.
messageStringThe sentence naming the option that is wrong — for the developer.
CodewireHTTPMeansRaised byFix
INVALID_KEYinvalid_key401The publishable key is not one, or belongs to no agentEvery request to GraineCopy the key again from Agent → Embed & Widgets → Install
ORIGIN_REFUSEDorigin_refused403app:// is missing from the agent's Allowed domainsEvery requestAdd app://
RATE_LIMITEDrate_limited429Too many requests from this install, key or addressEvery requestWait retryAfterSeconds, then retry
INVALID_REQUESTinvalid_request400A field is missing or malformed — a time without its offset, a reason over 120 characters, a blank IDscheduleCall, cancelScheduledCall, device registrationFix the request
IDENTITY_REQUIREDidentity_required400No user is identified, or the identifier is a placeholdermemories, forgetMemory, bookings that need a useridentify() with the identifier first
DEVICE_LIMITdevice_limit403This install is not one of the user's allowed devicesmemories, forgetMemory, newConversation, scheduleCallSign out on another device, raise Devices per user, or Reset devices for the user
SCHEDULING_OFFscheduling_off403 (on the device first)Users may not book from the appscheduleCallTurn it on, or hide booking when bookingAvailable is false
TOO_SOONtoo_soon400 (on the device first)Earlier than earliest(now)scheduleCallOffer times from earliest(now)
TOO_FARtoo_far400 (on the device first)Later than latest(now)scheduleCallOffer times up to latest(now)
OUTSIDE_WINDOWoutside_window400The time falls outside the agent's calling hours, in the user's time zonescheduleCallShow the message (it names a time that works); offer another time
TOO_MANY_PENDINGtoo_many_pending409The user already has the most calls waitingscheduleCallCancel one first
NOT_FOUNDnot_found404No such booking for this user or installcancelScheduledCallRead scheduledCalls() again
UNAVAILABLEunavailable503 (504 on a timeout)Graine could not answer just now — including a booking refused because the agent is switched off or out of creditEvery requestRetry with backoff; check the agent is on and the workspace has credit
NETWORKnetwork—No answer came back: offline, or the connection droppedEvery requestRetry when online
Unknown(raw)the wordotherA code newer than the SDKEvery requestLog the code and message; show the message

When the voice bar or chat cannot start, the reason arrives through GraineListener.onError, not as a coded error: key_missing, key_malformed (publishable keys start with pk_live_), key_unknown (copy the key again); no_origin (add app://); no_domains_configured (the list is empty); origin_not_allowed; not_configured (high-quality audio is not enabled for the workspace; calls use the standard path); and, when every reload of the voice engine is spent, “The assistant could not load. Check your connection and try again.”. A refused microphone arrives as onMicDenied("NotAllowedError") (or NotFoundError for no usable microphone). A native call that cannot go on is not an error at all: it is its outcome (GraineCallOutcome).

Rate limits and quotas

LimitValueWhere it applies
Bookings from one install10 an hourscheduleCall, the booking sheet, and the agent's bookings in that install's conversations
Bookings in a workspace1,000 an hourAll in-app bookings
Calls waiting per user3 by default (1–10)At most this many upcoming calls per user
Devices per user2 by default (1–5)Devices per user; idle devices free their place after 30 days
Rings per user per day3 by default (1–10)Ring each user at most, in the user's own day
Quiet period after a decline6 hoursNo ring for that user
Notice and horizon5 minutes (2–1440) · 14 days (1–30)With at least this much notice · Booked at most this far ahead
Ring time · re-ring45 s (20–60) · once after 10 min (0–120)Ring for · After a missed call, ring once more after
Lateness10 minutesA booked call more than 10 minutes late is not rung
Booking reason · variables120 characters · 12 variables, about 1 KBBookings and calls
Memories listedAt most 100, newest firstmemories()
Conversation kept50 messages, 7 days after the lastConversations that continue
Requests per key and address, per minuteStarting a conversation 30 (and 300 per key); high-quality audio sessions 10 (120 per key); booking, listing, cancelling, device registration, memory list and sign-out 30 (300 per key); call outcomes 60 (600 per key); forgetting memory 10 (120 per key); listing memories also 30 per installAnswered RATE_LIMITED with Retry-After
Action handlers · component data6 seconds · 8 secondsAfter that, the agent is told the action was not done; a component shows its authored content

Lower-level building blocks

Public so a custom interface — or a React Native or Flutter bridge — can reuse them; an integration does not need them.

GraineCallsHost

Use it only to build a React Native or Flutter bridge to the native call engine; a native Android app uses GraineInApp.get().calls.

object GraineCallsHost

The one object Graine's React Native and Flutter call packages drive the native calls through, in plain values — strings, numbers, booleans, null, lists and string-keyed maps. The packages only pass values; every rule — the options, the snapshot, where events go, how actions are answered — is here. Several agents: configure names the others under agents, startCall takes {agent}, and the snapshot and the incoming, outcome and action events say which agent (agent, "default" for the default one).

What to expect — Main thread: a call from another thread moves there first (configure, restore, snapshot, handlePush, startCall, addSink and needsMicrophonePermission wait for their answer), and events are sent on it. Events: state (the snapshot, at most once per turn of the main loop, only when it changed), outcome, incoming and action.

Its functions: restore, configure, snapshot, enableVoIPPushes, handlePush, onNewToken, startCall, answer, decline, end, setMuted, setSpeaker, setIdentity, setMemory, installId, setVariables, setScreen, setActions, resolveAction, addSink, removeSink, onRequestPermissionsResult, onNewIntent, needsMicrophonePermission, microphoneDenied — each with its parameters, what it returns, its errors and an example. Permissions and the setup check, for a bridge, are below.

GraineCallsHost: permissions and the setup check

Use them only from a bridge: GraineCalls' permissions and setup check, as plain values.

@JvmStatic fun callPermissions(context: Context): Map<String, Any?>
@JvmStatic fun requestCallPermissions(context: Context, activity: Activity?, done: (Map<String, Any?>) -> Unit)
@JvmStatic fun openCallSettings(context: Context, activity: Activity?, item: Any?): Boolean
@JvmStatic fun callSetupProblems(): List<Map<String, Any?>>
FunctionWhat it does
GraineCallsHost.callPermissions(context){microphone, notifications, fullScreen}, each granted, denied, blocked or notNeeded — safe before anything is configured.
GraineCallsHost.requestCallPermissions(context, activity, done)Asks, over activity, for what the system's dialog can still ask for, from the SDK's own transparent screen; done gets the same map after, on the main thread.
GraineCallsHost.openCallSettings(context, activity, item)Opens Settings for microphone, notifications or fullScreen (full_screen and any case too); false for any other word.
GraineCallsHost.callSetupProblems()[{code, message}]: what the app's setup still lacks — empty when nothing is, and before anything is configured.

Returns — as above. Errors — none.

val missing = GraineCallsHost.callSetupProblems()   // [{code=no_push_token, message=…}]

GraineCallsHost.restore

Call it at a cold start, before any JavaScript or Dart runs, to apply the options the last configure kept.

@JvmStatic
fun restore(context: Context): Boolean

So a launch woken by a push can ring before your bridge has configured anything. handlePush does this by itself.

ParameterTypeDefaultMeaning
contextContextrequiredAny context of your app.

Returns — Boolean — true when something was restored; false when nothing was kept, or this launch is already configured. Example value: true.

Errors — none.

GraineCallsHost.restore(applicationContext)

GraineCallsHost.configure

Call it from your bridge to configure the calls, and keep the options for the next cold start.

@JvmStatic
fun configure(context: Context, options: Map<*, *>?)

The first call makes the SDK's agent (GraineInApp.init); later ones update the calls. The options:

OptionTypeDefaultMeaning
publishableKeystringrequiredThe default agent's publishable key.
displayNamestringrequiredThe name on the ring and the call screen when the push names nobody.
baseUrlstring"https://www.graine.ai"Where Graine is.
ringtoneSoundstring or nullnullA res/raw sound name for the ring.
ringTimeoutMsnumber30000How long a call from your server rings; anything not above 0 is the default.
appVersionstring or nullnullLands on the call record.
callScreen"sdk" or "app""sdk"Who draws the call while the app is open: the SDK's call screen, or your app's own.
callTransport"auto", "websocket" or "webrtc""auto"How calls carry their audio (GraineCallsConfig.callTransport); WebRTC needs the WebRTC module in the app. Exactly these words: anything else is refused.
debugbooleanfalseThe SDK's debug log (GraineConfig.debug).
android{ smallIcon }noneA drawable name for the status-bar icon.
ios{ iconTemplate, includesCallsInRecents }noneThe iOS options, checked here too so one set of options is valid on both platforms.
agents{ name: { publishableKey, baseUrl? } }noneAny further agents the app uses, each with its own publishable key.

Unknown keys are ignored and null is the same as absent; a known key of the wrong type is refused, naming it.

ParameterTypeDefaultMeaning
contextContextrequiredAny context of your app.
optionsMap<*, *>?requiredThe options above.

Returns — nothing.

Errors — GraineCallsHostException (invalid_options) naming the option that is wrong — “publishableKey is required”, “callScreen must be "sdk" or "app"”, “callTransport must be "auto", "websocket" or "webrtc"”.

GraineCallsHost.configure(context, mapOf("publishableKey" to "pk_live_…", "displayName" to "Acme"))

GraineCallsHost.snapshot

Call it to draw the call screen in your bridge's interface.

@JvmStatic
fun snapshot(): Map<String, Any?>

The call as a call screen draws it, every key always present: state (idle, ringing, connecting, active, ended), callId, direction (incoming, outgoing), reason, callerName, muted, speakerOn, agentSpeaking, caption ({role, text, live}), connectedAt (milliseconds since the epoch), error (why the last call failed), agent, transport (webrtc or websocket, once decided) and transportFallbackReason (why a call that would have used WebRTC runs on the WebSocket). With no call, everything but error is empty.

Parameters — none.

Returns — Map<String, Any?>. Example value: {state=active, callId=call_6f1c…, direction=outgoing, reason=billing_help, callerName=null, muted=false, speakerOn=false, agentSpeaking=true, caption={role=agent, text=Sure…, live=true}, connectedAt=1792049400000, error=null, agent=default, transport=webrtc, transportFallbackReason=null}.

Errors — none.

val call = GraineCallsHost.snapshot()

GraineCallsHost.enableVoIPPushes

Nothing to do on Android: iOS registers for VoIP pushes here, and Android apps hand their pushes to handlePush.

@JvmStatic
fun enableVoIPPushes()

Parameters — none.

Returns — nothing.

Errors — none.

GraineCallsHost.enableVoIPPushes()   // does nothing on Android

GraineCallsHost.handlePush

Call it from a messaging service of your app's own with each push's data, before any JavaScript or Dart runs. Without one, the SDK's own messaging service hands every push to the engine itself.

@JvmStatic
fun handlePush(context: Context, data: Map<*, *>?): Boolean

When the push woke the app and nothing has set up calls yet in this launch, it first applies the call options saved on the device, so the call still rings.

ParameterTypeDefaultMeaning
contextContextrequiredThe messaging service.
dataMap<*, *>?requiredThe push's data, {String: String}, as received.

Returns — Boolean — true when it was a call (ringing, refused as busy, or malformed) and the app should do nothing more with it. Example value: true.

Errors — none.

override fun onMessageReceived(message: RemoteMessage) {
    if (GraineCallsHost.handlePush(this, message.data)) return
}

GraineCallsHost.onNewToken

Call it from a messaging service of your app's own with each new token. Without one, the SDK receives each token itself.

@JvmStatic
fun onNewToken(context: Context, token: String)

The same as GraineCalls.onNewToken. Safe before anything is configured.

ParameterTypeDefaultMeaning
contextContextrequiredThe messaging service.
tokenStringrequiredThe new Firebase Cloud Messaging token.

Returns — nothing.

Errors — none.

override fun onNewToken(token: String) = GraineCallsHost.onNewToken(this, token)

GraineCallsHost.startCall

Call it from your bridge when the user calls the agent.

@JvmStatic
fun startCall(activity: Activity?, options: Map<*, *>?): String?
ParameterTypeDefaultMeaning
activityActivity?requiredThe activity in front — the call's microphone may only start from there.
optionsMap<*, *>?required{reason?, variables?, agent?}: why the user calls (the agent's call_reason), prompt variables for this call, and which configured agent (the default one when left out).

Returns — String? — the call's ID; null when the calls are not configured, a call is live, there is no activity, or no agent has that name. Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — none.

val callId = GraineCallsHost.startCall(activity, mapOf("reason" to "billing_help"))

GraineCallsHost.answer

Call it from your bridge's call screen to pick up the ringing call.

@JvmStatic
fun answer()

Parameters — none.

Returns — nothing.

Errors — none.

GraineCallsHost.answer()

GraineCallsHost.decline

Call it from your bridge's call screen to decline the ringing call.

@JvmStatic
fun decline()

Parameters — none.

Returns — nothing.

Errors — none.

GraineCallsHost.decline()

GraineCallsHost.end

Call it from your bridge's hang-up control.

@JvmStatic
fun end()

Hangs up — or, while it rings, declines.

Parameters — none.

Returns — nothing.

Errors — none.

GraineCallsHost.end()

GraineCallsHost.setMuted

Call it from your bridge's mute control.

@JvmStatic
fun setMuted(muted: Boolean)
ParameterTypeDefaultMeaning
mutedBooleanrequiredtrue to stop sending the user's voice, false to send it again.

Returns — nothing.

Errors — none.

GraineCallsHost.setMuted(true)

GraineCallsHost.setSpeaker

Call it from your bridge's loudspeaker control.

@JvmStatic
fun setSpeaker(on: Boolean)
ParameterTypeDefaultMeaning
onBooleanrequiredtrue for the loudspeaker; false for a headset or the earpiece.

Returns — nothing.

Errors — none.

GraineCallsHost.setSpeaker(true)

GraineCallsHost.setIdentity

Call it from your bridge when the user signs in or out.

@JvmStatic
fun setIdentity(traits: Map<*, *>?)

The raw traits, as GraineInApp.identify takes them: the SDK masks them and reads the identity itself. null signs out.

ParameterTypeDefaultMeaning
traitsMap<*, *>?requiredWho the user is, by trait name.

Returns — nothing.

Errors — none.

GraineCallsHost.setIdentity(mapOf("user_id" to "u_8812", "name" to "Asha"))

GraineCallsHost.setMemory

Call it from your bridge when the user changes their memory choice.

@JvmStatic
fun setMemory(enabled: Boolean)

GraineInApp.setMemory. Before the SDK's agent exists, the choice is held and applied when it is made.

ParameterTypeDefaultMeaning
enabledBooleanrequiredtrue to remember this user's conversations, false to stop.

Returns — nothing.

Errors — none.

GraineCallsHost.setMemory(true)

GraineCallsHost.installId

Call it for this install's ID — the one ID the native SDK, React Native and Flutter share on a device.

@JvmStatic
fun installId(context: Context): String

GraineInApp.installId. Safe before anything is configured.

ParameterTypeDefaultMeaning
contextContextrequiredAny context of your app.

Returns — String — a UUID. Example value: "9b2e7c4a-1f3d-4e8a-b6c5-0d7f2a9e4b13".

Errors — none.

val id = GraineCallsHost.installId(context)

GraineCallsHost.setVariables

Call it from your bridge to set the prompt variables your app knows.

@JvmStatic
fun setVariables(values: Map<*, *>?)

The variables become exactly this map: a key the last map set and this one leaves out is removed.

ParameterTypeDefaultMeaning
valuesMap<*, *>?requiredPrompt variables by name; null clears them.

Returns — nothing.

Errors — none.

GraineCallsHost.setVariables(mapOf("plan" to "gold"))

GraineCallsHost.setScreen

Call it from your bridge whenever what the user can see changes.

@JvmStatic
fun setScreen(screen: Map<*, *>?)

The screen as {screen, title?, journey?, fields?, error?, data?, availableActions?} — the fields of GraineScreen. null, or no screen name, clears it.

ParameterTypeDefaultMeaning
screenMap<*, *>?requiredWhat the user can see now.

Returns — nothing.

Errors — none.

GraineCallsHost.setScreen(mapOf("screen" to "checkout_address", "title" to "Delivery address"))

GraineCallsHost.setActions

Call it from your bridge to say which actions your JavaScript or Dart answers.

@JvmStatic
fun setActions(names: List<*>?, sink: Int?)

Each request for one of them is sent as an action event to the sink that last listed a non-empty set, and waits for resolveAction — within the SDK's own six seconds. Names no longer listed are unregistered.

ParameterTypeDefaultMeaning
namesList<*>?requiredThe action names your code answers.
sinkInt?requiredYour sink's token, from addSink.

Returns — nothing.

Errors — none.

GraineCallsHost.setActions(listOf("set_language"), sinkToken)

GraineCallsHost.resolveAction

Call it from your bridge with the answer to an action request.

@JvmStatic
fun resolveAction(requestId: String, result: Map<*, *>?)

The first answer wins; a later one is dropped.

ParameterTypeDefaultMeaning
requestIdStringrequiredThe request's requestId, from the action event.
resultMap<*, *>?required{status: "ok" | "refused" | "error", message?, data?}.

Returns — nothing.

Errors — none.

GraineCallsHost.resolveAction(requestId, mapOf("status" to "ok", "message" to "Done."))

GraineCallsHost.addSink

Call it from your bridge to start receiving the calls' events.

@JvmStatic
fun addSink(sink: Sink): Int

Outcomes and rings that came while nothing listened (the last 20) are delivered to the first sink at once; the state never waits — read snapshot(). An action goes only to the sink that last listed actions.

ParameterTypeDefaultMeaning
sinkSinkrequiredReceives each event (GraineCallsHost.Sink).

Returns — Int — the sink's token, for setActions and removeSink. Example value: 1.

Errors — none.

val token = GraineCallsHost.addSink { event, payload -> bridge.emit(event, payload) }

GraineCallsHost.removeSink

Call it from your bridge to stop receiving events.

@JvmStatic
fun removeSink(token: Int)

Any action request it was still holding unanswered is refused, so the agent is not left waiting.

ParameterTypeDefaultMeaning
tokenIntrequiredThe token addSink returned.

Returns — nothing.

Errors — none.

GraineCallsHost.removeSink(token)

GraineCallsHost.onRequestPermissionsResult

Call it from the activity that draws the call, when your app draws it (callScreen: "app").

@JvmStatic
fun onRequestPermissionsResult(requestCode: Int, grantResults: IntArray): Boolean

The SDK asks for the microphone from that screen; forwarding the answer lets the call go ahead.

ParameterTypeDefaultMeaning
requestCodeIntrequiredAs Android passes it.
grantResultsIntArrayrequiredAs Android passes them.

Returns — Boolean — true when the request was the SDK's. Example value: true.

Errors — none.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    if (!GraineCallsHost.onRequestPermissionsResult(requestCode, grantResults))
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

GraineCallsHost.onNewIntent

Call it from the activity that draws the call's onNewIntent, when your app draws it (callScreen: "app").

@JvmStatic
fun onNewIntent(activity: Activity, intent: Intent?): Boolean

The ring's Answer opens that activity with the call in its intent; an activity already running (single-top) receives it as a new intent instead, which this makes its own intent.

ParameterTypeDefaultMeaning
activityActivityrequiredThe activity that draws the call.
intentIntent?requiredThe new intent.

Returns — Boolean — true when it was a call's. Example value: true.

Errors — none.

override fun onNewIntent(intent: Intent) { super.onNewIntent(intent); GraineCallsHost.onNewIntent(this, intent) }

GraineCallsHost.needsMicrophonePermission

Call it before placing or answering a call, when your app draws the call and cannot hear a permission answer in its own activity.

@JvmStatic
fun needsMicrophonePermission(context: Context): Boolean
ParameterTypeDefaultMeaning
contextContextrequiredAny context of your app.

Returns — Boolean — true when your app draws the call (callScreen: "app") and has no microphone permission yet: ask first. Example value: false.

Errors — none.

if (GraineCallsHost.needsMicrophonePermission(context)) askForMicrophone()

GraineCallsHost.microphoneDenied

Call it when the user refused the microphone a call needed.

@JvmStatic
fun microphoneDenied()

No call is placed or answered, and the snapshot's error says “The microphone permission was denied.”.

Parameters — none.

Returns — nothing.

Errors — none.

GraineCallsHost.microphoneDenied()

GraineCallsHost.Sink

Implement it in your bridge to receive the calls' events.

fun interface Sink

What to expect — Main thread. Events: state (the snapshot), outcome ({callId, outcome, reason?, error?, durationSeconds?, hosted?, agent?}), incoming ({callId, reason, callerName, hosted?, agent?}) and action ({requestId, name, arguments, agent?}).

Its functions: onEvent — each with its parameters, what it returns, its errors and an example.

GraineCallsHost.Sink.onEvent

Implement it to pass each event to your JavaScript or Dart.

fun onEvent(event: String, payload: Map<String, Any?>)
ParameterTypeDefaultMeaning
eventStringrequiredstate, outcome, incoming or action.
payloadMap<String, Any?>requiredWhat the event carries, in plain values.

Returns — nothing.

Errors — none: a sink that throws is logged, and the other sinks still hear the event.

val sink = GraineCallsHost.Sink { event, payload -> channel.invokeMethod(event, payload) }

Placement

Use it to place a draggable bar of your own the way the voice bar is placed.

object Placement

The bar is anchored to the bottom; the user drags it, and the result is an offset above that anchor, held so the bar never leaves the screen. Units are yours; the SDK passes dp, so an offset kept on one phone means the same on another.

PropertyTypeMeaning
const val DRAG_SLOP = 8.0DoubleA drag that moved less than this in both directions is a tap — and the tap must still work.

Its functions: clampOffset, offsetDuringDrag, isTap — each with its parameters, what it returns, its errors and an example.

Placement.Bounds

Pass it to Placement's functions to say how much room the bar has.

data class Bounds(val windowHeight: Double, val baseBottom: Double, val barHeight: Double, val topMargin: Double = 48.0)

FieldTypeDefaultMeaning
windowHeightDoublerequiredThe screen's height.
baseBottomDoublerequiredWhere the bar sits with no offset.
barHeightDoublerequiredThe bar's height as last measured; 0 before the first layout (64 is assumed).
topMarginDouble48.0Room to keep clear at the top — the status bar, a notch.

Placement.clampOffset

Call it to hold an offset to where the bar can be.

fun clampOffset(offset: Double, b: Bounds): Int
ParameterTypeDefaultMeaning
offsetDoublerequiredThe offset above the anchor.
bBoundsrequiredThe room the bar has.

Returns — Int — the offset, between 0 and the highest the bar can go; 0 for a value that is not a number. Example value: 120.

Errors — none.

val offset = Placement.clampOffset(saved.toDouble(), bounds)

Placement.offsetDuringDrag

Call it while the user drags, for the bar's offset now.

fun offsetDuringDrag(startOffset: Double, dy: Double, b: Bounds): Int

Where the drag started plus how far the finger moved up (dy grows downward), held to the screen.

ParameterTypeDefaultMeaning
startOffsetDoublerequiredThe offset when the drag began.
dyDoublerequiredHow far the finger has moved, downward positive.
bBoundsrequiredThe room the bar has.

Returns — Int — the offset. Example value: 80.

Errors — none.

val offset = Placement.offsetDuringDrag(startOffset, (event.rawY - startY).toDouble(), bounds)

Placement.isTap

Call it when the finger lifts, to know whether it was a tap rather than a drag.

fun isTap(dx: Double, dy: Double): Boolean
ParameterTypeDefaultMeaning
dxDoublerequiredHow far the finger moved across.
dyDoublerequiredHow far it moved down.

Returns — Boolean — true when it moved less than DRAG_SLOP both ways. Example value: true.

Errors — none.

if (Placement.isTap((event.rawX - startX).toDouble(), (event.rawY - startY).toDouble())) bar.performClick()

KeyboardLift

Use it to keep a bottom-anchored view of your own above the keyboard.

object KeyboardLift

How far it must rise: the keyboard's height less whatever the window already shrank — right under adjustResize, adjustPan and edge-to-edge alike.

Its functions: liftFor — each with its parameters, what it returns, its errors and an example.

KeyboardLift.liftFor

Call it when the keyboard shows or hides, for how far to raise your view.

fun liftFor(keyboardHeight: Double, windowShrink: Double): Int
ParameterTypeDefaultMeaning
keyboardHeightDoublerequiredThe keyboard's height (0 when hidden).
windowShrinkDoublerequiredHow much the window already shrank for it.

Returns — Int — how far to raise it, never below 0. Example value: 280.

Errors — none.

view.translationY = -KeyboardLift.liftFor(imeHeight.toDouble(), shrink.toDouble()).toFloat()

WidgetLook

Read it for a component's look with every default applied — to draw one in a view of your own.

data class WidgetLook(…)

The preset's defaults, with the component's own choices on top.

FieldTypeDefaultMeaning
presetStringrequiredgallery, cards, list, spotlight or compact.
imageSizeStringrequiredsmall, medium or large.
densityStringrequiredcomfortable or compact.
buttonStringrequiredfilled, outline or row.
factsBooleanrequiredWhether rows show their extra facts.
columnsIntrequired1, or 2 for gallery and cards when chosen.
surfaceStringrequiredWhere it goes — screen, sheet or card — auto already decided by the kind.
radiusInt?nullIts own corner rounding, when it has one.
accentString?nullIts own accent, when it has one.
platformStringrequiredWhich phone's shapes: as chosen; auto is decided when it is drawn.
selectStringrequiredone or many.
maxSelectIntrequiredHow many may be picked with many (3 by default).

PlatformGrammar

Read it for the shapes people on a given phone already know — to draw a component in that phone's style.

data class PlatformGrammar(…)

What a platform's design style changes; the colours stay the brand's. radius is used only for a component pinned to a platform; under auto the design's radius wins.

FieldTypeDefaultMeaning
radiusIntrequiredA card's corner radius, in dp.
chipRadiusIntrequiredA chip's corner radius (999 for fully round).
buttonRadiusIntrequiredA button's corner radius (999 for fully round).
pillButtonsBooleanrequiredWhether buttons are pills.
separatorInsetIntrequiredHow far row separators are inset, in dp.
grabberWidthIntrequiredA sheet's drag handle width, in dp.
grabberHeightIntrequiredIts height, in dp.
titleWeightIntrequiredTitles' weight: 500, 600 or 700.
padBoostIntrequiredExtra padding the style adds, in dp.
chipOutlinedBooleanrequiredWhether chips are outlined rather than filled.

SelectCall

Read it for the action one tap on a component runs, with its arguments filled in.

data class SelectCall(val name: String, val args: Map<String, String>)

FieldTypeDefaultMeaning
nameStringrequiredThe registered action to run (open_screen for a path).
argsMap<String, String>requiredIts arguments, filled from the tapped item; a path travels as path.

Notes for Java

Every entry point works from Java with only the arguments you need. The companion functions are static (@JvmStatic) and the functions with defaults have Java overloads (@JvmOverloads), so GraineInApp.init(application, config), GraineInApp.get(), new GraineConfig(baseUrl, publishableKey), ActionResult.ok("Done.") and new GraineScreen("checkout") read as they do in Kotlin. GraineCallback is a functional interface: pass a lambda (value, error) -> { … }; it is called once, on the main thread, with the value or the error. GraineListener and GraineCallListener have default methods, so a Java listener overrides only the callbacks it wants. Kotlin Maps and Lists are Java's java.util.Map and java.util.List. A companion function without @JvmStatic — FrictionConfig.fromJson — is reached through FrictionConfig.Companion.

Java

// Application.onCreate
GraineInApp.init(this, new GraineConfig("https://www.graine.ai", "pk_live_…"));
 
// Who the user is
Map<String, Object> traits = new HashMap<>();
traits.put("user_id", "u_8812");
GraineInApp.get().identify(traits, null);
 
// What the agent remembers: a callback, on the main thread
GraineInApp.get().memories((memories, error) -> {
    if (error != null) showMessage(error.getMessage());
});
 
// The booking sheet
GraineInApp.get().presentBooking(activity, (call, error) -> {
    if (call != null) showMessage("Booked.");
});

What's new in 0.9.0

  • Calls over WebRTC, opt-in. Add ai.graine:inapp-android-rtc — the same version as the SDK — and calls in both directions carry their audio over WebRTC: steadier on a weak network, prepared at the tap or while the phone rings, and carried on over the WebSocket inside the same call when WebRTC cannot set one up. GraineCallsConfig.callTransport chooses; calls.transport and calls.transportFallbackReason say how a call went; the agent's In-app call audio can switch WebRTC off without a release (Adding WebRTC calls).
  • chat.reconnecting: say Reconnecting…, not Offline, while a dropped chat is made again.
  • answerWidget on each agent: answer a component from a view of your own, on whichever conversation is live — the call or the chat.
  • What was said, and the captions switch: voice.captions (GraineCaption), voice.captionsOn, setCaptionsOn and clearCaptions; the voice bar shows its caption only while the switch is on.
  • ChatRows: the rows the SDK's chat window draws — day lines, one row per earlier call, a bubble per message — for a chat of your own.
  • voice.attached, and voice.viewOpened() / viewClosed() for a voice interface of your own.
  • Variables over about 1 KB are trimmed to fit, the biggest left off first and who_called and call_reason last, instead of all being dropped.
  • scheduleCall's own refusals say the booking sheet's words — “Pick a time at least 5 minutes from now”, “Pick a time within 14 days”, “Booking a call isn't available right now”.
  • The full list of changes is in the Changelog.

What's new in 0.8.0

  • Booking a call, drawn by the SDK. presentBooking(activity, reason) { call, error -> } shows the booking sheet over an activity — a sheet up from the bottom on a phone, a centred panel on a tablet — in the agent's design: the day, the time (Android's own time picker, its minute wheel in 5-minute steps), the phone's time zone, What's it about? (optional) and Book call. done hears the booked call, or null when the user closed it without booking; a refusal stays in the sheet as one plain line. GraineBookingDialog is the sheet itself. Still no dependencies.
  • bookingAvailable, on each agent: Graine rings this phone, the agent lets users book, and the user is identified. Show your own “Call me” button only then.
  • The chat window offers “Book a call” under the booked calls in its Upcoming calls card, or on its own when none are booked — never when the design hides the card. A booking refreshes the card.
  • Calls hold up on slow networks. Sending never waits for the network: at most 300 ms of the user's audio waits, the oldest dropped first. Hanging up lets go of the microphone at once, and a close the network cannot carry is forced within about a second.
  • A dead connection ends the call: nothing at all arriving for 50 seconds ends it as failed — “Nothing arrived for 50 seconds: the connection was lost.”
  • A call starts at once on the configuration already kept on the device, and refreshes it behind the call.
  • The prompt's variables no longer travel in the address of a call's or a chat's connection (they ride its one-time pass); the direct connection still carries them.
  • The full list of changes is in the Changelog.

Every symbol, A to Z

Every public symbol of the Android SDK — 958 in all — by name, each linked to where this page documents it: a type or function first, then its members.

On this page

Requirements, install, permissions and initialisationWhat the library's manifest merges into your appAdding WebRTC callsInitialiseInstall and the first answerGraineInApp.initGraineInAppGraineConfigGraineConfig.TimingSessionConfigIdentity, memory and conversations that continueGraineInApp.identifyGraineInApp.setMemoryGraineInApp.memoriesGraineInApp.forgetMemoryGraineChat.newConversationGraineCallbackGraineCallback.onResultGraineMemorySessionIdentityChatGraineChatGraineChat.openGraineChat.connectGraineChat.sendGraineChat.closeGraineChat.clearGraineChat.shownGraineChat.addChangeListenerGraineChat.removeChangeListenerChatSourceChatSource.sendChatSource.shownChatSource.addChangeListenerChatSource.removeChangeListenerGraineChatTurnGraineChatTurn.DeliveryGraineChatTurn.ChannelRoleChatRowsChatRows.RowChatRows.buildChatRows.dayLabelChatRows.callLabelChatRows.callSpokenChatRows.offersNewConversationChatRows.offersStartersGraineChatSizeGraineChatSize.fromGraineChatViewGraineChatActivityGraineChatActivity.openThe voice barGraineVoiceGraineVoice.attachGraineVoice.detachGraineVoice.startGraineVoice.endGraineVoice.setMutedGraineVoice.setCaptionsOnGraineVoice.clearCaptionsGraineVoice.retryGraineVoice.onRequestPermissionsResultGraineVoice.viewOpenedGraineVoice.viewClosedGraineCaptionGraineAgentBarViewGraineAgentBarView.resetPlacementGraineLauncherViewGraineLauncherView.setOnClickListenerGraineLoaderViewGraineLoaderView.configureLoaderLookLoaderStyleLoaderTextLoaderText.forMomentLoadersLoaders.normaliseLoaders.resolveConnectivityConnectionQualityConnectionQuality.classifyConnectStageConnectStage.fromCallPhaseCallPhase.ofTransportTransport.fromSeveral agents in one appGraineInApp.getGraineInApp.findScreen context, actions and componentsGraineInApp.setScreenGraineInApp.updateFieldGraineInApp.removeFieldGraineInApp.clearScreenGraineInApp.setVariablesGraineInApp.currentVariablesGraineInApp.trackGraineInApp.tapGraineInApp.reportEventGraineVoice.reportEventGraineInApp.noteInteractionGraineInApp.configureFrictionGraineInApp.registerActionGraineInApp.registerDeferredActionGraineInApp.checkActionsGraineInApp.clearPendingWidgetGraineScreenGraineScreen.JourneyGraineFieldGraineField.StatusActionResultActionResult.StatusActionResult.okActionResult.refusedActionResult.errorMaskingMasking.maskMasking.addRuleMasking.clearCustomRulesMasking.deepFrictionConfigFrictionConfig.ScreenFrictionConfig.fromJsonFrictionRulesFrictionRules.forScreenFrictionRules.ForScreen (type)FrictionSignalGraineWidgetViewGraineWidgetView.ThemeGraineWidgetView.presentGraineWidgetView.presentPendingGraineWidgetView.dismissGraineWidgetView.reopenGraineInApp.answerWidgetGraineVoice.answerWidgetGraineWidgetGraineWidget.adoptGraineAnswerWidgetAnswerWidgetStyleWidgetSelectActionCalls the user placesGraineCallsGraineCalls.configureGraineCallsConfigGraineCallTransportGraineCalls.startCallGraineCalls.endGraineCalls.setMutedGraineCalls.setSpeakerGraineCalls.onRequestPermissionsResultGraineCallGraineCall.DirectionGraineCallStateGraineCallOutcomeGraineCallOutcome.KindGraineCallOutcome.EndReasonGraineCallOutcome.endedGraineCallOutcome.failedGraineCallActivityGraineCallServiceGraineCallReceiverPermissions and the setup checkGraineCalls.callPermissionsGraineCalls.requestCallPermissionsGraineCalls.openCallSettingsGraineCalls.callSetupProblemsGraineCallPermissionGraineCallPermissionStatusGraineCallPermissionsGraineCallPermissionsCallbackGraineCallSetupProblemCalls Graine ringsGraineCalls.onNewTokenGraineCalls.handlePushGraineCalls.answerGraineCalls.declineGraineCalls.callBackBooking with the SDK's sheetGraineInApp.bookingAvailableGraineInApp.presentBookingGraineBookingDialogGraineBookingDialog (constructor)The scheduled-calls listGraineSchedulingRulesGraineSchedulingRules.earliestGraineSchedulingRules.latestGraineSchedulingRules.parseGraineInApp.scheduleCallGraineInApp.scheduledCallsGraineInApp.cancelScheduledCallGraineChat.refreshUpcomingCallsGraineChat.cancelUpcomingCallGraineScheduledCallGraineCallStatusGraineCallSourceGraineCallStatus.fromGraineCallSource.fromDesignGraineInApp.themeTokensGraineInApp.previewDesignResolvedDesignResolvedDesign.pickResolvedDesign.toJsonResolvedDesign.parseDesignConfigDesignConfig.toJsonThemeTokensThemeTokens.toJsonThemeColorsThemeFontFontScaleFontWeightsFontPairThemeShapeThemeRadiusThemeSpacingThemeElevationThemeShadowThemeOffsetThemeMotionThemeIconsLauncherGlyphThemeGlassChatTokensCallTokensCallStatusTextDesign (object)Design.normaliseHexDesign.glyphUrlDesign.launcherCornerDesign.motionMsDesign.androidShadowDesign.fontFamilyDesign.fontForNativeAppearanceAppearanceAppearance.resolveAppearance.isDarkAppearance.paletteAppearance.luminanceAppearance.argbAppearance.tintAppearance.encodeURIComponentWidgetPaletteLogoPlateLogoPlate.fromLogoLogo.boxLogo.plate (function)Logo.Stroke (type)Logo.Plate (type)Going liveGraineVersionEvents and listenersGraineInApp.addListenerGraineInApp.removeListenerGraineListenerGraineListener.onSessionReadyGraineListener.onCallStateGraineListener.onCaptionGraineListener.onWidgetGraineListener.onWidgetPendingGraineListener.onEndedGraineListener.onMicDeniedGraineListener.onQualityGraineListener.onNetworkGraineListener.onReconnectingGraineListener.onConnectProgressGraineListener.onTransportGraineListener.onConnectivityGraineListener.onActionRequestedGraineListener.onActionCompletedGraineListener.onFrictionGraineListener.onErrorGraineListener.onChatStateGraineListener.onChatMessageGraineCalls.addListenerGraineCalls.removeListenerGraineCallListenerGraineCallListener.onIncomingCallGraineCallListener.onCallStateGraineCallListener.onCallOutcomeGraineCallListener.onCallCaptionGraineCallListener.onAgentSpeakingGraineCallListener.onCallControlsErrorsGraineErrorGraineErrorCodeGraineErrorCode.fromGraineCallsHostExceptionRate limits and quotasLower-level building blocksGraineCallsHostGraineCallsHost: permissions and the setup checkGraineCallsHost.restoreGraineCallsHost.configureGraineCallsHost.snapshotGraineCallsHost.enableVoIPPushesGraineCallsHost.handlePushGraineCallsHost.onNewTokenGraineCallsHost.startCallGraineCallsHost.answerGraineCallsHost.declineGraineCallsHost.endGraineCallsHost.setMutedGraineCallsHost.setSpeakerGraineCallsHost.setIdentityGraineCallsHost.setMemoryGraineCallsHost.installIdGraineCallsHost.setVariablesGraineCallsHost.setScreenGraineCallsHost.setActionsGraineCallsHost.resolveActionGraineCallsHost.addSinkGraineCallsHost.removeSinkGraineCallsHost.onRequestPermissionsResultGraineCallsHost.onNewIntentGraineCallsHost.needsMicrophonePermissionGraineCallsHost.microphoneDeniedGraineCallsHost.SinkGraineCallsHost.Sink.onEventPlacementPlacement.BoundsPlacement.clampOffsetPlacement.offsetDuringDragPlacement.isTapKeyboardLiftKeyboardLift.liftForWidgetLookPlatformGrammarSelectCallNotes for JavaWhat's new in 0.9.0What's new in 0.8.0Every symbol, A to Z