diff --git a/app/build.gradle b/app/build.gradle index af555d2..8890da4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,10 +6,10 @@ android { compileSdkVersion 29 defaultConfig { applicationId "io.github.chronosx88.yggdrasil" - minSdkVersion 21 + minSdkVersion 15 targetSdkVersion 29 - versionCode 3 - versionName "1.3" + versionCode 4 + versionName "1.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" project.ext.set("archivesBaseName", project.getParent().name+"-"+versionName) } @@ -36,6 +36,12 @@ android { kotlinOptions { jvmTarget = '1.8' } + lintOptions { + checkReleaseBuilds false + // Or, if you prefer, you can continue to check for errors in release builds, + // but continue the build even when errors are found: + abortOnError false + } } task ndkBuild(type: Exec) { @@ -48,7 +54,6 @@ gradle.projectsEvaluated { tasks.compileDebugKotlin.dependsOn(ndkBuild) } - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(path: ':yggdrasil') @@ -59,7 +64,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' - implementation 'com.google.android.material:material:1.3.0-alpha01' + implementation 'com.google.android.material:material:1.3.0-alpha02' implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.hbb20:ccp:2.4.0' diff --git a/app/src/main/java/io/github/chronosx88/yggdrasil/DNSListActivity.kt b/app/src/main/java/io/github/chronosx88/yggdrasil/DNSListActivity.kt index 270246b..7baa89c 100644 --- a/app/src/main/java/io/github/chronosx88/yggdrasil/DNSListActivity.kt +++ b/app/src/main/java/io/github/chronosx88/yggdrasil/DNSListActivity.kt @@ -107,7 +107,7 @@ class DNSListActivity : AppCompatActivity() { var ccpInput = view.findViewById(R.id.ccp) var ip = ipInput.text.toString().toLowerCase() var ccp = ccpInput.selectedCountryNameCode - GlobalScope.launch { + thread(start = true) { var di = DNSInfo(InetAddress.getByName("["+ip+"]"), ccp, "User DNS") try { var ping = ping(di.address, 53) @@ -115,8 +115,8 @@ class DNSListActivity : AppCompatActivity() { } catch(e: Throwable){ di.ping = Int.MAX_VALUE } - withContext(Dispatchers.Main) { - var selectAdapter = (findViewById(R.id.peerList).adapter as SelectDNSInfoListAdapter) + runOnUiThread { + var selectAdapter = (findViewById(R.id.dnsList).adapter as SelectDNSInfoListAdapter) selectAdapter.addItem(0, di) selectAdapter.notifyDataSetChanged() ad.dismiss() diff --git a/app/src/main/java/io/github/chronosx88/yggdrasil/MainActivity.kt b/app/src/main/java/io/github/chronosx88/yggdrasil/MainActivity.kt index 839faef..6209ae0 100644 --- a/app/src/main/java/io/github/chronosx88/yggdrasil/MainActivity.kt +++ b/app/src/main/java/io/github/chronosx88/yggdrasil/MainActivity.kt @@ -3,7 +3,10 @@ package io.github.chronosx88.yggdrasil import android.app.Activity import android.app.ActivityManager import android.content.* +import android.net.ConnectivityManager +import android.net.Network import android.net.VpnService +import android.os.Build import android.os.Bundle import android.util.Log import android.view.Gravity @@ -11,9 +14,11 @@ import android.view.View import android.widget.* import androidx.appcompat.app.AppCompatActivity import androidx.preference.PreferenceManager +import dalvik.system.DexFile import io.github.chronosx88.yggdrasil.models.DNSInfo import io.github.chronosx88.yggdrasil.models.PeerInfo import io.github.chronosx88.yggdrasil.models.config.DNSInfoListAdapter +import io.github.chronosx88.yggdrasil.models.config.NetworkUtils import io.github.chronosx88.yggdrasil.models.config.PeerInfoListAdapter import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializePeerStringList2PeerInfoSet import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2DNSInfoSet @@ -22,11 +27,6 @@ import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeS import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringSet2PeerInfoSet import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.serializeDNSInfoSet2StringList import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.serializePeerInfoSet2StringList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.net.InetAddress import kotlin.concurrent.thread @@ -62,12 +62,12 @@ class MainActivity : AppCompatActivity() { @JvmStatic var isStarted = false @JvmStatic var isCancelled = false - @JvmStatic var address = "" + @JvmStatic var address:String? = "" } private var currentPeers = setOf() private var currentDNS = setOf() - private var meshPeersReceiver: BroadcastReceiver? = null + private var networkStateReceiver: BroadcastReceiver? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -143,11 +143,60 @@ class MainActivity : AppCompatActivity() { ipLayout.visibility = View.VISIBLE findViewById(R.id.ip).text = address } - + /* + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivityManager?.let { + it.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + if(isStarted) { + stopVpn() + Thread.sleep(1000) + startVpn() + } + } + override fun onLost(network: Network?) { + if(isStarted) { + stopVpn() + Thread.sleep(1000) + startVpn() + } + } + }) + } + } else { + networkStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val status: Int = NetworkUtils.getConnectivityStatusString(context) + Log.i(TAG, "Network state has been changed") + if ("android.net.conn.CONNECTIVITY_CHANGE" == intent.action) { + if (status == NetworkUtils.NETWORK_STATUS_NOT_CONNECTED) { + if(isStarted) { + stopVpn() + Thread.sleep(1000) + startVpn() + } + } else { + if(isStarted) { + stopVpn() + Thread.sleep(1000) + startVpn() + } + } + } + } + } + }*/ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + val sourceDir: String = this.applicationInfo.sourceDir + val dexFile = DexFile(sourceDir) + val cl = classLoader + val c: Class<*> = dexFile.loadClass("dummy/Dummy", cl) + } } private fun stopVpn(){ - Log.d(TAG,"Stop") + Log.i(TAG,"Stop") val intent = Intent(this, YggdrasilTunService::class.java) val TASK_CODE = 100 val pi = createPendingResult(TASK_CODE, intent, 0) @@ -157,7 +206,7 @@ class MainActivity : AppCompatActivity() { } private fun startVpn(){ - Log.d(TAG,"Start") + Log.i(TAG,"Start") val intent= VpnService.prepare(this) if (intent!=null){ startActivityForResult(intent, VPN_REQUEST_CODE) @@ -167,7 +216,7 @@ class MainActivity : AppCompatActivity() { } private fun updateDNS(){ - Log.d(TAG,"Update DNS") + Log.i(TAG,"Update DNS") val intent = Intent(this, YggdrasilTunService::class.java) val TASK_CODE = 100 val pi = createPendingResult(TASK_CODE, intent, 0) @@ -230,7 +279,7 @@ class MainActivity : AppCompatActivity() { //save to shared preferences val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) - preferences.edit().putStringSet(CURRENT_PEERS, HashSet(currentPeers)).apply() + preferences.edit().putStringSet(CURRENT_PEERS, currentPeers!!.toHashSet()).apply() if(isStarted){ //TODO implement UpdateConfig method in native interface and apply peer changes stopVpn() @@ -254,7 +303,7 @@ class MainActivity : AppCompatActivity() { //save to shared preferences val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext) - preferences.edit().putStringSet(CURRENT_DNS, HashSet(currentDNS)).apply() + preferences.edit().putStringSet(CURRENT_DNS, currentDNS!!.toHashSet()).apply() if(isStarted){ updateDNS() } @@ -332,8 +381,8 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() - if (meshPeersReceiver != null){ - unregisterReceiver(meshPeersReceiver); + if (networkStateReceiver != null){ + unregisterReceiver(networkStateReceiver); } } diff --git a/app/src/main/java/io/github/chronosx88/yggdrasil/PeerListActivity.kt b/app/src/main/java/io/github/chronosx88/yggdrasil/PeerListActivity.kt index 4f74ab2..786cddc 100644 --- a/app/src/main/java/io/github/chronosx88/yggdrasil/PeerListActivity.kt +++ b/app/src/main/java/io/github/chronosx88/yggdrasil/PeerListActivity.kt @@ -1,7 +1,6 @@ package io.github.chronosx88.yggdrasil import android.app.Activity -import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle @@ -140,7 +139,9 @@ class PeerListActivity : AppCompatActivity() { var schemaInput = view.findViewById(R.id.schemaInput) var ipInput = view.findViewById(R.id.ipInput) ipInput.requestFocus() - schemaInput.showSoftInputOnFocus = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + schemaInput.showSoftInputOnFocus = false + } schemaInput.setOnFocusChangeListener { v, _ -> if(schemaInput.isFocused) { onClickSchemaList(v) @@ -159,8 +160,20 @@ class PeerListActivity : AppCompatActivity() { var portInput = view.findViewById(R.id.portInput) var ccpInput = view.findViewById(R.id.ccp) var schema = schemaInput.text.toString().toLowerCase() + if(schema.isEmpty()){ + schemaInput.error = "Schema is required" + } var ip = ipInput.text.toString().toLowerCase() + if(ip.isEmpty()){ + ipInput.error = "IP address is required" + } var port = portInput.text.toString().toInt() + if(port<=0){ + portInput.error = "Port should be > 0" + } + if(port>=Short.MAX_VALUE){ + portInput.error = "Port should be < "+Short.MAX_VALUE + } var ccp = ccpInput.selectedCountryNameCode GlobalScope.launch { var pi = PeerInfo(schema, InetAddress.getByName(ip), port, ccp) diff --git a/app/src/main/java/io/github/chronosx88/yggdrasil/YggdrasilTunService.kt b/app/src/main/java/io/github/chronosx88/yggdrasil/YggdrasilTunService.kt index b0e593a..7fac9e5 100644 --- a/app/src/main/java/io/github/chronosx88/yggdrasil/YggdrasilTunService.kt +++ b/app/src/main/java/io/github/chronosx88/yggdrasil/YggdrasilTunService.kt @@ -31,35 +31,29 @@ import java.net.Inet6Address class YggdrasilTunService : VpnService() { private lateinit var ygg: Yggdrasil + private lateinit var tunInputStream: InputStream + private lateinit var tunOutputStream: OutputStream + private lateinit var address: String private var isClosed = false /** Maximum packet size is constrained by the MTU, which is given as a signed short/2 */ private val MAX_PACKET_SIZE = Short.MAX_VALUE/2 + private var tunInterface: ParcelFileDescriptor? = null companion object { private const val TAG = "Yggdrasil-service" } - private var tunInterface: ParcelFileDescriptor? = null - private var tunInputStream: InputStream? = null - private var tunOutputStream: OutputStream? = null - private var scope: CoroutineScope? = null - private var address: String? = null - private var mNotificationManager: NotificationManager? = null + private var scope: CoroutineScope? = null private val FOREGROUND_ID = 1338 - override fun onCreate() { - super.onCreate() - mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val pi: PendingIntent? = intent?.getParcelableExtra(MainActivity.PARAM_PINTENT) when(intent?.getStringExtra(MainActivity.COMMAND)){ MainActivity.STOP ->{ stopVpn(pi) - startForeground(FOREGROUND_ID, foregroundNotification("Yggdrasil service stopped")) + foregroundNotification(FOREGROUND_ID, "Yggdrasil service stopped") } MainActivity.START ->{ val peers = deserializeStringList2PeerInfoSet(intent.getStringArrayListExtra(MainActivity.CURRENT_PEERS)) @@ -67,7 +61,7 @@ class YggdrasilTunService : VpnService() { val staticIP: Boolean = intent.getBooleanExtra(MainActivity.STATIC_IP, false) ygg = Yggdrasil() setupTunInterface(pi, peers, dns, staticIP) - startForeground(FOREGROUND_ID, foregroundNotification("Yggdrasil service started")) + foregroundNotification(FOREGROUND_ID, "Yggdrasil service started") } MainActivity.UPDATE_DNS ->{ val dns = deserializeStringList2DNSInfoSet(intent.getStringArrayListExtra(MainActivity.CURRENT_DNS)) @@ -84,11 +78,18 @@ class YggdrasilTunService : VpnService() { private fun setupIOStreams(dns: MutableSet){ address = ygg.addressString - var builder = Builder() - .addAddress(address!!, 7) - .allowFamily(OsConstants.AF_INET) - .allowBypass() - .setMtu(MAX_PACKET_SIZE) + var builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Builder() + .addAddress(address, 7) + .allowFamily(OsConstants.AF_INET) + .allowBypass() + .setBlocking(true) + .setMtu(MAX_PACKET_SIZE) + } else { + Builder() + .addAddress(address, 7) + .setMtu(MAX_PACKET_SIZE) + } if (dns.size > 0) { for (d in dns) { builder.addDnsServer(d.address) @@ -100,15 +101,9 @@ class YggdrasilTunService : VpnService() { if(!hasIpv6DefaultRoute()){ builder.addRoute("2000::",3) } - if(tunInterface!=null){ - tunInterface!!.close() - tunInputStream!!.close() - tunOutputStream!!.close() - } tunInterface = builder.establish() tunInputStream = FileInputStream(tunInterface!!.fileDescriptor) tunOutputStream = FileOutputStream(tunInterface!!.fileDescriptor) - } private fun setupTunInterface( @@ -132,7 +127,7 @@ class YggdrasilTunService : VpnService() { val job = SupervisorJob() scope = CoroutineScope(Dispatchers.Default + job) scope!!.launch { - val buffer = ByteArray(2048) + val buffer = ByteArray(MAX_PACKET_SIZE) while (!isClosed) { readPacketsFromTun(yggConduitEndpoint, buffer) } @@ -148,7 +143,7 @@ class YggdrasilTunService : VpnService() { private fun sendMeshPeerStatus(pi: PendingIntent?){ class Token : TypeToken>() - var add = ygg.addressString + ygg.addressString var meshPeers: List = gson.fromJson(ygg.peersJSON, Token().type) val intent: Intent = Intent().putStringArrayListExtra( MainActivity.MESH_PEERS, @@ -219,12 +214,8 @@ class YggdrasilTunService : VpnService() { private fun readPacketsFromTun(yggConduitEndpoint: ConduitEndpoint, buffer: ByteArray) { try { // Read the outgoing packet from the input stream. - val length = tunInputStream?.read(buffer) ?: 1 - if (length > 0){ - yggConduitEndpoint.send(buffer.sliceArray(IntRange(0, length - 1))) - } else { - Thread.sleep(100) - } + val length = tunInputStream.read(buffer) + yggConduitEndpoint.send(buffer.sliceArray(IntRange(0, length - 1))) } catch (e: IOException) { e.printStackTrace() } @@ -234,7 +225,7 @@ class YggdrasilTunService : VpnService() { val buffer = yggConduitEndpoint.recv() if(buffer!=null) { try { - tunOutputStream?.write(buffer) + tunOutputStream.write(buffer) } catch (e: IOException) { e.printStackTrace() } @@ -244,10 +235,9 @@ class YggdrasilTunService : VpnService() { private fun stopVpn(pi: PendingIntent?) { isClosed = true; scope!!.coroutineContext.cancelChildren() - tunInputStream!!.close() - tunOutputStream!!.close() + tunInputStream.close() + tunOutputStream.close() tunInterface!!.close() - tunInterface = null Log.d(TAG,"Stop is running from service") ygg.stop() val intent: Intent = Intent() @@ -265,14 +255,17 @@ class YggdrasilTunService : VpnService() { private fun hasIpv6DefaultRoute(): Boolean { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val networks = cm.allNetworks - for (network in networks) { - val linkProperties = cm.getLinkProperties(network) - if(linkProperties!=null) { - val routes = linkProperties.routes - for (route in routes) { - if (route.isDefaultRoute && route.gateway is Inet6Address) { - return true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val networks = cm.allNetworks + + for (network in networks) { + val linkProperties = cm.getLinkProperties(network) + if(linkProperties!=null) { + val routes = linkProperties.routes + for (route in routes) { + if (route.isDefaultRoute && route.gateway is Inet6Address) { + return true + } } } } @@ -280,27 +273,29 @@ class YggdrasilTunService : VpnService() { return false } - private fun foregroundNotification(text: String): Notification? { - val channelId = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(TAG, "Yggdrasil service") - } else { - // If earlier version channel ID is not used - // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) - "" - } - var intent = Intent(this, MainActivity::class.java) - var stackBuilder = TaskStackBuilder.create(this) - stackBuilder.addNextIntentWithParentStack(intent) - var pi = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) - val b = NotificationCompat.Builder(this, channelId) - b.setOngoing(true) - .setContentIntent(pi) - .setContentTitle(getString(R.string.app_name)) - .setContentText(text) - .setSmallIcon(R.mipmap.ic_launcher) - .setTicker(text) - return b.build() + private fun foregroundNotification(FOREGROUND_ID: Int, text: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(TAG, "Yggdrasil service") + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + var intent = Intent(this, MainActivity::class.java) + var stackBuilder = TaskStackBuilder.create(this) + stackBuilder.addNextIntentWithParentStack(intent) + var pi = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) + val b = NotificationCompat.Builder(this, channelId) + b.setOngoing(true) + .setContentIntent(pi) + .setContentTitle(getString(R.string.app_name)) + .setContentText(text) + .setSmallIcon(R.mipmap.ic_launcher) + .setTicker(text) + startForeground(FOREGROUND_ID, b.build()) + } } @RequiresApi(Build.VERSION_CODES.O) diff --git a/app/src/main/java/io/github/chronosx88/yggdrasil/models/config/NetworkUtils.kt b/app/src/main/java/io/github/chronosx88/yggdrasil/models/config/NetworkUtils.kt new file mode 100644 index 0000000..5239389 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/yggdrasil/models/config/NetworkUtils.kt @@ -0,0 +1,44 @@ +package io.github.chronosx88.yggdrasil.models.config + +import android.content.Context +import android.net.ConnectivityManager + + +class NetworkUtils { + + companion object { + + val TYPE_WIFI = 1 + val TYPE_MOBILE = 2 + val TYPE_NOT_CONNECTED = 0 + val NETWORK_STATUS_NOT_CONNECTED = 0 + val NETWORK_STATUS_WIFI = 1 + val NETWORK_STATUS_MOBILE = 2 + + @JvmStatic + fun getConnectivityStatus(context: Context): Int { + val cm = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = cm.activeNetworkInfo + if (null != activeNetwork) { + if (activeNetwork.type == ConnectivityManager.TYPE_WIFI) return TYPE_WIFI + if (activeNetwork.type == ConnectivityManager.TYPE_MOBILE) return TYPE_MOBILE + } + return TYPE_NOT_CONNECTED + } + + @JvmStatic + fun getConnectivityStatusString(context: Context): Int { + val conn: Int = getConnectivityStatus(context) + var status = 0 + if (conn == TYPE_WIFI) { + status = NETWORK_STATUS_WIFI + } else if (conn == TYPE_MOBILE) { + status = NETWORK_STATUS_MOBILE + } else if (conn == TYPE_NOT_CONNECTED) { + status = NETWORK_STATUS_NOT_CONNECTED + } + return status + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 11d6687..b73ee8e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -27,7 +27,8 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:popupTheme="@style/AppTheme.PopupOverlay" /> + app:popupTheme="@style/AppTheme.PopupOverlay" + android:layout_marginRight="240dp" /> + android:textColor="@color/white" + android:paddingRight="20dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/host_list_item_edit.xml b/app/src/main/res/layout/host_list_item_edit.xml index d8947e4..e6ed5ae 100644 --- a/app/src/main/res/layout/host_list_item_edit.xml +++ b/app/src/main/res/layout/host_list_item_edit.xml @@ -11,7 +11,8 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@+id/hostInfoText" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent" + android:layout_marginLeft="10dp" /> + app:layout_constraintTop_toTopOf="parent" + android:layout_marginLeft="10dp" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d53b11f..764b9fa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,7 +10,7 @@