mirror of
https://github.com/BrendanGreenlee/openclaw-android-heartbeat.git
synced 2026-08-17 16:49:14 +00:00
v1: replace native UI with full-screen WebView web shell
WebShellActivity is now the launcher: full-screen WebView pointed at the Gateway web chat (https://tech.greenlee.website/), no browser chrome, mobile viewport, persistent session (cookies + DOM storage), file uploads and downloads wired through. The official app's MainActivity and WebSocket node infrastructure stay intact for the v2 heartbeat watchdog.
This commit is contained in:
parent
95069ff263
commit
b9e4ab1cba
2 changed files with 231 additions and 4 deletions
|
|
@ -103,6 +103,19 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<!-- v1 launcher: full-screen WebView web shell pointed at the Gateway web chat.
|
||||
MainActivity + WebSocket node infrastructure stay intact for v2 heartbeat. -->
|
||||
<activity
|
||||
android:name=".WebShellActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|uiMode|density|keyboard|keyboardHidden|navigation">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- External sender tasks must converge on one visible composer and share queue. -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
|
@ -113,10 +126,6 @@
|
|||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ASSIST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
|
|||
218
app/src/main/java/ai/openclaw/app/WebShellActivity.kt
Normal file
218
app/src/main/java/ai/openclaw/app/WebShellActivity.kt
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
package ai.openclaw.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.DownloadListener
|
||||
import android.webkit.URLUtil
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
|
||||
/**
|
||||
* v1 Web shell: a full-screen WebView pointed at the OpenClaw Gateway web chat.
|
||||
*
|
||||
* The official app's native UI (MainActivity) and WebSocket node connection
|
||||
* infrastructure (NodeRuntime, NodeForegroundService, ...) remain intact in
|
||||
* this fork — v2 will reuse them for the heartbeat watchdog.
|
||||
*/
|
||||
class WebShellActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
/** Same Gateway the node WebSocket connects to; the web chat lives at its root. */
|
||||
const val DEFAULT_GATEWAY_URL = "https://tech.greenlee.website/"
|
||||
|
||||
private const val EXTRA_GATEWAY_URL = "ai.openclaw.app.extra.GATEWAY_URL"
|
||||
|
||||
/** Launch the web shell, optionally overriding the default gateway URL. */
|
||||
fun start(context: Context, gatewayUrl: String = DEFAULT_GATEWAY_URL) {
|
||||
context.startActivity(
|
||||
Intent(context, WebShellActivity::class.java)
|
||||
.putExtra(EXTRA_GATEWAY_URL, gatewayUrl),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var webView: WebView? = null
|
||||
private var filePathCallback: ValueCallback<Array<Uri>>? = null
|
||||
|
||||
private val fileChooserLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val results =
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val clipData = result.data!!.clipData
|
||||
if (clipData != null) {
|
||||
Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
|
||||
} else {
|
||||
result.data!!.data?.let { arrayOf(it) }
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
filePathCallback?.onReceiveValue(results)
|
||||
filePathCallback = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Full screen, edge-to-edge, no browser chrome.
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
|
||||
val gatewayUrl = intent?.getStringExtra(EXTRA_GATEWAY_URL) ?: DEFAULT_GATEWAY_URL
|
||||
|
||||
webView =
|
||||
WebView(this).apply {
|
||||
configureWebView(this, gatewayUrl)
|
||||
}
|
||||
setContentView(webView)
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (webView?.canGoBack() == true) {
|
||||
webView?.goBack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun configureWebView(view: WebView, gatewayUrl: String) {
|
||||
view.settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
setSupportMultipleWindows(false)
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
// Proper mobile viewport: let the page's viewport meta drive layout.
|
||||
useWideViewPort = true
|
||||
loadWithOverviewMode = true
|
||||
builtInZoomControls = false
|
||||
displayZoomControls = false
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
}
|
||||
|
||||
// Persistent session: cookies + DOM storage survive app restarts.
|
||||
CookieManager.getInstance().apply {
|
||||
setAcceptCookie(true)
|
||||
setAcceptThirdPartyCookies(view, true)
|
||||
}
|
||||
|
||||
view.webViewClient =
|
||||
object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
val url = request?.url ?: return false
|
||||
val gatewayHost = Uri.parse(DEFAULT_GATEWAY_URL).host
|
||||
// Keep the gateway chat (and its assets) inside the WebView; open
|
||||
// external links in the system browser.
|
||||
if (url.host == gatewayHost) return false
|
||||
return runCatching {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url))
|
||||
true
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
view.webChromeClient =
|
||||
object : WebChromeClient() {
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView?,
|
||||
filePathCallback: ValueCallback<Array<Uri>>?,
|
||||
fileChooserParams: FileChooserParams?,
|
||||
): Boolean {
|
||||
this@WebShellActivity.filePathCallback?.onReceiveValue(null)
|
||||
this@WebShellActivity.filePathCallback = filePathCallback
|
||||
val intent =
|
||||
fileChooserParams?.createIntent()
|
||||
?: Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
return try {
|
||||
fileChooserLauncher.launch(intent)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
this@WebShellActivity.filePathCallback = null
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.setDownloadListener(
|
||||
DownloadListener { url, userAgent, contentDisposition, mimetype, _ ->
|
||||
runCatching {
|
||||
val request =
|
||||
DownloadManager.Request(Uri.parse(url))
|
||||
.setMimeType(mimetype)
|
||||
.addRequestHeader("User-Agent", userAgent)
|
||||
.setNotificationVisibility(
|
||||
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED,
|
||||
)
|
||||
.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
URLUtil.guessFileName(url, contentDisposition, mimetype),
|
||||
)
|
||||
(getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).enqueue(request)
|
||||
Toast.makeText(this@WebShellActivity, "Download started", Toast.LENGTH_SHORT).show()
|
||||
}.onFailure {
|
||||
Toast.makeText(this@WebShellActivity, "Download failed", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
view.loadUrl(gatewayUrl)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
webView?.saveState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
webView?.restoreState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
webView?.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
webView?.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
webView?.destroy()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue