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.

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.

ArtifactPackageUse it to
com.kustomer.chat:uicom.kustomer.uiOpen Kustomer's chat UI from your app. Most apps want this.
com.kustomer.chat:corecom.kustomer.coreBuild your own UI on the chat API.

Sample apps: kustomer/android-chat-sdk-samples.

Requirements

Version
SDK4.6.2
minSdk24
compileSdk / targetSdk35
Java17
Kotlin (your app's toolchain)2.2 or later, since SDK 4.6.0

Install

  1. Add the dependency

    The artifacts are on Maven Central:

    settings.gradle.kts
    dependencyResolutionManagement {
      repositories {
        google()
        mavenCentral()
      }
    }
    app/build.gradle.kts
    dependencies {
      implementation("com.kustomer.chat:ui:4.6.2")
    }
  2. Enable desugaring on minSdk 24 and 25

    Only apps with a minSdk below 26 need this:

    app/build.gradle.kts
    android {
      compileOptions { isCoreLibraryDesugaringEnabled = true }
    }
    dependencies {
      coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
    }
  3. 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.

  4. Initialize in your Application class

    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.

  5. Open chat

    Kotlin
    Kustomer.getInstance().open()

Options

KustomerOptions.Builder() methods all return the builder:

KustomerOptions.Builder
setBrandId(String)String
setBusinessScheduleId(String)String
setChatAssistant(KusActiveAssistant)KusActiveAssistant
setUserLocale(Locale)Locale
useAppLocales(Boolean)Boolean
hideNewConversationButton(Boolean)Boolean
hideHistoryNavigation(Boolean)Boolean
hideKbArticleShare(Boolean)Boolean
showInAppNotifications(Boolean)Boolean
setShowAllBrandsConversationHistory(Boolean)Boolean
setEnableUrlUnfurling(Boolean)Boolean
setEnableHistoryStatusFilter(Boolean)Boolean
setHistoryStatusFilter(KusHistoryStatusFilter)enum
setLogLevel(KusLogLevel)KusLogLevel

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()
KusPreferredViewOpens
DEFAULTWhatever the brand's widget setting says
CHAT_HISTORYChat and knowledge base, on the chat tab
KNOWLEDGE_BASE_HOMEChat and knowledge base, on the knowledge base tab
CHAT_ONLYChat only
KB_ONLYKnowledge 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.

MethodPurpose
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 KusUploadAttachments
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.

Attachments

LimitValue
Per file5 MB, after image compression
Per message9.5 MB in the UI
Message text10,240 characters
Blocked typesExecutables 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:

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>
AttributeColors
kusColorPrimary, kusColorOnPrimaryPrimary actions
kusColorSecondary, kusColorOnSecondarySelected navigation items
kusColorHeader, kusColorOnHeader, kusColorOnHeaderVariantThe app bar
kusColorAgentBubble, kusColorOnAgentBubble, kusColorUrlLinkOnAgentBubbleAgent messages
kusColorCustomerBubble, kusColorOnCustomerBubble, kusColorUrlLinkOnCustomerBubbleCustomer messages
kusColorBackground, kusColorSurface, kusColorOnSurface, kusColorSurfaceVariantBackgrounds and cards
kusColorError, kusColorListDividerErrors 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

SymptomFix
AssertionError: Kustomer API key contains invalid rolesUse a key with only org.tracking.
Build fails reading Kotlin metadataUpgrade your Kotlin toolchain to 2.2 or later.
KusNotInitializedExceptionKustomer.init hasn't run. Call it from Application.onCreate().
Listener never firesYou added it before init. Add it after.
Manifest merger failed on a FileProviderYour provider uses ${applicationId}.kustomersdk. Rename yours.
No notifications on Android 13+Declare POST_NOTIFICATIONS and request it. See Push notifications.