Skip to content

Commit

Permalink
Support Mi Browser
Browse files Browse the repository at this point in the history
Unable to test the China-only version of Mi Browser yet.
Close #138
  • Loading branch information
JingMatrix committed Dec 3, 2023
1 parent aa2a4c9 commit 0dc7bb1
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 92 deletions.
7 changes: 6 additions & 1 deletion app/src/main/java/org/matrix/chromext/Chrome.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ object Chrome {
var isBrave = false
var isDev = false
var isEdge = false
var isMi = false
var isSamsung = false
var isVivaldi = false
var isCocCoc = false
Expand All @@ -58,6 +59,7 @@ object Chrome {
isCocCoc = packageName.startsWith("com.coccoc.trinhduyet")
isDev = packageName.endsWith("canary") || packageName.endsWith("dev")
isEdge = packageName.startsWith("com.microsoft.emmx")
isMi = packageName == "com.mi.globalbrowser" || packageName == "com.android.browser"
isSamsung = packageName.startsWith("com.sec.android.app.sbrowser")
isVivaldi = packageName == "com.vivaldi.browser"
@Suppress("DEPRECATION") val packageInfo = ctx.packageManager?.getPackageInfo(packageName, 0)
Expand Down Expand Up @@ -256,7 +258,10 @@ object Chrome {
val code = "Symbol.${Local.name}.unlock(${Local.key}).post('${event}', ${data});"
Log.d("broadcasting ${event}")
if (WebViewHook.isInit) {
val tabs = WebViewHook.records.filter { matching(it.get()?.getUrl()) }
val tabs =
WebViewHook.records.filter {
matching(it.get()?.invokeMethod() { name == "getUrl" } as String?)
}
if (tabs.size > 1 || !excludeSelf)
tabs.forEach { WebViewHook.evaluateJavascript(code, it.get()) }
return
Expand Down
23 changes: 20 additions & 3 deletions app/src/main/java/org/matrix/chromext/MainHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.matrix.chromext
import android.app.AndroidAppHelper
import android.content.Context
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.IXposedHookZygoteInit
Expand Down Expand Up @@ -70,16 +71,32 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
}
} else {
val ctx = AndroidAppHelper.currentApplication()

Chrome.isMi =
lpparam.packageName == "com.mi.globalbrowser" ||
lpparam.packageName == "com.android.browser"
if (ctx == null && Chrome.isMi) return
// Wait to get the browser context of Mi Browser

if (ctx != null && lpparam.packageName != "android") {
Chrome.init(ctx, ctx.packageName)
}

if (Chrome.isMi) {
WebViewHook.WebView = Chrome.load("com.miui.webkit.WebView")
WebViewHook.ViewClient = Chrome.load("com.android.browser.tab.TabWebViewClient")
WebViewHook.ChromeClient = Chrome.load("com.android.browser.tab.TabWebChromeClient")
initHooks(WebViewHook, ContextMenuHook)
return
}

WebViewHook.WebView = WebView::class.java
WebView.setWebContentsDebuggingEnabled(true)

WebViewClient::class.java.declaredConstructors[0].hookAfter {
if (it.thisObject::class != WebViewClient::class) {
WebViewHook.ViewClient = it.thisObject::class.java
if (WebViewHook.ChromeClient != null) {
initHooks(WebViewHook, ContextMenuHook)
}
if (WebViewHook.ChromeClient != null) {}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ fun connectDevTools(client: LocalSocket) {
} else {
"chrome_devtools_remote"
}
} else if (Chrome.isMi) {
"miui_webview_devtools_remote"
} else if (WebViewHook.isInit) {
"webview_devtools_remote"
} else {
Expand Down
156 changes: 105 additions & 51 deletions app/src/main/java/org/matrix/chromext/hook/ContextMenu.kt
Original file line number Diff line number Diff line change
@@ -1,77 +1,131 @@
package org.matrix.chromext.hook

import android.content.Context
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.webkit.WebView
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.ImageView
import de.robv.android.xposed.XC_MethodHook.Unhook
import java.lang.Class
import org.matrix.chromext.Chrome
import org.matrix.chromext.Listener
import org.matrix.chromext.R
import org.matrix.chromext.Resource
import org.matrix.chromext.script.Local
import org.matrix.chromext.utils.findMethod
import org.matrix.chromext.utils.hookAfter
import org.matrix.chromext.utils.hookBefore
import org.matrix.chromext.utils.isChromeXtFrontEnd
import org.matrix.chromext.utils.isUserScript
import org.matrix.chromext.utils.shouldBypassSandbox
import org.matrix.chromext.utils.*

object ContextMenuHook : BaseHook() {

override fun init() {

val erudaMenuId = 31415926
val clickListnerFactory = { mode: ActionMode, url: String ->
MenuItem.OnMenuItemClickListener {
if (WebViewHook.isInit) {
val webSettings = (Chrome.getTab() as WebView).settings
val javaScriptEnabled = webSettings.javaScriptEnabled
if (!javaScriptEnabled) {
webSettings.javaScriptEnabled = true
Chrome.evaluateJavascript(listOf(Local.initChromeXt))
}
}
if (isChromeXtFrontEnd(url)) {
Listener.on("inspectPages")
} else if (isUserScript(url)) {
val sandBoxed = shouldBypassSandbox(url)
Chrome.evaluateJavascript(listOf("Symbol.installScript(true);"), null, sandBoxed)
} else {
Chrome.evaluateJavascript(listOf(Local.openEruda))
}
mode.finish()
true
val erudaMenuId = 31415926
private fun openEruda(url: String) {
if (WebViewHook.isInit) {
val webSettings = Chrome.getTab()?.invokeMethod { name == "getSettings" }
if (webSettings == null) return
val javaScriptEnabled = webSettings.invokeMethod { name == "getJavaScriptEnabled" } as Boolean
if (!javaScriptEnabled) {
webSettings.invokeMethod(true) { name == "setJavaScriptEnabled" }
Chrome.evaluateJavascript(listOf(Local.initChromeXt))
}
}
if (isChromeXtFrontEnd(url)) {
Listener.on("inspectPages")
} else if (isUserScript(url)) {
val sandBoxed = shouldBypassSandbox(url)
Chrome.evaluateJavascript(listOf("Symbol.installScript(true);"), null, sandBoxed)
} else {
Chrome.evaluateJavascript(listOf(Local.openEruda))
}
}

var actionModeFinder: Unhook? = null
fun hookActionMode(cls: Class<*>) {
actionModeFinder?.unhook()
findMethod(cls) { name == "onCreateActionMode" }
// public abstract boolean onCreateActionMode (ActionMode mode, Menu menu)
.hookAfter {
val url = Chrome.getUrl()
val mode = it.args[0] as ActionMode
val menu = it.args[1] as Menu
if (menu.findItem(erudaMenuId) != null) return@hookAfter
Resource.enrich(Chrome.getContext())
val titleId =
if (isChromeXtFrontEnd(url)) R.string.main_menu_developer_tools
else if (isUserScript(url)) R.string.main_menu_install_script
else R.string.main_menu_eruda_console
val erudaMenu = menu.add(titleId)
val mId = erudaMenu::class.java.getDeclaredField("mId")
mId.setAccessible(true)
mId.set(erudaMenu, erudaMenuId)
erudaMenu.setOnMenuItemClickListener(clickListnerFactory(mode, url!!))
}
private val clickListnerFactory = { mode: ActionMode, url: String ->
MenuItem.OnMenuItemClickListener {
openEruda(url)
mode.finish()
true
}
}

private var actionModeFinder: Unhook? = null
private fun hookActionMode(cls: Class<*>) {
actionModeFinder?.unhook()
findMethod(cls) { name == "onCreateActionMode" }
// public abstract boolean onCreateActionMode (ActionMode mode, Menu menu)
.hookAfter {
val url = Chrome.getUrl()
val mode = it.args[0] as ActionMode
val menu = it.args[1] as Menu
if (menu.findItem(erudaMenuId) != null) return@hookAfter
Resource.enrich(Chrome.getContext())
val titleId =
if (isChromeXtFrontEnd(url)) R.string.main_menu_developer_tools
else if (isUserScript(url)) R.string.main_menu_install_script
else R.string.main_menu_eruda_console
val erudaMenu = menu.add(titleId)
val mId = erudaMenu::class.java.getDeclaredField("mId")
mId.setAccessible(true)
mId.set(erudaMenu, erudaMenuId)
erudaMenu.setOnMenuItemClickListener(clickListnerFactory(mode, url!!))
}
}

override fun init() {
if (Chrome.isSamsung) {
hookActionMode(Chrome.load("com.sec.terrace.content.browser.TinActionModeCallback"))
} else if (Chrome.isMi) {
val miuiFloatingSelectPopupWindow =
Chrome.load("com.miui.org.chromium.content.browser.miui.MiuiFloatingSelectPopupWindow")
val mContentView = findField(miuiFloatingSelectPopupWindow) { name == "mContentView" }
val mContext = findField(miuiFloatingSelectPopupWindow) { name == "mContext" }
val mParent = findField(miuiFloatingSelectPopupWindow) { name == "mParent" }
val mDelegate = findField(miuiFloatingSelectPopupWindow) { name == "mDelegate" }
val mShareImageView = findField(miuiFloatingSelectPopupWindow) { name == "mShareImageView" }

val selectionPopupController =
Chrome.load(
"com.miui.org.chromium.content.browser.selection.SelectionPopupControllerImpl")
val miuiSelectPopupMenuDelegate =
Chrome.load("com.miui.org.chromium.content.browser.miui.MiuiSelectPopupMenuDelegate")
val getDarkOrNightModeEnabled =
findMethod(miuiSelectPopupMenuDelegate) { name == "getDarkOrNightModeEnabled" }

val miuiTextSelectResources =
Chrome.load("com.miui.org.chromium.content.browser.miui.MiuiTextSelectResources")
val createImageView = findMethod(miuiTextSelectResources) { name == "createImageView" }
findMethod(miuiFloatingSelectPopupWindow) { name == "showPopup" }
.hookAfter {
val popupWindow = it.thisObject
val context = mContext.get(popupWindow) as Context
val view = mParent.get(popupWindow)
val delegate = mDelegate.get(popupWindow)!!
val this0 = findField(delegate::class.java) { type == selectionPopupController }
val controller = this0.get(delegate)!!
if (WebViewHook.WebView!!.isAssignableFrom(view::class.java)) Chrome.updateTab(view)
Resource.enrich(context)
val url = Chrome.getUrl()

val contentView = mContentView.get(it.thisObject) as ViewGroup
val listener = OnClickListener {
controller.invokeMethod { name == "finishActionMode" }
openEruda(url!!)
}

val shareImageView = mShareImageView.get(popupWindow) as ImageView
val erudaImageView =
createImageView.invoke(
null, context, listener, getDarkOrNightModeEnabled.invoke(delegate))
as ImageView
erudaImageView.setImageResource(R.drawable.ic_devtools)
erudaImageView.setId(erudaMenuId)
erudaImageView.getLayoutParams().height = shareImageView.getMeasuredHeight()
erudaImageView.getLayoutParams().width = shareImageView.getMeasuredWidth()
val paddingY = contentView.getMeasuredHeight() - shareImageView.getMeasuredHeight() - 10
erudaImageView.setPadding(
shareImageView.paddingLeft, paddingY / 2, shareImageView.paddingRight, paddingY / 2)
contentView.addView(erudaImageView)
}
} else {
actionModeFinder =
findMethod(View::class.java) { name == "startActionMode" && parameterTypes.size == 2 }
Expand Down
66 changes: 35 additions & 31 deletions app/src/main/java/org/matrix/chromext/hook/WebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package org.matrix.chromext.hook
import android.app.Activity
import android.os.Build
import android.os.Handler
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebView
import java.lang.ref.WeakReference
import org.matrix.chromext.Chrome
import org.matrix.chromext.Listener
Expand All @@ -15,67 +12,74 @@ import org.matrix.chromext.utils.Log
import org.matrix.chromext.utils.findMethod
import org.matrix.chromext.utils.hookAfter
import org.matrix.chromext.utils.hookBefore
import org.matrix.chromext.utils.invokeMethod

object WebViewHook : BaseHook() {

var ViewClient: Class<*>? = null
var ChromeClient: Class<*>? = null
val records = mutableListOf<WeakReference<WebView>>()
var WebView: Class<*>? = null
val records = mutableListOf<WeakReference<Any>>()

fun evaluateJavascript(code: String?, view: Any?) {
val webView = (view ?: Chrome.getTab()) as WebView?
if (code != null && code.length > 0 && webView != null && webView.settings.javaScriptEnabled) {
Handler(Chrome.getContext().mainLooper).post { webView.evaluateJavascript(code, null) }
val webView = (view ?: Chrome.getTab())
if (code != null && code.length > 0 && webView != null) {
val webSettings = webView.invokeMethod { name == "getSettings" }
if (webSettings?.invokeMethod { name == "getJavaScriptEnabled" } == true)
Handler(Chrome.getContext().mainLooper).post {
webView.invokeMethod(code, null) { name == "evaluateJavascript" }
}
}
}

override fun init() {

WebView.setWebContentsDebuggingEnabled(true)

findMethod(ChromeClient!!, true) {
name == "onConsoleMessage" &&
parameterTypes contentDeepEquals arrayOf(ConsoleMessage::class.java)
}
findMethod(ChromeClient!!, true) { name == "onConsoleMessage" }
// public boolean onConsoleMessage (ConsoleMessage consoleMessage)
.hookAfter {
// This should be the way to communicate with the front-end of ChromeXt
val chromeClient = it.thisObject as WebChromeClient
val consoleMessage = it.args[0] as ConsoleMessage
if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.TIP &&
consoleMessage.sourceId() == "local://ChromeXt/init" &&
consoleMessage.lineNumber() == Local.anchorInChromeXt) {
val chromeClient = it.thisObject
val consoleMessage = it.args[0]
val messageLevel = consoleMessage.invokeMethod { name == "messageLevel" }
val sourceId = consoleMessage.invokeMethod { name == "sourceId" }
val lineNumber = consoleMessage.invokeMethod { name == "lineNumber" }
val message = consoleMessage.invokeMethod { name == "message" } as String
if (messageLevel.toString() == "TIP" &&
sourceId == "local://ChromeXt/init" &&
lineNumber == Local.anchorInChromeXt) {
val webView =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
records.find { it.get()?.getWebChromeClient() == chromeClient }?.get()
} else Chrome.getTab() as WebView?
Listener.startAction(consoleMessage.message(), webView)
records
.find {
it.get()?.invokeMethod { name == "getWebChromeClient" } == chromeClient
}
?.get()
} else Chrome.getTab()
Listener.startAction(message, webView)
} else {
Log.d(
consoleMessage.messageLevel().toString() +
": [${consoleMessage.sourceId()}@${consoleMessage.lineNumber()}] ${consoleMessage.message()}")
Log.d(messageLevel.toString() + ": [${sourceId}@${lineNumber}] ${message}")
}
}

fun onUpdateUrl(url: String, view: WebView) {
if (url.startsWith("javascript")) return
fun onUpdateUrl(url: String, view: Any?) {
if (url.startsWith("javascript") || view == null) return
Chrome.updateTab(view)
ScriptDbManager.invokeScript(url, view)
}

findMethod(WebView::class.java) { name == "setWebChromeClient" }
findMethod(WebView!!) { name == "setWebChromeClient" }
.hookAfter {
val webView = it.thisObject as WebView
val webView = it.thisObject
records.removeAll(records.filter { it.get() == null || it.get() == webView })
if (it.args[0] != null) records.add(WeakReference(webView))
}

findMethod(WebView::class.java) { name == "onAttachedToWindow" }
.hookAfter { Chrome.updateTab(it.thisObject as WebView) }
findMethod(WebView!!) { name == "onAttachedToWindow" }
.hookAfter { Chrome.updateTab(it.thisObject) }

findMethod(ViewClient!!, true) { name == "onPageStarted" }
// public void onPageStarted (WebView view, String url, Bitmap favicon)
.hookAfter { onUpdateUrl(it.args[1] as String, it.args[0] as WebView) }
.hookAfter { onUpdateUrl(it.args[1] as String, it.args[0]) }

findMethod(Activity::class.java) { name == "onStop" }
.hookBefore { ScriptDbManager.updateScriptStorage() }
Expand Down
Loading

0 comments on commit 0dc7bb1

Please sign in to comment.