androidkotlinandroid-intentservicexposed

Unable to bind service - Unable to start service Intent


I'm working on an Xposed module for an Android app and I'm trying to put together a manager that lets users toggle hooks on and off through a GUI. A key part of this involves writing configurations from the Xposed part, which initializes in handleLoadPackage() (IXposedHookLoadPackage).

After digging through a bunch of docs and forums, I figured using a bridge service would be the best way to go about this. I found another Xposed Module that does exactly what I'm trying to do, so I borrowed the service/client/bridge logic from there.

Unfortunately, here's where I'm stuck: my module can't seem to bind the bridge service because the Xposed part doesn't find the BridgeService and only relevant error I can find in the logcat is this:

W ActivityManager: Unable to start service Intent { cmp=com.friction/.bridge.BridgeService } U=0: not found

I've identified that the issue lies with the bindService() call. This function is supposed to create and bind the service (as per the flags I'm using), but it's failing in my case. It looks like the Intent I'm passing to this function is not being recognized as existing, despite having defined all the necessary components for it to work properly.

In other questions about the same scenario I've seen people who recommended adding the package name, where the service class resides, to the <queries> element in the manifest. However, this approach hasn't resolved my issue. I'm also questioning the necessity of this step, especially since both the client and the service are within the same package.

I have also tried using android:name="com.friction.bridge.BridgeService" in the manifest with no success at all.

I've been struggling with this problem for several weeks now, and I'm still at a loss. It's likely something simple that I'm overlooking, as the Xposed Module I mentioned earlier seems to work correctly using the same approach to mine.


This is how I defined my service (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
        }
    }
}

This is how I defined my client (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)
    }
}

This is my manifest with the service declaration (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>

If more information is required, please let me know and I'll be happy to provide more and update this post accordingly. Thanks in advance.


EDIT: When I try to bind to a service from within the app itself, it just doesn't work. However, I created a separate external application for testing purposes, and surprisingly, I was able to bind to the same service successfully from there. This issue seems quite strange, as the service binding only fails when attempted from within the main app but not from an external application:

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()
            }
        }
    }
}

Solution

  • The root cause turned out to be the package visibility restrictions introduced in Android 11. The app I was hooking into couldn't access all installed applications, which included my manager service. Since I couldn't edit its manifest, I needed an alternative solution.

    I decided to list all the installed packages for user 0 using the context of the target app. Interestingly, it seemed capable of querying only user-installed browsers. Diving deeper, I used dumpsys to examine the app's intent filters, which included actions like android.intent.action.VIEW, android.media.action.IMAGE_CAPTURE, and android.intent.action.GET_CONTENT, among others.

    Spotting this, I tried adding the android.intent.action.GET_CONTENT intent filter to my service. Surprisingly, this made my manager visible to the target app, allowing it to bind to my BridgeService. Although this might not be the best solution, it was the only effective one I found under the circumstances.

    <intent-filter>
        <action android:name="android.intent.action.GET_CONTENT" />
        <category android:name="android.intent.category.OPENABLE" />
    </intent-filter>