@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user