forked from core/mobile_nebula
Fix majority of Android Studio warnings (#88)
This commit is contained in:
parent
a5684e1978
commit
e4bbd0a31c
|
@ -26,6 +26,8 @@ apply plugin: 'kotlin-android'
|
|||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
namespace "net.defined.mobile_nebula"
|
||||
|
||||
compileSdkVersion 33
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
|
@ -44,8 +46,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "net.defined.mobile_nebula"
|
||||
minSdkVersion 23 //flutter.minSdkVersion
|
||||
targetSdkVersion 31 //flutter.targetSdkVersion
|
||||
minSdkVersion 26 //flutter.minSdkVersion
|
||||
targetSdkVersion 33 //flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
@ -81,7 +83,7 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation "androidx.security:security-crypto:1.0.0"
|
||||
implementation "androidx.work:work-runtime-ktx:$workVersion"
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation "com.google.guava:guava:31.0.1-android"
|
||||
implementation project(':mobileNebula')
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.defined.mobile_nebula">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="net.defined.mobile_nebula">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
In most cases you can leave this as-is, but you if you want to provide
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
|
||||
class InvalidCredentialsException(): Exception("Invalid credentials")
|
||||
class InvalidCredentialsException: Exception("Invalid credentials")
|
||||
|
||||
class APIClient(context: Context) {
|
||||
private val packageInfo = PackageInfo(context)
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.util.Log
|
|||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
@ -35,10 +34,10 @@ class DNUpdateWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, param
|
|||
}
|
||||
}
|
||||
|
||||
return if (failed) Result.failure() else Result.success();
|
||||
return if (failed) Result.failure() else Result.success()
|
||||
}
|
||||
|
||||
fun updateSite(site: Site) {
|
||||
private fun updateSite(site: Site) {
|
||||
try {
|
||||
DNUpdateLock(site).use {
|
||||
if (updater.updateSite(site)) {
|
||||
|
@ -60,7 +59,7 @@ class DNUpdateWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, param
|
|||
}
|
||||
}
|
||||
|
||||
class DNUpdateLock(private val site: Site): Closeable {
|
||||
class DNUpdateLock(site: Site): Closeable {
|
||||
private val fileChannel = FileChannel.open(
|
||||
Paths.get(site.path+"/update.lock"),
|
||||
StandardOpenOption.CREATE,
|
||||
|
|
|
@ -5,7 +5,7 @@ import androidx.security.crypto.EncryptedFile
|
|||
import androidx.security.crypto.MasterKeys
|
||||
import java.io.*
|
||||
|
||||
class EncFile(var context: Context) {
|
||||
class EncFile(private val context: Context) {
|
||||
private val scheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
||||
private val master: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
|
||||
|
|
|
@ -10,11 +10,7 @@ import android.content.ServiceConnection
|
|||
import android.net.VpnService
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.work.*
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
@ -52,12 +48,12 @@ class MainActivity: FlutterActivity() {
|
|||
fun getContext(): Context? { return appContext }
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
appContext = context
|
||||
//TODO: Initializing in the constructor leads to a context lacking info we need, figure out the right way to do this
|
||||
sites = Sites(flutterEngine)
|
||||
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine)
|
||||
|
||||
ui = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
ui!!.setMethodCallHandler { call, result ->
|
||||
|
@ -228,12 +224,12 @@ class MainActivity: FlutterActivity() {
|
|||
private fun validateOrDeleteSite(siteDir: File): Boolean {
|
||||
try {
|
||||
// Try to render a full site, if this fails the config was bad somehow
|
||||
val site = Site(context, siteDir)
|
||||
Site(context, siteDir)
|
||||
} catch(err: java.io.FileNotFoundException) {
|
||||
Log.e(TAG, "Site not found at ${siteDir}")
|
||||
Log.e(TAG, "Site not found at $siteDir")
|
||||
return false
|
||||
} catch(err: Exception) {
|
||||
Log.e(TAG, "Deleting site at ${siteDir} due to error: ${err}")
|
||||
Log.e(TAG, "Deleting site at $siteDir due to error: $err")
|
||||
siteDir.deleteRecursively()
|
||||
return false
|
||||
}
|
||||
|
@ -268,8 +264,9 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
|
||||
private fun stopSite() {
|
||||
val intent = Intent(this, NebulaVpnService::class.java)
|
||||
intent.setAction(NebulaVpnService.ACTION_STOP)
|
||||
val intent = Intent(this, NebulaVpnService::class.java).apply {
|
||||
action = NebulaVpnService.ACTION_STOP
|
||||
}
|
||||
|
||||
// We can't stopService because we have to close the fd first. The service will call stopSelf when ready.
|
||||
// See the official example: https://android.googlesource.com/platform/development/+/master/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnClient.java#116
|
||||
|
@ -286,9 +283,9 @@ class MainActivity: FlutterActivity() {
|
|||
return result.success(null)
|
||||
}
|
||||
|
||||
var msg = Message.obtain()
|
||||
val msg = Message.obtain()
|
||||
msg.what = NebulaVpnService.MSG_LIST_HOSTMAP
|
||||
msg.replyTo = Messenger(object: Handler() {
|
||||
msg.replyTo = Messenger(object: Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
result.success(msg.data.getString("data"))
|
||||
}
|
||||
|
@ -306,9 +303,9 @@ class MainActivity: FlutterActivity() {
|
|||
return result.success(null)
|
||||
}
|
||||
|
||||
var msg = Message.obtain()
|
||||
val msg = Message.obtain()
|
||||
msg.what = NebulaVpnService.MSG_LIST_PENDING_HOSTMAP
|
||||
msg.replyTo = Messenger(object: Handler() {
|
||||
msg.replyTo = Messenger(object: Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
result.success(msg.data.getString("data"))
|
||||
}
|
||||
|
@ -333,11 +330,11 @@ class MainActivity: FlutterActivity() {
|
|||
return result.success(null)
|
||||
}
|
||||
|
||||
var msg = Message.obtain()
|
||||
val msg = Message.obtain()
|
||||
msg.what = NebulaVpnService.MSG_GET_HOSTINFO
|
||||
msg.data.putString("vpnIp", vpnIp)
|
||||
msg.data.putBoolean("pending", pending)
|
||||
msg.replyTo = Messenger(object: Handler() {
|
||||
msg.replyTo = Messenger(object: Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
result.success(msg.data.getString("data"))
|
||||
}
|
||||
|
@ -357,7 +354,7 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
|
||||
val addr = call.argument<String>("addr")
|
||||
if (vpnIp == "") {
|
||||
if (addr == "") {
|
||||
return result.error("required_argument", "addr is a required argument", null)
|
||||
}
|
||||
|
||||
|
@ -365,11 +362,11 @@ class MainActivity: FlutterActivity() {
|
|||
return result.success(null)
|
||||
}
|
||||
|
||||
var msg = Message.obtain()
|
||||
val msg = Message.obtain()
|
||||
msg.what = NebulaVpnService.MSG_SET_REMOTE_FOR_TUNNEL
|
||||
msg.data.putString("vpnIp", vpnIp)
|
||||
msg.data.putString("addr", addr)
|
||||
msg.replyTo = Messenger(object: Handler() {
|
||||
msg.replyTo = Messenger(object: Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
result.success(msg.data.getString("data"))
|
||||
}
|
||||
|
@ -392,10 +389,10 @@ class MainActivity: FlutterActivity() {
|
|||
return result.success(null)
|
||||
}
|
||||
|
||||
var msg = Message.obtain()
|
||||
val msg = Message.obtain()
|
||||
msg.what = NebulaVpnService.MSG_CLOSE_TUNNEL
|
||||
msg.data.putString("vpnIp", vpnIp)
|
||||
msg.replyTo = Messenger(object: Handler() {
|
||||
msg.replyTo = Messenger(object: Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
result.success(msg.data.getBoolean("data"))
|
||||
}
|
||||
|
@ -475,7 +472,7 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
|
||||
// Handle and route messages coming from the vpn service
|
||||
inner class IncomingHandler: Handler() {
|
||||
inner class IncomingHandler: Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
val id = msg.data.getString("id")
|
||||
|
||||
|
@ -523,7 +520,7 @@ class MainActivity: FlutterActivity() {
|
|||
|
||||
inner class RefreshReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.getAction() != ACTION_REFRESH_SITES) return
|
||||
if (intent?.action != ACTION_REFRESH_SITES) return
|
||||
if (sites == null) return
|
||||
|
||||
Log.d(TAG, "Refreshing sites in MainActivity")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package net.defined.mobile_nebula
|
||||
|
||||
import io.flutter.view.FlutterMain
|
||||
import io.flutter.embedding.engine.loader.FlutterLoader
|
||||
import android.app.Application
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
|
@ -14,6 +14,6 @@ class MyApplication : Application() {
|
|||
val myConfig = Configuration.Builder().build()
|
||||
WorkManager.initialize(this, myConfig)
|
||||
|
||||
FlutterMain.startInitialization(applicationContext)
|
||||
FlutterLoader().startInitialization(applicationContext)
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import android.net.*
|
|||
import android.os.*
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.*
|
||||
import mobileNebula.CIDR
|
||||
import java.io.File
|
||||
|
@ -57,7 +56,7 @@ class NebulaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.getAction() == ACTION_STOP) {
|
||||
if (intent?.action == ACTION_STOP) {
|
||||
stopVpn()
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
@ -75,10 +74,10 @@ class NebulaVpnService : VpnService() {
|
|||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
path = intent?.getStringExtra("path")
|
||||
path = intent!!.getStringExtra("path")!!
|
||||
//TODO: if we fail to start, android will attempt a restart lacking all the intent data we need.
|
||||
// Link active site config in Main to avoid this
|
||||
site = Site(this, File(path))
|
||||
site = Site(this, File(path!!))
|
||||
|
||||
if (site!!.cert == null) {
|
||||
announceExit(id, "Site is missing a certificate")
|
||||
|
@ -96,7 +95,7 @@ class NebulaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
private fun startVpn() {
|
||||
var ipNet: CIDR
|
||||
val ipNet: CIDR
|
||||
|
||||
try {
|
||||
ipNet = mobileNebula.MobileNebula.parseCIDR(site!!.cert!!.cert.details.ips[0])
|
||||
|
@ -110,16 +109,16 @@ class NebulaVpnService : VpnService() {
|
|||
.setMtu(site!!.mtu)
|
||||
.setSession(TAG)
|
||||
.allowFamily(OsConstants.AF_INET)
|
||||
.allowFamily(OsConstants.AF_INET6);
|
||||
.allowFamily(OsConstants.AF_INET6)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false);
|
||||
builder.setMetered(false)
|
||||
}
|
||||
|
||||
// Add our unsafe routes
|
||||
site!!.unsafeRoutes.forEach { unsafeRoute ->
|
||||
val ipNet = mobileNebula.MobileNebula.parseCIDR(unsafeRoute.route)
|
||||
builder.addRoute(ipNet.network, ipNet.maskSize.toInt())
|
||||
val unsafeIPNet = mobileNebula.MobileNebula.parseCIDR(unsafeRoute.route)
|
||||
builder.addRoute(unsafeIPNet.network, unsafeIPNet.maskSize.toInt())
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -140,7 +139,7 @@ class NebulaVpnService : VpnService() {
|
|||
|
||||
nebula!!.start()
|
||||
running = true
|
||||
sendSimple(MSG_IS_RUNNING, if (running) 1 else 0)
|
||||
sendSimple(MSG_IS_RUNNING, 1)
|
||||
}
|
||||
|
||||
// Used to detect network changes (wifi -> cell or vice versa) and rebinds the udp socket/updates LH
|
||||
|
@ -158,7 +157,7 @@ class NebulaVpnService : VpnService() {
|
|||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
inner class NetworkCallback() : ConnectivityManager.NetworkCallback () {
|
||||
inner class NetworkCallback : ConnectivityManager.NetworkCallback () {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
nebula!!.rebind("network change")
|
||||
|
@ -200,7 +199,7 @@ class NebulaVpnService : VpnService() {
|
|||
}
|
||||
|
||||
private fun reload() {
|
||||
site = Site(this, File(path))
|
||||
site = Site(this, File(path!!))
|
||||
nebula?.reload(site!!.config, site!!.getKey(this))
|
||||
}
|
||||
|
||||
|
@ -241,9 +240,9 @@ class NebulaVpnService : VpnService() {
|
|||
|
||||
inner class ReloadReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent?.getAction() != ACTION_RELOAD) return
|
||||
if (intent?.action != ACTION_RELOAD) return
|
||||
if (!running) return
|
||||
if (intent?.getStringExtra("id") != site!!.id) return
|
||||
if (intent.getStringExtra("id") != site!!.id) return
|
||||
|
||||
Log.d(TAG, "Reloading Nebula")
|
||||
|
||||
|
@ -254,7 +253,7 @@ class NebulaVpnService : VpnService() {
|
|||
/**
|
||||
* Handler of incoming messages from clients.
|
||||
*/
|
||||
inner class IncomingHandler(context: Context, private val applicationContext: Context = context.applicationContext) : Handler() {
|
||||
inner class IncomingHandler : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
//TODO: how do we limit what can talk to us?
|
||||
//TODO: Make sure replyTo is actually a messenger
|
||||
|
@ -295,7 +294,7 @@ class NebulaVpnService : VpnService() {
|
|||
if (protect(msg)) { return }
|
||||
|
||||
val res = nebula!!.listHostmap(msg.what == MSG_LIST_PENDING_HOSTMAP)
|
||||
var m = Message.obtain(null, msg.what)
|
||||
val m = Message.obtain(null, msg.what)
|
||||
m.data.putString("data", res)
|
||||
msg.replyTo.send(m)
|
||||
}
|
||||
|
@ -304,7 +303,7 @@ class NebulaVpnService : VpnService() {
|
|||
if (protect(msg)) { return }
|
||||
|
||||
val res = nebula!!.getHostInfoByVpnIp(msg.data.getString("vpnIp"), msg.data.getBoolean("pending"))
|
||||
var m = Message.obtain(null, msg.what)
|
||||
val m = Message.obtain(null, msg.what)
|
||||
m.data.putString("data", res)
|
||||
msg.replyTo.send(m)
|
||||
}
|
||||
|
@ -313,7 +312,7 @@ class NebulaVpnService : VpnService() {
|
|||
if (protect(msg)) { return }
|
||||
|
||||
val res = nebula!!.setRemoteForTunnel(msg.data.getString("vpnIp"), msg.data.getString("addr"))
|
||||
var m = Message.obtain(null, msg.what)
|
||||
val m = Message.obtain(null, msg.what)
|
||||
m.data.putString("data", res)
|
||||
msg.replyTo.send(m)
|
||||
}
|
||||
|
@ -322,7 +321,7 @@ class NebulaVpnService : VpnService() {
|
|||
if (protect(msg)) { return }
|
||||
|
||||
val res = nebula!!.closeTunnel(msg.data.getString("vpnIp"))
|
||||
var m = Message.obtain(null, msg.what)
|
||||
val m = Message.obtain(null, msg.what)
|
||||
m.data.putBoolean("data", res)
|
||||
msg.replyTo.send(m)
|
||||
}
|
||||
|
@ -356,7 +355,7 @@ class NebulaVpnService : VpnService() {
|
|||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
messenger = Messenger(IncomingHandler(this))
|
||||
messenger = Messenger(IncomingHandler())
|
||||
return messenger.binder
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,27 @@ package net.defined.mobile_nebula
|
|||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
|
||||
class PackageInfo(val context: Context) {
|
||||
private val pInfo: PackageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0)
|
||||
private val appInfo: ApplicationInfo = context.getApplicationInfo()
|
||||
class PackageInfo(private val context: Context) {
|
||||
private val pInfo: PackageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0))
|
||||
else
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
|
||||
private val appInfo: ApplicationInfo = context.applicationInfo
|
||||
|
||||
fun getVersion(): String {
|
||||
val version: String = pInfo.versionName
|
||||
val build: Int = pInfo.versionCode
|
||||
val build: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
pInfo.longVersionCode
|
||||
else
|
||||
@Suppress("DEPRECATION")
|
||||
pInfo.versionCode.toLong()
|
||||
return "%s-%d".format(version, build)
|
||||
}
|
||||
|
||||
|
@ -22,6 +32,6 @@ class PackageInfo(val context: Context) {
|
|||
}
|
||||
|
||||
fun getSystemVersion(): String {
|
||||
return Build.VERSION.RELEASE;
|
||||
return Build.VERSION.RELEASE
|
||||
}
|
||||
}
|
|
@ -3,12 +3,10 @@ package net.defined.mobile_nebula
|
|||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.Expose
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
data class SiteContainer(
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.defined.mobile_nebula">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
buildscript {
|
||||
ext {
|
||||
workVersion = "2.7.1"
|
||||
kotlinVersion = '1.6.10'
|
||||
kotlinVersion = '1.7.20'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -10,7 +10,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue