# Android

> Add Kustomer Chat to an Android app with the Kotlin SDK. Gradle setup, Kustomer.init, opening the chat UI, the headless KustomerCore API and KusChatListener, identity, attachments, theming, localization, and R8.

Source: https://developer.kustomer.com/docs/chat/android

The Android SDK is two Maven artifacts. `ui` gives you the full chat and knowledge base experience as an Activity; `core` is the headless layer it's built on. Depending on `ui` brings in `core`.

| Artifact                 | Package             | Use it to                                                   |
| ------------------------ | ------------------- | ----------------------------------------------------------- |
| `com.kustomer.chat:ui`   | `com.kustomer.ui`   | Open Kustomer's chat UI from your app. Most apps want this. |
| `com.kustomer.chat:core` | `com.kustomer.core` | Build your own UI on the chat API.                          |

Sample apps: [`kustomer/android-chat-sdk-samples`](https://github.com/kustomer/android-chat-sdk-samples).

## Requirements

|                               | Version                           |
| ----------------------------- | --------------------------------- |
| SDK                           | 4.6.2                             |
| minSdk                        | 24                                |
| compileSdk / targetSdk        | 35                                |
| Java                          | 17                                |
| Kotlin (your app's toolchain) | **2.2 or later**, since SDK 4.6.0 |

## Install

#### Add the dependency

The artifacts are on Maven Central:

```kotlin title="settings.gradle.kts"
dependencyResolutionManagement {
  repositories {
    google()
    mavenCentral()
  }
}
```

```kotlin title="app/build.gradle.kts"
dependencies {
  implementation("com.kustomer.chat:ui:4.6.2")
}
```

#### Enable desugaring on minSdk 24 and 25

Only apps with a minSdk below 26 need this:

```kotlin title="app/build.gradle.kts"
android {
  compileOptions { isCoreLibraryDesugaringEnabled = true }
}
dependencies {
  coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}
```

#### Get a chat key

Create an API key in **Settings › Security › API Keys** with **only** the `org.tracking` role and no expiry. The SDK rejects keys with any other role.

#### Initialize in your Application class

```kotlin title="MyApplication.kt"
class MyApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    val options = KustomerOptions.Builder()
      .setBrandId("YOUR_BRAND_ID")
      .setLogLevel(KusLogLevel.Error)
      .build()
    try {
      Kustomer.init(application = this, apiKey = BuildConfig.KUSTOMER_CHAT_KEY, options = options) { result ->
        Log.i("Kustomer", "Initialized: ${result.dataOrNull}")
      }
    } catch (e: AssertionError) {
      Log.e("Kustomer", "Invalid chat key", e)
    }
  }
}
```

Initialize in `Application.onCreate()`, not in an Activity, so the SDK is ready when Android restarts your process. An invalid key throws `AssertionError`. Calling `Kustomer.getInstance()` before `init` throws `KusNotInitializedException`.

#### Open chat

```kotlin
Kustomer.getInstance().open()
```

> **Add listeners after init:** `Kustomer.init` removes every chat listener before it registers its own. Call `addChatListener` after `init`, and again if you ever call `init` a second time.

## Options

`KustomerOptions.Builder()` methods all return the builder:

**KustomerOptions.Builder**

You can change some of these at runtime on `Kustomer.getInstance()`: `overrideBrand(brandId)`, `overrideBusinessSchedule(scheduleId)`, `changeActiveAssistant(activeAssistant)`, `setHistoryStatusFilter(filter)`.

## Open the UI

All of these are on `Kustomer.getInstance()`. Callbacks run on the main thread.

```kotlin
val kustomer = Kustomer.getInstance()

kustomer.open()                                       // follows the brand's widget setting
kustomer.open(KusPreferredView.CHAT_ONLY)
kustomer.open(KusPreferredView.KB_ONLY)

kustomer.startNewConversation(
  initialMessage = KusInitialMessage("Where is my order?", KusChatMessageDirection.CUSTOMER),
  describeAttributes = KusDescribeAttributes(hashMapOf("orderNumberStr" to "4417")),
  title = "Order 4417",
) { result ->
  if (result is KusResult.Success) Log.i("Kustomer", "Conversation ${result.data.id}")
}

kustomer.openConversationWithId("6716a3a9c4e5f60012ab34c1") { }
kustomer.openChatAssistant(assistantId = "YOUR_ASSISTANT_ID")
kustomer.openKbArticle("ARTICLE_ID") { }
kustomer.openKbCategory("CATEGORY_ID") { }
kustomer.close()                                      // closes the UI; doesn't log out
kustomer.isVisible()
```

| `KusPreferredView`    | Opens                                              |
| --------------------- | -------------------------------------------------- |
| `DEFAULT`             | Whatever the brand's widget setting says           |
| `CHAT_HISTORY`        | Chat and knowledge base, on the chat tab           |
| `KNOWLEDGE_BASE_HOME` | Chat and knowledge base, on the knowledge base tab |
| `CHAT_ONLY`           | Chat only                                          |
| `KB_ONLY`             | Knowledge base only                                |

About `startNewConversation`:

- A `CUSTOMER` initial message is sent right away and creates the conversation. An `AGENT` one is shown as a greeting, and the conversation is created when the customer replies. Assistants ignore `AGENT` initial messages.
- `title` is capped at 256 characters. Without one, the first message is used.
- `KusDescribeAttributes` sets custom attributes on the new conversation, using attribute names such as `orderNumberStr`.

`openConversationWithId` follows merges: if the conversation was merged, it opens the one it was merged into. The chat UI runs in its own portrait Activity.

## Unread counts and availability

```kotlin
Kustomer.getInstance().observeUnreadCount().observe(this) { count ->
  badge.isVisible = count > 0
  badge.text = count.toString()
}

Kustomer.getInstance().isChatAvailable { result ->
  val availability = result.dataOrNull   // KUS_ONLINE, KUS_OFFLINE, KUS_HIDDEN or KUS_DISABLED
  helpButton.isVisible = availability != KusChatAvailability.KUS_HIDDEN && availability != KusChatAvailability.KUS_DISABLED
}
```

`observeUnreadCount()` counts conversations updated in the last 30 days. `observeActiveConversationIds()` gives the IDs of open conversations.

## Headless: KustomerCore

To build your own UI, use the providers on `KustomerCore`. With the `ui` artifact, call `Kustomer.init` (it initializes Core for you); with only `core`, call `KustomerCore.init(application, apiKey, options)`.

```kotlin
val chat = KustomerCore.getInstance().kusChatProvider()

lifecycleScope.launch {
  when (val created = chat.createConversation(title = "Order 4417", messages = listOf("Where is my order?"), attachments = null)) {
    is KusResult.Success -> {
      val (conversation, messages) = created.data
      chat.sendMessageWithAttachments(body = "It was due Monday.", messageAction = null, attachments = null, conversationId = conversation.id)
    }
    is KusResult.Error -> Log.e("Kustomer", "Create failed", created.exception)
    KusResult.Loading -> Unit
  }
}
```

Provider methods are `suspend` functions that return `KusResult<T>`: `Success(data)` or `Error(exception)`. `KusResult` also has a `Loading` object, so a `when` expression needs a branch for it.

| Method                                                                         | Purpose                                                                               |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `createConversation(title, messages, attachments, …, attributes, assistantId)` | Always creates a new conversation. Returns the conversation and its first messages.   |
| `fetchConversations(page, pageSize)`                                           | The customer's conversations. Only open ones when the brand hides history.            |
| `getConversation(id)` / `fetchConversation(id)`                                | One conversation, from cache or forcing the network                                   |
| `endConversation(id)`                                                          | End it                                                                                |
| `sendMessageWithAttachments(body, messageAction, attachments, conversationId)` | Send a message, with optional `KusUploadAttachment`s                                  |
| `fetchChatMessages(conversationId, page, pageSize)`                            | Messages and CSAT items, mixed in one list                                            |
| `getChatMessagesBeforeTimestamp(conversationId, timeStamp, pageSize)`          | Scroll back through history                                                           |
| `markSessionAsRead(conversationId)`                                            | Mark everything read. Call it when the customer leaves the conversation.              |
| `sendTypingStatus(conversationId, KusTypingStatus.CUST_TYPING_STARTED)`        | The customer's typing indicator. End it with `CUST_TYPING_ENDED`.                     |
| `subscribeToTypingStatus(channel)` / `unsubscribeFromTypingStatus(channel)`    | Agent typing, using the `SHARED_CONVERSATION` channel from `conversation.channelInfo` |
| `describeConversation(id, customAttributes)`                                   | Custom attributes on a conversation                                                   |
| `submitSatisfactionForm(satisfactionId, response, conversationId)`             | Submit CSAT                                                                           |
| `isChatAvailable()` / `getChatSettings()`                                      | Availability and the brand's chat settings                                            |

The knowledge base is on `KustomerCore.getInstance().kusKbProvider()`: `fetchRootCategory(locale)`, `fetchFeaturedArticles(locale)`, `fetchCategoryById(id, locale)`, `searchArticles(term, page, pageSize, locale)` and `getArticleById(id, locale, knowledgeBaseId)`.

### Listen for events

Implement `KusChatListener` and register it after `init`:

```kotlin
val listener = object : KusChatListener {
  override fun onChatMessageReceived(conversationId: String, chatMessage: KusChatMessage) { }
  override fun onAgentIsTyping(conversationId: String, typingIndicator: KusTypingIndicator) { }
  override fun onUnreadCountChange(conversationId: String, unreadCount: Int) { }
  override fun onAgentJoined(conversationId: String, agent: KusUser) { }
  override fun onPreviewChanged(conversationId: String, preview: KusConversationPreview) { }
  override fun onConversationLastMessageAtChanged(conversationId: String, lastMessageAt: Long) { }
  override fun onConversationCreated(conversation: KusConversation) { }
  override fun onConversationEnded(conversation: KusConversation) { }
  override fun onConversationDeleted(conversation: KusConversation) { }
  override fun onConversationUnended(conversation: KusConversation) { }
  override fun onCustomerMerged(customerId: String) { }
  override fun onCustomerDeleted() { }
  override fun onSatisfactionEventReceived(conversationId: String, satisfaction: KusSatisfaction) { }
  override fun onAssistantEnded(conversation: KusConversation) { }
  override fun onConversationMerged(source: KusConversation, target: KusConversation) { }
}
Kustomer.getInstance().addChatListener(listener)
```

`onChatMessageReceived` fires for the customer's own messages too; check `chatMessage.direction`. `onAgentJoined` fires on the first agent reply, not on assignment. `onSessionOpened`, `onSessionTitleChanged`, `onSessionCustomAttributesUpdated` and `onUrlPreviewReceived` have default implementations, so override them only if you need them.

## Identity

```kotlin
val kustomer = Kustomer.getInstance()

if (kustomer.isLoggedIn(userEmail = user.email).dataOrNull != true) {
  kustomer.logIn(jwtFromYourServer) { result ->
    if (result is KusResult.Error) Log.e("Kustomer", "Login failed", result.exception)
  }
}

kustomer.describeCustomer(
  KusCustomerDescribeAttributes(sharedPhones = listOf(KusPhone("+15555550123")), custom = mapOf("loyaltyTierStr" to "gold"))
) { }

// on sign-out
lifecycleScope.launch {
  kustomer.deregisterDeviceForPushNotifications()
  kustomer.logOut()
}
```

- `isLoggedIn(userEmail, userId)` is synchronous and compares case-insensitively with the identity in the stored token.
- Call `logIn` after your user signs in, not on every launch.
- `logOut()` clears the SDK's data but doesn't deregister push, so deregister first.

The JWT comes from your server. See [Customer identity](https://developer.kustomer.com/docs/chat/identity).

## Attachments

| Limit         | Value                                                                               |
| ------------- | ----------------------------------------------------------------------------------- |
| Per file      | 5 MB, after image compression                                                       |
| Per message   | 9.5 MB in the UI                                                                    |
| Message text  | 10,240 characters                                                                   |
| Blocked types | Executables and scripts, such as `exe`, `bat`, `msi`, `ps1`, `vbs`, `scr` and `lnk` |

Images are re-encoded as JPEG and downscaled to about one megapixel before upload. Admins can turn attachments off in chat settings.

The SDK registers a `FileProvider` with the authority `${applicationId}.kustomersdk`, so don't use that authority yourself. It doesn't declare the `CAMERA` permission: photos use the system camera app through `ACTION_IMAGE_CAPTURE`.

## Theming

The chat Activity uses the theme `KusAppTheme.Overlay`, based on `Theme.MaterialComponents.DayNight`. Override it in your app to apply your colors:

```xml title="res/values/styles.xml"
<style name="KusAppTheme.Overlay" parent="KusAppTheme">
  <item name="kusColorPrimary">@color/brand</item>
  <item name="kusColorOnPrimary">@android:color/white</item>
  <item name="kusColorCustomerBubble">@color/brand</item>
  <item name="kusColorOnCustomerBubble">@android:color/white</item>
</style>
```

| Attribute                                                                               | Colors                    |
| --------------------------------------------------------------------------------------- | ------------------------- |
| `kusColorPrimary`, `kusColorOnPrimary`                                                  | Primary actions           |
| `kusColorSecondary`, `kusColorOnSecondary`                                              | Selected navigation items |
| `kusColorHeader`, `kusColorOnHeader`, `kusColorOnHeaderVariant`                         | The app bar               |
| `kusColorAgentBubble`, `kusColorOnAgentBubble`, `kusColorUrlLinkOnAgentBubble`          | Agent messages            |
| `kusColorCustomerBubble`, `kusColorOnCustomerBubble`, `kusColorUrlLinkOnCustomerBubble` | Customer messages         |
| `kusColorBackground`, `kusColorSurface`, `kusColorOnSurface`, `kusColorSurfaceVariant`  | Backgrounds and cards     |
| `kusColorError`, `kusColorListDivider`                                                  | Errors and dividers       |

The SDK follows your app's night mode. Put dark values in `res/values-night/styles.xml` too. Text styles (`KusTextAppearance.*`) can be overridden the same way.

## Localization

The SDK includes translations for more than 100 locales and follows the device language. Force one with `setUserLocale`, or follow per-app languages with `useAppLocales(true)`. Every UI string is a `kus_*` resource you can override in your own `strings.xml`.

## R8 and ProGuard

Rules ship inside the AARs and apply automatically, including in R8 full mode.

## Troubleshooting

| Symptom                                                   | Fix                                                                                                                                         |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `AssertionError: Kustomer API key contains invalid roles` | Use a key with only `org.tracking`.                                                                                                         |
| Build fails reading Kotlin metadata                       | Upgrade your Kotlin toolchain to 2.2 or later.                                                                                              |
| `KusNotInitializedException`                              | `Kustomer.init` hasn't run. Call it from `Application.onCreate()`.                                                                          |
| Listener never fires                                      | You added it before `init`. Add it after.                                                                                                   |
| `Manifest merger failed` on a FileProvider                | Your provider uses `${applicationId}.kustomersdk`. Rename yours.                                                                            |
| No notifications on Android 13+                           | Declare `POST_NOTIFICATIONS` and request it. See [Push notifications](https://developer.kustomer.com/docs/chat/push-notifications#android). |
