mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-18 03:45:56 +08:00
refactor: extension system.
This commit is contained in:
@ -41,7 +41,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":data:extension"))
|
implementation(project(":extension:api"))
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<permission
|
|
||||||
android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN"
|
|
||||||
android:protectionLevel="normal" />
|
|
||||||
<uses-permission android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN"/>
|
<uses-permission android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN"/>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@ -21,16 +18,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.m3u.data.extension.RemoteServer"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="com.m3u.permission.CONNECT_EXTENSION_PLUGIN">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="com.m3u.smartphone" />
|
<package android:name="com.m3u.smartphone" />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.m3u.extension
|
package com.m3u.extension
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -18,6 +19,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.m3u.data.extension.Const
|
||||||
import com.m3u.data.extension.RemoteClient
|
import com.m3u.data.extension.RemoteClient
|
||||||
import com.m3u.extension.ui.theme.M3UTheme
|
import com.m3u.extension.ui.theme.M3UTheme
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -28,6 +30,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
val callToken = handleArguments(intent)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
M3UTheme {
|
M3UTheme {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@ -42,20 +46,33 @@ class MainActivity : ComponentActivity() {
|
|||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!isConnected) client.connect(this@MainActivity)
|
val callToken = callToken
|
||||||
else client.disconnect(this@MainActivity)
|
if (!isConnected && callToken != null) {
|
||||||
|
client.connect(
|
||||||
|
context = this@MainActivity,
|
||||||
|
targetPackageName = callToken.packageName,
|
||||||
|
targetClassName = callToken.className,
|
||||||
|
targetPermission = callToken.permission,
|
||||||
|
accessKey = callToken.accessKey
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
client.disconnect(this@MainActivity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (isConnected) "Disconnect" else "Connect"
|
text = when {
|
||||||
|
isConnected -> "Disconnect"
|
||||||
|
else -> "Connect"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
enabled = isConnected,
|
enabled = isConnected,
|
||||||
onClick = {
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
channelCount = client.call("test", "read-channel-count", "{}")
|
// channelCount = client.call("test", "read-channel-count", "{}")
|
||||||
?.toIntOrNull() ?: -1
|
// ?.toIntOrNull() ?: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@ -68,4 +85,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleArguments(intent: Intent): CallToken? {
|
||||||
|
val packageName = intent.getStringExtra(Const.PACKAGE_NAME) ?: return null
|
||||||
|
val className = intent.getStringExtra(Const.CLASS_NAME) ?: return null
|
||||||
|
val permission = intent.getStringExtra(Const.PERMISSION) ?: return null
|
||||||
|
val accessKey = intent.getStringExtra(Const.ACCESS_KEY) ?: return null
|
||||||
|
return CallToken(packageName, className, permission, accessKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class CallToken(
|
||||||
|
val packageName: String,
|
||||||
|
val className: String,
|
||||||
|
val permission: String,
|
||||||
|
val accessKey: String
|
||||||
|
)
|
@ -1,16 +0,0 @@
|
|||||||
package com.m3u.extension
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.google.auto.service.AutoService
|
|
||||||
import com.m3u.data.extension.IRemoteCallback
|
|
||||||
import com.m3u.data.extension.OnRemoteServerCall
|
|
||||||
|
|
||||||
@AutoService(OnRemoteServerCall::class)
|
|
||||||
class OnRemoteServiceCallImpl: OnRemoteServerCall {
|
|
||||||
override fun onCall(module: String, method: String, param: String, callback: IRemoteCallback?) {
|
|
||||||
Log.d(TAG, "onCall: $module, $method, $param, $callback")
|
|
||||||
}
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "OnRemoteServiceCallImpl"
|
|
||||||
}
|
|
||||||
}
|
|
@ -122,7 +122,8 @@ dependencies {
|
|||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":core:foundation"))
|
implementation(project(":core:foundation"))
|
||||||
implementation(project(":data"))
|
implementation(project(":data"))
|
||||||
implementation(project(":data:extension"))
|
implementation(project(":extension:api"))
|
||||||
|
implementation(project(":extension:runtime"))
|
||||||
// business
|
// business
|
||||||
implementation(project(":business:foryou"))
|
implementation(project(":business:foryou"))
|
||||||
implementation(project(":business:favorite"))
|
implementation(project(":business:favorite"))
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
<permission
|
<permission
|
||||||
android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN"
|
android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN"
|
||||||
android:protectionLevel="normal" />
|
android:protectionLevel="normal" />
|
||||||
<uses-permission android:name="com.m3u.permission.CONNECT_EXTENSION_PLUGIN"/>
|
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
@ -76,7 +75,7 @@
|
|||||||
android:theme="@style/Theme.M3U" />
|
android:theme="@style/Theme.M3U" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.m3u.data.extension.RemoteServer"
|
android:name="com.m3u.data.extension.RemoteService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="com.m3u.permission.CONNECT_EXTENSION_PLUGIN">
|
android:permission="com.m3u.permission.CONNECT_EXTENSION_PLUGIN">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -14,6 +14,7 @@ plugins {
|
|||||||
alias(libs.plugins.org.jetbrains.kotlin.serialization) apply false
|
alias(libs.plugins.org.jetbrains.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.org.jetbrains.kotlin.jvm) apply false
|
alias(libs.plugins.org.jetbrains.kotlin.jvm) apply false
|
||||||
alias(libs.plugins.androidx.baselineprofile) apply false
|
alias(libs.plugins.androidx.baselineprofile) apply false
|
||||||
|
id("com.squareup.wire") version "4.9.2" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
@ -26,7 +26,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
implementation(project(":data:extension"))
|
implementation(project(":extension:api"))
|
||||||
implementation(project(":lint:annotation"))
|
implementation(project(":lint:annotation"))
|
||||||
ksp(project(":lint:processor"))
|
ksp(project(":lint:processor"))
|
||||||
|
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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?)
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.m3u.data.extension"
|
namespace = "com.m3u.extension.api"
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package com.m3u.data.extension;
|
package com.m3u.data.extension;
|
||||||
|
|
||||||
interface IRemoteCallback {
|
interface IRemoteCallback {
|
||||||
void onSuccess(String module, String method, String param);
|
void onSuccess(String module, String method, in byte[] param);
|
||||||
void onError(String module, String method, int errorCode, String errorMessage);
|
void onError(String module, String method, int errorCode, String errorMessage);
|
||||||
}
|
}
|
@ -3,5 +3,5 @@ package com.m3u.data.extension;
|
|||||||
import com.m3u.data.extension.IRemoteCallback;
|
import com.m3u.data.extension.IRemoteCallback;
|
||||||
|
|
||||||
interface IRemoteService {
|
interface IRemoteService {
|
||||||
void call(String module, String method, String param, IRemoteCallback callback);
|
void call(String module, String method, in byte[] param, IRemoteCallback callback);
|
||||||
}
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.m3u.data.extension
|
||||||
|
|
||||||
|
object Const {
|
||||||
|
const val PACKAGE_NAME = "package_name"
|
||||||
|
const val CLASS_NAME = "class_name"
|
||||||
|
const val PERMISSION = "permission"
|
||||||
|
const val ACCESS_KEY = "access_key"
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package com.m3u.data.extension
|
package com.m3u.data.extension
|
||||||
|
|
||||||
interface OnRemoteServerCall {
|
interface OnRemoteCall {
|
||||||
fun onCall(module: String, method: String, param: String, callback: IRemoteCallback?)
|
operator fun invoke(module: String, method: String, bytes: ByteArray, callback: IRemoteCallback?)
|
||||||
companion object {
|
companion object {
|
||||||
const val ERROR_CODE_MODULE_NOT_FOUNDED = -1
|
const val ERROR_CODE_MODULE_NOT_FOUNDED = -1
|
||||||
const val ERROR_CODE_METHOD_NOT_FOUNDED = -2
|
const val ERROR_CODE_METHOD_NOT_FOUNDED = -2
|
||||||
@ -12,4 +12,4 @@ interface OnRemoteServerCall {
|
|||||||
class RemoteCallException(
|
class RemoteCallException(
|
||||||
val errorCode: Int,
|
val errorCode: Int,
|
||||||
val errorMessage: String?
|
val errorMessage: String?
|
||||||
): RuntimeException(errorMessage)
|
) : RuntimeException(errorMessage)
|
@ -42,12 +42,16 @@ class RemoteClient {
|
|||||||
|
|
||||||
fun connect(
|
fun connect(
|
||||||
context: Context,
|
context: Context,
|
||||||
targetPackageName: String = PACKAGE_NAME_HOST
|
targetPackageName: String,
|
||||||
|
targetClassName: String,
|
||||||
|
targetPermission: String,
|
||||||
|
accessKey: String
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "connect")
|
Log.d(TAG, "connect")
|
||||||
val intent = Intent(context, RemoteServer::class.java).apply {
|
val intent = Intent(context, RemoteService::class.java).apply {
|
||||||
action = "com.m3u.permission.CONNECT_EXTENSION_PLUGIN"
|
action = targetPermission
|
||||||
component = ComponentName(targetPackageName, "com.m3u.data.extension.RemoteServer")
|
component = ComponentName(targetPackageName, targetClassName)
|
||||||
|
putExtra(Const.ACCESS_KEY, accessKey)
|
||||||
}
|
}
|
||||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
}
|
}
|
||||||
@ -60,11 +64,11 @@ class RemoteClient {
|
|||||||
suspend fun call(
|
suspend fun call(
|
||||||
module: String,
|
module: String,
|
||||||
method: String,
|
method: String,
|
||||||
param: String
|
param: ByteArray
|
||||||
): String? = suspendCoroutine { cont ->
|
): ByteArray = suspendCoroutine { cont ->
|
||||||
val remoteService = requireNotNull(server) { "RemoteService is not connected!" }
|
val remoteService = requireNotNull(server) { "RemoteService is not connected!" }
|
||||||
remoteService.call(module, method, param, object : IRemoteCallback.Stub() {
|
remoteService.call(module, method, param, object : IRemoteCallback.Stub() {
|
||||||
override fun onSuccess(module: String, method: String, param: String?) {
|
override fun onSuccess(module: String, method: String, param: ByteArray) {
|
||||||
Log.d(TAG, "onSuccess: $method, $param")
|
Log.d(TAG, "onSuccess: $method, $param")
|
||||||
cont.resume(param)
|
cont.resume(param)
|
||||||
}
|
}
|
||||||
@ -89,6 +93,5 @@ class RemoteClient {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RemoteClient"
|
private const val TAG = "RemoteClient"
|
||||||
const val PACKAGE_NAME_HOST = "com.m3u.smartphone"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
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
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class RemoteService : Service() {
|
||||||
|
private val onRemoteCall: OnRemoteCall by lazy {
|
||||||
|
ServiceLoader.load<OnRemoteCall>(
|
||||||
|
OnRemoteCall::class.java
|
||||||
|
).let {
|
||||||
|
val count = it.count()
|
||||||
|
if (count == 0) {
|
||||||
|
throw IllegalStateException("No implementation of OnRemoteCall found")
|
||||||
|
} else if (count > 1) {
|
||||||
|
throw IllegalStateException("Multiple implementations of OnRemoteCall found")
|
||||||
|
} else {
|
||||||
|
it.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binders = ConcurrentHashMap<String, IRemoteService.Stub>()
|
||||||
|
|
||||||
|
private inner class RemoteServiceImpl: IRemoteService.Stub() {
|
||||||
|
override fun call(
|
||||||
|
module: String,
|
||||||
|
method: String,
|
||||||
|
param: ByteArray,
|
||||||
|
callback: IRemoteCallback?
|
||||||
|
) {
|
||||||
|
onRemoteCall(module, method, param, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
Log.d(TAG, "onBind: $intent")
|
||||||
|
intent?: return null
|
||||||
|
val packageName = intent.`package` ?: return null
|
||||||
|
val binder = binders.getOrPut(packageName) {
|
||||||
|
RemoteServiceImpl()
|
||||||
|
}
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbind(intent: Intent?): Boolean {
|
||||||
|
Log.d(TAG, "onUnbind: $intent")
|
||||||
|
intent ?: return super.onUnbind(intent)
|
||||||
|
val packageName = intent.`package` ?: return super.onUnbind(intent)
|
||||||
|
val binder = binders.remove(packageName)
|
||||||
|
if (binder != null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onUnbind(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
1
extension/runtime/.gitignore
vendored
Normal file
1
extension/runtime/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
38
extension/runtime/build.gradle.kts
Normal file
38
extension/runtime/build.gradle.kts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.com.android.library)
|
||||||
|
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||||
|
alias(libs.plugins.org.jetbrains.kotlin.serialization)
|
||||||
|
alias(libs.plugins.com.google.devtools.ksp)
|
||||||
|
id("com.squareup.wire")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.m3u.extension.runtime"
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
aidl = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wire {
|
||||||
|
kotlin {}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":extension:api"))
|
||||||
|
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
|
// reflect
|
||||||
|
implementation(libs.org.jetbrains.kotlin.kotlin.reflect)
|
||||||
|
|
||||||
|
// auto
|
||||||
|
implementation(libs.auto.service.annotations)
|
||||||
|
ksp(libs.auto.service.ksp)
|
||||||
|
|
||||||
|
// wire
|
||||||
|
implementation("com.squareup.wire:wire-runtime:4.9.2")
|
||||||
|
}
|
0
extension/runtime/consumer-rules.pro
Normal file
0
extension/runtime/consumer-rules.pro
Normal file
21
extension/runtime/proguard-rules.pro
vendored
Normal file
21
extension/runtime/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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
|
4
extension/runtime/src/main/AndroidManifest.xml
Normal file
4
extension/runtime/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,133 @@
|
|||||||
|
package com.m3u.extension.runtime
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.auto.service.AutoService
|
||||||
|
import com.m3u.data.extension.IRemoteCallback
|
||||||
|
import com.m3u.data.extension.OnRemoteCall
|
||||||
|
import com.m3u.data.extension.RemoteCallException
|
||||||
|
import com.squareup.wire.ProtoAdapter
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Parameter
|
||||||
|
import java.util.ServiceLoader
|
||||||
|
import kotlin.collections.orEmpty
|
||||||
|
import kotlin.reflect.full.companionObject
|
||||||
|
import kotlin.reflect.full.declaredMemberProperties
|
||||||
|
|
||||||
|
@AutoService(OnRemoteCall::class)
|
||||||
|
class OnRemoteCallImpl : OnRemoteCall {
|
||||||
|
private val remoteModules = ServiceLoader.load(RemoteModule::class.java)
|
||||||
|
?.toList().orEmpty().filterNotNull().associateBy { it.module }
|
||||||
|
|
||||||
|
// Map<module-name, Map<method-name, method>>
|
||||||
|
private val remoteMethods = mutableMapOf<String, Map<String, Method>>()
|
||||||
|
|
||||||
|
// Map<type-name, adapter>
|
||||||
|
private val protobufAdapters = mutableMapOf<String, ProtoAdapter<*>>()
|
||||||
|
|
||||||
|
override fun invoke(
|
||||||
|
module: String,
|
||||||
|
method: String,
|
||||||
|
bytes: ByteArray,
|
||||||
|
callback: IRemoteCallback?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "$module, $method, ${bytes.size}, $callback")
|
||||||
|
try {
|
||||||
|
val instance = remoteModules[module]
|
||||||
|
if (instance == null) {
|
||||||
|
callback?.onError(
|
||||||
|
module,
|
||||||
|
method,
|
||||||
|
OnRemoteCall.ERROR_CODE_MODULE_NOT_FOUNDED,
|
||||||
|
"Module $module not founded"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
invokeImpl(instance, module, method, bytes, callback)
|
||||||
|
} catch (e: RemoteCallException) {
|
||||||
|
callback?.onError(
|
||||||
|
module,
|
||||||
|
method,
|
||||||
|
e.errorCode,
|
||||||
|
e.errorMessage
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callback?.onError(
|
||||||
|
module,
|
||||||
|
method,
|
||||||
|
OnRemoteCall.ERROR_CODE_UNCAUGHT,
|
||||||
|
e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invokeImpl(
|
||||||
|
instance: RemoteModule,
|
||||||
|
module: String,
|
||||||
|
method: String,
|
||||||
|
param: ByteArray,
|
||||||
|
callback: IRemoteCallback?
|
||||||
|
) {
|
||||||
|
val methods = remoteMethods.getOrPut(module) {
|
||||||
|
val moduleClass = instance::class.java
|
||||||
|
moduleClass.declaredMethods
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.isAnnotationPresent(RemoteMethod::class.java) }
|
||||||
|
.filter { it.isAccessible }
|
||||||
|
.toList()
|
||||||
|
.associateBy { it.getAnnotation(RemoteMethod::class.java)!!.name }
|
||||||
|
}
|
||||||
|
val remoteMethod = methods[method]
|
||||||
|
if (remoteMethod == null) {
|
||||||
|
callback?.onError(
|
||||||
|
module,
|
||||||
|
method,
|
||||||
|
OnRemoteCall.ERROR_CODE_METHOD_NOT_FOUNDED,
|
||||||
|
"Method $method not founded"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// handle protobuf param
|
||||||
|
val pbArg = remoteMethod.parameters
|
||||||
|
.find { it.isAnnotationPresent(RemoteMethodParam::class.java) }
|
||||||
|
?.let { decodeParamFromBytes(it, param) }
|
||||||
|
|
||||||
|
val args = listOfNotNull(
|
||||||
|
pbArg,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
|
||||||
|
remoteMethod.invoke(instance, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeParamFromBytes(
|
||||||
|
param: Parameter,
|
||||||
|
bytes: ByteArray
|
||||||
|
): Any? {
|
||||||
|
val adapter = protobufAdapters.getOrPut(param.type.typeName) {
|
||||||
|
val companionObject = Class.forName(param.type.typeName).kotlin.companionObject
|
||||||
|
// TODO: fix this
|
||||||
|
// Class.forName(param.type.typeName).kotlin.companionObjectInstance
|
||||||
|
val adapter = companionObject?.declaredMemberProperties.orEmpty().find { it.name == "ADAPTER" }
|
||||||
|
adapter as ProtoAdapter<*>
|
||||||
|
}
|
||||||
|
return adapter.decode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "Host-OnRemoteCallImpl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoteModule {
|
||||||
|
val module: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
annotation class RemoteMethod(
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||||
|
annotation class RemoteMethodParam
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.m3u.extension.runtime.business
|
||||||
|
|
||||||
|
import GetAppInfoRequest
|
||||||
|
import GetAppInfoResponse
|
||||||
|
import com.m3u.extension.runtime.RemoteMethod
|
||||||
|
import com.m3u.extension.runtime.RemoteMethodParam
|
||||||
|
import com.m3u.extension.runtime.RemoteModule
|
||||||
|
|
||||||
|
class InfoModule constructor(): RemoteModule {
|
||||||
|
override val module: String = "info"
|
||||||
|
|
||||||
|
@RemoteMethod("getAppInfo")
|
||||||
|
fun getAppInfo(
|
||||||
|
@RemoteMethodParam param: GetAppInfoRequest,
|
||||||
|
callback: (GetAppInfoResponse) -> Unit
|
||||||
|
) {
|
||||||
|
callback(
|
||||||
|
GetAppInfoResponse(
|
||||||
|
"com.m3u.extension.runtime",
|
||||||
|
"InfoModule",
|
||||||
|
"1.0.0"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
14
extension/runtime/src/main/proto/GetAppInfo.proto
Normal file
14
extension/runtime/src/main/proto/GetAppInfo.proto
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
message GetAppInfoResponse {
|
||||||
|
string app_id = 1;
|
||||||
|
string app_version = 2;
|
||||||
|
string app_name = 3;
|
||||||
|
string app_icon = 4;
|
||||||
|
string app_description = 5;
|
||||||
|
string app_package_name = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAppInfoRequest {
|
||||||
|
|
||||||
|
}
|
@ -173,13 +173,8 @@ androidx-graphics-shapes = { group = "androidx.graphics", name = "graphics-shape
|
|||||||
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "kotlin-symbol-processor" }
|
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "kotlin-symbol-processor" }
|
||||||
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
||||||
jakewharton-disklrucache = { group = "com.jakewharton", name = "disklrucache", version.ref = "jakewharton-disklrucache" }
|
jakewharton-disklrucache = { group = "com.jakewharton", name = "disklrucache", version.ref = "jakewharton-disklrucache" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
||||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
org-jetbrains-kotlin-kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
|
||||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
|
||||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
|
||||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
|
||||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||||
|
@ -15,7 +15,7 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "M3U"
|
rootProject.name = "M3U"
|
||||||
include(":app:smartphone", ":app:tv")
|
include(":app:smartphone", ":app:tv", ":app:extension")
|
||||||
include(":core", ":core:foundation")
|
include(":core", ":core:foundation")
|
||||||
include(":data")
|
include(":data")
|
||||||
include(":data:codec", ":data:codec:lite", ":data:codec:rich")
|
include(":data:codec", ":data:codec:lite", ":data:codec:rich")
|
||||||
@ -33,5 +33,7 @@ include(
|
|||||||
":lint:annotation",
|
":lint:annotation",
|
||||||
":lint:processor"
|
":lint:processor"
|
||||||
)
|
)
|
||||||
include(":data:extension")
|
include(
|
||||||
include(":app:extension")
|
":extension:api",
|
||||||
|
":extension:runtime"
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user