Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/java/org/matrix/chromext/Chrome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ object Chrome {
isMi = packageName == "com.mi.globalbrowser" || packageName == "com.android.browser"
isQihoo = packageName == "com.qihoo.contents"
isSamsung = packageName.startsWith("com.sec.android.app.sbrowser")
isVivaldi = packageName == "com.vivaldi.browser"
// com.vivaldi.browser.snapshot is supported too, and it needs the same special cases as stable
isVivaldi = packageName.startsWith("com.vivaldi.browser")
@Suppress("DEPRECATION") val packageInfo = ctx.packageManager?.getPackageInfo(packageName, 0)
version = packageInfo?.versionName
version = (if (version?.startsWith("v") == true) "" else "v") + version
Expand Down
86 changes: 73 additions & 13 deletions app/src/main/java/org/matrix/chromext/Listener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import android.os.Handler
import android.webkit.WebView
import java.io.File
import java.io.FileReader
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONArray
Expand All @@ -37,6 +39,7 @@ import org.matrix.chromext.utils.ERUD_URL
import org.matrix.chromext.utils.Log
import org.matrix.chromext.utils.XMLHttpRequest
import org.matrix.chromext.utils.findMethod
import org.matrix.chromext.utils.findMethodOrNull
import org.matrix.chromext.utils.invalidUserScriptUrls
import org.matrix.chromext.utils.invokeMethod
import org.matrix.chromext.utils.isChromeXtFrontEnd
Expand Down Expand Up @@ -88,6 +91,61 @@ object Listener {
return true
}

// TabModel#closeTab(Tab tab, Tab recommendedNextTab, boolean...[, int]), dropped by Chrome 131
// but still shipped by older forks.
private fun findLegacyCloseTab(model: Class<*>, tab: Class<*>): Method? {
fun Method.closesTab() =
parameterTypes.firstOrNull() == tab &&
parameterTypes.drop(1).all {
it == tab || it == Boolean::class.java || it == Int::class.java
}
return findMethodOrNull(model, true) { name == "closeTab" && closesTab() }
?: findMethodOrNull(model, true) {
returnType == Boolean::class.java && parameterTypes.size >= 5 && closesTab()
}
}

// TabClosureParams is the only class offering a static Tab factory whose result builds one back,
// a shape that survives the renaming of all three classes involved.
private fun isTabClosureParams(clazz: Class<*>, tab: Class<*>): Boolean =
clazz.declaredMethods.any {
Modifier.isStatic(it.modifiers) &&
it.parameterTypes contentDeepEquals arrayOf(tab) &&
it.returnType.declaredMethods.any { build ->
build.parameterTypes.size == 0 && build.returnType == clazz
}
}

// Since Chrome 131: TabModel#getTabRemover()#forceCloseTabs(TabClosureParams#closeTab(tab))
private fun forceCloseTab(model: Any, tabModel: Class<*>, tab: Class<*>, currentTab: Any) {
val getTabRemover =
findMethod(tabModel) {
parameterTypes.size == 0 &&
returnType.declaredMethods.any {
it.returnType == Void.TYPE &&
it.parameterTypes.size == 1 &&
isTabClosureParams(it.parameterTypes[0], tab)
}
}
val remover = getTabRemover.invoke(model)!!
val forceCloseTabs =
findMethod(getTabRemover.returnType) {
returnType == Void.TYPE &&
parameterTypes.size == 1 &&
isTabClosureParams(parameterTypes[0], tab)
}
val closureParams = forceCloseTabs.parameterTypes[0]
val builder =
findMethod(closureParams) {
Modifier.isStatic(modifiers) && parameterTypes contentDeepEquals arrayOf(tab)
}
.invoke(null, currentTab)!!
val params =
findMethod(builder::class.java) { parameterTypes.size == 0 && returnType == closureParams }
.invoke(builder)
forceCloseTabs.invoke(remover, params)
}

private fun checkErudaVerison(ctx: Context, callback: (String?) -> Unit) {
val url = URL(ERUD_URL + "@latest/eruda.js")
val connection = url.openConnection() as HttpURLConnection
Expand Down Expand Up @@ -177,19 +235,21 @@ object Listener {
parameterTypes.size == 0 && returnType == tabModel
}
val model = getCurrentTabModel.invoke(activity)!!
val closeTab =
findMethod(model::class.java) {
returnType == Boolean::class.java &&
parameterTypes contentDeepEquals
arrayOf(
tab,
tab,
Boolean::class.java,
Boolean::class.java,
Boolean::class.java,
Int::class.java)
}
closeTab.invoke(model, currentTab, null, false, false, false, 0)
val legacyClose = findLegacyCloseTab(model::class.java, tab)
if (legacyClose != null) {
val args =
legacyClose.parameterTypes.mapIndexed { i, type ->
when {
i == 0 -> currentTab
type == tab -> null
type == Boolean::class.java -> false
else -> 0
}
}
legacyClose.invoke(model, *args.toTypedArray())
} else {
forceCloseTab(model, tabModel, tab, currentTab)
}
} else {
val msg = "Closing tab ${currentTab} with context ${activity}"
callback = "console.error(new TypeError('ChromeXt Action failure', {cause: '${msg}'}));"
Expand Down
50 changes: 35 additions & 15 deletions app/src/main/java/org/matrix/chromext/MainHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,42 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
Log.d(lpparam.processName + " started")
if (lpparam.packageName == "org.matrix.chromext") return
if (supportedPackages.contains(lpparam.packageName)) {
lpparam.classLoader
.loadClass("org.chromium.ui.base.WindowAndroid")
.declaredConstructors[1]
.hookAfter {
Chrome.init(it.args[0] as Context, lpparam.packageName)
initHooks(UserScriptHook)
if (ContextMenuHook.isInit) return@hookAfter
runCatching {
if (!Chrome.isVivaldi) initHooks(PreferenceHook)
initHooks(if (Chrome.isEdge || Chrome.isCocCoc) PageInfoHook else PageMenuHook)
}
.onFailure {
initHooks(ContextMenuHook)
if (BuildConfig.DEBUG && !(Chrome.isSamsung || Chrome.isEdge)) Log.ex(it)
}
// Every constructor taking a Context, rather than a fixed index into an order the JLS leaves
// unspecified: browsers do not agree on how many constructors WindowAndroid has, and Whale in
// particular has no one-argument overload, so index 1 there is a test-only constructor the
// browser never calls and the whole module stays inert. The overloads delegate into one
// another, so hooking all of them just means the callback can run twice, which Chrome.init
// and initHooks both already absorb.
val windowAndroid = lpparam.classLoader.loadClass("org.chromium.ui.base.WindowAndroid")
val constructors =
windowAndroid.declaredConstructors.filter {
val first = it.parameterTypes.firstOrNull()
first != null && Context::class.java.isAssignableFrom(first)
}
if (constructors.isEmpty()) Log.e("No WindowAndroid constructor takes a Context")
constructors.forEach { constructor ->
constructor.hookAfter {
Chrome.init(it.args[0] as Context, lpparam.packageName)
// Keep the menu hooks reachable even when the UserScript hook cannot be installed
runCatching { initHooks(UserScriptHook) }.onFailure { Log.ex(it) }
if (ContextMenuHook.isInit) return@hookAfter
// The settings screen and the browser menu are independent features, so a browser that
// cannot host our preferences must still get its menu entries. Edge is exactly that case:
// it builds its settings programmatically, so PreferenceFragmentCompat carries no
// addPreferencesFromResource for PreferenceHook to hook.
if (!Chrome.isVivaldi) {
runCatching { initHooks(PreferenceHook) }
.onFailure { if (BuildConfig.DEBUG && !Chrome.isSamsung) Log.ex(it) }
}
runCatching {
initHooks(if (Chrome.isEdge || Chrome.isCocCoc) PageInfoHook else PageMenuHook)
}
.onFailure {
initHooks(ContextMenuHook)
if (BuildConfig.DEBUG && !(Chrome.isSamsung || Chrome.isEdge)) Log.ex(it)
}
}
}
} else {
val ctx = AndroidAppHelper.currentApplication()

Expand Down
97 changes: 64 additions & 33 deletions app/src/main/java/org/matrix/chromext/hook/PageInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package org.matrix.chromext.hook
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import org.matrix.chromext.Chrome
import org.matrix.chromext.Listener
Expand All @@ -18,52 +17,84 @@ object PageInfoHook : BaseHook() {
override fun init() {

if (ContextMenuHook.isInit) return
var controller: Any? = null
val proxy = PageInfoProxy
val newRow = proxy.rowConstructor ?: throw NoSuchMethodException("PageInfoRowView(Context)")
val title = proxy.mTitle ?: throw NoSuchFieldException("PageInfoRowView.mTitle")
val show = proxy.showPageInfo ?: throw NoSuchMethodException("PageInfoController.show")
val destroy = proxy.destroy ?: throw NoSuchMethodException("PageInfoController.destroy")
if (proxy.nativeCallbacks.isEmpty()) throw NoSuchMethodException("PageInfoController callbacks")

// The row container is looked up by resource id and never kept in a field, and Edge can swap
// the whole PageInfoView for a bottom sheet behind a feature flag, so rather than chase it
// through the controller we keep the first row built during show() and take its parent.
var firstRow: ViewGroup? = null
var controller: Any? = null
var showing = false
var inserted = false

fun addErudaRow(url: String): ViewGroup {
val infoRow =
proxy.pageInfoRowView.declaredConstructors[0].newInstance(Chrome.getContext(), null)
as ViewGroup
infoRow.setVisibility(View.VISIBLE)
val icon = proxy.mIcon.get(infoRow) as ImageView
icon.setImageResource(R.drawable.ic_devtools)
val subTitle = proxy.mSubtitle.get(infoRow) as TextView
(subTitle.getParent() as? ViewGroup)?.removeView(subTitle)
val title = proxy.mTitle.get(infoRow) as TextView
fun erudaRow(url: String, host: Any, parent: ViewGroup): View {
val row = newRow.newInstance(parent.getContext(), null) as ViewGroup
row.setVisibility(View.VISIBLE)
(proxy.mIcon?.get(row) as? ImageView)?.setImageResource(R.drawable.ic_devtools)
val subTitle = proxy.mSubtitle?.get(row) as? TextView
(subTitle?.getParent() as? ViewGroup)?.removeView(subTitle)
val label = title.get(row) as TextView
val onClick: () -> Unit
if (isChromeXtFrontEnd(url)) {
title.setText(R.string.main_menu_developer_tools)
infoRow.setOnClickListener {
Listener.on("inspectPages")
controller!!.invokeMethod() { name == "destroy" }
}
label.setText(R.string.main_menu_developer_tools)
onClick = { Listener.on("inspectPages") }
} else if (isUserScript(url)) {
title.setText(R.string.main_menu_install_script)
infoRow.setOnClickListener {
label.setText(R.string.main_menu_install_script)
onClick = {
val sandBoxed = shouldBypassSandbox(url)
Chrome.evaluateJavascript(listOf("Symbol.installScript(true);"), null, null, sandBoxed)
controller!!.invokeMethod() { name == "destroy" }
}
} else {
title.setText(R.string.main_menu_eruda_console)
infoRow.setOnClickListener {
UserScriptProxy.evaluateJavascript(Local.openEruda)
controller!!.invokeMethod() { name == "destroy" }
}
label.setText(R.string.main_menu_eruda_console)
onClick = { UserScriptProxy.evaluateJavascript(Local.openEruda) }
}
row.setOnClickListener {
onClick()
runCatching { destroy.invoke(host) }.onFailure { Log.ex(it) }
}
return infoRow
return row
}

proxy.pageInfoControllerRef.declaredConstructors[0].hookAfter { controller = it.thisObject }
fun insertRow(host: Any) {
if (inserted) return
val parent = firstRow?.getParent() as? ViewGroup ?: return
val url = Chrome.getUrl() ?: return
if (isChromeScheme(url)) return
inserted = true
parent.addView(erudaRow(url, host, parent))
}

show.hookBefore {
showing = true
inserted = false
firstRow = null
controller = null
}

newRow.hookAfter { if (showing && firstRow == null) firstRow = it.thisObject as ViewGroup }

// Native runs these while show() is still on the stack, which is both where the controller
// instance becomes reachable and the earliest point at which the rows are already laid out.
proxy.nativeCallbacks.forEach { callback ->
callback.hookAfter {
if (!showing) return@hookAfter
controller = it.thisObject
insertRow(it.thisObject)
}
}

proxy.pageInfoController.declaredConstructors[0].hookAfter {
val url = Chrome.getUrl()!!
if (isChromeScheme(url) || controller == null) return@hookAfter
(proxy.mRowWrapper.get(proxy.mView.get(it.thisObject)) as LinearLayout).addView(
addErudaRow(url))
show.hookAfter {
showing = false
controller?.let { host -> insertRow(host) }
firstRow = null
controller = null
}

// readerMode.init(Chrome.load("org.chromium.chrome.browser.dom_distiller.ReaderModeManager"))
isInit = true
}
}
Loading