pack and upload
Deploy to FTP / deploy (push) Successful in 5s

This commit is contained in:
Sebastian Molenda
2026-06-13 11:16:53 +02:00
parent 332fef0fd3
commit e4db4bb31e
141 changed files with 516 additions and 211 deletions
+1
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:theme="@style/AppTheme"
android:label="@string/app_name"
@@ -5,6 +5,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import java.io.File
import androidx.appcompat.app.AppCompatActivity
import androidx.webkit.WebViewAssetLoader
@@ -21,7 +22,11 @@ class MainActivity : AppCompatActivity() {
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
// Use WebViewAssetLoader to serve files from /assets/ over a secure origin.
val updater = WebAppUpdater(this)
// prefer to serve files from internal storage (filesDir/webapp), fallback to packaged assets
val internalPath = updater.getLocalWebAppPath()
val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(this))
.build()
@@ -29,13 +34,49 @@ class MainActivity : AppCompatActivity() {
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if (request == null) return null
// If we have a local webapp in filesDir/webapp, serve files directly from there
val localBase = updater.getLocalWebAppPath()
if (localBase != null) {
val uri = request.url
val path = uri.path ?: ""
// expecting requests like /localweb/... mapped to filesDir/webapp/...
if (path.startsWith("/localweb/")) {
val rel = path.removePrefix("/localweb/")
val f = File(localBase, rel)
if (f.exists() && f.isFile) {
val mime = when {
f.name.endsWith(".html") -> "text/html"
f.name.endsWith(".js") -> "application/javascript"
f.name.endsWith(".css") -> "text/css"
f.name.endsWith(".png") -> "image/png"
f.name.endsWith(".jpg") || f.name.endsWith(".jpeg") -> "image/jpeg"
else -> "application/octet-stream"
}
return WebResourceResponse(mime, "utf-8", f.inputStream())
}
}
}
// fallback to packaged assets via assetLoader
return assetLoader.shouldInterceptRequest(request.url)
}
}
// Load the app via the mapped secure origin so fetch() requests are allowed
webView.loadUrl("https://appassets.androidplatform.net/assets/index.html")
// Start update in background; when finished, reload WebView to pick up local files
updater.checkAndUpdate {
runOnUiThread {
val local = updater.getLocalWebAppPath()
if (local != null) {
// if we have a local copy, load it via mapped origin
webView.loadUrl("https://appassets.androidplatform.net/localweb/index.html")
} else {
webView.loadUrl("https://appassets.androidplatform.net/assets/index.html")
}
}
}
// show WebView immediately (it will navigate when updater completes)
setContentView(webView)
}
}
@@ -0,0 +1,120 @@
package com.example.app
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.BufferedInputStream
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipInputStream
import org.json.JSONObject
/**
* Simple updater that checks a remote JSON for latest version and zip URL,
* downloads and unpacks into internal storage under filesDir/webapp.
* This is intentionally minimal — adapt URLs and error handling as needed.
*/
class WebAppUpdater(private val context: Context) {
private val TAG = "WebAppUpdater"
// control URL on your server; update if your JSON lives at a different path
private val controlUrl = "https://edu.aln.webd.pl/app/latest.json"
private val targetDirName = "webapp"
fun checkAndUpdate(onComplete: (() -> Unit)? = null) {
CoroutineScope(Dispatchers.IO).launch {
try {
val conn = URL(controlUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 5000
conn.readTimeout = 10000
conn.requestMethod = "GET"
if (conn.responseCode != 200) {
Log.w(TAG, "control.json fetch failed: ${conn.responseCode}")
onComplete?.invoke()
return@launch
}
val text = conn.inputStream.bufferedReader().use { it.readText() }
// expecting: { "version": "1.0.1", "zip": "https://.../app.zip" }
val json = JSONObject(text)
val zipUrl = if (json.has("zip")) json.getString("zip") else null
val version = if (json.has("version")) json.getString("version") else null
if (zipUrl.isNullOrEmpty() || version.isNullOrEmpty()) {
Log.w(TAG, "invalid control.json: $text")
onComplete?.invoke()
return@launch
}
val versionFile = File(context.filesDir, "$targetDirName/.version")
val currentVersion = if (versionFile.exists()) versionFile.readText().trim() else ""
if (currentVersion == version) {
Log.i(TAG, "webapp up-to-date: $version")
onComplete?.invoke()
return@launch
}
// download zip
val tmpZip = File.createTempFile("webapp", ".zip", context.cacheDir)
downloadToFile(zipUrl, tmpZip)
// unpack to temp dir then move
val tmpDir = File.createTempFile("webapptemp", "", context.cacheDir)
tmpDir.delete()
tmpDir.mkdirs()
unzipToDir(tmpZip, tmpDir)
val targetDir = File(context.filesDir, targetDirName)
if (targetDir.exists()) targetDir.deleteRecursively()
tmpDir.renameTo(targetDir)
versionFile.writeText(version)
Log.i(TAG, "webapp updated to $version")
} catch (e: Exception) {
Log.w(TAG, "update failed", e)
} finally {
onComplete?.invoke()
}
}
}
private fun downloadToFile(urlStr: String, outFile: File) {
val url = URL(urlStr)
val conn = url.openConnection() as HttpURLConnection
conn.connectTimeout = 5000
conn.readTimeout = 20000
conn.requestMethod = "GET"
conn.connect()
if (conn.responseCode != 200) throw RuntimeException("download failed: ${conn.responseCode}")
conn.inputStream.use { input ->
FileOutputStream(outFile).use { fos ->
input.copyTo(fos)
}
}
}
private fun unzipToDir(zipFile: File, targetDir: File) {
ZipInputStream(BufferedInputStream(zipFile.inputStream())).use { zis ->
var entry = zis.nextEntry
while (entry != null) {
val out = File(targetDir, entry.name)
if (entry.isDirectory) {
out.mkdirs()
} else {
out.parentFile?.mkdirs()
FileOutputStream(out).use { fos ->
zis.copyTo(fos)
}
}
entry = zis.nextEntry
}
}
}
fun getLocalWebAppPath(): String? {
val dir = File(context.filesDir, targetDirName)
return if (dir.exists()) dir.absolutePath else null
}
}