1. implemented peers list. iteration#1

This commit is contained in:
vadym 2020-06-20 13:40:56 -07:00
parent 9a2f658515
commit 108bb009bb
21 changed files with 656 additions and 102 deletions

View File

@ -29,8 +29,16 @@ 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")
@ -41,17 +49,21 @@ 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.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
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.7'
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'org.hjson:hjson:3.0.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.hbb20:ccp:2.4.0'
implementation "androidx.preference:preference-ktx:1.1.1"
}

View File

@ -2,8 +2,10 @@
<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.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@ -12,20 +14,28 @@
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" />
<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">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,56 +1,89 @@
package io.github.chronosx88.yggdrasil
import android.R.attr
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
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.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
class MainActivity : AppCompatActivity() {
companion object {
const val COMMAND = "COMMAND"
const val STOP = "STOP"
const val START = "START"
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 PEER_LIST_CODE = 1000
const val PEER_LIST = "PEERS_LIST"
const val CURRENT_PEERS = "CURRENT_PEERS"
const val START_VPN = "START_VPN"
private const val TAG="Yggdrasil"
private const val VPN_REQUEST_CODE = 0x0F
}
private var startVpnFlag = false
private var currentPeers = arrayListOf<String>()
private var isStarted = false
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
}
}
val listView = findViewById<ListView>(R.id.peers)
//save to shared preferences
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
currentPeers = ArrayList(preferences.getStringSet(CURRENT_PEERS, HashSet())!!)
if(currentPeers.size==0) {
currentPeers.add("tcp://194.177.21.156:5066")
currentPeers.add("tcp://46.151.26.194:60575")
currentPeers.add("tcp://188.226.125.64:54321")
}
val adapter = ArrayAdapter(this, R.layout.peers_list_item, currentPeers)
listView.adapter = adapter
val editBeersButton = findViewById<Button>(R.id.edit)
editBeersButton.setOnClickListener {
val intent = Intent(this, PeerListActivity::class.java)
intent.putStringArrayListExtra(PEER_LIST, currentPeers)
startActivityForResult(intent, PEER_LIST_CODE)
}
if(intent.extras!==null) {
startVpnFlag = intent.extras!!.getBoolean(START_VPN, false)
}
}
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 ipLayout = findViewById<LinearLayout>(R.id.ipLayout)
ipLayout.visibility = View.VISIBLE
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)
}
}
@ -59,20 +92,83 @@ class MainActivity : AppCompatActivity() {
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, currentPeers)
startService(intent)
}
if (requestCode == PEER_LIST_CODE && resultCode== Activity.RESULT_OK){
if(data!!.extras!=null){
var currentPeers = data.extras!!.getStringArrayList(PEER_LIST)
if(currentPeers==null || currentPeers.size==0){
val text = "No peers selected!"
val duration = Toast.LENGTH_SHORT
val toast = Toast.makeText(applicationContext, text, duration)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
} else {
val adapter = ArrayAdapter(this, R.layout.peers_list_item, currentPeers)
val listView = findViewById<ListView>(R.id.peers)
listView.adapter = adapter
this.currentPeers = currentPeers
//save to shared preferences
val preferences =
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
preferences.edit().putStringSet(CURRENT_PEERS, HashSet(this.currentPeers)).apply()
if(isStarted){
//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)
finish()
}
}
}
}
when (resultCode) {
STATUS_START -> print("service started")
STATUS_FINISH -> {
val result: String = data!!.getStringExtra(IPv6)
findViewById<TextView>(R.id.ip).setText(result)
findViewById<TextView>(R.id.ip).text = result
isStarted = true
}
STATUS_STOP -> {
isStarted = false
finish()
}
else -> { // Note the block
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main_menu, menu)
val item = menu.findItem(R.id.switchId) as MenuItem
item.setActionView(R.layout.menu_switch)
val switchOn = item
.actionView.findViewById<Switch>(R.id.switchOn)
if(startVpnFlag){
switchOn.isChecked = true
startVpnFlag = false
startVpn()
} else {
switchOn.isChecked = false
}
switchOn.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
startVpn()
} else {
stopVpn()
}
}
return true
}
}

