Merge pull request #10 from vikulin/master

Added more functionality: peers dynamic list sorted by ping, DNS list, Static IP
This commit is contained in:
ChronosX88 2020-07-03 00:05:50 +04:00 committed by GitHub
commit 1997f260c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2388 additions and 206 deletions

1
.idea/gradle.xml generated
View File

@ -12,7 +12,6 @@
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/yggdrasil" />
<option value="$PROJECT_DIR$/yggdrasil.aar" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

2
.idea/misc.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -6,15 +6,15 @@ android {
compileSdkVersion 29
defaultConfig {
applicationId "io.github.chronosx88.yggdrasil"
minSdkVersion 22
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
project.ext.set("archivesBaseName", project.getParent().name+"-"+versionName)
}
signingConfigs {
release {
storeFile file(KEYSTORE_FILE_PATH)
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
@ -29,19 +29,41 @@ android {
}
}
ndkVersion "21.2.6472646"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
task ndkBuild(type: Exec) {
def rootDir = project.rootDir
workingDir = new File(rootDir,"yggdrasil")
commandLine 'make'
}
gradle.projectsEvaluated {
tasks.compileDebugKotlin.dependsOn(ndkBuild)
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(path: ':yggdrasil')
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "androidx.preference:preference-ktx:1.1.1"
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.code.gson:gson:2.8.6'
implementation 'com.hbb20:ccp:2.4.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC2'
implementation 'com.google.android.material:material:1.1.0-alpha09'
implementation 'org.hjson:hjson:3.0.0'
implementation 'com.google.code.gson:gson:2.8.6'
}
}

View File

@ -2,30 +2,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.github.chronosx88.yggdrasil">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".PeerListActivity"
android:label="@string/title_activity_peer_list"
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait"/>
<activity
android:name=".DNSListActivity"
android:label="@string/title_activity_dns_list"
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait"/>
<service
android:name=".YggdrasilTunService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService"/>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<activity android:name=".MainActivity">
<activity android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,3 +0,0 @@
package io.github.chronosx88.yggdrasil
const val YGGDRASIL_VERSION = "0.3.8"

View File

@ -0,0 +1,150 @@
package io.github.chronosx88.yggdrasil
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.*
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import io.github.chronosx88.yggdrasil.models.DNSInfo
import io.github.chronosx88.yggdrasil.models.config.SelectDNSInfoListAdapter
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2DNSInfoSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.ping
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.serializeDNSInfoSet2StringList
import kotlinx.coroutines.*
import java.net.InetAddress
import kotlin.concurrent.thread
class DNSListActivity : AppCompatActivity() {
companion object {
val allDNS = arrayListOf(
DNSInfo(
InetAddress.getByName("[301:2522::53]"),
"CZ",
"DNS implementation for Yggdrasil. https://github.com/Revertron/wyrd"),
DNSInfo(InetAddress.getByName("[301:2923::53]"),
"SK",
"DNS implementation for Yggdrasil. https://github.com/Revertron/wyrd"),
DNSInfo(InetAddress.getByName("[300:4523::53]"),
"DE",
"DNS implementation for Yggdrasil. https://github.com/Revertron/wyrd"),
DNSInfo(InetAddress.getByName("[303:8b1a::53]"),
"RU",
"DNS implementation for Yggdrasil. https://github.com/Revertron/wyrd")
)
}
var isLoading = true;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dns_list)
setSupportActionBar(findViewById(R.id.toolbar))
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
addNewDNS()
}
var extras = intent.extras
var dnsList = findViewById<ListView>(R.id.dnsList)
var adapter = SelectDNSInfoListAdapter(this, arrayListOf(), mutableSetOf())
dnsList.adapter = adapter
thread(start = true) {
try {
var cd = deserializeStringList2DNSInfoSet(
extras!!.getStringArrayList(MainActivity.DNS_LIST)!!
)
for (d in cd) {
var ping = ping(d.address, 53)
d.ping = ping
}
for (dns in allDNS) {
if (cd.contains(dns)) {
continue
}
var ping = ping(dns.address, 53)
dns.ping = ping
runOnUiThread(
Runnable
{
adapter.addItem(dns)
adapter.sort()
isLoading = false
}
)
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
private fun addNewDNS() {
val view: View = LayoutInflater.from(this).inflate(R.layout.new_dns_dialog, null)
val countryCode: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.resources.configuration.locales[0].country
} else {
this.resources.configuration.locale.country
}
var ccp = view.findViewById<com.hbb20.CountryCodePicker>(R.id.ccp)
ccp.setCountryForNameCode(countryCode)
val ab: AlertDialog.Builder = AlertDialog.Builder(this)
ab.setCancelable(true).setView(view)
var ad = ab.show()
var addButton = view.findViewById<Button>(R.id.add)
addButton.setOnClickListener{
var ipInput = view.findViewById<TextView>(R.id.ipInput)
var ccpInput = view.findViewById<com.hbb20.CountryCodePicker>(R.id.ccp)
var ip = ipInput.text.toString().toLowerCase()
var ccp = ccpInput.selectedCountryNameCode
GlobalScope.launch {
var di = DNSInfo(InetAddress.getByName("["+ip+"]"), ccp, "User DNS")
try {
var ping = ping(di.address, 53)
di.ping = ping
} catch(e: Throwable){
di.ping = Int.MAX_VALUE
}
withContext(Dispatchers.Main) {
var selectAdapter = (findViewById<ListView>(R.id.peerList).adapter as SelectDNSInfoListAdapter)
selectAdapter.addItem(0, di)
selectAdapter.notifyDataSetChanged()
ad.dismiss()
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.save, menu)
val item = menu.findItem(R.id.saveItem) as MenuItem
item.setActionView(R.layout.menu_save)
val saveButton = item
.actionView.findViewById<Button>(R.id.saveButton)
saveButton.setOnClickListener {
if(isLoading){
return@setOnClickListener
}
val result = Intent(this, MainActivity::class.java)
var adapter = findViewById<ListView>(R.id.dnsList).adapter as SelectDNSInfoListAdapter
val selectedDNS = adapter.getSelectedDNS()
if(selectedDNS.isNotEmpty()) {
result.putExtra(MainActivity.DNS_LIST, serializeDNSInfoSet2StringList(selectedDNS))
setResult(Activity.RESULT_OK, result)
finish()
} else {
val text = "Select at least one DNS"
val duration = Toast.LENGTH_SHORT
val toast = Toast.makeText(applicationContext, text, duration)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
}
}
return true
}
}

View File

@ -1,78 +1,340 @@
package io.github.chronosx88.yggdrasil
import android.R.attr
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.app.ActivityManager
import android.content.*
import android.net.VpnService
import android.os.Bundle
import android.util.Log
import android.widget.RadioGroup
import android.widget.TextView
import android.view.Gravity
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
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.PeerInfoListAdapter
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializePeerStringList2PeerInfoSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2DNSInfoSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2PeerInfoSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringSet2DNSInfoSet
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
class MainActivity : AppCompatActivity() {
companion object {
const val STATUS_PEERS_UPDATE = 12
const val MESH_PEERS = "MESH_PEERS"
const val STATIC_IP = "STATIC_IP_FLAG"
const val signingPrivateKey = "signingPrivateKey"
const val signingPublicKey = "signingPublicKey"
const val encryptionPrivateKey = "encryptionPrivateKey"
const val encryptionPublicKey = "encryptionPublicKey"
const val COMMAND = "COMMAND"
const val STOP = "STOP"
const val START = "START"
const val UPDATE_DNS = "UPDATE_DNS"
const val UPDATE_PEERS = "UPDATE_PEERS"
const val PARAM_PINTENT = "pendingIntent"
const val STATUS_START = 1
const val STATUS_FINISH = 0
const val STATUS_START = 7
const val STATUS_FINISH = 8
const val STATUS_STOP = 9
const val IPv6: String = "IPv6"
const val PEERS: String = "PEERS"
const val DNS: String = "DNS"
const val PEER_LIST_CODE = 1000
const val DNS_LIST_CODE = 2000
const val PEER_LIST = "PEERS_LIST"
const val DNS_LIST = "DNS_LIST"
const val CURRENT_PEERS = "CURRENT_PEERS_v1.2.1"
const val CURRENT_DNS = "CURRENT_DNS_v1.2"
const val START_VPN = "START_VPN"
private const val TAG="Yggdrasil"
private const val VPN_REQUEST_CODE = 0x0F
@JvmStatic var isStarted = false
@JvmStatic var isCancelled = false
@JvmStatic var address = ""
}
private var currentPeers = setOf<PeerInfo>()
private var currentDNS = setOf<DNSInfo>()
private var meshPeersReceiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val connectRadioGroup = findViewById<RadioGroup>(R.id.connectRadioGroup)
connectRadioGroup.setOnCheckedChangeListener { group, checkedId ->
when (checkedId) {
R.id.disconnectButton -> stopVpn()
R.id.connectButton -> startVpn()
else -> { // Note the block
setSupportActionBar(findViewById(R.id.toolbar))
isStarted = isYggServiceRunning(this)
val switchOn = findViewById<Switch>(R.id.switchOn)
switchOn.isChecked = isStarted
}
switchOn.setOnCheckedChangeListener { _, isChecked ->
if(isCancelled){
switchOn.isChecked = false
isCancelled = false
return@setOnCheckedChangeListener
}
if (isChecked) {
startVpn()
} else {
stopVpn()
}
}
//save to shared preferences
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
val staticIP = findViewById<Switch>(R.id.staticIP)
staticIP.isChecked =
preferences.getString(STATIC_IP, null) != null
val peersListView = findViewById<ListView>(R.id.peers)
currentPeers = deserializeStringSet2PeerInfoSet(preferences.getStringSet(CURRENT_PEERS, HashSet())!!)
val adapter = PeerInfoListAdapter(this, currentPeers.sortedWith(compareBy { it.ping }))
peersListView.adapter = adapter
val copyAddressButton = findViewById<Button>(R.id.copyIp)
copyAddressButton.setOnClickListener {
val ip = findViewById<TextView>(R.id.ip)
val clipboard: ClipboardManager =
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip =
ClipData.newPlainText("IP address", ip.text.toString())
clipboard.setPrimaryClip(clip)
showToast(getString(R.string.address_copied))
}
val editPeersButton = findViewById<Button>(R.id.edit)
editPeersButton.setOnClickListener {
if(isStarted){
showToast("Service is running. Please stop service before edit Peers list")
return@setOnClickListener
}
val intent = Intent(this@MainActivity, PeerListActivity::class.java)
intent.putStringArrayListExtra(PEER_LIST, serializePeerInfoSet2StringList(currentPeers))
startActivityForResult(intent, PEER_LIST_CODE)
}
val listViewDNS = findViewById<ListView>(R.id.dns)
currentDNS = deserializeStringSet2DNSInfoSet(preferences.getStringSet(CURRENT_DNS, HashSet())!!)
val adapterDns = DNSInfoListAdapter(this, currentDNS.sortedWith(compareBy { it.ping }))
listViewDNS.adapter = adapterDns
val editDnsButton = findViewById<Button>(R.id.editDNS)
editDnsButton.setOnClickListener {
if(!isStarted){
showToast("Service is not running. DNS ping will not be run")
return@setOnClickListener
}
val intent = Intent(this@MainActivity, DNSListActivity::class.java)
intent.putStringArrayListExtra(DNS_LIST, serializeDNSInfoSet2StringList(currentDNS))
startActivityForResult(intent, DNS_LIST_CODE)
}
if(isStarted){
val ipLayout = findViewById<LinearLayout>(R.id.ipLayout)
ipLayout.visibility = View.VISIBLE
findViewById<TextView>(R.id.ip).text = address
}
}
fun stopVpn(){
private fun stopVpn(){
Log.d(TAG,"Stop")
val intent = Intent(this, YggdrasilTunService::class.java)
intent.putExtra("COMMAND", "STOP")
val TASK_CODE = 100
val pi = createPendingResult(TASK_CODE, intent, 0)
intent.putExtra(PARAM_PINTENT, pi)
intent.putExtra(COMMAND, STOP)
startService(intent)
}
fun startVpn(){
private fun startVpn(){
Log.d(TAG,"Start")
val intent= VpnService.prepare(this)
if (intent!=null){
startActivityForResult(intent, VPN_REQUEST_CODE);
startActivityForResult(intent, VPN_REQUEST_CODE)
}else{
onActivityResult(VPN_REQUEST_CODE, Activity.RESULT_OK, null);
onActivityResult(VPN_REQUEST_CODE, Activity.RESULT_OK, null)
}
}
private fun updateDNS(){
Log.d(TAG,"Update DNS")
val intent = Intent(this, YggdrasilTunService::class.java)
val TASK_CODE = 100
val pi = createPendingResult(TASK_CODE, intent, 0)
intent.putExtra(PARAM_PINTENT, pi)
intent.putExtra(COMMAND, UPDATE_DNS)
intent.putStringArrayListExtra(DNS, serializeDNSInfoSet2StringList(currentDNS))
startService(intent)
}
private fun updatePeers(){
Log.d(TAG,"Update Peers")
val intent = Intent(this, YggdrasilTunService::class.java)
val TASK_CODE = 100
val pi = createPendingResult(TASK_CODE, intent, 0)
intent.putExtra(PARAM_PINTENT, pi)
intent.putExtra(COMMAND, UPDATE_PEERS)
startService(intent)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == VPN_REQUEST_CODE && resultCode== Activity.RESULT_OK){
val intent = Intent(this, YggdrasilTunService::class.java)
val TASK_CODE = 100
var pi = createPendingResult(TASK_CODE, intent, 0);
intent.putExtra("COMMAND", "START")
val pi = createPendingResult(TASK_CODE, intent, 0)
intent.putExtra(PARAM_PINTENT, pi)
intent.putExtra(COMMAND, START)
intent.putStringArrayListExtra(PEERS, serializePeerInfoSet2StringList(currentPeers))
intent.putStringArrayListExtra(DNS, serializeDNSInfoSet2StringList(currentDNS))
intent.putExtra(STATIC_IP, findViewById<Switch>(R.id.staticIP).isChecked)
startService(intent)
}
if (requestCode == VPN_REQUEST_CODE && resultCode== Activity.RESULT_CANCELED){
isCancelled = true
}
if (requestCode == PEER_LIST_CODE && resultCode== Activity.RESULT_OK){
if(data!!.extras!=null){
var currentPeers = data.extras!!.getStringArrayList(PEER_LIST)
/*WiFi Direct test. need peer empty list*/
this.currentPeers = deserializeStringList2PeerInfoSet(currentPeers)
val adapter = PeerInfoListAdapter(this, this.currentPeers.sortedWith(compareBy { it.ping }))
val listView = findViewById<ListView>(R.id.peers)
listView.adapter = adapter
//save to shared preferences
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
preferences.edit().putStringSet(CURRENT_PEERS, HashSet(currentPeers)).apply()
if(isStarted){
//TODO implement UpdateConfig method in native interface and apply peer changes
stopVpn()
val i = baseContext.packageManager
.getLaunchIntentForPackage(baseContext.packageName)
i!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.putExtra(START_VPN, true)
startActivity(i)
}
}
}
if (requestCode == DNS_LIST_CODE && resultCode== Activity.RESULT_OK){
if(data!!.extras!=null){
var currentDNS = data.extras!!.getStringArrayList(DNS_LIST)
if(currentDNS==null || currentDNS.size==0){
showToast("No DNS selected!")
} else {
this.currentDNS = deserializeStringList2DNSInfoSet(currentDNS)
val adapter = DNSInfoListAdapter(this, this.currentDNS.sortedWith(compareBy { it.ping }))
val listView = findViewById<ListView>(R.id.dns)
listView.adapter = adapter
//save to shared preferences
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
preferences.edit().putStringSet(CURRENT_DNS, HashSet(currentDNS)).apply()
if(isStarted){
updateDNS()
}
}
}
}
when (resultCode) {
STATUS_START -> print("service started")
STATUS_START -> {
print("service started")
if(this.currentPeers.isEmpty()){
//this is Mesh mode, send Peers update every 5 sec
thread(start = true) {
while(true) {
Thread.sleep(5000)
if(isStarted && this.currentPeers.isEmpty()) {
updatePeers()
}
}
}
}
}
STATUS_FINISH -> {
val result: String = data!!.getStringExtra(IPv6)
findViewById<TextView>(R.id.ip).setText(result)
isStarted = true
val ipLayout = findViewById<LinearLayout>(R.id.ipLayout)
ipLayout.visibility = View.VISIBLE
address = data!!.getStringExtra(IPv6)
findViewById<TextView>(R.id.ip).text = address
}
STATUS_STOP -> {
isStarted = false
val ipLayout = findViewById<LinearLayout>(R.id.ipLayout)
ipLayout.visibility = View.GONE
}
STATUS_PEERS_UPDATE ->{
if(data!!.extras!=null) {
thread(start = true) {
val meshPeers = deserializePeerStringList2PeerInfoSet(
data.extras!!.getStringArrayList(MESH_PEERS)
)
val listView = findViewById<ListView>(R.id.peers)
val adapter = PeerInfoListAdapter(
this@MainActivity,
meshPeers.filter { it.schema!="self" }.sortedWith(compareBy { it.ping })
)
runOnUiThread {
listView.adapter = adapter
}
}
}
}
else -> { // Note the block
}
}
}
private fun showToast(text: String){
val duration = Toast.LENGTH_SHORT
val toast = Toast.makeText(applicationContext, text, duration)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
}
//TODO reimplement it
private fun isYggServiceRunning(context: Context): Boolean {
val manager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
if (YggdrasilTunService::class.java.getName() == service.service.className) {
return true
}
}
return false
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
findViewById<Switch>(R.id.staticIP).isChecked =
preferences.getString(STATIC_IP, null) != null
}
override fun onDestroy() {
super.onDestroy()
if (meshPeersReceiver != null){
unregisterReceiver(meshPeersReceiver);
}
}
}

View File

@ -0,0 +1,249 @@
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
import android.view.*
import android.widget.Button
import android.widget.ListView
import android.widget.PopupWindow
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.hbb20.CCPCountry
import io.github.chronosx88.yggdrasil.models.PeerInfo
import io.github.chronosx88.yggdrasil.models.Status
import io.github.chronosx88.yggdrasil.models.config.DropDownAdapter
import io.github.chronosx88.yggdrasil.models.config.SelectPeerInfoListAdapter
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2PeerInfoSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.ping
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.io.ByteArrayOutputStream
import java.lang.reflect.Type
import java.net.InetAddress
import java.net.URI
import java.net.URL
import java.nio.charset.Charset
class PeerListActivity : AppCompatActivity() {
companion object {
const val PEER_LIST_URL = "https://publicpeers.neilalexander.dev/publicnodes.json"
}
fun downloadJson(link: String): String {
URL(link).openStream().use { input ->
var outStream = ByteArrayOutputStream()
outStream.use { output ->
input.copyTo(output)
}
return String(outStream.toByteArray(), Charset.forName("UTF-8"))
}
}
var isLoading = true;
var popup: PopupWindow? = null
var adapter: DropDownAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_peer_list)
setSupportActionBar(findViewById(R.id.toolbar))
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
addNewPeer()
}
var extras = intent.extras
var peerList = findViewById<ListView>(R.id.peerList)
var adapter = SelectPeerInfoListAdapter(this, arrayListOf(), mutableSetOf())
peerList.adapter = adapter
GlobalScope.launch {
try {
var cp = deserializeStringList2PeerInfoSet(
extras!!.getStringArrayList(MainActivity.PEER_LIST)!!
)
for(pi in cp){
var ping = ping(pi.address, pi.port)
pi.ping = ping
}
var json = downloadJson(PEER_LIST_URL)
var countries = CCPCountry.getLibraryMasterCountriesEnglish()
val mapType: Type = object :
TypeToken<Map<String?, Map<String, Status>>>() {}.type
val peersMap: Map<String, Map<String, Status>> = Gson().fromJson(json, mapType)
for ((country, peers) in peersMap.entries) {
for ((peer, status) in peers) {
if (status.up) {
for (ccp in countries) {
if (ccp.name.toLowerCase()
.contains(country.replace(".md", "").replace("-", " "))
) {
var url = URI(peer)
try {
var address = InetAddress.getByName(url.host)
var peerInfo =
PeerInfo(url.scheme, address, url.port, ccp.nameCode)
if(cp.contains(peerInfo)){
continue
}
var ping = ping(address, url.port)
peerInfo.ping = ping
withContext(Dispatchers.Main) {
adapter.addItem(peerInfo)
if(adapter.count % 5 == 0) {
adapter.sort()
}
}
} catch (e: Throwable){
e.printStackTrace()
}
}
}
}
}
}
var currentPeers = ArrayList(cp.sortedWith(compareBy { it.ping }))
withContext(Dispatchers.Main) {
adapter.addAll(0, currentPeers)
isLoading = false
adapter.setLoading(isLoading)
}
} catch (e: Throwable){
e.printStackTrace()
}
}
}
private fun addNewPeer() {
val view: View = LayoutInflater.from(this).inflate(R.layout.new_peer_dialog, null)
val countryCode: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.resources.configuration.locales[0].country
} else {
this.resources.configuration.locale.country
}
var schemaInput = view.findViewById<TextView>(R.id.schemaInput)
var ipInput = view.findViewById<TextView>(R.id.ipInput)
ipInput.requestFocus()
schemaInput.showSoftInputOnFocus = false
schemaInput.setOnFocusChangeListener { v, b ->
if(schemaInput.isFocused) {
onClickSchemaList(v)
}
}
schemaInput.setOnClickListener { v->
onClickSchemaList(v)
}
getPopupWindow(R.layout.spinner_item, resources.getStringArray(R.array.schemas), schemaInput, getString(R.string.schema));
var ccp = view.findViewById<com.hbb20.CountryCodePicker>(R.id.ccp)
ccp.setCountryForNameCode(countryCode)
val ab: AlertDialog.Builder = AlertDialog.Builder(this)
ab.setCancelable(true).setView(view)
var ad = ab.show()
var addButton = view.findViewById<Button>(R.id.add)
addButton.setOnClickListener{
var portInput = view.findViewById<TextView>(R.id.portInput)
var ccpInput = view.findViewById<com.hbb20.CountryCodePicker>(R.id.ccp)
var schema = schemaInput.text.toString().toLowerCase()
var ip = ipInput.text.toString().toLowerCase()
var port = portInput.text.toString().toInt()
var ccp = ccpInput.selectedCountryNameCode
GlobalScope.launch {
var pi = PeerInfo(schema, InetAddress.getByName(ip), port, ccp)
try {
var ping = ping(pi.address, pi.port)
pi.ping = ping
} catch(e: Throwable){
pi.ping = Int.MAX_VALUE
}
withContext(Dispatchers.Main) {
var selectAdapter = (findViewById<ListView>(R.id.peerList).adapter as SelectPeerInfoListAdapter)
selectAdapter.addItem(0, pi)
selectAdapter.notifyDataSetChanged()
ad.dismiss()
}
}
}
}
fun onClickSchemaList(v: View) {
val height = -1 * v.height +30
getAddressListPopup()?.showAsDropDown(v, -5, height)
}
private fun getAddressListPopup(): PopupWindow? {
return popup
}
private fun getPopupWindow(
textViewResourceId: Int,
objects: Array<String>,
editText: TextView,
hint: String?
): PopupWindow? {
// initialize a pop up window type
val popupWindow = PopupWindow(this)
// the drop down list is a list view
val listView = ListView(this)
listView.dividerHeight = 0
// set our adapter and pass our pop up window contents
adapter = DropDownAdapter(this, textViewResourceId, objects, popupWindow, editText)
listView.adapter = adapter
// set the item click listener
listView.onItemClickListener = adapter
// some other visual settings
popupWindow.isFocusable = true
//popupWindow.setWidth(400);
val display: Display =
(this.getSystemService(Context.WINDOW_SERVICE) as WindowManager).getDefaultDisplay()
popupWindow.width = 320
popupWindow.height = WindowManager.LayoutParams.WRAP_CONTENT
// set the list view as pop up window content
popupWindow.contentView = listView
popup = popupWindow
return popupWindow
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.save, menu)
val item = menu.findItem(R.id.saveItem) as MenuItem
item.setActionView(R.layout.menu_save)
val saveButton = item
.actionView.findViewById<Button>(R.id.saveButton)
saveButton.setOnClickListener {
if(isLoading){
return@setOnClickListener
}
val result = Intent(this, MainActivity::class.java)
var adapter = findViewById<ListView>(R.id.peerList).adapter as SelectPeerInfoListAdapter
val selectedPeers = adapter.getSelectedPeers()
/* WiFi Direct test - no peers is needed
if(selectedPeers.size>0) {
result.putExtra(MainActivity.PEER_LIST, serializePeerInfoSet2StringList(selectedPeers))
setResult(Activity.RESULT_OK, result)
finish()
} else {
val text = "Select at least one peer"
val duration = Toast.LENGTH_SHORT
val toast = Toast.makeText(applicationContext, text, duration)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
}*/
result.putExtra(MainActivity.PEER_LIST, serializePeerInfoSet2StringList(selectedPeers))
setResult(Activity.RESULT_OK, result)
finish()
}
return true
}
}

