# WebView Kotlin

## WebView Kotlin

Integration of LiveCaller Chat Widget for Android

This guide explains how to integrate the LiveCaller chat widget into an Android application using WebView.

## Main Activity

```kotlin
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

```kotlin
@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

```kotlin
@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...")
                    }
                }
            }
        }
    }
}

```

***

## WebView Configuration Extension

```kotlin
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:"))
        }
    }
}

```

***

## Loading the Chat Widget

```html
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
    )
}

```

***

## Required Imports

Add the following imports to your Activity file:

```swift
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

```

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.livecaller.io/livecaller/mobile-sdks/webview-kotlin.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