View File

@ -0,0 +1,132 @@
package io.github.chronosx88.yggdrasil
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.widget.Button
import android.widget.ListView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.hbb20.CountryCodePicker
import io.github.chronosx88.yggdrasil.models.config.PeerInfo
import io.github.chronosx88.yggdrasil.models.config.PeerInfoListAdapter
import java.net.Inet4Address
class PeerListActivity : AppCompatActivity() {
companion object {
val peers = arrayListOf(
PeerInfo(
"tcp",
Inet4Address.getByName("194.177.21.156"),
5066,
"RU",
CountryCodePicker.Language.RUSSIAN
),
PeerInfo(
"tcp",
Inet4Address.getByName("46.151.26.194"),
60575,
"RU",
CountryCodePicker.Language.RUSSIAN
),
PeerInfo(
"tcp",
Inet4Address.getByName("188.226.125.64"),
54321,
"RU",
CountryCodePicker.Language.RUSSIAN
),
PeerInfo(
"tcp",
Inet4Address.getByName("88.201.129.205"),
8777,
"RU",
CountryCodePicker.Language.RUSSIAN
),
PeerInfo(
"tcp",
Inet4Address.getByName("45.11.19.26"),
5001,
"DE",
CountryCodePicker.Language.GERMAN
),
PeerInfo(
"tcp",
Inet4Address.getByName("82.165.69.111"),
61216,
"DE",
CountryCodePicker.Language.GERMAN
),
PeerInfo(
"tcp",
Inet4Address.getByName("104.248.15.125"),
31337,
"US",
CountryCodePicker.Language.ENGLISH
),
PeerInfo(
"tcp",
Inet4Address.getByName("108.175.10.127"),
61216,
"US",
CountryCodePicker.Language.ENGLISH
)
)
}
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 ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
var extras = intent.getExtras()
var peerList = findViewById<ListView>(R.id.peerList)
if (extras != null) {
var currentPeers = extras.getStringArrayList(MainActivity.PEER_LIST)!!
var adapter = PeerInfoListAdapter(this, peers, currentPeers)
peerList.adapter = adapter
} else {
var adapter = PeerInfoListAdapter(this, peers, ArrayList())
peerList.adapter = adapter
}
}
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_peers, 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 {
val result = Intent(this, MainActivity::class.java)
var adapter = findViewById<ListView>(R.id.peerList).adapter as PeerInfoListAdapter
val selectedPeers = adapter.getSelectedPeers()
if(selectedPeers.size>0) {
result.putExtra(MainActivity.PEER_LIST, adapter.getSelectedPeers())
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()
}
}
return true
}
}

View File

