diff --git a/app/build.gradle b/app/build.gradle
index 363bfea..8dd5919 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -22,6 +22,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ applicationVariants.all { variant ->
+ variant.resValue "string", "coreName", "4.17.0"
+ variant.resValue "string", "versionName", variant.versionName
+ }
productFlavors {
}
}
@@ -36,6 +40,7 @@ dependencies {
implementation 'com.google.android.material:material:1.1.0-alpha02'
implementation "androidx.paging:paging-runtime:2.1.0-rc01"
implementation 'com.beust:klaxon:3.0.1'
+
implementation(name: 'tun2socks', ext: 'aar')
implementation 'androidx.room:room-runtime:2.1.0-alpha03'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6eb3a65..8e2a19f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,7 +16,8 @@
+ android:theme="@style/AppTheme.NoActionBar"
+ android:windowSoftInputMode="stateHidden">
@@ -59,6 +60,12 @@
android:parentActivityName=".ui.perapp.PerAppActivity">
+
+
+
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/contants.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/contants.kt
new file mode 100644
index 0000000..134b709
--- /dev/null
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/contants.kt
@@ -0,0 +1,200 @@
+package `fun`.kitsunebi.kitsunebi4android.common
+
+class Constants {
+ companion object {
+ val PREFERENCE_CONFIG_KEY = "preference_config_key"
+ val SUBSCRIBE_CONFIG_URL_KEY = "subscribe_config_url_key"
+ val DEFAULT_CONFIG = """
+ {
+ "outbounds": [
+ {
+ "protocol": "vmess",
+ "settings": {
+ "vnext": [
+ {
+ "address": "1.2.3.4",
+ "port": 10086,
+ "users": [
+ {
+ "id": "0e8575fb-a71f-455b-877f-b74e19d3f495"
+ }
+ ]
+ }
+ ]
+ },
+ "streamSettings": {
+ "network": "tcp"
+ },
+ "tag": "proxy"
+ },
+ {
+ "protocol": "freedom",
+ "settings": {
+ "domainStrategy": "UseIP"
+ },
+ "streamSettings": {},
+ "tag": "direct"
+ },
+ {
+ "protocol": "blackhole",
+ "settings": {},
+ "tag": "block"
+ },
+ {
+ "protocol": "dns",
+ "tag": "dns-out"
+ }
+ ],
+ "dns": {
+ "clientIp": "115.239.211.92",
+ "hosts": {
+ "localhost": "127.0.0.1"
+ },
+ "servers": [
+ "114.114.114.114",
+ {
+ "address": "8.8.8.8",
+ "domains": [
+ "google",
+ "android",
+ "fbcdn",
+ "facebook",
+ "domain:fb.com",
+ "instagram",
+ "whatsapp",
+ "akamai",
+ "domain:line-scdn.net",
+ "domain:line.me",
+ "domain:naver.jp"
+ ],
+ "port": 53
+ }
+ ]
+ },
+ "log": {
+ "loglevel": "warning"
+ },
+ "policy": {
+ "levels": {
+ "0": {
+ "bufferSize": 4096,
+ "connIdle": 30,
+ "downlinkOnly": 0,
+ "handshake": 4,
+ "uplinkOnly": 0
+ }
+ }
+ },
+ "routing": {
+ "domainStrategy": "IPIfNonMatch",
+ "rules": [
+ {
+ "inboundTag": ["tun2socks"],
+ "network": "udp",
+ "port": 53,
+ "outboundTag": "dns-out",
+ "type": "field"
+ },
+ {
+ "domain": [
+ "domain:setup.icloud.com"
+ ],
+ "outboundTag": "proxy",
+ "type": "field"
+ },
+ {
+ "ip": [
+ "8.8.8.8/32",
+ "8.8.4.4/32",
+ "1.1.1.1/32",
+ "1.0.0.1/32",
+ "9.9.9.9/32",
+ "149.112.112.112/32",
+ "208.67.222.222/32",
+ "208.67.220.220/32"
+ ],
+ "outboundTag": "proxy",
+ "type": "field"
+ },
+ {
+ "ip": [
+ "geoip:cn",
+ "geoip:private"
+ ],
+ "outboundTag": "direct",
+ "type": "field"
+ },
+ {
+ "outboundTag": "direct",
+ "port": "123",
+ "type": "field"
+ },
+ {
+ "domain": [
+ "domain:pstatp.com",
+ "domain:snssdk.com",
+ "domain:toutiao.com",
+ "domain:ixigua.com",
+ "domain:apple.com",
+ "domain:crashlytics.com",
+ "domain:icloud.com",
+ "cctv",
+ "umeng",
+ "domain:weico.cc",
+ "domain:jd.com",
+ "domain:360buy.com",
+ "domain:360buyimg.com",
+ "domain:douyu.tv",
+ "domain:douyu.com",
+ "domain:douyucdn.cn",
+ "geosite:cn"
+ ],
+ "outboundTag": "direct",
+ "type": "field"
+ },
+ {
+ "ip": [
+ "149.154.167.0/24",
+ "149.154.175.0/24",
+ "91.108.56.0/24",
+ "125.209.222.0/24"
+ ],
+ "outboundTag": "proxy",
+ "type": "field"
+ },
+ {
+ "domain": [
+ "twitter",
+ "domain:twimg.com",
+ "domain:t.co",
+ "google",
+ "domain:ggpht.com",
+ "domain:gstatic.com",
+ "domain:youtube.com",
+ "domain:ytimg.com",
+ "pixiv",
+ "domain:pximg.net",
+ "tumblr",
+ "instagram",
+ "domain:line-scdn.net",
+ "domain:line.me",
+ "domain:naver.jp",
+ "domain:facebook.com",
+ "domain:fbcdn.net",
+ "pinterest",
+ "github",
+ "dropbox",
+ "netflix",
+ "domain:medium.com",
+ "domain:fivecdm.com"
+ ],
+ "outboundTag": "proxy",
+ "type": "field"
+ }
+ ],
+ "strategy": "rules"
+ }
+}
+ """.trimIndent()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/utils.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/utils.kt
index 6748e75..aae9f7d 100644
--- a/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/utils.kt
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/common/utils.kt
@@ -1,5 +1,8 @@
package `fun`.kitsunebi.kitsunebi4android.common
+import android.app.AlertDialog
+import android.content.Context
+
open class SingletonHolder(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile private var instance: T? = null
@@ -30,4 +33,11 @@ public fun humanReadableByteCount(bytes: Long, si: Boolean): String {
val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre)
+}
+
+public fun showAlert(context: Context, msg: String) {
+ val dialog = AlertDialog.Builder(context).setTitle("Message").setMessage(msg)
+ .setPositiveButton("Ok", { dialog, i ->
+ })
+ dialog.show()
}
\ No newline at end of file
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/service/SimpleVpnService.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/service/SimpleVpnService.kt
index 136f4e2..c673dc1 100644
--- a/app/src/main/java/fun/kitsunebi/kitsunebi4android/service/SimpleVpnService.kt
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/service/SimpleVpnService.kt
@@ -1,6 +1,7 @@
package `fun`.kitsunebi.kitsunebi4android.service
import `fun`.kitsunebi.kitsunebi4android.R
+import `fun`.kitsunebi.kitsunebi4android.common.Constants
import `fun`.kitsunebi.kitsunebi4android.storage.PROXY_LOG_DB_NAME
import `fun`.kitsunebi.kitsunebi4android.storage.Preferences
import `fun`.kitsunebi.kitsunebi4android.storage.ProxyLog
@@ -160,14 +161,19 @@ open class SimpleVpnService : VpnService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- configString = Preferences.getString(applicationContext, getString(R.string.preference_config_key), getString(R.string.default_config))
+ configString = Preferences.getString(applicationContext, Constants.PREFERENCE_CONFIG_KEY, Constants.DEFAULT_CONFIG)
bgThread = thread(start = true) {
- val config = Klaxon().parse(configString)
+ val config = try { Klaxon().parse(configString) } catch (e: Exception) {
+ sendBroadcast(android.content.Intent("vpn_start_err_config"))
+ stopVPN()
+ return@thread
+ }
if (config != null) {
if (config.dns == null || config.dns.servers == null || config.dns.servers.size == 0) {
println("must configure dns servers since v2ray will use localhost if there isn't any dns servers")
sendBroadcast(Intent("vpn_start_err_dns"))
+ stopVPN()
return@thread
}
@@ -176,12 +182,14 @@ open class SimpleVpnService : VpnService() {
if (dnsServer != null && dnsServer == "localhost") {
println("using local dns resolver is not allowed since it will cause infinite loop")
sendBroadcast(Intent("vpn_start_err_dns"))
+ stopVPN()
return@thread
}
}
} else {
println("parsing v2ray config failed")
sendBroadcast(Intent("vpn_start_err"))
+ stopVPN()
return@thread
}
@@ -191,7 +199,6 @@ open class SimpleVpnService : VpnService() {
.setMtu(1500)
.addAddress("10.233.233.233", 30)
.addDnsServer(localDns)
- .addSearchDomain("local")
.addRoute("0.0.0.0", 0)
val isEnablePerAppVpn = Preferences.getBool(applicationContext, getString(R.string.is_enable_per_app_vpn), null)
@@ -225,6 +232,7 @@ open class SimpleVpnService : VpnService() {
if ((pfd == null) || !Tun2socks.setNonblock(pfd!!.fd.toLong(), false)) {
println("failed to put tunFd in blocking mode")
sendBroadcast(Intent("vpn_start_err"))
+ stopVPN()
return@thread
}
@@ -244,23 +252,49 @@ open class SimpleVpnService : VpnService() {
}
val files = filesDir.list()
- if (!files.contains("geoip.dat") || !files.contains("geosite.dat")) {
- val geoipBytes = resources.openRawResource(R.raw.geoip).readBytes()
- val fos = openFileOutput("geoip.dat", Context.MODE_PRIVATE)
- fos.write(geoipBytes)
- fos.close()
-
- val geositeBytes = resources.openRawResource(R.raw.geosite).readBytes()
- val fos2 = openFileOutput("geosite.dat", Context.MODE_PRIVATE)
- fos2.write(geositeBytes)
- fos2.close()
- }
+ // FIXME copy only when update
+ val geoipBytes = resources.openRawResource(R.raw.geoip).readBytes()
+ val fos = openFileOutput("geoip.dat", Context.MODE_PRIVATE)
+ fos.write(geoipBytes)
+ fos.close()
+
+ val geositeBytes = resources.openRawResource(R.raw.geosite).readBytes()
+ val fos2 = openFileOutput("geosite.dat", Context.MODE_PRIVATE)
+ fos2.write(geositeBytes)
+ fos2.close()
+
+// if (!files.contains("geoip.dat") || !files.contains("geosite.dat")) {
+// val geoipBytes = resources.openRawResource(R.raw.geoip).readBytes()
+// val fos = openFileOutput("geoip.dat", Context.MODE_PRIVATE)
+// fos.write(geoipBytes)
+// fos.close()
+//
+// val geositeBytes = resources.openRawResource(R.raw.geosite).readBytes()
+// val fos2 = openFileOutput("geosite.dat", Context.MODE_PRIVATE)
+// fos2.write(geositeBytes)
+// fos2.close()
+// }
ProxyLogDatabase.getInstance(applicationContext).proxyLogDao().getAllCount()
- val dbPath = getDatabasePath(PROXY_LOG_DB_NAME).absolutePath
+
+ var sniffing = Preferences.getString(applicationContext, getString(R.string.sniffing), "http,tls")
+ // Just ensure no whitespaces in the the string.
+ val sniffingList = sniffing.split(",")
+ var sniffings = ArrayList()
+ for (s in sniffingList) {
+ sniffings.add(s.trim())
+ }
+ sniffing = sniffings.joinToString(",")
+
+ val inboundTag = Preferences.getString(applicationContext, getString(R.string.inbound_tag), "tun2socks")
Tun2socks.setLocalDNS("$localDns:53")
- Tun2socks.startV2Ray(flow, service, dbService, configString.toByteArray(), filesDir.absolutePath, dbPath)
+ val ret = Tun2socks.startV2Ray(flow, service, dbService, configString.toByteArray(), inboundTag, sniffing, filesDir.absolutePath)
+ if (ret.toInt() != 0) {
+ sendBroadcast(Intent("vpn_start_err_config"))
+ stopVPN()
+ return@thread
+ }
sendBroadcast(Intent("vpn_started"))
Preferences.putBool(applicationContext, getString(R.string.vpn_is_running), true)
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/storage/ProxyLog.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/storage/ProxyLog.kt
index 3329ab8..27212d5 100644
--- a/app/src/main/java/fun/kitsunebi/kitsunebi4android/storage/ProxyLog.kt
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/storage/ProxyLog.kt
@@ -31,7 +31,7 @@ interface ProxyLogDao {
@Query("SELECT * FROM proxy_log ORDER BY end_time DESC")
fun getAllPaged(): DataSource.Factory
- @Query("SELECT * FROM proxy_log WHERE record_type != 1 ORDER BY end_time DESC")
+ @Query("SELECT * FROM proxy_log WHERE record_type != 1 AND target NOT LIKE 'udp:%:53' ORDER BY end_time DESC")
fun getAllNonDnsPaged(): DataSource.Factory
@Insert
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/LogcatActivity.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/LogcatActivity.kt
index b63c7c4..e47be7a 100644
--- a/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/LogcatActivity.kt
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/LogcatActivity.kt
@@ -10,6 +10,7 @@ import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
+import kotlinx.android.synthetic.main.activity_logcat.*
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
@@ -29,7 +30,7 @@ class LogcatActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_logcat)
logcatTextView = findViewById(R.id.logcat_text)
- logcatTextView.movementMethod = ScrollingMovementMethod()
+ logcatScroll.isSmoothScrollingEnabled = true
bgThread = object : Thread() {
override fun run() {
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/MainActivity.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/MainActivity.kt
index 3ff8e64..d7cd5f2 100644
--- a/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/MainActivity.kt
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/MainActivity.kt
@@ -1,13 +1,13 @@
package `fun`.kitsunebi.kitsunebi4android.ui
import `fun`.kitsunebi.kitsunebi4android.R
+import `fun`.kitsunebi.kitsunebi4android.common.Constants
+import `fun`.kitsunebi.kitsunebi4android.common.showAlert
import `fun`.kitsunebi.kitsunebi4android.service.SimpleVpnService
import `fun`.kitsunebi.kitsunebi4android.storage.Preferences
-import `fun`.kitsunebi.kitsunebi4android.ui.perapp.PerAppActivity
import `fun`.kitsunebi.kitsunebi4android.ui.proxylog.ProxyLogActivity
import `fun`.kitsunebi.kitsunebi4android.ui.settings.SettingsActivity
import android.app.Activity
-import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -23,6 +23,11 @@ import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main.*
import org.json.JSONException
import org.json.JSONObject
+import android.view.MotionEvent
+import android.view.View.OnTouchListener
+import android.view.View
+
+
class MainActivity : AppCompatActivity() {
@@ -30,11 +35,13 @@ class MainActivity : AppCompatActivity() {
var running = false
private var starting = false
private var stopping = false
+ private lateinit var configString: String
+
// val mNotificationId = 1
// var mNotificationManager: NotificationManager? = null
val broadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(contxt: Context?, intent: Intent?) {
+ override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
"vpn_stopped" -> {
running = false
@@ -52,13 +59,25 @@ class MainActivity : AppCompatActivity() {
running = false
starting = false
fab.setImageResource(android.R.drawable.ic_media_play)
- showAlert("Start VPN service failed")
+ context?.let {
+ showAlert(it, "Start VPN service failed")
+ }
}
"vpn_start_err_dns" -> {
running = false
starting = false
fab.setImageResource(android.R.drawable.ic_media_play)
- showAlert("Start VPN service failed: Not configuring DNS right, must has at least 1 dns server and mustn't include \"localhost\"")
+ context?.let {
+ showAlert(it, "Start VPN service failed: Not configuring DNS right, must has at least 1 dns server and mustn't include \"localhost\"")
+ }
+ }
+ "vpn_start_err_config" -> {
+ running = false
+ starting = false
+ fab.setImageResource(android.R.drawable.ic_media_play)
+ context?.let {
+ showAlert(it, "Start VPN service failed: Invalid V2Ray config.")
+ }
}
"pong" -> {
fab.setImageResource(android.R.drawable.ic_media_pause)
@@ -69,6 +88,15 @@ class MainActivity : AppCompatActivity() {
}
}
+ private fun updateUI() {
+ configString = Preferences.getString(applicationContext, Constants.PREFERENCE_CONFIG_KEY, Constants.DEFAULT_CONFIG)
+ configString?.let {
+ formatJsonString(it).let {
+ configView.setText(it, TextView.BufferType.EDITABLE)
+ }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -78,25 +106,26 @@ class MainActivity : AppCompatActivity() {
registerReceiver(broadcastReceiver, IntentFilter("vpn_stopped"))
registerReceiver(broadcastReceiver, IntentFilter("vpn_started"))
+
+ // TODO make a list
registerReceiver(broadcastReceiver, IntentFilter("vpn_start_err"))
registerReceiver(broadcastReceiver, IntentFilter("vpn_start_err_dns"))
+ registerReceiver(broadcastReceiver, IntentFilter("vpn_start_err_config"))
+
registerReceiver(broadcastReceiver, IntentFilter("pong"))
sendBroadcast(Intent("ping"))
- var configString = Preferences.getString(applicationContext, getString(R.string.preference_config_key), getString(R.string.default_config))
- configString?.let {
- formatJsonString(it).let {
- configView.setText(it, TextView.BufferType.EDITABLE)
- }
- }
+ updateUI()
+
+ configScroll.isSmoothScrollingEnabled = true
fab.setOnClickListener { view ->
if (!running && !starting) {
starting = true
fab.setImageResource(android.R.drawable.ic_media_ff)
configString = configView.text.toString()
- Preferences.putString(applicationContext, getString(R.string.preference_config_key), configString)
+ Preferences.putString(applicationContext, Constants.Companion.PREFERENCE_CONFIG_KEY, configString)
val intent = VpnService.prepare(this)
if (intent != null) {
startActivityForResult(intent, 1)
@@ -119,6 +148,7 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
+ updateUI()
sendBroadcast(Intent("ping"))
}
@@ -133,6 +163,11 @@ class MainActivity : AppCompatActivity() {
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
+ R.id.subscribe_config_btn -> {
+ val intent = Intent(this, SubscribeConfigActivity::class.java)
+ startActivity(intent)
+ return true
+ }
R.id.format_btn -> {
val prettyText = formatJsonString(configView.text.toString())
prettyText?.let {
@@ -144,10 +179,10 @@ class MainActivity : AppCompatActivity() {
val config = configView.text.toString()
val prettyText = formatJsonString(config)
if (prettyText == null) {
- showAlert("Invalid JSON")
+ showAlert(this, "Invalid JSON")
return true
}
- Preferences.putString(applicationContext, getString(R.string.preference_config_key), prettyText)
+ Preferences.putString(applicationContext, Constants.PREFERENCE_CONFIG_KEY, prettyText)
return true
}
R.id.log_btn -> {
@@ -183,18 +218,11 @@ class MainActivity : AppCompatActivity() {
return try {
JSONObject(json).toString(2)
} catch (e: JSONException) {
- showAlert("Invalid JSON")
+ showAlert(this, "Invalid JSON")
return null
}
}
- fun showAlert(msg: String) {
- val dialog = AlertDialog.Builder(this).setTitle("Message").setMessage(msg)
- .setPositiveButton("Ok", { dialog, i ->
- })
- dialog.show()
- }
-
// private fun startNotification() {
// // Build Notification , setOngoing keeps the notification always in status bar
// val mBuilder = NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
diff --git a/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/SubscribeConfigActivity.kt b/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/SubscribeConfigActivity.kt
new file mode 100644
index 0000000..3dd903e
--- /dev/null
+++ b/app/src/main/java/fun/kitsunebi/kitsunebi4android/ui/SubscribeConfigActivity.kt
@@ -0,0 +1,67 @@
+package `fun`.kitsunebi.kitsunebi4android.ui
+
+import `fun`.kitsunebi.kitsunebi4android.R
+import `fun`.kitsunebi.kitsunebi4android.common.Constants
+import `fun`.kitsunebi.kitsunebi4android.common.showAlert
+import `fun`.kitsunebi.kitsunebi4android.storage.Preferences
+import android.content.Context
+import android.os.AsyncTask
+import android.os.Bundle
+import android.widget.Button
+import android.widget.EditText
+import androidx.appcompat.app.AppCompatActivity
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+
+class SubscribeConfigActivity : AppCompatActivity() {
+
+ private lateinit var subUrl: EditText
+ private lateinit var subBtn: Button
+
+ class RetrieveConfigurationTask internal constructor(context: Context): AsyncTask() {
+ private var ctx: Context = context
+
+ override fun doInBackground(vararg p0: String?): String? {
+ if (p0[0] != null) {
+ val obj = URL(p0[0])
+ with(obj.openConnection() as HttpsURLConnection) {
+ BufferedReader(InputStreamReader(inputStream)).use {
+ val response = StringBuffer()
+ var inputLine = it.readLine()
+ while (inputLine != null) {
+ response.append(inputLine)
+ inputLine = it.readLine()
+ }
+ it.close()
+ return response.toString()
+ }
+ }
+ }
+ return null
+ }
+
+ override fun onPostExecute(result: String?) {
+ if (result != null) {
+ Preferences.putString(ctx, Constants.PREFERENCE_CONFIG_KEY, result)
+ showAlert(ctx, "Configuration updated!")
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_subscribe_config)
+ subUrl = findViewById(R.id.sub_url)
+ subBtn = findViewById