WebView Kotlin
WebView Kotlin
LiveCaller Chat Widget Integration for Android
This guide explains how to integrate the LiveCaller Chat Widget into an Android app using WebView.
Main Activity
package com.example.livecaller_integration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.*
import com.example.livecaller_integration.ui.theme.LiveCallerTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
LiveCallerTheme {
AppContent()
}
}
}
}
@Composable
fun AppContent() {
var currentScreen by remember { mutableStateOf("home") }
when (currentScreen) {
"home" -> HomeScreen { currentScreen = "chat" }
"chat" -> ChatScreen { currentScreen = "home" }
}
}
Home Screen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(onOpenChat: () -> Unit) {
Scaffold(
topBar = { TopAppBar(title = { Text("Home") }) }
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Button(onClick = onOpenChat) {
Text("Open LiveCaller Chat")
}
}
}
}
Chat Screen with WebView
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatScreen(onBack: () -> Unit) {
var isLoading by remember { mutableStateOf(true) }
var error by remember { mutableStateOf<String?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
var fileChooserCallback by remember { mutableStateOf<ValueCallback<Array<Uri>>?>(null) }
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
) { uris ->
fileChooserCallback?.onReceiveValue(uris.toTypedArray())
fileChooserCallback = null
}
val singleFilePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
val result = if (uri != null) arrayOf(uri) else null
fileChooserCallback?.onReceiveValue(result)
fileChooserCallback = null
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("LiveCaller Chat") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
// Error view
if (error != null) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Outlined.Error,
contentDescription = "Error",
tint = Color.Red,
modifier = Modifier.size(64.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = error ?: "",
color = Color.Red,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
error = null
isLoading = true
webView?.reload()
}) {
Text("Retry")
}
}
} else {
// WebView
AndroidView(
factory = { context ->
WebView(context).apply {
webView = this
configureWebView(
onLoadingChanged = { loading -> isLoading = loading },
onError = { errorMsg -> error = errorMsg },
fileChooserCallback = { callback -> fileChooserCallback = callback },
filePickerLauncher = filePickerLauncher,
singleFilePickerLauncher = singleFilePickerLauncher
)
loadChatWidget()
}
},
modifier = Modifier.fillMaxSize()
)
}
if (isLoading && error == null) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White.copy(alpha = 0.9f)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.height(16.dp))
Text("Loading LiveCaller Chat...")
}
}
}
}
}
}
6. WebView Configuration Extension
fun WebView.configureWebView(
onLoadingChanged: (Boolean) -> Unit,
onError: (String) -> Unit,
fileChooserCallback: (ValueCallback<Array<Uri>>?) -> Unit,
filePickerLauncher: ManagedActivityResultLauncher<String, List<Uri>>,
singleFilePickerLauncher: ManagedActivityResultLauncher<String, Uri?>
) {
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
allowFileAccess = true
allowContentAccess = true
allowFileAccessFromFileURLs = true
allowUniversalAccessFromFileURLs = true
builtInZoomControls = false
setSupportZoom(false)
loadWithOverviewMode = true
useWideViewPort = true
cacheMode = WebSettings.LOAD_DEFAULT
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
Log.d("LiveCaller WebView", "${consoleMessage?.message()} -- ${consoleMessage?.sourceId()}:${consoleMessage?.lineNumber()}")
return true
}
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
fileChooserCallback(filePathCallback)
try {
val acceptMultiple = fileChooserParams?.mode == FileChooserParams.MODE_OPEN_MULTIPLE
val acceptTypes = fileChooserParams?.acceptTypes?.joinToString(",") ?: "*/*"
if (acceptMultiple) {
filePickerLauncher.launch(acceptTypes)
} else {
singleFilePickerLauncher.launch(acceptTypes)
}
return true
} catch (e: Exception) {
Log.e("LiveCaller", "File picker error", e)
filePathCallback?.onReceiveValue(null)
return false
}
}
override fun onPermissionRequest(request: PermissionRequest?) {
request?.grant(request.resources)
}
}
// Set WebViewClient
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
onLoadingChanged(true)
}
override fun onPageFinished(view: WebView?, url: String?) {
onLoadingChanged(false)
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
onLoadingChanged(false)
onError("Failed to load: ${error?.description}")
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val url = request?.url?.toString() ?: ""
return !(url.startsWith("https://cdn.livecaller.io") ||
url.startsWith("https://livecaller.io") ||
url.startsWith("data:") ||
url.startsWith("blob:"))
}
}
}
7. Load Chat Widget
fun WebView.loadChatWidget() {
val chatHtml = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>LiveCaller Chat</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
background: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
}
#live-caller-widget {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
flex-direction: column;
color: #666;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #2196F3;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="live-caller-widget">
<div class="loading">
<div class="spinner"></div>
<p>Initializing LiveCaller...</p>
</div>
</div>
<script>
console.log('Starting LiveCaller initialization...');
(function(w,t,c,p,s,l){
p = new Promise(function(resolve,reject){
w[c] = { client: () => p };
l = document.getElementById('live-caller-widget');
s = document.createElement(t);
s.async = true;
s.setAttribute('data-livecaller', 'script');
s.src='https://cdn.livecaller.io/js/app.js';
s.onload = function(){ resolve(w[c]); };
s.onerror = function(error){
l.innerHTML='<div class="loading"><p>Failed to load chat.</p></div>';
reject(error);
};
(document.head||document.body).appendChild(s);
});
return p;
})(window,'script','LiveCaller')
.then(function(){
LiveCaller.config.merge({
widget: {
id: 'YOUR_WIDGET_ID_HERE',
},
app: {
locale: 'en',
}
});
LiveCaller.liftOff();
setTimeout(function() { LiveCaller.${'$'}emit('ui.widget.open'); }, 500);
})
.catch(function(error){
console.error('LiveCaller initialization failed:', error);
document.getElementById('live-caller-widget').innerHTML =
'<div class="loading"><p>Chat initialization failed. Please try again.</p></div>';
});
</script>
</body>
</html>
"""
loadDataWithBaseURL(
"https://cdn.livecaller.io/",
chatHtml,
"text/html",
"UTF-8",
null
)
}
8. Required Imports
Add these imports to your Activity file:
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import android.webkit.*
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ManagedActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.Error
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
Last updated