我正在为 Android 应用程序开发 Xposed 模块,并且正在尝试构建一个管理器,让用户可以通过 GUI 打开和关闭挂钩。其中一个关键部分涉及从 Xposed 部分编写配置,该部分在
handleLoadPackage()
(IXposedHookLoadPackage
) 中初始化。
在深入研究了一堆文档和论坛之后,我认为使用桥接服务将是解决此问题的最佳方法。我发现另一个 Xposed 模块完全可以完成我想要做的事情,所以我从那里借用了服务/客户端/桥接逻辑。
不幸的是,这就是我陷入困境的地方:我的模块似乎无法绑定桥接服务,因为 Xposed 部分找不到
BridgeService
并且我在 logcat 中可以找到的唯一相关错误是:
W ActivityManager: Unable to start service Intent { cmp=com.friction/.bridge.BridgeService } U=0: not found
bindService()
调用上。该函数应该创建并绑定服务(根据我正在使用的标志),但在我的情况下失败了。看起来我传递给这个函数的 Intent 没有被识别为存在,尽管已经定义了它正常工作所需的所有组件。
在有关同一场景的其他问题中我见过有人建议将服务类所在的包名称添加到清单中的
<queries>
元素中。然而,这种方法并没有解决我的问题。我还质疑这一步的必要性,特别是因为客户端和服务都在同一个包中。
我也尝试在清单中使用
android:name="com.friction.bridge.BridgeService"
,但没有成功。
我已经为这个问题苦苦挣扎了几个星期,但我仍然不知所措。这可能是我忽略的一些简单的事情,因为我之前提到的 Xposed 模块似乎使用与我相同的方法可以正常工作。
这就是我定义我的服务的方式(
BridgeService.kt
):
package com.friction.bridge
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.friction.bridge.types.ActionType
import com.friction.bridge.types.FileType
import com.friction.xposed.context.RemoteSideContext
import com.friction.xposed.context.SharedContextHolder
class BridgeService : Service() {
private lateinit var remoteSideContext: RemoteSideContext
override fun onDestroy() {
if (::remoteSideContext.isInitialized) {
remoteSideContext.bridgeService = null
}
}
override fun onBind(intent: Intent): IBinder? {
remoteSideContext = SharedContextHolder.remote(this)
remoteSideContext.apply {
bridgeService = this@BridgeService
}
return BridgeBinder()
}
inner class BridgeBinder : BridgeInterface.Stub() {
override fun fileOperation(action: Int, fileType: Int, content: ByteArray?): ByteArray {
val resolvedFile = FileType.fromValue(fileType)?.resolve(this@BridgeService)
return when (ActionType.entries[action]) {
ActionType.CREATE_AND_READ -> {
resolvedFile?.let {
if (!it.exists()) {
return content?.also { content -> it.writeBytes(content) } ?: ByteArray(
0
)
}
it.readBytes()
} ?: ByteArray(0)
}
ActionType.READ -> {
resolvedFile?.takeIf { it.exists() }?.readBytes() ?: ByteArray(0)
}
ActionType.WRITE -> {
content?.also { resolvedFile?.writeBytes(content) } ?: ByteArray(0)
}
ActionType.DELETE -> {
resolvedFile?.takeIf { it.exists() }?.delete()
ByteArray(0)
}
ActionType.EXISTS -> {
if (resolvedFile?.exists() == true)
ByteArray(1)
else ByteArray(0)
}
}
}
override fun registerConfigStateListener(listener: ConfigStateListener?) {
remoteSideContext.config.configStateListener = listener
}
}
}
这就是我定义我的客户的方式(
BridgeClient.kt
):
package com.friction.bridge
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import android.util.Log
import com.friction.BuildConfig
import com.friction.bridge.types.ActionType
import com.friction.bridge.types.FileType
import com.friction.core.Constants.LOG_TAG
import com.friction.xposed.context.ModContext
import de.robv.android.xposed.XposedHelpers
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
fun FileLoaderWrapper.loadFromBridge(bridgeClient: BridgeClient) {
isFileExists = { bridgeClient.isFileExists(fileType) }
read = { bridgeClient.createAndReadFile(fileType, defaultData) }
write = { bridgeClient.writeFile(fileType, it) }
delete = { bridgeClient.deleteFile(fileType) }
}
class BridgeClient(private val context: ModContext): ServiceConnection {
private lateinit var future: CompletableFuture<Boolean>
private lateinit var service: BridgeInterface
fun connect(onFailure: (Throwable) -> Unit, onResult: (Boolean) -> Unit) {
this.future = CompletableFuture()
with(context.androidContext) {
startActivity(Intent()
.setClassName(BuildConfig.APPLICATION_ID, BuildConfig.APPLICATION_ID + ".bridge.ForceStartActivity")
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
)
val intent = Intent()
.setClassName(BuildConfig.APPLICATION_ID, BuildConfig.APPLICATION_ID + ".bridge.BridgeService")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (!bindService(
intent,
Context.BIND_AUTO_CREATE,
Executors.newSingleThreadExecutor(),
this@BridgeClient
)) {
onFailure(IllegalStateException("Cannot bind to bridge service"))
} else {
onResult(true)
}
} else {
XposedHelpers.callMethod(
this,
"bindServiceAsUser",
intent,
this@BridgeClient,
Context.BIND_AUTO_CREATE,
Handler(HandlerThread("BridgeClient").apply {
start()
}.looper),
android.os.Process.myUserHandle()
)
}
}
runCatching {
onResult(future.get(15, TimeUnit.SECONDS))
}.onFailure {
onFailure(it)
}
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
this.service = BridgeInterface.Stub.asInterface(service)
future.complete(true)
}
override fun onNullBinding(name: ComponentName?) {
Log.i("BridgeClient", "cannot connect to bridge service")
exitProcess(1)
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.i("BridgeClient", "service disconnected")
exitProcess(1)
}
fun createAndReadFile(type: FileType, defaultData: ByteArray): ByteArray {
return service.fileOperation(ActionType.CREATE_AND_READ.ordinal, type.value, defaultData)
}
fun writeFile(type: FileType, data: ByteArray) {
service.fileOperation(ActionType.WRITE.ordinal, type.value, data)
}
fun readFile(type: FileType): ByteArray {
return service.fileOperation(ActionType.READ.ordinal, type.value, null)
}
fun deleteFile(type: FileType) {
service.fileOperation(ActionType.DELETE.ordinal, type.value, null)
}
fun isFileExists(type: FileType): Boolean {
return service.fileOperation(ActionType.EXISTS.ordinal, type.value, null).isNotEmpty()
}
fun registerConfigStateListener(listener: ConfigStateListener) {
service.registerConfigStateListener(listener)
}
}
这是我的清单和服务声明 (
AndroidManifest.xml
):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Friction"
tools:targetApi="34">
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="@string/module_description" />
<meta-data
android:name="xposedminversion"
android:value="93" />
<service
android:name=".bridge.BridgeService"
android:exported="true"
tools:ignore="ExportedService">
</service>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".bridge.ForceStartActivity"
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"
android:exported="true" />
</application>
</manifest>
如果需要更多信息,请告诉我,我很乐意提供更多信息并相应地更新这篇文章。预先感谢。
编辑:当我尝试从应用程序本身绑定到服务时,它不起作用。但是,我出于测试目的创建了一个单独的外部应用程序,令人惊讶的是,我能够从那里成功绑定到相同的服务。这个问题看起来很奇怪,因为服务绑定仅在从主应用程序内部尝试时失败,而不是从外部应用程序尝试失败:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
runCatching {
val intent = Intent()
.setClassName("com.friction",
"com.friction.bridge.BridgeService")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.i("BridgeClient", "Service connected")
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.i("BridgeClient", "Service disconnected")
}
fun connect() {
val ret = bindService(
intent,
Context.BIND_AUTO_CREATE,
Executors.newSingleThreadExecutor(),
this
)
Log.i("BridgeClient", "bindService returned $ret")
}
}
serviceConnection.connect()
}
}
}
}
根本原因是 Android 11 中引入的 程序包可见性限制。我连接的应用程序无法访问所有已安装的应用程序,其中包括我的管理器服务。由于我无法编辑其清单,因此我需要一个替代解决方案。
我决定使用目标应用程序的上下文列出用户 0 的所有已安装的软件包。有趣的是,它似乎只能查询用户安装的浏览器。更深入地了解后,我使用
dumpsys
检查应用程序的意图过滤器,其中包括 android.intent.action.VIEW
、android.media.action.IMAGE_CAPTURE
和 android.intent.action.GET_CONTENT
等操作。
发现这一点,我尝试将
android.intent.action.GET_CONTENT
意图过滤器添加到我的服务中。令人惊讶的是,这使得我的经理对目标应用程序可见,从而允许它绑定到我的 BridgeService
。虽然这可能不是最有效的解决方案,但这是我在这种情况下找到的唯一有效的解决方案。
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
我希望这对遇到类似情况的人有帮助:)