@ -19,6 +19,9 @@ import kotlin.coroutines.CoroutineContext
class YggdrasilTunService : VpnService() {
private lateinit var ygg: Yggdrasil
private var isClosed = false
/** Maximum packet size is constrained by the MTU, which is given as a signed short. */
private val MAX_PACKET_SIZE = Short.MAX_VALUE.toInt()
@ -33,26 +36,29 @@ class YggdrasilTunService : VpnService() {
private lateinit var writeCoroutine: CoroutineContext
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.getStringExtra("COMMAND") == "STOP") {
stopVpn()
}
if (intent?.getStringExtra("COMMAND") == "START") {
if (intent?.getStringExtra(MainActivity.COMMAND) == MainActivity.STOP) {
val pi: PendingIntent = intent.getParcelableExtra(MainActivity.PARAM_PINTENT)
setupTunInterface(pi)
stopVpn(pi)
}
if (intent?.getStringExtra(MainActivity.COMMAND) == MainActivity.START) {
val peers = intent.getStringArrayListExtra(MainActivity.PEERS)
val pi: PendingIntent = intent.getParcelableExtra(MainActivity.PARAM_PINTENT)
ygg = Yggdrasil()
setupTunInterface(pi, peers)
}
return super.onStartCommand(intent, flags, startId);
return super.onStartCommand(intent, flags, startId)
}
private fun setupTunInterface(pi: PendingIntent) {
pi.send(MainActivity.STATUS_START);
private fun setupTunInterface(pi: PendingIntent, peers: ArrayList<String>) {
pi.send(MainActivity.STATUS_START)
val builder = Builder()
val ygg = Yggdrasil()
var configJson = Mobile.generateConfigJSON()
val gson = Gson()
var config = gson.fromJson(String(configJson), Map::class.java).toMutableMap()
config = fixConfig(config)
config = fixConfig(config, peers)
configJson = gson.toJson(config).toByteArray()
yggConduitEndpoint = ygg.startJSON(configJson)
@ -67,13 +73,14 @@ class YggdrasilTunService : VpnService() {
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()
val buffer = ByteArray(2048)
try{
while (true) {
readPacketsFromTun(buffer)
}
} catch (e: IOException){
e.printStackTrace()
tunInputStream!!.close()
}
}
writeCoroutine = GlobalScope.launch {
@ -85,11 +92,8 @@ class YggdrasilTunService : VpnService() {
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 fixConfig(config: MutableMap<Any?, Any?>, peers: ArrayList<String>): MutableMap<Any?, Any?> {
val whiteList = arrayListOf<String>()
whiteList.add("")
val blackList = arrayListOf<String>()
@ -120,11 +124,11 @@ class YggdrasilTunService : VpnService() {
}
private fun readPacketsFromTun(buffer: ByteArray) {
if(tunInputStream != null) {
if(!isClosed) {
// Read the outgoing packet from the input stream.
var length = tunInputStream!!.read(buffer)
val length = tunInputStream!!.read(buffer)
if (length > 0) {
var byteBuffer = ByteBuffer.allocate(length);
val byteBuffer = ByteBuffer.allocate(length)
byteBuffer.put(buffer, 0, length)
yggConduitEndpoint.send(byteBuffer.array())
} else {
@ -140,12 +144,19 @@ class YggdrasilTunService : VpnService() {
}
}
fun stopVpn() {
readCoroutine.cancel()
writeCoroutine.cancel()
private fun stopVpn(pi: PendingIntent) {
isClosed = true;
readCoroutine!!.cancel()
writeCoroutine!!.cancel()
tunInputStream!!.close()
tunOutputStream!!.close()
tunInterface!!.close()
tunInterface = null
stopSelf()
//this hack due to https://github.com/yggdrasil-network/yggdrasil-go/issues/714 bug
ygg.startAutoconfigure()
ygg.stop()
val intent: Intent = Intent()
pi.send(this, MainActivity.STATUS_STOP, intent)
}
override fun onDestroy() {

View File

@ -0,0 +1,32 @@
package io.github.chronosx88.yggdrasil.models.config
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, language: CountryCodePicker.Language){
this.schema = schema
this.address = address
this.port = port
this.countryCode = countryCode
this.language = language
}
var schema: String
var address: InetAddress
var port = 0
var countryCode: String
var language: CountryCodePicker.Language
var ping: Float = Float.MAX_VALUE
override fun toString(): String {
return this.schema+":/"+address.toString()+":"+port
}
fun getCountry(context: Context): CCPCountry? {
return CCPCountry.getCountryForNameCodeFromLibraryMasterList(context, language, countryCode)
}
}

View File

@ -0,0 +1,59 @@
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.CheckBox
import android.widget.ImageView
import android.widget.TextView
import io.github.chronosx88.yggdrasil.R
import java.util.ArrayList
class PeerInfoListAdapter(
context: Context,
allPeers: List<PeerInfo>,
currentPeers: ArrayList<String>
) : ArrayAdapter<PeerInfo?> (context, 0, allPeers) {
private val mContext: Context = context
private var allPeers: List<PeerInfo> = allPeers
private var currentPeers: ArrayList<String> = currentPeers
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var listItem: View? = convertView
if (listItem == null) {
listItem = LayoutInflater.from(mContext).inflate(R.layout.peers_list_item_edit, parent, false)
}
val currentPeer = allPeers[position]
val image: ImageView = listItem?.findViewById(R.id.countryFlag) as ImageView
image.setImageResource(currentPeer.getCountry(mContext)!!.flagID)
val name = listItem.findViewById(R.id.peerInfoText) as TextView
val peerId = currentPeer.toString()
name.text = peerId
val checkbox = listItem.findViewById(R.id.checkbox) as CheckBox
checkbox.setOnCheckedChangeListener { buttonView, isChecked ->
if(isChecked){
if(!currentPeers.contains(peerId)){
currentPeers.add(peerId)
}
} else {
if(currentPeers.contains(peerId)){
currentPeers.remove(peerId)
}
}
}
if(this.currentPeers.contains(peerId)){
checkbox.isChecked = true
}
return listItem
}
public fun getSelectedPeers(): ArrayList<String> {
return currentPeers
}
}

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

@ -6,78 +6,92 @@
android:layout_height="match_parent"
android:background="@color/grey"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/ipLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toTopOf="@+id/connectRadioGroup"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@drawable/rounded_corner"
android:gravity="left"
android:orientation="vertical"
android:gravity="left">
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
android:animateLayoutChanges="true">
<TextView
android:id="@+id/ipLabel"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/dark_10"
android:background="@android:color/transparent"
android:elevation="8dp"
android:gravity="center_vertical"
android:paddingLeft="20dp"
android:text="Your IP address:"
android:textColor="@color/white"
android:paddingLeft="20dp"/>
android:textColor="@color/dark_30" />
<TextView
android:id="@+id/ip"
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@color/dark_10"
android:background="@android:color/transparent"
android:elevation="8dp"
android:gravity="center_vertical"
android:paddingLeft="20dp"
android:layout_marginBottom="10dp"
android:text=""
android:textColor="@color/white"
android:paddingLeft="20dp"
app:layout_constraintBottom_toTopOf="@+id/connectRadioGroup"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<RadioGroup
android:id="@+id/connectRadioGroup"
<LinearLayout
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">
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:background="@drawable/rounded_corner"
android:gravity="left"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/ipLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="20dp"
app:layout_constraintTop_toTopOf="parent"
android:orientation="horizontal">
<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"/>
<TextView
android:id="@+id/ipPeers"
android:layout_width="match_parent"
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_constraintEnd_toStartOf="@+id/edit"
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"/>
<RadioButton
android:id="@+id/connectButton"
android:layout_width="0dp"
</androidx.constraintlayout.widget.ConstraintLayout>
<ListView
android:id="@+id/peers"
android:layout_width="match_parent"
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>
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,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=".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"
android:visibility="gone"/>
</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/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,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,14 @@
<?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">
<Switch
android:id="@+id/switchOn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_margin="10dp"
android:theme="@style/SwitchTheme"/>
</RelativeLayout>

View File

@ -0,0 +1,9 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:minHeight="25dp"
android:textSize="13sp"
android:textColor="@color/white"/>

View File

@ -0,0 +1,39 @@
<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="wrap_content"
android:scaleX="0.4"
android:scaleY="0.4"
android:background="@color/white"
android:layout_marginStart="5dp">
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:theme="@style/SwitchTheme"
android:scaleX="3"
android:scaleY="3"
android:layout_margin="10dp"
android:background="@android:color/transparent"/>
</FrameLayout>
<ImageView
android:id="@+id/countryFlag"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="5dp"/>
<TextView
android:id="@+id/peerInfoText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginStart="10dp"
android:paddingEnd="20dp"
android:minHeight="45dp"
android:textSize="14sp"
android:textColor="@color/white"/>
</LinearLayout>

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

@ -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,14 @@
<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>
<!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="hello_first_fragment">Hello first fragment</string>
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
</resources>

View File

@ -8,4 +8,17 @@
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="SwitchTheme" parent="Theme.AppCompat.Light">
<item name="android:colorControlActivated">@color/green</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>