View File

@ -1,15 +1,10 @@
package io.github.chronosx88.yggdrasil
import android.content.Context
import android.os.Build.CPU_ABI
import android.util.Log
import com.google.gson.Gson
import config.NodeConfig
import io.github.chronosx88.yggdrasil.models.config.Config
import org.hjson.JsonValue
import org.hjson.Stringify
import java.io.File
import java.lang.Runtime.getRuntime
val gson = Gson()

View File

@ -1,107 +1,207 @@
package io.github.chronosx88.yggdrasil
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.OsConstants
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dummy.ConduitEndpoint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import io.github.chronosx88.yggdrasil.models.DNSInfo
import io.github.chronosx88.yggdrasil.models.PeerInfo
import io.github.chronosx88.yggdrasil.models.config.Peer
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.convertPeer2PeerStringList
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.convertPeerInfoSet2PeerIdSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2DNSInfoSet
import io.github.chronosx88.yggdrasil.models.config.Utils.Companion.deserializeStringList2PeerInfoSet
import kotlinx.coroutines.*
import mobile.Mobile
import mobile.Yggdrasil
import java.io.*
import java.nio.ByteBuffer
import kotlin.coroutines.CoroutineContext
import java.net.Inet6Address
class YggdrasilTunService : VpnService() {
/** Maximum packet size is constrained by the MTU, which is given as a signed short. */
private val MAX_PACKET_SIZE = Short.MAX_VALUE.toInt()
private lateinit var ygg: Yggdrasil
private var isClosed = false
/** Maximum packet size is constrained by the MTU, which is given as a signed short - 256 */
private val MAX_PACKET_SIZE = 65535
companion object {
private const val TAG = "Yggdrasil-service"
}
private var tunInterface: ParcelFileDescriptor? = null
private lateinit var yggConduitEndpoint: ConduitEndpoint
private var tunInputStream: InputStream? = null
private var tunOutputStream: OutputStream? = null
private lateinit var readCoroutine: CoroutineContext
private lateinit var writeCoroutine: CoroutineContext
private var scope: CoroutineScope? = null
private var address: String? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.getStringExtra("COMMAND") == "STOP") {
stopVpn()
}
if (intent?.getStringExtra("COMMAND") == "START") {
val pi: PendingIntent = intent.getParcelableExtra(MainActivity.PARAM_PINTENT)
setupTunInterface(pi)
}
private var mNotificationManager: NotificationManager? = null
return super.onStartCommand(intent, flags, startId);
private val FOREGROUND_ID = 1338
override fun onCreate() {
super.onCreate()
mNotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
private fun setupTunInterface(pi: PendingIntent) {
pi.send(MainActivity.STATUS_START);
val builder = Builder()
val ygg = Yggdrasil()
var configJson = Mobile.generateConfigJSON()
val gson = Gson()
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"))
}
MainActivity.START ->{
val peers = deserializeStringList2PeerInfoSet(intent.getStringArrayListExtra(MainActivity.PEERS))
val dns = deserializeStringList2DNSInfoSet(intent.getStringArrayListExtra(MainActivity.DNS))
val staticIP: Boolean = intent.getBooleanExtra(MainActivity.STATIC_IP, false)
ygg = Yggdrasil()
setupTunInterface(pi, peers, dns, staticIP)
startForeground(FOREGROUND_ID, foregroundNotification("Yggdrasil service started"))
}
MainActivity.UPDATE_DNS ->{
val dns = deserializeStringList2DNSInfoSet(intent.getStringArrayListExtra(MainActivity.DNS))
setupIOStreams(dns)
}
MainActivity.UPDATE_PEERS ->{
sendMeshPeerStatus(pi)
}
}
var config = gson.fromJson(String(configJson), Map::class.java).toMutableMap()
config = fixConfig(config)
configJson = gson.toJson(config).toByteArray()
return START_NOT_STICKY
}
yggConduitEndpoint = ygg.startJSON(configJson)
val address = ygg.addressString // hack for getting generic ipv6 string from NodeID
private fun setupIOStreams(dns: MutableSet<DNSInfo>){
address = ygg.addressString
tunInterface = builder
var builder = Builder()
.addAddress(address, 7)
.allowFamily(OsConstants.AF_INET)
.setMtu(MAX_PACKET_SIZE)
.establish()
tunInputStream = FileInputStream(tunInterface!!.fileDescriptor)
tunOutputStream = FileOutputStream(tunInterface!!.fileDescriptor)
readCoroutine = GlobalScope.launch {
var buffer = ByteArray(2048)
while (true) {
try{
readPacketsFromTun(buffer)
} catch (e: IOException){
e.printStackTrace()
}
if (dns.size > 0) {
builder.addDnsServer(address)
for (d in dns) {
builder.addDnsServer(d.address)
}
}
writeCoroutine = GlobalScope.launch {
while (true) {
writePacketsToTun()
/*
fix for DNS unavailability
*/
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(
pi: PendingIntent?,
peers: Set<PeerInfo>,
dns: MutableSet<DNSInfo>,
staticIP: Boolean
) {
pi!!.send(MainActivity.STATUS_START)
var configJson = Mobile.generateConfigJSON()
val gson = Gson()
var config = gson.fromJson(String(configJson), Map::class.java).toMutableMap()
config = fixConfig(config, peers, staticIP)
configJson = gson.toJson(config).toByteArray()
var yggConduitEndpoint = ygg.startJSON(configJson)
setupIOStreams(dns)
val job = SupervisorJob()
scope = CoroutineScope(Dispatchers.Default + job)
scope!!.launch {
val buffer = ByteArray(2048)
while (!isClosed) {
readPacketsFromTun(yggConduitEndpoint, buffer)
}
}
scope!!.launch {
while (!isClosed) {
writePacketsToTun(yggConduitEndpoint)
}
}
val intent: Intent = Intent().putExtra(MainActivity.IPv6, address)
pi.send(this, MainActivity.STATUS_FINISH, intent)
}
private fun fixConfig(config: MutableMap<Any?, Any?>): MutableMap<Any?, Any?> {
val peers = arrayListOf<String>();
peers.add("tcp://194.177.21.156:5066")
peers.add("tcp://46.151.26.194:60575")
peers.add("tcp://188.226.125.64:54321")
private fun sendMeshPeerStatus(pi: PendingIntent?){
class Token : TypeToken<List<Peer>>()
var add = ygg.addressString
var meshPeers: List<Peer> = gson.fromJson(ygg.peersJSON, Token().type)
val intent: Intent = Intent().putStringArrayListExtra(
MainActivity.MESH_PEERS,
convertPeer2PeerStringList(meshPeers)
);
pi?.send(this, MainActivity.STATUS_PEERS_UPDATE, intent)
}
private fun fixConfig(
config: MutableMap<Any?, Any?>,
peers: Set<PeerInfo>,
staticIP: Boolean
): MutableMap<Any?, Any?> {
val whiteList = arrayListOf<String>()
whiteList.add("")
val blackList = arrayListOf<String>()
blackList.add("")
config["Peers"] = peers
config["Peers"] = convertPeerInfoSet2PeerIdSet(peers)
config["Listen"] = ""
config["AdminListen"] = "tcp://localhost:9001"
config["IfName"] = "tun0"
//config["EncryptionPublicKey"] = "b15633cf66e63a04f03e9d1a5b2ac6411af819cde9e74175cf574d5599b1296c"
//config["EncryptionPrivateKey"] = "a39e2da3ccbb5afc3854574a2e3823e881d2d720754d6fdc877f57b252d3b521"
//config["SigningPublicKey"] = "4f248483c094aea370fba86f1630ba5099cb230aa1337ab6ef6ff0b132be2c2b"
//config["SigningPrivateKey"] = "e4d56eb2e15e25d9098731e39d661a80c523f31d38b71cbd0ad25a5cde745eac4f248483c094aea370fba86f1630ba5099cb230aa1337ab6ef6ff0b132be2c2b"
if(staticIP) {
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
if(preferences.getString(MainActivity.STATIC_IP, null)==null) {
val encryptionPublicKey = config["EncryptionPublicKey"].toString()
val encryptionPrivateKey = config["EncryptionPrivateKey"].toString()
val signingPublicKey = config["SigningPublicKey"].toString()
val signingPrivateKey = config["SigningPrivateKey"].toString()
preferences.edit()
.putString(MainActivity.signingPrivateKey, signingPrivateKey)
.putString(MainActivity.signingPublicKey, signingPublicKey)
.putString(MainActivity.encryptionPrivateKey, encryptionPrivateKey)
.putString(MainActivity.encryptionPublicKey, encryptionPublicKey)
.putString(MainActivity.STATIC_IP,MainActivity.STATIC_IP).apply()
} else {
val signingPrivateKey = preferences.getString(MainActivity.signingPrivateKey, null)
val signingPublicKey = preferences.getString(MainActivity.signingPublicKey, null)
val encryptionPrivateKey = preferences.getString(MainActivity.encryptionPrivateKey, null)
val encryptionPublicKey = preferences.getString(MainActivity.encryptionPublicKey, null)
config["SigningPrivateKey"] = signingPrivateKey
config["SigningPublicKey"] = signingPublicKey
config["EncryptionPrivateKey"] = encryptionPrivateKey
config["EncryptionPublicKey"] = encryptionPublicKey
}
}
(config["SessionFirewall"] as MutableMap<Any, Any>)["Enable"] = false
//(config["SessionFirewall"] as MutableMap<Any, Any>)["AllowFromDirect"] = true
//(config["SessionFirewall"] as MutableMap<Any, Any>)["AllowFromRemote"] = true
@ -119,37 +219,94 @@ class YggdrasilTunService : VpnService() {
return config
}
private fun readPacketsFromTun(buffer: ByteArray) {
if(tunInputStream != null) {
private fun readPacketsFromTun(yggConduitEndpoint: ConduitEndpoint, buffer: ByteArray) {
try {
// Read the outgoing packet from the input stream.
var length = tunInputStream!!.read(buffer)
if (length > 0) {
var byteBuffer = ByteBuffer.allocate(length);
byteBuffer.put(buffer, 0, length)
yggConduitEndpoint.send(byteBuffer.array())
val length = tunInputStream?.read(buffer) ?: 1
if (length > 0){
yggConduitEndpoint.send(buffer.sliceArray(IntRange(0, length - 1)))
} else {
Thread.sleep(10)
Thread.sleep(100)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun writePacketsToTun(yggConduitEndpoint: ConduitEndpoint) {
val buffer = yggConduitEndpoint.recv()
if(buffer!=null) {
try {
tunOutputStream?.write(buffer)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun writePacketsToTun() {
if(tunOutputStream != null) {
val buffer = yggConduitEndpoint.recv()
tunOutputStream!!.write(buffer)
}
}
fun stopVpn() {
readCoroutine.cancel()
writeCoroutine.cancel()
private fun stopVpn(pi: PendingIntent?) {
isClosed = true;
tunInputStream!!.close()
tunOutputStream!!.close()
tunInterface!!.close()
tunInterface = null
scope!!.coroutineContext.cancelChildren()
Log.d(TAG,"Stop is running from service")
ygg.stop()
val intent: Intent = Intent()
pi!!.send(this, MainActivity.STATUS_STOP, intent)
stopForeground(true)
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
stopForeground(true)
stopSelf()
}
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)
val routes = linkProperties.routes
for (route in routes) {
if (route.isDefaultRoute && route.gateway is Inet6Address) {
return true
}
}
}
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)
""
}
val b = NotificationCompat.Builder(this, channelId)
b.setOngoing(true)
.setContentTitle(getString(R.string.app_name))
.setContentText(text)
.setSmallIcon(R.mipmap.ic_launcher)
.setTicker(text)
return b.build()
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_NONE)
chan.lightColor = getColor(R.color.dark_10)
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
}

View File

@ -0,0 +1,34 @@
package io.github.chronosx88.yggdrasil.models
import android.content.Context
import com.hbb20.CCPCountry
import com.hbb20.CountryCodePicker
import java.net.InetAddress
class DNSInfo {
constructor(address: InetAddress, countryCode: String, description: String){
this.address = address
this.countryCode = countryCode
this.description = description
}
var address: InetAddress
var countryCode: String
var description: String
var ping: Int = Int.MAX_VALUE
override fun toString(): String {
return "[" + address.toString().substring(1) + "]"
}
override fun equals(other: Any?): Boolean {
return toString() == other.toString()
}
fun getCountry(context: Context): CCPCountry? {
return CCPCountry.getCountryForNameCodeFromLibraryMasterList(context, CountryCodePicker.Language.ENGLISH, countryCode)
}
}

View File

@ -0,0 +1,68 @@
package io.github.chronosx88.yggdrasil.models
import android.content.Context
import com.hbb20.CCPCountry
import com.hbb20.CountryCodePicker
import java.net.InetAddress
class PeerInfo {
constructor(schema: String, address: InetAddress, port: Int, countryCode: String){
this.schema = schema
this.address = address
var a = address.toString();
if(a.lastIndexOf('/')>0){
this.hostName = a.split("/")[0]
} else {
this.hostName = a.substring(1)
}
this.port = port
this.countryCode = countryCode
}
constructor(schema: String, address: InetAddress, port: Int, countryCode: String?, isMeshPeer: Boolean){
this.schema = schema
this.address = address
var a = address.toString();
if(a.lastIndexOf('/')>0){
this.hostName = a.split("/")[0]
} else {
this.hostName = a.substring(1)
}
this.port = port
this.countryCode = countryCode
this.isMeshPeer = isMeshPeer
}
var schema: String
var address: InetAddress
var hostName: String
var port = 0
var countryCode: String?=null
var ping: Int = Int.MAX_VALUE
var isMeshPeer = false
override fun toString(): String {
if(this.hostName.contains(":")) {
return this.schema + "://[" + this.hostName + "]:" + port
} else {
return this.schema + "://" + this.hostName + ":" + port
}
}
override fun equals(other: Any?): Boolean {
return toString() == other.toString()
}
fun getCountry(context: Context): CCPCountry? {
if(countryCode==null){
return null
} else {
return CCPCountry.getCountryForNameCodeFromLibraryMasterList(
context,
CountryCodePicker.Language.ENGLISH,
countryCode
)
}
}
}

View File

@ -0,0 +1,5 @@
package io.github.chronosx88.yggdrasil.models
class Status {
var up: Boolean = false
}

View File

@ -0,0 +1,44 @@
package io.github.chronosx88.yggdrasil.models.config
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import io.github.chronosx88.yggdrasil.R
import io.github.chronosx88.yggdrasil.models.DNSInfo
class DNSInfoListAdapter(
context: Context,
allDNS: List<DNSInfo>
) : ArrayAdapter<DNSInfo?> (context, 0, allDNS) {
private val mContext: Context = context
private var allDNS: List<DNSInfo> = allDNS
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var dnsInfoHolder = DNSInfoHolder()
var listItem: View? = convertView
if (listItem == null) {
listItem = LayoutInflater.from(mContext).inflate(R.layout.host_list_item, parent, false)
dnsInfoHolder.countryFlag = listItem.findViewById(R.id.countryFlag) as ImageView
dnsInfoHolder.dnsInfoText = listItem.findViewById(R.id.hostInfoText) as TextView
listItem.tag = dnsInfoHolder
} else {
dnsInfoHolder = listItem.tag as DNSInfoHolder
}
val currentDNS = allDNS[position]
dnsInfoHolder.countryFlag.setImageResource(currentDNS.getCountry(mContext)!!.flagID)
dnsInfoHolder.dnsInfoText.text = currentDNS.toString()
return listItem!!
}
class DNSInfoHolder {
lateinit var countryFlag: ImageView
lateinit var dnsInfoText: TextView
}
}

View File

@ -0,0 +1,69 @@
package io.github.chronosx88.yggdrasil.models.config
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import android.widget.ArrayAdapter
import android.widget.PopupWindow
import android.widget.TextView
import io.github.chronosx88.yggdrasil.R
class DropDownAdapter(
context: Context,
textViewResourceId: Int,
objects: Array<String>,
popup: PopupWindow,
editText: TextView
) :
ArrayAdapter<String?>(context, textViewResourceId, objects), OnItemClickListener {
private val objects: Array<String>
private val popup: PopupWindow
private val editText: TextView
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View? {
return getCustomView(position, convertView, parent)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return getCustomView(position, convertView, parent)
}
fun getCustomView(position: Int, convertView: View?, parent: ViewGroup?): View {
var convertView: View? = convertView
if (convertView == null) {
convertView =
LayoutInflater.from(context).inflate(R.layout.dropdown_item, parent, false)
}
val sub = convertView?.findViewById(R.id.sub) as TextView
val address = objects[position]
sub.text = address
return convertView!!
}
override fun onItemClick(arg0: AdapterView<*>?, v: View, arg2: Int, arg3: Long) {
// get the context and main activity to access variables
// add some animation when a list item was clicked
val fadeInAnimation: Animation =
AnimationUtils.loadAnimation(context, android.R.anim.fade_in)
fadeInAnimation.duration = 10
v.startAnimation(fadeInAnimation)
val text: View = v.findViewById(R.id.sub) ?: return
val address = (text as TextView).text.toString()
// dismiss the pop up
popup.dismiss()
editText.text = address
}
init {
this.objects = objects
this.popup = popup
this.editText = editText
}
}

View File

@ -0,0 +1,13 @@
package io.github.chronosx88.yggdrasil.models.config
import com.google.gson.annotations.SerializedName
data class Peer (
//Example [{"PublicKey":[154,201,118,156,19,74,134,115,94,159,76,86,36,192,221,105,220,254,226,161,108,226,17,192,75,243,225,15,42,195,155,2],"Endpoint":"(self)","BytesSent":0,"BytesRecvd":0,"Protocol":"self","Port":0,"Uptime":209900460}]
@SerializedName("Endpoint") var endpoint : String,
@SerializedName("Port") var port : Int,
@SerializedName("Uptime") var uptime : Long,
@SerializedName("Protocol") var protocol : String,
@SerializedName("BytesSent") var bytesSent : Long,
@SerializedName("BytesRecvd") var bytesReceived : Long
)

View File

@ -0,0 +1,48 @@
package io.github.chronosx88.yggdrasil.models.config
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import io.github.chronosx88.yggdrasil.R
import io.github.chronosx88.yggdrasil.models.PeerInfo
class PeerInfoListAdapter(
context: Context,
allPeers: List<PeerInfo>
) : ArrayAdapter<PeerInfo?> (context, 0, allPeers) {
private val mContext: Context = context
private var allPeers: List<PeerInfo> = allPeers
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var peerInfoHolder = PeerInfoHolder()
var listItem: View? = convertView
if (listItem == null) {
listItem = LayoutInflater.from(mContext).inflate(R.layout.host_list_item, parent, false)
peerInfoHolder.countryFlag = listItem.findViewById(R.id.countryFlag) as ImageView
peerInfoHolder.peerInfoText = listItem.findViewById(R.id.hostInfoText) as TextView
listItem.tag = peerInfoHolder
} else {
peerInfoHolder = listItem.tag as PeerInfoHolder
}
val currentPeer = allPeers[position]
if(currentPeer.isMeshPeer){
//TODO set mesh icon
} else {
peerInfoHolder.countryFlag.setImageResource(currentPeer.getCountry(mContext)!!.flagID)
}
peerInfoHolder.peerInfoText.text = currentPeer.toString()
return listItem!!
}
class PeerInfoHolder {
lateinit var countryFlag: ImageView
lateinit var peerInfoText: TextView
}
}

View File

@ -0,0 +1,96 @@
package io.github.chronosx88.yggdrasil.models.config
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.TextView
import io.github.chronosx88.yggdrasil.R
import io.github.chronosx88.yggdrasil.models.DNSInfo
class SelectDNSInfoListAdapter(
context: Context,
allDNS: List<DNSInfo>,
currentDNS: MutableSet<DNSInfo>
) : ArrayAdapter<DNSInfo?> (context, 0, allDNS) {
private val mContext: Context = context
private var allDNS: MutableList<DNSInfo> = allDNS as MutableList<DNSInfo>
private var currentDNS: MutableSet<DNSInfo> = currentDNS
override fun getItem(position: Int): DNSInfo? {
return allDNS[position]
}
override fun getCount(): Int {
return allDNS.size
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var dnsInfoHolder = DNSInfoHolder()
var listItem: View? = convertView
if (listItem == null) {
listItem = LayoutInflater.from(mContext).inflate(R.layout.host_list_item_edit, parent, false)
dnsInfoHolder.checkbox = listItem.findViewById(R.id.checkbox) as CheckBox
dnsInfoHolder.countryFlag = listItem.findViewById(R.id.countryFlag) as ImageView
dnsInfoHolder.dnsInfoText = listItem.findViewById(R.id.hostInfoText) as TextView
dnsInfoHolder.ping = listItem.findViewById(R.id.ping) as TextView
listItem.tag = dnsInfoHolder
} else {
dnsInfoHolder = listItem.tag as DNSInfoHolder
}
val currentDNS = allDNS[position]
dnsInfoHolder.countryFlag.setImageResource(currentDNS.getCountry(mContext)!!.flagID)
val dnsId = currentDNS.toString()
if(currentDNS.ping == Int.MAX_VALUE){
dnsInfoHolder.dnsInfoText.text = dnsId
dnsInfoHolder.ping.text=""
dnsInfoHolder.dnsInfoText.setTextColor(Color.GRAY)
} else {
dnsInfoHolder.dnsInfoText.text = dnsId
dnsInfoHolder.ping.text = currentDNS.ping.toString() + " ms"
dnsInfoHolder.dnsInfoText.setTextColor(Color.WHITE)
}
dnsInfoHolder.checkbox.setOnCheckedChangeListener { _, isChecked ->
if(isChecked){
if(!this.currentDNS.contains(currentDNS)){
this.currentDNS.add(currentDNS)
}
} else {
if(this.currentDNS.contains(currentDNS)){
this.currentDNS.remove(currentDNS)
}
}
}
dnsInfoHolder.checkbox.isChecked = this.currentDNS.contains(currentDNS)
return listItem!!
}
fun getSelectedDNS(): Set<DNSInfo> {
return currentDNS
}
fun addItem(peerInfo: DNSInfo){
allDNS.add(peerInfo)
}
fun addItem(index: Int, peerInfo: DNSInfo){
allDNS.add(index, peerInfo)
}
fun sort(){
allDNS = ArrayList(allDNS.sortedWith(compareBy { it.ping }))
this.notifyDataSetChanged()
}
class DNSInfoHolder {
lateinit var checkbox: CheckBox
lateinit var countryFlag: ImageView
lateinit var dnsInfoText: TextView
lateinit var ping: TextView
}
}

View File

@ -0,0 +1,111 @@
package io.github.chronosx88.yggdrasil.models.config
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.TextView
import io.github.chronosx88.yggdrasil.R
import io.github.chronosx88.yggdrasil.models.PeerInfo
class SelectPeerInfoListAdapter(
context: Context,
allPeers: List<PeerInfo>,
currentPeers: MutableSet<PeerInfo>
) : ArrayAdapter<PeerInfo?> (context, 0, allPeers) {
private var isLoading = true
private val mContext: Context = context
private var allPeers: MutableList<PeerInfo> = allPeers as MutableList<PeerInfo>
private var currentPeers: MutableSet<PeerInfo> = currentPeers
override fun getItem(position: Int): PeerInfo? {
return allPeers.get(position)
}
override fun getCount(): Int {
return allPeers.size
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var peerInfoHolder = PeerInfoHolder()
var listItem: View? = convertView
if (listItem == null) {
listItem = LayoutInflater.from(mContext).inflate(R.layout.host_list_item_edit, parent, false)
peerInfoHolder.checkbox = listItem.findViewById(R.id.checkbox) as CheckBox
peerInfoHolder.countryFlag = listItem.findViewById(R.id.countryFlag) as ImageView
peerInfoHolder.peerInfoText = listItem.findViewById(R.id.hostInfoText) as TextView
peerInfoHolder.ping = listItem.findViewById(R.id.ping) as TextView
listItem.tag = peerInfoHolder
} else {
peerInfoHolder = listItem.tag as PeerInfoHolder
}
val currentPeer = allPeers[position]
peerInfoHolder.countryFlag.setImageResource(currentPeer.getCountry(mContext)!!.flagID)
val peerId = currentPeer.toString()
if(currentPeer.ping == Int.MAX_VALUE){
peerInfoHolder.peerInfoText.text = peerId
peerInfoHolder.ping.text=""
peerInfoHolder.peerInfoText.setTextColor(Color.GRAY)
} else {
peerInfoHolder.peerInfoText.text = peerId
peerInfoHolder.ping.text = currentPeer.ping.toString() + " ms"
peerInfoHolder.peerInfoText.setTextColor(Color.WHITE)
}
peerInfoHolder.checkbox.setOnCheckedChangeListener { _, isChecked ->
if(!isLoading) {
if (isChecked) {
if (!currentPeers.contains(currentPeer)) {
currentPeers.add(currentPeer)
}
} else {
if (currentPeers.contains(currentPeer)) {
currentPeers.remove(currentPeer)
}
}
}
}
peerInfoHolder.checkbox.isChecked = this.currentPeers.contains(currentPeer)
return listItem!!
}
fun getSelectedPeers(): Set<PeerInfo> {
return currentPeers
}
fun addItem(peerInfo: PeerInfo){
allPeers.add(peerInfo)
}
fun addItem(index: Int, peerInfo: PeerInfo){
allPeers.add(index, peerInfo)
}
fun addAll(index: Int, peerInfo: ArrayList<PeerInfo>){
currentPeers.addAll(peerInfo)
allPeers.removeAll(peerInfo)
allPeers.addAll(index, peerInfo)
this.notifyDataSetChanged()
}
fun sort(){
allPeers = ArrayList(allPeers.sortedWith(compareBy { it.ping }))
this.notifyDataSetChanged()
}
fun setLoading(loading: Boolean){
this.isLoading = loading
}
class PeerInfoHolder {
lateinit var checkbox: CheckBox
lateinit var countryFlag: ImageView
lateinit var peerInfoText: TextView
lateinit var ping: TextView
}
}

View File

@ -0,0 +1,141 @@
package io.github.chronosx88.yggdrasil.models.config
import com.google.gson.Gson
import io.github.chronosx88.yggdrasil.models.DNSInfo
import io.github.chronosx88.yggdrasil.models.PeerInfo
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.net.URI
class Utils {
companion object {
@JvmStatic
fun deserializeStringList2PeerInfoSet(list: List<String>?): MutableSet<PeerInfo> {
var gson = Gson()
var out = mutableSetOf<PeerInfo>()
if (list != null) {
for(s in list) {
out.add(gson.fromJson(s, PeerInfo::class.java))
}
}
return out
}
@JvmStatic
fun deserializeStringList2DNSInfoSet(list: List<String>?): MutableSet<DNSInfo> {
var gson = Gson()
var out = mutableSetOf<DNSInfo>()
if (list != null) {
for(s in list) {
out.add(gson.fromJson(s, DNSInfo::class.java))
}
}
return out
}
@JvmStatic
fun deserializeStringSet2PeerInfoSet(list: Set<String>): MutableSet<PeerInfo> {
var gson = Gson()
var out = mutableSetOf<PeerInfo>()
for(s in list) {
out.add(gson.fromJson(s, PeerInfo::class.java))
}
return out
}
@JvmStatic
fun deserializeStringSet2DNSInfoSet(list: Set<String>): MutableSet<DNSInfo> {
var gson = Gson()
var out = mutableSetOf<DNSInfo>()
for(s in list) {
out.add(gson.fromJson(s, DNSInfo::class.java))
}
return out
}
@JvmStatic
fun serializePeerInfoSet2StringList(list: Set<PeerInfo>): ArrayList<String> {
var gson = Gson()
var out = ArrayList<String>()
for(p in list) {
out.add(gson.toJson(p))
}
return out
}
@JvmStatic
fun serializeDNSInfoSet2StringList(list: Set<DNSInfo>): ArrayList<String> {
var gson = Gson()
var out = ArrayList<String>()
for(p in list) {
out.add(gson.toJson(p))
}
return out
}
@JvmStatic
fun ping(address: InetAddress, port:Int): Int {
val start = System.currentTimeMillis()
val socket = Socket()
try {
socket.connect(InetSocketAddress(address, port), 5000)
socket.close()
} catch (e: Exception) {
e.printStackTrace()
print(address)
return Int.MAX_VALUE
}
return (System.currentTimeMillis() - start).toInt()
}
@JvmStatic
fun convertPeerInfoSet2PeerIdSet(list: Set<PeerInfo>): Set<String> {
var out = mutableSetOf<String>()
for(p in list) {
out.add(p.toString())
}
return out
}
@JvmStatic
fun convertPeer2PeerStringList(list: List<Peer>): ArrayList<String> {
var out = ArrayList<String>()
var gson = Gson()
for(p in list) {
out.add(gson.toJson(p))
}
return out
}
@JvmStatic
fun deserializePeerStringList2PeerInfoSet(list: List<String>?): MutableSet<PeerInfo> {
var gson = Gson()
var out = mutableSetOf<PeerInfo>()
if (list != null) {
for(s in list) {
var p = gson.fromJson(s, Peer::class.java)
if(p.endpoint == "(self)"){
out.add(PeerInfo(p.protocol, InetAddress.getByName("localhost"), p.port, null, true))
} else {
var fixWlanPart = p.endpoint.substring(p.endpoint.indexOf('%'), p.endpoint.indexOf(']'))
var fixedUrlString = p.endpoint.replace(fixWlanPart, "")
var url = URI(fixedUrlString)
out.add(
PeerInfo(
url.scheme,
InetAddress.getByName(url.host),
url.port,
null,
true
)
)
}
}
}
return out
}
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="1dp"
android:color="@color/grey" />
<solid android:color="@color/dark_30" />
<padding
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:top="1dp" />
<corners android:radius="15dp" />
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="1dp"
android:color="@color/green" />
<solid android:color="@color/green" />
<padding
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:top="1dp" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/button_background" android:state_focused="true" />
<item android:drawable="@drawable/button_background" android:state_pressed="true" />
<item android:drawable="@drawable/button_background" />
</selector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="2dp"
android:color="@android:color/transparent" />
<solid android:color="@color/white" />
<padding
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:top="1dp" />
<corners android:radius="3dp" />
</shape>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="1dp"
android:color="@color/grey" />
<solid android:color="@color/dark_10" />
<padding
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:top="1dp" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<stroke
android:width="1dp"
android:color="@color/grey" />
<solid android:color="@color/dark_10" />
<padding
android:left="1dp"
android:right="1dp"
android:bottom="1dp"
android:top="1dp" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DNSListActivity"
android:background="@color/grey">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_dns_list" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="@dimen/fab_margin"
app:backgroundTint="@color/green"
app:tint="@color/white"
app:borderWidth="0dp"
app:elevation="6dp"
app:fabSize="normal"
app:srcCompat="@android:drawable/ic_input_add"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,83 +1,62 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/grey"
tools:context=".MainActivity">
<LinearLayout
tools:context=".MainActivity"
android:background="@color/grey">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@+id/connectRadioGroup"
app:layout_constraintTop_toTopOf="parent"
android:orientation="vertical"
android:gravity="left">
<TextView
android:id="@+id/ipLabel"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/dark_10"
android:elevation="8dp"
android:gravity="center_vertical"
android:text="Your IP address:"
android:textColor="@color/white"
android:paddingLeft="20dp"/>
<TextView
android:id="@+id/ip"
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@color/dark_10"
android:elevation="8dp"
android:gravity="center_vertical"
android:text=""
android:textColor="@color/white"
android:paddingLeft="20dp"
app:layout_constraintBottom_toTopOf="@+id/connectRadioGroup"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>
<RadioGroup
android:id="@+id/connectRadioGroup"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@drawable/out_line"
android:checkedButton="@+id/offer"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="50dp">
<RadioButton
android:id="@+id/disconnectButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginLeft="1dp"
android:layout_marginTop="1dp"
android:layout_marginBottom="1dp"
android:layout_weight="1"
android:background="@drawable/toggle_widget_background"
android:button="@null"
android:gravity="center"
android:text="@string/disconnect_button"
android:textColor="@color/white"
android:checked="true"/>
android:orientation="horizontal">
<RadioButton
android:id="@+id/connectButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="1dp"
android:layout_marginRight="1dp"
android:layout_marginBottom="1dp"
android:layout_weight="1"
android:background="@drawable/toggle_widget_background"
android:button="@null"
android:gravity="center"
android:text="@string/connect_button"
android:textColor="@color/white" />
</RadioGroup>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginEnd="240dp"
android:background="?attr/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Switch
android:id="@+id/staticIP"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:theme="@style/SwitchTheme"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switchOn"
app:layout_constraintTop_toTopOf="parent"
android:text="Static IP"
android:textColor="@color/white"/>
<Switch
android:id="@+id/switchOn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:theme="@style/SwitchTheme"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="Ygg me!"
android:textColor="@color/white"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PeerListActivity"
android:background="@color/grey">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_peer_list" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="@dimen/fab_margin"
app:backgroundTint="@color/green"
app:tint="@color/white"
app:borderWidth="0dp"
app:elevation="6dp"
app:fabSize="normal"
app:srcCompat="@android:drawable/ic_input_add"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ListView
android:id="@+id/dnsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/dark_20"
android:dividerHeight="2px"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,156 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:background="@color/grey">
<LinearLayout
android:id="@+id/ipLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@drawable/info_panel_rounded_corner"
android:gravity="left"
android:paddingLeft="20dp"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
android:animateLayoutChanges="true"
android:visibility="gone">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/ipLabel"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:elevation="8dp"
android:gravity="center_vertical"
android:text="IP address:"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textColor="@color/dark_30"
/>
<Button
android:id="@+id/copyIp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="COPY"
app:layout_constraintEnd_toEndOf="parent"
android:background="@android:color/transparent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/ip"
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@android:color/transparent"
android:elevation="8dp"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/copyIp"
android:text=""
android:textColor="@color/white"/>
</LinearLayout>
<LinearLayout
android:id="@+id/peerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@drawable/info_panel_rounded_corner"
android:gravity="left"
android:paddingLeft="20dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/ipLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/ipPeers"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:elevation="8dp"
android:gravity="center_vertical"
android:text="Peers:"
android:textColor="@color/dark_30"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="EDIT"
app:layout_constraintEnd_toEndOf="parent"
android:background="@android:color/transparent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<ListView
android:id="@+id/peers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginBottom="10dp"
android:dividerHeight="0dp"
android:divider="@null"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@drawable/info_panel_rounded_corner"
android:gravity="left"
android:paddingLeft="20dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/peerLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/dnsLabel"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:elevation="8dp"
android:gravity="center_vertical"
android:text="DNS:"
android:textColor="@color/dark_30"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/editDNS"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="EDIT"
app:layout_constraintEnd_toEndOf="parent"
android:background="@android:color/transparent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<ListView
android:id="@+id/dns"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_marginBottom="10dp"
android:dividerHeight="0dp"
android:divider="@null"
app:layout_constraintTop_toTopOf="parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ListView
android:id="@+id/peerList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/dark_20"
android:dividerHeight="2px"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="@color/dark_10">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/sub"
android:padding="10dp"
android:textColor="@color/white"
android:textSize="14dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Schema"
android:ellipsize="middle"
android:singleLine="true"
android:textIsSelectable="false"/>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,26 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="2dp">
<ImageView
android:id="@+id/countryFlag"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitStart"/>
</FrameLayout>
<TextView
android:id="@+id/hostInfoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingEnd="20dp"
android:minHeight="22dp"
android:textSize="13sp"
android:textColor="@color/white"/>
</LinearLayout>

View File

@ -0,0 +1,64 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal">
<ImageView
android:id="@+id/countryFlag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@+id/hostInfoText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/hostInfoText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="45dp"
android:textSize="14sp"
android:textColor="@color/white"
android:layout_marginStart="10dp"
app:layout_constraintEnd_toStartOf="@+id/ping"
app:layout_constraintStart_toEndOf="@+id/countryFlag"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/ping"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="45dp"
android:textSize="14sp"
android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@+id/frame"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<FrameLayout
android:id="@+id/frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/checkbox_rounded_corner"
android:scaleX="0.4"
android:scaleY="0.4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:background="@android:color/transparent"
android:checked="false"
android:scaleX="3"
android:scaleY="3"
android:theme="@style/SwitchTheme" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical">
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/saveButton"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="SAVE"
android:background="@android:color/transparent"/>
</RelativeLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</RelativeLayout>

View File

@ -0,0 +1,63 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="10dp"
android:background="@color/grey">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="10dp"
app:boxBackgroundMode="none"
android:background="@drawable/edit_text_rounded_corner"
android:textColorHint="@color/white"
style="@style/EditText.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ipInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits="0,1,2,3,4,5,6,7,8,9,:"
android:hint="IPv6"
android:inputType="textNoSuggestions"
android:textColor="@color/white"
android:textCursorDrawable="@null"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.hbb20.CountryCodePicker
android:id="@+id/ccp"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:layout_margin="10dp"
app:ccp_contentColor="@color/white"
app:ccp_showFullName="true"
app:ccp_showPhoneCode="false"
app:ccp_showNameCode="false"
app:ccpDialog_backgroundColor="@color/grey"
app:ccpDialog_textColor="@color/white"
app:layout_constraintEnd_toEndOf="@+id/ip"
app:layout_constraintTop_toBottomOf="@+id/ip"
android:background="@drawable/edit_text_rounded_corner"/>
<Button
android:id="@+id/add"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
app:layout_constraintEnd_toEndOf="@+id/schema"
app:layout_constraintTop_toBottomOf="@+id/ccp"
android:background="@drawable/button_selector"
app:backgroundTint="@null"
android:text="ADD"
android:textColor="@color/white"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,107 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="10dp"
android:background="@color/grey">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/schema"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="10dp"
app:boxBackgroundMode="none"
android:background="@drawable/edit_text_rounded_corner"
android:textColorHint="@color/white"
style="@style/EditText.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/schemaInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Schema"
android:textColor="@color/white"
android:inputType="textNoSuggestions"
android:textCursorDrawable="@null"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/schema"
app:layout_constraintTop_toBottomOf="@+id/schema"
android:layout_margin="10dp"
app:boxBackgroundMode="none"
android:background="@drawable/edit_text_rounded_corner"
android:textColorHint="@color/white"
style="@style/EditText.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ipInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits="0,1,2,3,4,5,6,7,8,9,.,qwertzuiopasdfghjklyxcvbnm,_,-"
android:hint="IP/domain"
android:inputType="textNoSuggestions"
android:textColor="@color/white"
android:textCursorDrawable="@null"
android:focusedByDefault="true"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/schema"
app:layout_constraintTop_toBottomOf="@+id/ip"
android:layout_margin="10dp"
app:boxBackgroundMode="none"
android:background="@drawable/edit_text_rounded_corner"
android:textColorHint="@color/white"
style="@style/EditText.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/portInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits="0123456789"
android:hint="Port"
android:textColor="@color/white"
android:inputType="number"
android:textCursorDrawable="@null"/>
</com.google.android.material.textfield.TextInputLayout>
<com.hbb20.CountryCodePicker
android:id="@+id/ccp"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:layout_margin="10dp"
app:ccp_contentColor="@color/white"
app:ccp_showFullName="true"
app:ccp_showPhoneCode="false"
app:ccp_showNameCode="false"
app:ccpDialog_backgroundColor="@color/grey"
app:ccpDialog_textColor="@color/white"
app:layout_constraintEnd_toEndOf="@+id/schema"
app:layout_constraintTop_toBottomOf="@+id/port"
android:background="@drawable/edit_text_rounded_corner"/>
<Button
android:id="@+id/add"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
app:layout_constraintEnd_toEndOf="@+id/schema"
app:layout_constraintTop_toBottomOf="@+id/ccp"
android:background="@drawable/button_selector"
app:backgroundTint="@null"
android:text="ADD"
android:textColor="@color/white"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:textSize="20dp"
android:gravity="left"
android:textColor="@color/white"
android:textIsSelectable="false"/>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/switchId"
android:title=""
app:actionLayout="@layout/menu_switch"
app:showAsAction="always" />
</menu>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/saveItem"
android:title=""
app:actionLayout="@layout/menu_save"
app:showAsAction="always"/>
</menu>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="schemas">
<item>TCP</item>
<item>TLS</item>
</string-array>
</resources>

View File

@ -9,4 +9,5 @@
<color name="dark_5">#555555</color>
<color name="dark_10">#666666</color>
<color name="dark_20">#777777</color>
<color name="dark_30">#acacac</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -2,4 +2,9 @@
<string name="app_name">Yggdrasil</string>
<string name="connect_button">Connect</string>
<string name="disconnect_button">Disconnect</string>
<string name="switch_button_title">SwitchOn</string>
<string name="title_activity_peer_list">Edit peers</string>
<string name="title_activity_dns_list">Edit DNS</string>
<string name="address_copied">Address copied</string>
<string name="schema">Schema</string>
</resources>

View File

@ -6,6 +6,33 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorControlActivated">@color/white</item>
</style>
<style name="SwitchTheme" parent="Theme.AppCompat.Light">
<item name="android:colorControlActivated">@color/green</item>
</style>
<style name="EditText.OutlinedBox" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="android:textColorHint"> @color/white </item>
<item name="hintTextColor"> @color/white </item>
</style>
<style name="EditTextTheme" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/white</item>
<item name="android:textColorHint">@color/white</item>
<item name="colorAccent">@color/white</item>
<item name="android:textSize">13sp</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@ -5,7 +5,6 @@ buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
@ -19,7 +18,6 @@ allprojects {
repositories {
google()
jcenter()
}
}

View File

@ -1,2 +1 @@
include ':yggdrasil.aar'
include ':app', ':yggdrasil'

View File

@ -1,2 +0,0 @@
configurations.maybeCreate("default")
artifacts.add("default", file('yggdrasil.aar'))

Binary file not shown.

View File

@ -4,7 +4,7 @@ all:
-go get -u github.com/yggdrasil-network/yggdrasil-go;
-cd $(GOPATH)/src/github.com/yggdrasil-network/yggdrasil-go; \
go get -v -d ./...; \
go get -u github.com/yggdrasil-network/yggdrasil-extras; \
go get -u github.com/vikulin/yggdrasil-extras@ab56805; \
ANDROID=true ./build;
mv $(GOPATH)/src/github.com/yggdrasil-network/yggdrasil-go/yggdrasil.aar yggdrasil.aar;
mv $(GOPATH)/src/github.com/yggdrasil-network/yggdrasil-go/yggdrasil-sources.jar yggdrasil-sources.jar;
mv -f $(GOPATH)/src/github.com/yggdrasil-network/yggdrasil-go/yggdrasil.aar yggdrasil.aar;
mv -f $(GOPATH)/src/github.com/yggdrasil-network/yggdrasil-go/yggdrasil-sources.jar yggdrasil-sources.jar;

View File

@ -1,2 +1,3 @@
configurations.maybeCreate("default")
artifacts.add("default", file('yggdrasil.aar'))