pythonandroidmultithreadingkotlinchaquopy

Run a Quart web server in Python with Chaquopy on Android (Kotlin)


I am trying to make an Android app with a Quart app running on Python with Chaquopy but as soon as i add a call to Kotlin from Python i get Error starting Python server: Can't create handler inside thread Thread[Thread-4,5,main] that has not called Looper.prepare() from MainActivity. So i added a call to Looper but that gives me Error starting Python server: Method addObserver must be called on the main thread even though the call to run the Quart app is in the thread where the Python interpreter is started.

Kotlin:

package com.example.chatterbot

import android.annotation.SuppressLint
import android.net.http.SslError
import android.os.Bundle
import android.os.Looper
import android.util.Log
import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.chaquo.python.Python
import com.chaquo.python.android.AndroidPlatform

class MainActivity : AppCompatActivity() {

    private val tag: String = "MainActivity" // Define a tag for your logs
    private var serverPort: Int? = null
    private lateinit var webView: WebView
    private lateinit var swipeRefreshLayout: SwipeRefreshLayout

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Run the Python server in a background thread
        Thread {
            Looper.prepare()
            Log.i(tag, "Starting server")
            Log.i(tag, "IS STARTED")
            Log.i(tag, Python.isStarted().toString())
            if (! Python.isStarted()) {
                Python.start(AndroidPlatform(this))
            }
            try {
                val py = Python.getInstance()
                py.getModule("app").callAttr("run")
            } catch (e: Exception) {
                Log.e(tag, "Error starting Python server: ${e.message}")
            }
            Looper.loop()
        }.start()

        // Set webview
        Log.i(tag, "Starting webview")
        webView = findViewById(R.id.webview)
        webView.webViewClient = object : WebViewClient() {
            override fun onReceivedSslError(
                view: WebView?,
                handler: SslErrorHandler?,
                error: SslError?
            ) {
                // Ignore SSL certificate errors (not recommended for production)
                handler?.proceed()
            }
        }
        webView.settings.javaScriptEnabled = true
        webView.loadUrl("file:///android_asset/iframe/iframe.html")
        swipeRefreshLayout = findViewById(R.id.swipe_refresh_layout)
        swipeRefreshLayout.setOnRefreshListener {
            webView.reload() // Reload the current page
            swipeRefreshLayout.isRefreshing = false // Stop the refreshing animation
        }
    }

    fun closeLoader() {
        Log.i(tag, "Close loader")
        webView.evaluateJavascript("closeLoader();", null)
    }

}

Python:

from hypercorn.asyncio import serve
from hypercorn.config import Config
from quart import Quart, render_template
from com.example.chatterbot import MainActivity

activity = MainActivity() # <-- **The problem comes when i add this**

app = Quart(__name__)

config = Config()
config.bind = [f"0.0.0.0:8000"]
config.debug = True

@app.route('/')
async def index():
    return await render_template('index.html')

@app.after_serving
async def after_startup():
    # This code will run after the server is fully loaded and ready to receive requests
    print("Server ready")
    activity.closeLoader()

def run():
    app.run(host="0.0.0.0", port=8000, debug=True)

Solution

  • Turns out i simply had to remove the line android:name="com.chaquo.python.android.PyApplication" from the manifest which caused the python interpreter to be started automatically outside the tread with the app therefore not using the Python.start() method inside the thread. As said in the comment, the solution is here.