mirror of
https://github.com/SagerNet/sing-box-for-android.git
synced 2025-03-31 10:27:38 +03:00
Implement send notification
This commit is contained in:
parent
e38f352c48
commit
108f8b05af
15 changed files with 116 additions and 45 deletions
|
@ -10,7 +10,7 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace "io.nekohasekai.sfa"
|
||||
compileSdk 34
|
||||
compileSdk 35
|
||||
|
||||
ndkVersion "27.2.12479018"
|
||||
|
||||
|
@ -22,7 +22,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "io.nekohasekai.sfa"
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
targetSdk 35
|
||||
versionCode getVersionProps("VERSION_CODE").toInteger()
|
||||
versionName getVersionProps("VERSION_NAME")
|
||||
setProperty("archivesBaseName", "SFA-" + versionName)
|
||||
|
@ -90,23 +90,23 @@ android {
|
|||
dependencies {
|
||||
implementation(fileTree("libs"))
|
||||
|
||||
implementation "androidx.core:core-ktx:1.13.1"
|
||||
implementation "androidx.core:core-ktx:1.15.0"
|
||||
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||
implementation "com.google.android.material:material:1.12.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:2.8.3"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:2.8.3"
|
||||
implementation "com.google.zxing:core:3.5.3"
|
||||
implementation "androidx.room:room-runtime:2.6.1"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.camera:camera-view:1.3.4"
|
||||
implementation "androidx.camera:camera-lifecycle:1.3.4"
|
||||
implementation "androidx.camera:camera-camera2:1.3.4"
|
||||
implementation "androidx.camera:camera-view:1.4.0"
|
||||
implementation "androidx.camera:camera-lifecycle:1.4.0"
|
||||
implementation "androidx.camera:camera-camera2:1.4.0"
|
||||
ksp "androidx.room:room-compiler:2.6.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.10.0"
|
||||
implementation "androidx.browser:browser:1.8.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
|
||||
|
||||
|
@ -120,6 +120,8 @@ dependencies {
|
|||
implementation "com.google.guava:guava:33.0.0-android"
|
||||
playImplementation "com.google.android.play:app-update-ktx:2.1.0"
|
||||
playImplementation "com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1"
|
||||
|
||||
implementation "com.github.tiann:FreeReflection:3.1.0"
|
||||
}
|
||||
|
||||
def playCredentialsJSON = rootProject.file("service-account-credentials.json")
|
||||
|
|
|
@ -16,13 +16,14 @@ import io.nekohasekai.sfa.bg.UpdateProfileWork
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.weishu.reflection.Reflection
|
||||
import io.nekohasekai.sfa.Application as BoxApplication
|
||||
|
||||
class Application : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
Reflection.unseal(base)
|
||||
application = this
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package io.nekohasekai.sfa.bg
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.PowerManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import go.Seq
|
||||
|
@ -17,6 +22,7 @@ import io.nekohasekai.libbox.BoxService
|
|||
import io.nekohasekai.libbox.CommandServer
|
||||
import io.nekohasekai.libbox.CommandServerHandler
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.Notification
|
||||
import io.nekohasekai.libbox.PlatformInterface
|
||||
import io.nekohasekai.libbox.SystemProxyStatus
|
||||
import io.nekohasekai.sfa.Application
|
||||
|
@ -27,6 +33,7 @@ import io.nekohasekai.sfa.constant.Status
|
|||
import io.nekohasekai.sfa.database.ProfileManager
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.hasPermission
|
||||
import io.nekohasekai.sfa.ui.MainActivity
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
@ -36,8 +43,7 @@ import kotlinx.coroutines.withContext
|
|||
import java.io.File
|
||||
|
||||
class BoxService(
|
||||
private val service: Service,
|
||||
private val platformInterface: PlatformInterface
|
||||
private val service: Service, private val platformInterface: PlatformInterface
|
||||
) : CommandServerHandler {
|
||||
|
||||
companion object {
|
||||
|
@ -101,8 +107,7 @@ class BoxService(
|
|||
}
|
||||
|
||||
private fun startCommandServer() {
|
||||
val commandServer =
|
||||
CommandServer(this, 300)
|
||||
val commandServer = CommandServer(this, 300)
|
||||
commandServer.start()
|
||||
this.commandServer = commandServer
|
||||
}
|
||||
|
@ -328,4 +333,45 @@ class BoxService(
|
|||
commandServer?.writeMessage(message)
|
||||
}
|
||||
|
||||
internal fun sendNotification(notification: Notification) {
|
||||
val builder =
|
||||
NotificationCompat.Builder(service, notification.identifier).setShowWhen(false)
|
||||
.setContentTitle(notification.title)
|
||||
.setContentText(notification.body)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(R.drawable.ic_menu)
|
||||
.setCategory(NotificationCompat.CATEGORY_EVENT)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
if (!notification.subtitle.isNullOrBlank()) {
|
||||
builder.setContentInfo(notification.subtitle)
|
||||
}
|
||||
if (!notification.openURL.isNullOrBlank()) {
|
||||
builder.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
service,
|
||||
0,
|
||||
Intent(
|
||||
service, MainActivity::class.java
|
||||
).apply {
|
||||
setAction(Action.OPEN_URL).setData(Uri.parse(notification.openURL))
|
||||
setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
},
|
||||
ServiceNotification.flags,
|
||||
)
|
||||
)
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Application.notification.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
notification.identifier,
|
||||
notification.typeName,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
)
|
||||
}
|
||||
Application.notification.notify(notification.typeID, builder.build())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package io.nekohasekai.sfa.bg
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
|
@ -136,6 +137,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
|
||||
.iterator()
|
||||
)
|
||||
runCatching {
|
||||
flags = element.flags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +150,13 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
|||
"${address.hostAddress}/${networkPrefixLength}"
|
||||
}
|
||||
}
|
||||
|
||||
private val NetworkInterface.flags: Int
|
||||
@SuppressLint("SoonBlockedPrivateApi")
|
||||
get() {
|
||||
val getFlagsMethod = NetworkInterface::class.java.getDeclaredMethod("getFlags")
|
||||
return getFlagsMethod.invoke(this) as Int
|
||||
}
|
||||
}
|
||||
|
||||
private class StringArray(private val iterator: Iterator<String>) : StringIterator {
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.nekohasekai.sfa.bg
|
|||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import io.nekohasekai.libbox.Notification
|
||||
|
||||
class ProxyService : Service(), PlatformInterfaceWrapper {
|
||||
|
||||
|
@ -14,4 +15,8 @@ class ProxyService : Service(), PlatformInterfaceWrapper {
|
|||
override fun onDestroy() = service.onDestroy()
|
||||
|
||||
override fun writeLog(message: String) = service.writeLog(message)
|
||||
|
||||
override fun sendNotification(notification: Notification) =
|
||||
service.sendNotification(notification)
|
||||
|
||||
}
|
|
@ -33,7 +33,7 @@ class ServiceNotification(
|
|||
companion object {
|
||||
private const val notificationId = 1
|
||||
private const val notificationChannel = "service"
|
||||
private val flags =
|
||||
val flags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
|
||||
fun checkPermission(): Boolean {
|
||||
|
@ -83,7 +83,7 @@ class ServiceNotification(
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Application.notification.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
notificationChannel, "sing-box service", NotificationManager.IMPORTANCE_LOW
|
||||
notificationChannel, "Service Notifications", NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.net.ProxyInfo
|
|||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import io.nekohasekai.libbox.Notification
|
||||
import io.nekohasekai.libbox.TunOptions
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.toIpPrefix
|
||||
|
@ -188,4 +189,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
|
|||
|
||||
override fun writeLog(message: String) = service.writeLog(message)
|
||||
|
||||
override fun sendNotification(notification: Notification) =
|
||||
service.sendNotification(notification)
|
||||
|
||||
}
|
|
@ -3,4 +3,5 @@ package io.nekohasekai.sfa.constant
|
|||
object Action {
|
||||
const val SERVICE = "io.nekohasekai.sfa.SERVICE"
|
||||
const val SERVICE_CLOSE = "io.nekohasekai.sfa.SERVICE_CLOSE"
|
||||
const val OPEN_URL = "io.nekohasekai.sfa.SERVICE_OPEN_URL"
|
||||
}
|
|
@ -33,6 +33,7 @@ import io.nekohasekai.sfa.Application
|
|||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.bg.ServiceConnection
|
||||
import io.nekohasekai.sfa.bg.ServiceNotification
|
||||
import io.nekohasekai.sfa.constant.Action
|
||||
import io.nekohasekai.sfa.constant.Alert
|
||||
import io.nekohasekai.sfa.constant.ServiceMode
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
|
@ -43,6 +44,7 @@ import io.nekohasekai.sfa.database.TypedProfile
|
|||
import io.nekohasekai.sfa.databinding.ActivityMainBinding
|
||||
import io.nekohasekai.sfa.ktx.errorDialogBuilder
|
||||
import io.nekohasekai.sfa.ktx.hasPermission
|
||||
import io.nekohasekai.sfa.ktx.launchCustomTab
|
||||
import io.nekohasekai.sfa.ui.profile.NewProfileActivity
|
||||
import io.nekohasekai.sfa.ui.settings.CoreFragment
|
||||
import io.nekohasekai.sfa.ui.shared.AbstractActivity
|
||||
|
@ -127,6 +129,12 @@ class MainActivity : AbstractActivity<ActivityMainBinding>(),
|
|||
override public fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
val uri = intent.data ?: return
|
||||
when (intent.action) {
|
||||
Action.OPEN_URL -> {
|
||||
launchCustomTab(uri.toString())
|
||||
return
|
||||
}
|
||||
}
|
||||
if (uri.scheme == "sing-box" && uri.host == "import-remote-profile") {
|
||||
val profile = try {
|
||||
Libbox.parseRemoteProfileImportLink(uri.toString())
|
||||
|
|
|
@ -25,7 +25,6 @@ import io.nekohasekai.sfa.databinding.ViewClashModeButtonBinding
|
|||
import io.nekohasekai.sfa.databinding.ViewProfileItemBinding
|
||||
import io.nekohasekai.sfa.ktx.errorDialogBuilder
|
||||
import io.nekohasekai.sfa.ktx.getAttrColor
|
||||
import io.nekohasekai.sfa.ktx.launchCustomTab
|
||||
import io.nekohasekai.sfa.ui.MainActivity
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -41,7 +40,7 @@ class OverviewFragment : Fragment() {
|
|||
private val activity: MainActivity? get() = super.getActivity() as MainActivity?
|
||||
private var binding: FragmentDashboardOverviewBinding? = null
|
||||
private val statusClient =
|
||||
CommandClient(lifecycleScope, CommandClient.ConnectionType.Status, StatusClient(), true)
|
||||
CommandClient(lifecycleScope, CommandClient.ConnectionType.Status, StatusClient())
|
||||
private val clashModeClient =
|
||||
CommandClient(lifecycleScope, CommandClient.ConnectionType.ClashMode, ClashModeClient())
|
||||
|
||||
|
@ -172,10 +171,6 @@ class OverviewFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun openURL(url: String) {
|
||||
requireContext().launchCustomTab(url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class ClashModeClient : CommandClient.Handler {
|
||||
|
|
|
@ -78,9 +78,9 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
|||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(element: AppInfo) {
|
||||
binding.appIcon.setImageDrawable(element.packageInfo.applicationInfo.loadIcon(binding.root.context.packageManager))
|
||||
binding.appIcon.setImageDrawable(element.packageInfo.applicationInfo!!.loadIcon(binding.root.context.packageManager))
|
||||
binding.appName.text =
|
||||
element.packageInfo.applicationInfo.loadLabel(binding.root.context.packageManager)
|
||||
element.packageInfo.applicationInfo!!.loadLabel(binding.root.context.packageManager)
|
||||
binding.packageName.text = element.packageInfo.packageName
|
||||
val appType = element.vpnType.appType
|
||||
if (appType != null) {
|
||||
|
@ -129,7 +129,8 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
|||
}
|
||||
val vpnAppList =
|
||||
installedPackages.filter {
|
||||
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE } ?: false
|
||||
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE && it.applicationInfo != null }
|
||||
?: false
|
||||
}
|
||||
for ((index, packageInfo) in vpnAppList.withIndex()) {
|
||||
val appType = runCatching { getVPNAppType(packageInfo) }.getOrNull()
|
||||
|
@ -181,7 +182,7 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
|||
}
|
||||
|
||||
private fun getVPNAppType(packageInfo: PackageInfo): String? {
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use { packageFile ->
|
||||
ZipFile(File(packageInfo.applicationInfo!!.publicSourceDir)).use { packageFile ->
|
||||
for (packageEntry in packageFile.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
|
@ -235,8 +236,8 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
|
|||
}
|
||||
|
||||
private fun getVPNCoreType(packageInfo: PackageInfo): VPNCoreType? {
|
||||
val packageFiles = mutableListOf(packageInfo.applicationInfo.publicSourceDir)
|
||||
packageInfo.applicationInfo.splitPublicSourceDirs?.also {
|
||||
val packageFiles = mutableListOf(packageInfo.applicationInfo!!.publicSourceDir)
|
||||
packageInfo.applicationInfo!!.splitPublicSourceDirs?.also {
|
||||
packageFiles.addAll(it)
|
||||
}
|
||||
val vpnType = try {
|
||||
|
|
|
@ -53,24 +53,25 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||
|
||||
inner class PackageCache(
|
||||
private val packageInfo: PackageInfo,
|
||||
private val appInfo: ApplicationInfo,
|
||||
) {
|
||||
|
||||
val packageName: String get() = packageInfo.packageName
|
||||
|
||||
val uid get() = packageInfo.applicationInfo.uid
|
||||
val uid get() = packageInfo.applicationInfo!!.uid
|
||||
|
||||
val installTime get() = packageInfo.firstInstallTime
|
||||
val updateTime get() = packageInfo.lastUpdateTime
|
||||
val isSystem get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
|
||||
val isSystem get() = appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
|
||||
val isOffline get() = packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) != true
|
||||
val isDisabled get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0
|
||||
val isDisabled get() = appInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0
|
||||
|
||||
val applicationIcon by lazy {
|
||||
packageInfo.applicationInfo.loadIcon(packageManager)
|
||||
appInfo.loadIcon(packageManager)
|
||||
}
|
||||
|
||||
val applicationLabel by lazy {
|
||||
packageInfo.applicationInfo.loadLabel(packageManager).toString()
|
||||
appInfo.loadLabel(packageManager).toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,7 +131,8 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||
val packages = mutableListOf<PackageCache>()
|
||||
for (packageInfo in installedPackages) {
|
||||
if (packageInfo.packageName == packageName) continue
|
||||
packages.add(PackageCache(packageInfo))
|
||||
val appInfo = packageInfo.applicationInfo ?: continue
|
||||
packages.add(PackageCache(packageInfo, appInfo))
|
||||
}
|
||||
val selectedPackageNames = Settings.perAppProxyList.toMutableSet()
|
||||
val selectedUIDs = mutableSetOf<Int>()
|
||||
|
@ -699,6 +701,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
val appInfo = packageInfo.applicationInfo ?: return false
|
||||
packageInfo.services?.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) {
|
||||
Log.d("PerAppProxyActivity", "Match service ${it.name} in $packageName")
|
||||
|
@ -723,7 +726,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
|
|||
return true
|
||||
}
|
||||
}
|
||||
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
|
||||
ZipFile(File(appInfo.publicSourceDir)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ open class CommandClient(
|
|||
private val scope: CoroutineScope,
|
||||
private val connectionType: ConnectionType,
|
||||
private val handler: Handler,
|
||||
private val isMainClient: Boolean = false
|
||||
) {
|
||||
|
||||
enum class ConnectionType {
|
||||
|
@ -34,7 +33,6 @@ open class CommandClient(
|
|||
fun onDisconnected() {}
|
||||
|
||||
fun updateStatus(status: StatusMessage) {}
|
||||
fun openURL(url: String) {}
|
||||
|
||||
fun clearLogs() {}
|
||||
fun appendLogs(message: List<String>) {}
|
||||
|
@ -57,7 +55,6 @@ open class CommandClient(
|
|||
ConnectionType.Log -> Libbox.CommandLog
|
||||
ConnectionType.ClashMode -> Libbox.CommandClashMode
|
||||
}
|
||||
options.isMainClient = isMainClient
|
||||
options.statusInterval = 1 * 1000 * 1000 * 1000
|
||||
val commandClient = CommandClient(clientHandler, options)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
|
@ -129,10 +126,6 @@ open class CommandClient(
|
|||
handler.updateStatus(message)
|
||||
}
|
||||
|
||||
override fun openURL(url: String) {
|
||||
handler.openURL(url)
|
||||
}
|
||||
|
||||
override fun initializeClashMode(modeList: StringIterator, currentMode: String) {
|
||||
handler.initializeClashMode(modeList.toList(), currentMode)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '8.7.1' apply false
|
||||
id 'com.android.library' version '8.7.1' apply false
|
||||
id 'com.android.application' version '8.7.2' apply false
|
||||
id 'com.android.library' version '8.7.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
||||
id 'com.google.devtools.ksp' version '1.9.23-1.0.20' apply false
|
||||
id 'com.github.triplet.play' version '3.8.4' apply false
|
||||
|
|
|
@ -10,6 +10,7 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
rootProject.name = "sing-box"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue