diff --git a/app/src/main/java/org/lineageos/jelly/js/JsManifest.kt b/app/src/main/java/org/lineageos/jelly/js/JsManifest.kt new file mode 100644 index 00000000..e983248f --- /dev/null +++ b/app/src/main/java/org/lineageos/jelly/js/JsManifest.kt @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2025 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.jelly.js + +import android.graphics.BitmapFactory +import android.webkit.JavascriptInterface +import androidx.annotation.Keep +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.lineageos.jelly.webview.WebViewExtActivity +import java.net.HttpURLConnection +import java.net.URI +import java.net.URL +import kotlin.reflect.cast + +@Keep +class JsManifest( + private val activity: WebViewExtActivity, +) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @JavascriptInterface + fun onIconResolved(baseUrl: String, iconUrl: String) { + val url = URI(baseUrl).resolve(iconUrl).toString() + scope.launch { + val bitmap = getIconBitmap(url) + if (bitmap != null) { + withContext(Dispatchers.Main) { + activity.onFaviconLoaded(bitmap) + } + } + } + } + + private fun getIconBitmap(url: String) = runCatching { + val connection = HttpURLConnection::class.cast( + URL(url).openConnection() + ) + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) return null + connection.inputStream.buffered().use { + BitmapFactory.decodeStream(it) + } + }.getOrNull() + + companion object { + const val INTERFACE = "JsManifest" + const val URL = "(() => document.querySelector('link[rel=\"manifest\"]')?.href ?? '')" + + private const val MONKEY_PATCH_ONCE_KEY = "JsManifestMonkeyPatch" + const val SCRIPT = """ + (async () => { + if (window.$MONKEY_PATCH_ONCE_KEY) return; + + window.$MONKEY_PATCH_ONCE_KEY = true; + const baseUrl = $URL(); + + if (!baseUrl) return; + + try { + const res = await fetch(baseUrl); + const manifest = await res.json(); + + let iconUrl = null; + let minWidth = 33; + const maxWidth = 333; + (manifest.icons ?? []).forEach((icon) => { + if (!icon.sizes) return; + if (icon.purpose?.includes('monochrome')) return; + const width = Number(icon.sizes.split('x')[0]); + if (width >= minWidth && width <= maxWidth) { + minWidth = width; + iconUrl = icon.src; + } + }); + if (iconUrl !== null) { + $INTERFACE.onIconResolved(baseUrl, iconUrl); + } + } catch (error) { + } + })(); + """ + } +} diff --git a/app/src/main/java/org/lineageos/jelly/webview/ChromeClient.kt b/app/src/main/java/org/lineageos/jelly/webview/ChromeClient.kt index 076e61c7..ad2fecdc 100644 --- a/app/src/main/java/org/lineageos/jelly/webview/ChromeClient.kt +++ b/app/src/main/java/org/lineageos/jelly/webview/ChromeClient.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020-2021 The LineageOS Project + * SPDX-FileCopyrightText: 2020-2025 The LineageOS Project * SPDX-License-Identifier: Apache-2.0 */ @@ -18,6 +18,7 @@ import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Toast import org.lineageos.jelly.R +import org.lineageos.jelly.js.JsManifest import org.lineageos.jelly.ui.UrlBarLayout import org.lineageos.jelly.utils.SharedPreferencesExt import org.lineageos.jelly.utils.TabUtils.openInNewTab @@ -43,7 +44,16 @@ internal class ChromeClient( } override fun onReceivedIcon(view: WebView, icon: Bitmap) { - activity.onFaviconLoaded(icon) + if (!view.settings.javaScriptEnabled) { + activity.onFaviconLoaded(icon) + return + } + + view.evaluateJavascript("${JsManifest.URL}()") { manifestUrl -> + if (manifestUrl.isBlank() || manifestUrl == "\"\"") { + activity.onFaviconLoaded(icon) + } + } } override fun onShowFileChooser( diff --git a/app/src/main/java/org/lineageos/jelly/webview/WebClient.kt b/app/src/main/java/org/lineageos/jelly/webview/WebClient.kt index 7ce1fc29..5a7e7b1a 100644 --- a/app/src/main/java/org/lineageos/jelly/webview/WebClient.kt +++ b/app/src/main/java/org/lineageos/jelly/webview/WebClient.kt @@ -31,6 +31,7 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.google.android.material.snackbar.Snackbar import org.lineageos.jelly.R +import org.lineageos.jelly.js.JsManifest import org.lineageos.jelly.js.JsSyncUrl import org.lineageos.jelly.ui.UrlBarLayout import org.lineageos.jelly.utils.IntentUtils @@ -48,6 +49,7 @@ internal class WebClient(private val urlBarLayout: UrlBarLayout) : WebViewClient urlBarLayout.onPageLoadFinished(view.certificate) if (view.settings.javaScriptEnabled) { view.evaluateJavascript(JsSyncUrl.SCRIPT, null) + view.evaluateJavascript(JsManifest.SCRIPT, null) } } diff --git a/app/src/main/java/org/lineageos/jelly/webview/WebViewExt.kt b/app/src/main/java/org/lineageos/jelly/webview/WebViewExt.kt index 2c9cafad..dd71ce76 100644 --- a/app/src/main/java/org/lineageos/jelly/webview/WebViewExt.kt +++ b/app/src/main/java/org/lineageos/jelly/webview/WebViewExt.kt @@ -13,6 +13,7 @@ import android.util.AttributeSet import android.util.Log import android.view.View import android.webkit.WebView +import org.lineageos.jelly.js.JsManifest import org.lineageos.jelly.js.JsSyncUrl import org.lineageos.jelly.ui.UrlBarLayout import org.lineageos.jelly.utils.SharedPreferencesExt @@ -114,6 +115,10 @@ class WebViewExt @JvmOverloads constructor( JsSyncUrl(urlBarLayout, activity), JsSyncUrl.INTERFACE ) + addJavascriptInterface( + JsManifest(activity), + JsManifest.INTERFACE + ) } }