diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1b571d6..fa093e6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -92,6 +92,7 @@ dependencies {
implementation(libs.shizuku.provider)
implementation(libs.shizuku.api)
implementation(libs.dhizuku.api)
+ implementation(libs.dhizuku.server.api)
implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass)
implementation(libs.libsu)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index fe428b9..b19edd8 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -20,3 +20,7 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
# -renamesourcefileattribute SourceFile
+
+-dontwarn android.app.ActivityThread
+-dontwarn android.app.ContextImpl
+-dontwarn android.app.LoadedApk
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d7a8975..7f3188f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,6 +68,17 @@
android:permission="com.bintianqi.owndroid.MyPermission"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt
new file mode 100644
index 0000000..293ae3e
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt
@@ -0,0 +1,147 @@
+package com.bintianqi.owndroid
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import com.rosan.dhizuku.aidl.IDhizukuClient
+import com.rosan.dhizuku.aidl.IDhizukuRequestPermissionListener
+import com.rosan.dhizuku.server_api.DhizukuProvider
+import com.rosan.dhizuku.server_api.DhizukuService
+import com.rosan.dhizuku.shared.DhizukuVariables
+import kotlinx.coroutines.delay
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+private const val TAG = "DhizukuServer"
+
+const val DHIZUKU_CLIENTS_FILE = "dhizuku_clients.json"
+
+class MyDhizukuProvider(): DhizukuProvider() {
+ override fun onCreateService(client: IDhizukuClient): DhizukuService? {
+ Log.d(TAG, "Creating MyDhizukuService")
+ return if (SharedPrefs(context!!).dhizukuServer) MyDhizukuService(context!!, MyAdminComponent, client) else null
+ }
+}
+
+class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuClient) :
+ DhizukuService(context, admin, client) {
+ override fun checkCallingPermission(func: String?, callingUid: Int, callingPid: Int): Boolean {
+ if (!SharedPrefs(mContext).dhizukuServer) return false
+ val pm = mContext.packageManager
+ val packageInfo = pm.getPackageInfo(
+ pm.getNameForUid(callingUid) ?: return false,
+ if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES
+ )
+ val file = mContext.filesDir.resolve(DHIZUKU_CLIENTS_FILE)
+ val clients = Json.decodeFromString>(file.readText())
+ val signature = getPackageSignature(packageInfo)
+ val hasPermission = DhizukuClientInfo(callingUid, signature, true) in clients
+ Log.d(TAG, "UID $callingUid, PID $callingPid, has permission: $hasPermission")
+ return hasPermission
+ }
+
+ override fun getVersionName() = "1.0"
+}
+
+class DhizukuActivity : ComponentActivity() {
+ @OptIn(ExperimentalStdlibApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (!SharedPrefs(this).dhizukuServer) {
+ finish()
+ return
+ }
+ val bundle = intent.extras ?: return
+ val uid = bundle.getInt(DhizukuVariables.PARAM_CLIENT_UID, -1)
+ if (uid == -1) return
+ val binder = bundle.getBinder(DhizukuVariables.PARAM_CLIENT_REQUEST_PERMISSION_BINDER) ?: return
+ val listener = IDhizukuRequestPermissionListener.Stub.asInterface(binder)
+ val packageName = packageManager.getPackagesForUid(uid)?.first() ?: return
+ val packageInfo = packageManager.getPackageInfo(
+ packageName,
+ if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES
+ )
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ val icon = appInfo.loadIcon(packageManager)
+ val label = appInfo.loadLabel(packageManager).toString()
+ fun close(grantPermission: Boolean) {
+ val file = filesDir.resolve(DHIZUKU_CLIENTS_FILE)
+ val clients = Json.decodeFromString>(file.readText())
+ val index = clients.indexOfFirst { it.uid == uid }
+ val clientInfo = DhizukuClientInfo(uid, getPackageSignature(packageInfo), grantPermission)
+ if (index == -1) clients += clientInfo
+ else clients[index] = clientInfo
+ file.writeText(Json.encodeToString(clients))
+ finish()
+ listener.onRequestPermission(
+ if (grantPermission) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED
+ )
+ }
+ setContent {
+ AlertDialog(
+ icon = {
+ Image(rememberDrawablePainter(icon), null, Modifier.size(35.dp))
+ },
+ title = {
+ Text(stringResource(R.string.request_permission))
+ },
+ text = {
+ Text("$label\n($packageName)")
+ },
+ confirmButton = {
+ var time by remember { mutableIntStateOf(3) }
+ LaunchedEffect(Unit) {
+ (1..3).forEach {
+ delay(1000)
+ time -= 1
+ }
+ }
+ TextButton({
+ close(true)
+ }, enabled = time == 0) {
+ val append = if (time > 0) " (${time}s)" else ""
+ Text(stringResource(R.string.allow) + append)
+ }
+ },
+ dismissButton = {
+ TextButton({
+ close(false)
+ }) {
+ Text(stringResource(R.string.reject))
+ }
+ },
+ onDismissRequest = {
+ finish()
+ }
+ )
+ }
+ }
+}
+
+
+@Serializable
+data class DhizukuClientInfo(
+ val uid: Int,
+ val signature: String?,
+ val allow: Boolean
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
index 96173e6..324c6b0 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
@@ -107,6 +107,8 @@ import com.bintianqi.owndroid.dpm.DeleteWorkProfile
import com.bintianqi.owndroid.dpm.DeleteWorkProfileScreen
import com.bintianqi.owndroid.dpm.DeviceInfo
import com.bintianqi.owndroid.dpm.DeviceInfoScreen
+import com.bintianqi.owndroid.dpm.DhizukuServerSettings
+import com.bintianqi.owndroid.dpm.DhizukuServerSettingsScreen
import com.bintianqi.owndroid.dpm.DisableAccountManagement
import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen
import com.bintianqi.owndroid.dpm.DisableMeteredData
@@ -324,6 +326,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
}, ::navigate)
}
+ composable { DhizukuServerSettingsScreen(::navigateUp) }
composable { DelegatedAdminsScreen(::navigateUp, ::navigate) }
composable{ AddDelegatedAdminScreen(it.toRoute(), ::navigateUp) }
diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
index 30b2017..2d7c43e 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
@@ -64,7 +64,7 @@ class Receiver : DeviceAdminReceiver() {
super.onSecurityLogsAvailable(context, intent)
if(VERSION.SDK_INT >= 24) {
CoroutineScope(Dispatchers.IO).launch {
- val events = getManager(context).retrieveSecurityLogs(ComponentName(context, this@Receiver::class.java)) ?: return@launch
+ val events = getManager(context).retrieveSecurityLogs(MyAdminComponent) ?: return@launch
val file = context.filesDir.resolve("SecurityLogs.json")
val fileExists = file.exists()
file.outputStream().use {
diff --git a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt
index e641e26..f75a5dc 100644
--- a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt
@@ -23,7 +23,8 @@ class SharedPrefs(context: Context) {
var biometricsUnlock by BooleanSharedPref("lock.biometrics")
var lockWhenLeaving by BooleanSharedPref("lock.onleave")
var applicationsListView by BooleanSharedPref("applications.list_view", true)
- var shortcuts by BooleanSharedPref("shortcuts", false)
+ var shortcuts by BooleanSharedPref("shortcuts")
+ var dhizukuServer by BooleanSharedPref("dhizuku_server")
}
private class BooleanSharedPref(val key: String, val defValue: Boolean = false): ReadWriteProperty {
diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt
index d999f10..ff080fe 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt
@@ -2,8 +2,10 @@ package com.bintianqi.owndroid
import android.content.ClipData
import android.content.ClipboardManager
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -131,3 +133,13 @@ fun String.hash(): String {
val md = MessageDigest.getInstance("SHA-256")
return md.digest(this.encodeToByteArray()).toHexString()
}
+
+val MyAdminComponent = ComponentName.unflattenFromString("com.bintianqi.owndroid/.Receiver")!!
+
+
+@OptIn(ExperimentalStdlibApi::class)
+fun getPackageSignature(info: PackageInfo): String? {
+ val signatures = if (Build.VERSION.SDK_INT >= 28) info.signingInfo?.apkContentsSigners else info.signatures
+ return signatures?.firstOrNull()?.toByteArray()
+ ?.let { MessageDigest.getInstance("SHA-256").digest(it) }?.toHexString()
+}
diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt
index da61b35..bab9bb3 100644
--- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt
@@ -24,11 +24,11 @@ import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.toBitmap
+import com.bintianqi.owndroid.MyAdminComponent
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.Receiver
import com.bintianqi.owndroid.SharedPrefs
import com.bintianqi.owndroid.ShortcutsReceiverActivity
-import com.bintianqi.owndroid.backToHomeStateFlow
import com.bintianqi.owndroid.createShortcuts
import com.bintianqi.owndroid.myPrivilege
import com.rosan.dhizuku.api.Dhizuku
@@ -130,7 +130,7 @@ fun Context.getReceiver(): ComponentName {
return if(SharedPrefs(this).dhizuku) {
Dhizuku.getOwnerComponent()
} else {
- ComponentName(this, Receiver::class.java)
+ MyAdminComponent
}
}
diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt
index 0f885e9..bb00140 100644
--- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt
@@ -19,6 +19,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -31,7 +32,10 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
@@ -57,6 +61,7 @@ import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -84,23 +89,28 @@ import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ChoosePackageContract
+import com.bintianqi.owndroid.DHIZUKU_CLIENTS_FILE
+import com.bintianqi.owndroid.DhizukuClientInfo
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.IUserService
+import com.bintianqi.owndroid.MyAdminComponent
import com.bintianqi.owndroid.R
-import com.bintianqi.owndroid.Receiver
import com.bintianqi.owndroid.Settings
import com.bintianqi.owndroid.SharedPrefs
import com.bintianqi.owndroid.myPrivilege
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CircularProgressDialog
import com.bintianqi.owndroid.ui.InfoItem
+import com.bintianqi.owndroid.ui.MyLazyScaffold
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.MySmallTitleScaffold
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.Notes
+import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.updatePrivilege
import com.bintianqi.owndroid.useShizuku
import com.bintianqi.owndroid.yesOrNo
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.rosan.dhizuku.api.Dhizuku
import com.rosan.dhizuku.api.DhizukuRequestPermissionListener
import com.topjohnwu.superuser.Shell
@@ -108,6 +118,8 @@ import com.topjohnwu.superuser.ipc.RootService
import dalvik.system.DexClassLoader
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import java.lang.invoke.MethodHandles
import java.lang.reflect.Proxy
@@ -258,6 +270,14 @@ fun WorkModesScreen(
tint = if(privilege.device) colorScheme.primary else colorScheme.onBackground
)
}
+ if ((privilege.device || privilege.profile) && !privilege.dhizuku) Row(
+ Modifier.padding(top = 20.dp).fillMaxWidth().clickable { onNavigate(DhizukuServerSettings) },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(painterResource(R.drawable.dhizuku_icon), null, Modifier.padding(8.dp).size(28.dp))
+ Text(stringResource(R.string.dhizuku_server), style = typography.titleLarge)
+ }
+
Column(Modifier.padding(HorizontalPadding, 20.dp)) {
Row(Modifier.padding(bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Outlined.Warning, null, Modifier.padding(end = 4.dp), colorScheme.error)
@@ -338,7 +358,7 @@ fun WorkModesScreen(
if(privilege.device) {
dpm.clearDeviceOwnerApp(context.packageName)
} else if(VERSION.SDK_INT >= 24) {
- dpm.clearProfileOwner(ComponentName(context, Receiver::class.java))
+ dpm.clearProfileOwner(MyAdminComponent)
}
}
dialog = 0
@@ -424,10 +444,7 @@ fun activateUsingDhizuku(context: Context, callback: (Boolean, Boolean, String?)
if(dpm == null) {
context.showOperationResultToast(false)
} else {
- dpm.transferOwnership(
- Dhizuku.getOwnerComponent(),
- ComponentName(context, Receiver::class.java), PersistableBundle()
- )
+ dpm.transferOwnership(Dhizuku.getOwnerComponent(), MyAdminComponent, PersistableBundle())
callback(true, true, null)
}
} catch (e: Exception) {
@@ -542,6 +559,65 @@ fun activateDhizukuMode(context: Context, callback: (Boolean, Boolean, String?)
const val ACTIVATE_DEVICE_OWNER_COMMAND = "dpm set-device-owner com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver"
+@Serializable object DhizukuServerSettings
+
+@Composable
+fun DhizukuServerSettingsScreen(onNavigateUp: () -> Unit) {
+ val context = LocalContext.current
+ val pm = context.packageManager
+ val sp = SharedPrefs(context)
+ val file = context.filesDir.resolve(DHIZUKU_CLIENTS_FILE)
+ var enabled by remember { mutableStateOf(sp.dhizukuServer) }
+ val clients = remember { mutableStateListOf() }
+ fun changeEnableState(status: Boolean) {
+ enabled = status
+ sp.dhizukuServer = status
+ }
+ fun writeList() {
+ file.writeText(Json.encodeToString(clients))
+ }
+ LaunchedEffect(Unit) {
+ if (!file.exists()) file.writeText("[]")
+ }
+ LaunchedEffect(enabled) {
+ if (enabled) {
+ clients.clear()
+ clients.addAll(Json.decodeFromString>(file.readText()))
+ }
+ }
+ MyLazyScaffold(R.string.dhizuku_server, onNavigateUp) {
+ item {
+ SwitchItem(R.string.enable, getState = { sp.dhizukuServer }, onCheckedChange = ::changeEnableState)
+ Spacer(Modifier.padding(vertical = 8.dp))
+ }
+ if (enabled) itemsIndexed(clients) { index, client ->
+ val name = pm.getNameForUid(client.uid)
+ if (name == null) {
+ clients.dropWhile { it.uid == client.uid }
+ writeList()
+ } else {
+ val info = pm.getApplicationInfo(name, 0)
+ Row(
+ Modifier
+ .fillMaxWidth().padding(8.dp)
+ .background(colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
+ .padding(8.dp),
+ Arrangement.SpaceBetween, Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Image(
+ rememberDrawablePainter(info.loadIcon(pm)), null,
+ Modifier.padding(end = 12.dp).size(50.dp)
+ )
+ Text(info.loadLabel(pm).toString(), style = typography.titleLarge)
+ }
+ Switch(client.allow, { clients[index] = client.copy(allow = it) })
+ }
+ }
+ }
+ }
+}
+
@Serializable object LockScreenInfo
@RequiresApi(24)
diff --git a/app/src/main/res/drawable/dhizuku_icon.xml b/app/src/main/res/drawable/dhizuku_icon.xml
new file mode 100644
index 0000000..7335bd4
--- /dev/null
+++ b/app/src/main/res/drawable/dhizuku_icon.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a7f7e4d..4096b7a 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -106,6 +106,11 @@
转移
%1$s 特权将被转移至 %2$s
+ Dhizuku服务器
+ 请求权限
+ 允许
+ 拒绝
+
创建工作资料成功
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1219145..38b0a81 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -112,6 +112,11 @@
Transfer
%1$s privilege will be transferred to %2$s
+ Dhizuku server
+ Request permission
+ Allow
+ Reject
+
Create work profile success
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7efdefa..3106ebd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,12 +3,13 @@ agp = "8.10.0"
kotlin = "2.1.20"
navigation-compose = "2.9.0"
-composeBom = "2025.05.00"
+composeBom = "2025.05.01"
accompanist-drawablepainter = "0.35.0-alpha"
accompanist-permissions = "0.37.0"
shizuku = "13.1.5"
-fragment = "1.8.6"
+fragment = "1.8.7"
dhizuku = "2.5.3"
+dhizuku-server = "0.0.5"
hiddenApiBypass = "4.3"
libsu = "6.0.0"
serialization = "1.7.3"
@@ -28,6 +29,7 @@ accompanist-permissions = { group = "com.google.accompanist", name = "accompanis
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }
dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku" }
+dhizuku-server-api = { group = "io.github.iamr0s", name = "Dhizuku-SERVER_API", version.ref = "dhizuku-server" }
hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" }
libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" }