refactor: extension system.

This commit is contained in:
oxy-macmini
2025-03-28 20:06:44 +08:00
parent ec24015aaf
commit e78dc3acd8
31 changed files with 379 additions and 173 deletions

View File

@ -26,7 +26,7 @@ android {
dependencies {
implementation(project(":core"))
implementation(project(":data:extension"))
implementation(project(":extension:api"))
implementation(project(":lint:annotation"))
ksp(project(":lint:processor"))

View File

@ -1 +0,0 @@
/build

View File

@ -1,20 +0,0 @@
plugins {
alias(libs.plugins.com.android.library)
alias(libs.plugins.org.jetbrains.kotlin.android)
alias(libs.plugins.org.jetbrains.kotlin.serialization)
}
android {
namespace = "com.m3u.data.extension"
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
aidl = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -1,6 +0,0 @@
package com.m3u.data.extension;
interface IRemoteCallback {
void onSuccess(String module, String method, String param);
void onError(String module, String method, int errorCode, String errorMessage);
}

View File

@ -1,7 +0,0 @@
package com.m3u.data.extension;
import com.m3u.data.extension.IRemoteCallback;
interface IRemoteService {
void call(String module, String method, String param, IRemoteCallback callback);
}

View File

@ -1,15 +0,0 @@
package com.m3u.data.extension
interface OnRemoteServerCall {
fun onCall(module: String, method: String, param: String, callback: IRemoteCallback?)
companion object {
const val ERROR_CODE_MODULE_NOT_FOUNDED = -1
const val ERROR_CODE_METHOD_NOT_FOUNDED = -2
const val ERROR_CODE_UNCAUGHT = -3
}
}
class RemoteCallException(
val errorCode: Int,
val errorMessage: String?
): RuntimeException(errorMessage)

View File

@ -1,94 +0,0 @@
package com.m3u.data.extension
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class RemoteClient {
private var server: IRemoteService? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(
name: ComponentName?,
service: IBinder?
) {
server = IRemoteService.Stub.asInterface(service)
_isConnectedObservable.value = true
Log.d(TAG, "onServiceConnected, $name")
}
override fun onServiceDisconnected(name: ComponentName?) {
server = null
_isConnectedObservable.value = false
Log.d(TAG, "onServiceDisconnected, $name")
}
override fun onBindingDied(name: ComponentName?) {
super.onBindingDied(name)
Log.e(TAG, "onBindingDied: $name")
}
override fun onNullBinding(name: ComponentName?) {
super.onNullBinding(name)
Log.e(TAG, "onNullBinding: $name")
}
}
fun connect(
context: Context,
targetPackageName: String = PACKAGE_NAME_HOST
) {
Log.d(TAG, "connect")
val intent = Intent(context, RemoteServer::class.java).apply {
action = "com.m3u.permission.CONNECT_EXTENSION_PLUGIN"
component = ComponentName(targetPackageName, "com.m3u.data.extension.RemoteServer")
}
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
fun disconnect(context: Context) {
context.unbindService(connection)
_isConnectedObservable.value = false
}
suspend fun call(
module: String,
method: String,
param: String
): String? = suspendCoroutine { cont ->
val remoteService = requireNotNull(server) { "RemoteService is not connected!" }
remoteService.call(module, method, param, object : IRemoteCallback.Stub() {
override fun onSuccess(module: String, method: String, param: String?) {
Log.d(TAG, "onSuccess: $method, $param")
cont.resume(param)
}
override fun onError(
module: String,
method: String,
errorCode: Int,
errorMessage: String?
) {
Log.e(TAG, "onError: $method, $errorCode, $errorMessage")
throw RuntimeException("Error: $method $param $errorCode, $errorMessage")
}
})
}
val isConnected: Boolean
get() = server != null
val isConnectedObservable: Flow<Boolean> get() = _isConnectedObservable
private val _isConnectedObservable = MutableStateFlow(false)
companion object {
private const val TAG = "RemoteClient"
const val PACKAGE_NAME_HOST = "com.m3u.smartphone"
}
}

View File

@ -1,46 +0,0 @@
package com.m3u.data.extension
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import java.util.ServiceLoader
class RemoteServer : Service() {
private val onRemoteServerCall: OnRemoteServerCall = ServiceLoader.load<OnRemoteServerCall>(
OnRemoteServerCall::class.java
).let {
val count = it.count()
if (count == 0) {
throw IllegalStateException("No implementation of OnRemoteServerCall found")
} else if (count > 1) {
throw IllegalStateException("Multiple implementations of OnRemoteServerCall found")
} else {
it.first()
}
}
private val binder: IRemoteService.Stub = object : IRemoteService.Stub() {
override fun call(
module: String,
method: String,
param: String,
callback: IRemoteCallback?
) {
onRemoteServerCall.onCall(module, method, param, callback)
}
}
override fun onBind(intent: Intent?): IBinder? {
Log.d(TAG, "onBind: $intent")
return binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: $intent, $flags, $startId")
return super.onStartCommand(intent, flags, startId)
}
companion object {
private const val TAG = "RemoteClient"
}
}

View File

@ -1,64 +0,0 @@
package com.m3u.data.service
import android.util.Log
import com.google.auto.service.AutoService
import com.m3u.data.extension.IRemoteCallback
import com.m3u.data.extension.OnRemoteServerCall
import com.m3u.data.extension.RemoteCallException
import java.util.ServiceLoader
@AutoService(OnRemoteServerCall::class)
class OnRemoteServiceCallImpl : OnRemoteServerCall {
private val modules = ServiceLoader.load(RemoteModule::class.java)
?.toList().orEmpty().filterNotNull().associateBy { it.module }
override fun onCall(module: String, method: String, param: String, callback: IRemoteCallback?) {
Log.d(TAG, "onCall: $module, $method, $param, $callback")
try {
val moduleInstance = modules[module]
if (moduleInstance == null) {
callback?.onError(
module,
method,
OnRemoteServerCall.ERROR_CODE_MODULE_NOT_FOUNDED,
"Module $module not founded"
)
return
}
if (method !in moduleInstance.methods) {
callback?.onError(
module,
method,
OnRemoteServerCall.ERROR_CODE_METHOD_NOT_FOUNDED,
"Method $method not founded"
)
return
}
moduleInstance.callMethod(method, param, callback)
} catch (e: RemoteCallException) {
callback?.onError(
module,
method,
e.errorCode,
e.errorMessage
)
} catch (e: Exception) {
callback?.onError(
module,
method,
OnRemoteServerCall.ERROR_CODE_UNCAUGHT,
e.message
)
}
}
companion object {
private const val TAG = "Host-OnRemoteServiceCallImpl"
}
}
interface RemoteModule {
val module: String
val methods: List<String>
fun callMethod(method: String, param: String, callback: IRemoteCallback?)
}