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