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/MainHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
.declaredConstructors[1]
.hookAfter {
Chrome.init(it.args[0] as Context, lpparam.packageName)
initHooks(UserScriptHook)
// 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
runCatching {
if (!Chrome.isVivaldi) initHooks(PreferenceHook)
Expand Down
39 changes: 24 additions & 15 deletions app/src/main/java/org/matrix/chromext/hook/PageMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,18 @@ object PageMenuHook : BaseHook() {
"org.matrix.chromext:id/eruda_console_id" ->
UserScriptProxy.evaluateJavascript(Local.openEruda)
"${ctx.packageName}:id/reload_menu_id" -> {
val isLoading = proxy.mIsLoading.get(Chrome.getTab()) as Boolean
if (!isLoading) return Listener.on("userAgentSpoof", getUrl()) != null
val tab = Chrome.getTab()
if (tab != null && !UserScriptProxy.isLoading(tab))
return Listener.on("userAgentSpoof", getUrl()) != null
}
}
return false
}

findMethod(proxy.chromeTabbedActivity) {
// public boolean onMenuOrKeyboardAction(int id, boolean fromMenu, ? triggeringMotion)
(parameterCount == 2 || parameterCount == 3) &&
// public boolean onMenuOrKeyboardAction(int id, boolean fromMenu, ? extras,
// ? triggeringMotion): the trailing parameters keep being added by Chromium
parameterCount in 2..4 &&
parameterTypes[0] == Int::class.java &&
parameterTypes[1] == Boolean::class.java &&
returnType == Boolean::class.java
Expand All @@ -143,8 +145,9 @@ object PageMenuHook : BaseHook() {
}

findMethod(proxy.customTabActivity) {
// public boolean onMenuOrKeyboardAction(int id, boolean fromMenu, ? triggeringMotion)
(parameterCount == 2 || parameterCount == 3) &&
// public boolean onMenuOrKeyboardAction(int id, boolean fromMenu, ? extras,
// ? triggeringMotion): the trailing parameters keep being added by Chromium
parameterCount in 2..4 &&
parameterTypes[0] == Int::class.java &&
parameterTypes[1] == Boolean::class.java &&
returnType == Boolean::class.java
Expand Down Expand Up @@ -340,7 +343,8 @@ object PageMenuHook : BaseHook() {
val mType = findField(MVCListAdapter_ListItem) { type == Int::class.java }
// the original field name was "type"

val mData = findField(proxy.propertyModel) { type == Map::class.java }
// Chromium declares this field as a HashMap since Chrome v150, it used to be a Map
val mData = findField(proxy.propertyModel) { Map::class.java.isAssignableFrom(type) }

return findMethod(tabbedAppMenuPropertiesDelegate) {
parameterTypes.size == 0 && returnType == MVCListAdapter_ModelList
Expand Down Expand Up @@ -411,17 +415,22 @@ object PageMenuHook : BaseHook() {
val menusToAdd = mutableListOf<Any>()

val itemConstuctor = MVCListAdapter_ListItem.declaredConstructors[0]
// Chromium swapped the parameter order of MVCListAdapter.ListItem in Chrome v150,
// it used to be ListItem(int type, PropertyModel model)
val newStandardItem = { model: Any? ->
if (itemConstuctor.parameterTypes.first() == Int::class.java) {
itemConstuctor.newInstance(AppMenuItemType.STANDARD.value, model)
} else {
itemConstuctor.newInstance(model, AppMenuItemType.STANDARD.value)
}
}
if (isChromeXtFrontEnd(url)) {
menusToAdd.add(
itemConstuctor.newInstance(AppMenuItemType.STANDARD.value, localMenus[0]))
menusToAdd.add(
itemConstuctor.newInstance(AppMenuItemType.STANDARD.value, localMenus[1]))
menusToAdd.add(newStandardItem(localMenus[0]))
menusToAdd.add(newStandardItem(localMenus[1]))
} else if (isUserScript(url)) {
menusToAdd.add(
itemConstuctor.newInstance(AppMenuItemType.STANDARD.value, localMenus[2]))
menusToAdd.add(newStandardItem(localMenus[2]))
} else {
menusToAdd.add(
itemConstuctor.newInstance(AppMenuItemType.STANDARD.value, localMenus[3]))
menusToAdd.add(newStandardItem(localMenus[3]))
}

val injectPosition =
Expand Down
16 changes: 14 additions & 2 deletions app/src/main/java/org/matrix/chromext/hook/UserScript.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ object UserScriptHook : BaseHook() {
.onFailure { if (BuildConfig.DEBUG) Log.ex(it) }
}

if (proxy.mIsLoading == null) {
// The field could not be located, keep track of the loading state ourselves. The name of
// loadingStateChanged is preserved by the obfuscation since it is called from the native
// side.
runCatching {
findMethod(proxy.tabWebContentsDelegateAndroidImpl) { name == "loadingStateChanged" }
// public void loadingStateChanged(boolean toDifferentDocument)
.hookAfter { proxy.setLoading(proxy.getTab(it.thisObject), it.args[0] as Boolean) }
proxy.startTrackingLoadingState()
}
.onFailure { Log.ex(it, "Fail to track the loading state of tabs") }
}

findMethod(if (Chrome.isSamsung) proxy.tabImpl else proxy.tabWebContentsDelegateAndroidImpl) {
name == "onUpdateUrl" || name == "onUpdateTargetUrl"
}
Expand All @@ -113,8 +126,7 @@ object UserScriptHook : BaseHook() {
if (url.isEmpty() && proxy.getUrl != null) {
url = proxy.parseUrl(proxy.getUrl(tab))!!
}
val isLoading = proxy.mIsLoading.get(tab) as Boolean
if (!url.startsWith("chrome") && isLoading) {
if (!url.startsWith("chrome") && proxy.isLoading(tab)) {
ScriptDbManager.invokeScript(url)
}
}
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/org/matrix/chromext/proxy/PageMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@ object PageMenuProxy {
val emptyTabObserver =
Chrome.load("org.chromium.chrome.browser.login.ChromeHttpAuthHandler").superclass as Class<*>
val tabImpl = UserScriptProxy.tabImpl
val mIsLoading = UserScriptProxy.mIsLoading
val mObservers = findField(tabImpl) { type.interfaces.contains(Iterable::class.java) }
}
80 changes: 56 additions & 24 deletions app/src/main/java/org/matrix/chromext/proxy/UserScript.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package org.matrix.chromext.proxy

import android.net.Uri
import android.view.ContextThemeWrapper
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import java.util.Collections
import java.util.WeakHashMap
import org.matrix.chromext.Chrome
import org.matrix.chromext.script.ScriptDbManager
import org.matrix.chromext.utils.Log
Expand Down Expand Up @@ -46,27 +49,33 @@ object UserScriptProxy {
Chrome.load("org.chromium.chrome.browser.tab.TabImpl")
}
private val getId = findMethodOrNull(tabImpl) { name == "getId" }
private val mId =
(if (Chrome.isSamsung) tabWebContentsDelegateAndroidImpl else tabImpl)
.declaredFields
.run {
val target = find { it.name == "mId" }
if (target == null) {
val profile = Chrome.load("org.chromium.chrome.browser.profiles.Profile")
val windowAndroid = Chrome.load("org.chromium.ui.base.WindowAndroid")
var startIndex = indexOfFirst { it.type == gURL }
val endIndex = indexOfFirst {
it.type == profile ||
it.type == ContextThemeWrapper::class.java ||
it.type == windowAndroid
}
if (startIndex == -1 || startIndex > endIndex) startIndex = 0
slice(startIndex..endIndex - 1).findLast { it.type == Int::class.java }!!
} else target
}
.also { it.isAccessible = true }
// Resolved lazily: the heuristic below relies on the declaration order of the fields, which no
// longer survives the renaming done by recent Chromium releases. It is dead weight anyway as soon
// as the getId method is found, so it must not break the initialization of this object.
private val mId: Field by lazy {
(if (Chrome.isSamsung) tabWebContentsDelegateAndroidImpl else tabImpl)
.declaredFields
.run {
val target = find { it.name == "mId" }
if (target == null) {
val profile = Chrome.load("org.chromium.chrome.browser.profiles.Profile")
val windowAndroid = Chrome.load("org.chromium.ui.base.WindowAndroid")
var startIndex = indexOfFirst { it.type == gURL }
val endIndex = indexOfFirst {
it.type == profile ||
it.type == ContextThemeWrapper::class.java ||
it.type == windowAndroid
}
if (startIndex == -1 || startIndex > endIndex) startIndex = 0
slice(startIndex..endIndex - 1).findLast { it.type == Int::class.java }!!
} else target
}
.also { it.isAccessible = true }
}
val mTab = findField(tabWebContentsDelegateAndroidImpl) { type == tabImpl }
val mIsLoading =
// Null when the field cannot be located, in which case the loading state is tracked by hooking
// the loadingStateChanged method, see UserScriptHook.
val mIsLoading: Field? =
tabImpl.declaredFields
.run {
// mIsLoading is used in method stopLoading, before calling
Expand All @@ -78,12 +87,17 @@ object UserScriptProxy {
maxOf(
indexOfFirst { it.type == webContents },
indexOfFirst { it.type == loadUrlParams })
slice(startIndex..size - 1).find {
it.type == Boolean::class.java && !Modifier.isStatic(it.modifiers)
}!!
if (startIndex == -1) null
else
slice(startIndex..size - 1).find {
it.type == Boolean::class.java && !Modifier.isStatic(it.modifiers)
}
} else target
}
.also { it.isAccessible = true }
?.also { it.isAccessible = true }

private val loadingTabs = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
private var trackLoadingState = false
val getUrl = findMethodOrNull(tabImpl) { returnType == gURL }
val loadUrl =
findMethod(if (Chrome.isSamsung) tabWebContentsDelegateAndroidImpl else tabImpl) {
Expand All @@ -98,6 +112,24 @@ object UserScriptProxy {
loadUrl.invoke(tab, newLoadUrlParams(url))
}

fun startTrackingLoadingState() {
trackLoadingState = true
}

fun setLoading(tab: Any?, loading: Boolean) {
if (tab == null) return
if (loading) loadingTabs.add(tab) else loadingTabs.remove(tab)
}

fun isLoading(tab: Any): Boolean {
mIsLoading?.let {
return it.get(tab) as Boolean
}
// Without any way to know the loading state, keep injecting: both the init script and
// GM.bootstrap are idempotent for a given document.
return if (trackLoadingState) loadingTabs.contains(tab) else true
}

fun getTabId(tab: Any): String {
val id = if (getId != null) getId.invoke(tab)!! else mId.get(tab)!!
return id.toString()
Expand Down