mirror of
https://github.com/yggdrasil-network/crispa-android.git
synced 2024-11-09 12:01:01 +00:00
commit
02694abab7
2
.gitignore
vendored
2
.gitignore
vendored
@ -17,3 +17,5 @@
|
||||
/app/src/main/assets/yggdrasil-0.3.8-linux-arm64
|
||||
/app/src/main/assets/yggdrasil-0.3.8-linux-armhf
|
||||
/.idea/
|
||||
|
||||
acra.properties
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "yggdrasil/yggdrasil-extras"]
|
||||
path = yggdrasil/yggdrasil-extras
|
||||
url = https://github.com/yggdrasil-network/yggdrasil-extras
|
@ -1,22 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" alias="false" withSubpackages="false" />
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||
<value>
|
||||
<package name="" alias="false" withSubpackages="true" />
|
||||
<package name="java" alias="false" withSubpackages="true" />
|
||||
<package name="javax" alias="false" withSubpackages="true" />
|
||||
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||
<package name="" alias="true" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8" />
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
@ -4,7 +4,7 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
@ -15,7 +15,6 @@
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
@ -21,5 +21,10 @@
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
@ -1,6 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="app/src/main/res/layout/activity_main.xml" value="0.3385416666666667" />
|
||||
<entry key="app/src/main/res/layout/activity_peer_list.xml" value="0.3385416666666667" />
|
||||
<entry key="app/src/main/res/layout/content_main.xml" value="0.24728260869565216" />
|
||||
<entry key="app/src/main/res/menu/save_peers.xml" value="0.3385416666666667" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
3
acra.properties.sample
Normal file
3
acra.properties.sample
Normal file
@ -0,0 +1,3 @@
|
||||
ACRA_BACKEND_URL=""
|
||||
ACRA_LOGIN=""
|
||||
ACRA_PASSWORD=""
|
@ -1,16 +1,25 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
def acraSecretsPropertiesFile = rootProject.file("acra.properties")
|
||||
def acraSecretsProperties = new Properties()
|
||||
acraSecretsProperties.load(new FileInputStream(acraSecretsPropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId "io.github.chronosx88.yggdrasil"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 29
|
||||
versionCode 23
|
||||
versionName "2.0.3"
|
||||
targetSdkVersion 31
|
||||
|
||||
versionCode 37
|
||||
versionName "2.0.19"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
setProperty("archivesBaseName", project.getParent().name+"-"+versionName)
|
||||
|
||||
buildConfigField("String", "ACRA_BACKEND_URL", acraSecretsProperties['ACRA_BACKEND_URL'])
|
||||
buildConfigField("String", "ACRA_LOGIN", acraSecretsProperties['ACRA_LOGIN'])
|
||||
buildConfigField("String", "ACRA_PASSWORD", acraSecretsProperties['ACRA_PASSWORD'])
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
@ -68,19 +77,19 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(path: ':yggdrasil')
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
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.8'
|
||||
implementation 'com.google.android.material:material:1.4.0-rc01'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
|
||||
implementation 'com.google.android.material:material:1.5.0-alpha04'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.hbb20:ccp:2.4.0'
|
||||
implementation 'com.vincentbrison.openlibraries.android:dualcache:3.1.1'
|
||||
implementation 'com.vincentbrison.openlibraries.android:dualcache-jsonserializer:3.1.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import androidx.test.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
@ -1,46 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.github.chronosx88.yggdrasil">
|
||||
package="org.yggdrasil.app.crispa">
|
||||
|
||||
<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:name=".YggApplication"
|
||||
android:name="org.yggdrasil.app.crispa.YggApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:name="org.yggdrasil.app.crispa.AboutActivity"
|
||||
android:parentActivityName="org.yggdrasil.app.crispa.MainActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name=".PeerListActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:name="org.yggdrasil.app.crispa.PeerListActivity"
|
||||
android:parentActivityName="org.yggdrasil.app.crispa.MainActivity"
|
||||
android:label="@string/title_activity_peer_list"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name=".DNSListActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:name="org.yggdrasil.app.crispa.DNSListActivity"
|
||||
android:parentActivityName="org.yggdrasil.app.crispa.MainActivity"
|
||||
android:label="@string/title_activity_dns_list"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name=".CopyLocalNodeInfoActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:name="org.yggdrasil.app.crispa.CopyLocalNodeInfoActivity"
|
||||
android:parentActivityName="org.yggdrasil.app.crispa.MainActivity"
|
||||
android:label="@string/title_activity_copy_local_node_info"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait"/>
|
||||
<service
|
||||
android:name=".YggdrasilTunService"
|
||||
android:name="org.yggdrasil.app.crispa.YggdrasilTunService"
|
||||
android:stopWithTask="true"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
@ -49,10 +51,11 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
<activity android:name="org.yggdrasil.app.crispa.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:screenOrientation="portrait">
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
@ -1,5 +0,0 @@
|
||||
package io.github.chronosx88.yggdrasil.models
|
||||
|
||||
class Status {
|
||||
var up: Boolean = false
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
@ -1,12 +1,12 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.chronosx88.yggdrasil.models.config.NodeInfoListAdapter
|
||||
import org.yggdrasil.app.crispa.models.NodeInfo
|
||||
import org.yggdrasil.app.crispa.models.config.NodeInfoListAdapter
|
||||
|
||||
class CopyLocalNodeInfoActivity: AppCompatActivity() {
|
||||
|
||||
@ -18,8 +18,8 @@ class CopyLocalNodeInfoActivity: AppCompatActivity() {
|
||||
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||
val ipv6Address = intent.extras!!.getString(MainActivity.IPv6, "")
|
||||
val publicKey = preferences.getString(MainActivity.publicKey, "")
|
||||
var nodeInfoListView = findViewById<ListView>(R.id.nodeInfoList)
|
||||
val nodeInfoList = listOf<NodeInfo>(NodeInfo("IP address", ipv6Address!!), NodeInfo("Public Key", publicKey!!));
|
||||
var nodeInfoListView = findViewById<RecyclerView>(R.id.node_info_list)
|
||||
val nodeInfoList = listOf(NodeInfo("IP address", ipv6Address!!), NodeInfo("Public Key", publicKey!!));
|
||||
val adapter =
|
||||
NodeInfoListAdapter(
|
||||
this,
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
@ -9,11 +9,11 @@ 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 org.yggdrasil.app.crispa.models.DNSInfo
|
||||
import org.yggdrasil.app.crispa.models.config.SelectDNSInfoListAdapter
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringList2DNSInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.ping
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.serializeDNSInfoSet2StringList
|
||||
import kotlinx.coroutines.*
|
||||
import java.net.InetAddress
|
||||
import kotlin.concurrent.thread
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
@ -14,17 +14,17 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import dalvik.system.DexFile
|
||||
import io.github.chronosx88.yggdrasil.models.DNSInfo
|
||||
import io.github.chronosx88.yggdrasil.models.PeerInfo
|
||||
import io.github.chronosx88.yggdrasil.models.config.DNSInfoListAdapter
|
||||
import io.github.chronosx88.yggdrasil.models.config.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 org.yggdrasil.app.crispa.models.DNSInfo
|
||||
import org.yggdrasil.app.crispa.models.PeerInfo
|
||||
import org.yggdrasil.app.crispa.models.config.DNSInfoListAdapter
|
||||
import org.yggdrasil.app.crispa.models.config.PeerInfoListAdapter
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializePeerStringList2PeerInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringList2DNSInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringList2PeerInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringSet2DNSInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringSet2PeerInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.serializeDNSInfoSet2StringList
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.serializePeerInfoSet2StringList
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
@ -398,6 +398,9 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
//TODO reimplement it
|
||||
private fun isYggServiceRunning(context: Context): Boolean {
|
||||
if(this.intent.hasExtra(YggdrasilTunService.IS_VPN_SERVICE_STOPPED)){
|
||||
return !this.intent.getBooleanExtra(YggdrasilTunService.IS_VPN_SERVICE_STOPPED, true)
|
||||
}
|
||||
val manager =
|
||||
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
@ -21,13 +21,13 @@ import com.hbb20.CCPCountry
|
||||
import com.vincentbrison.openlibraries.android.dualcache.Builder
|
||||
import com.vincentbrison.openlibraries.android.dualcache.SizeOf
|
||||
import com.vincentbrison.openlibraries.android.dualcache.JsonSerializer
|
||||
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 org.yggdrasil.app.crispa.models.PeerInfo
|
||||
import org.yggdrasil.app.crispa.models.Status
|
||||
import org.yggdrasil.app.crispa.models.config.DropDownAdapter
|
||||
import org.yggdrasil.app.crispa.models.config.SelectPeerInfoListAdapter
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringList2PeerInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.ping
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.serializePeerInfoSet2StringList
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
@ -37,6 +37,8 @@ import java.net.URI
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class PeerListActivity : AppCompatActivity() {
|
||||
@ -83,7 +85,9 @@ class PeerListActivity : AppCompatActivity() {
|
||||
}
|
||||
var extras = intent.extras
|
||||
var peerList = findViewById<ListView>(R.id.peerList)
|
||||
var adapter = SelectPeerInfoListAdapter(this, arrayListOf(), mutableSetOf())
|
||||
|
||||
val alreadySelectedPeers = deserializeStringList2PeerInfoSet(extras!!.getStringArrayList(MainActivity.PEER_LIST)!!)
|
||||
var adapter = SelectPeerInfoListAdapter(this, arrayListOf(), alreadySelectedPeers)
|
||||
peerList.adapter = adapter
|
||||
var peerInfoListCache = Builder<List<PeerInfo>>(CACHE_NAME, TEST_APP_VERSION)
|
||||
.enableLog()
|
||||
@ -95,10 +99,7 @@ class PeerListActivity : AppCompatActivity() {
|
||||
|
||||
GlobalScope.launch() {
|
||||
try {
|
||||
var cp = deserializeStringList2PeerInfoSet(
|
||||
extras!!.getStringArrayList(MainActivity.PEER_LIST)!!
|
||||
)
|
||||
for (pi in cp) {
|
||||
for (pi in alreadySelectedPeers) {
|
||||
var ping = ping(pi.hostName, pi.port)
|
||||
pi.ping = ping
|
||||
}
|
||||
@ -108,7 +109,7 @@ class PeerListActivity : AppCompatActivity() {
|
||||
for (peerInfo in peerInfoCache) {
|
||||
var ping = ping(peerInfo.hostName, peerInfo.port)
|
||||
peerInfo.ping = ping
|
||||
if (cp.contains(peerInfo)) {
|
||||
if (alreadySelectedPeers.contains(peerInfo)) {
|
||||
continue
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -129,7 +130,7 @@ class PeerListActivity : AppCompatActivity() {
|
||||
for ((peer, status) in peers) {
|
||||
if (status.up) {
|
||||
for (ccp in countries) {
|
||||
if (ccp.name.toLowerCase()
|
||||
if (ccp.name.lowercase(Locale.getDefault())
|
||||
.contains(country.replace(".md", "").replace("-", " "))
|
||||
) {
|
||||
if(!peerListPing){
|
||||
@ -147,7 +148,7 @@ class PeerListActivity : AppCompatActivity() {
|
||||
)
|
||||
var ping = ping(url.host, url.port)
|
||||
peerInfo.ping = ping
|
||||
if (cp.contains(peerInfo)) {
|
||||
if (alreadySelectedPeers.contains(peerInfo)) {
|
||||
continue
|
||||
}
|
||||
if (peerInfo.ping < Int.MAX_VALUE) {
|
||||
@ -181,7 +182,7 @@ class PeerListActivity : AppCompatActivity() {
|
||||
for (peerInfo in onlinePeerInfoList) {
|
||||
var ping = ping(peerInfo.hostName, peerInfo.port)
|
||||
peerInfo.ping = ping
|
||||
if (cp.contains(peerInfo)) {
|
||||
if (alreadySelectedPeers.contains(peerInfo)) {
|
||||
continue
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -197,7 +198,7 @@ class PeerListActivity : AppCompatActivity() {
|
||||
else -> e.printStackTrace()
|
||||
}
|
||||
}
|
||||
var currentPeers = ArrayList(cp.sortedWith(compareBy { it.ping }))
|
||||
var currentPeers = ArrayList(alreadySelectedPeers.sortedWith(compareBy { it.ping }))
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.addAll(0, currentPeers)
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import config.NodeConfig
|
||||
import io.github.chronosx88.yggdrasil.models.config.Config
|
||||
import org.yggdrasil.app.crispa.models.config.Config
|
||||
import java.io.File
|
||||
|
||||
val gson = Gson()
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
@ -18,9 +18,9 @@ class YggApplication : Application() {
|
||||
reportFormat = StringFormat.JSON
|
||||
//each plugin you chose above can be configured in a block like this:
|
||||
httpSender {
|
||||
uri = "http://<host>/report"
|
||||
basicAuthLogin="***"
|
||||
basicAuthPassword = "***"
|
||||
uri = BuildConfig.ACRA_BACKEND_URL
|
||||
basicAuthLogin = BuildConfig.ACRA_LOGIN
|
||||
basicAuthPassword = BuildConfig.ACRA_PASSWORD
|
||||
httpMethod = HttpSender.Method.POST
|
||||
}
|
||||
dialog {
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
@ -14,15 +14,16 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
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 org.yggdrasil.app.crispa.models.DNSInfo
|
||||
import org.yggdrasil.app.crispa.models.PeerInfo
|
||||
import org.yggdrasil.app.crispa.models.config.Peer
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.convertPeer2PeerStringList
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.convertPeerInfoSet2PeerIdSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringList2DNSInfoSet
|
||||
import org.yggdrasil.app.crispa.models.config.Utils.Companion.deserializeStringList2PeerInfoSet
|
||||
import mobile.Mobile
|
||||
import mobile.Yggdrasil
|
||||
import org.acra.ACRA
|
||||
import java.io.*
|
||||
import java.net.Inet6Address
|
||||
import kotlin.concurrent.thread
|
||||
@ -41,6 +42,7 @@ class YggdrasilTunService : VpnService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Yggdrasil-service"
|
||||
public const val IS_VPN_SERVICE_STOPPED = "VPN_STATUS"
|
||||
}
|
||||
|
||||
private val FOREGROUND_ID = 1338
|
||||
@ -139,6 +141,7 @@ class YggdrasilTunService : VpnService() {
|
||||
private fun sendMeshPeerStatus(pi: PendingIntent?){
|
||||
class Token : TypeToken<List<Peer>>()
|
||||
ygg.addressString
|
||||
ACRA.errorReporter.putCustomData("Peers JSON", ygg.peersJSON)
|
||||
var meshPeers: List<Peer> = gson.fromJson(ygg.peersJSON, Token().type)
|
||||
val intent: Intent = Intent().putStringArrayListExtra(
|
||||
MainActivity.MESH_PEERS,
|
||||
@ -161,6 +164,7 @@ class YggdrasilTunService : VpnService() {
|
||||
config["Listen"] = arrayListOf<String>()
|
||||
config["AdminListen"] = "tcp://localhost:9001"
|
||||
config["IfName"] = "tun0"
|
||||
config["IfMTU"] = 65535
|
||||
if(staticIP) {
|
||||
val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||
@ -179,12 +183,17 @@ class YggdrasilTunService : VpnService() {
|
||||
config["PublicKey"] = publicKey
|
||||
}
|
||||
}
|
||||
var multicastInterface = emptyMap<String, Any>().toMutableMap()
|
||||
multicastInterface["Regex"] = ".*"
|
||||
multicastInterface["Beacon"] = true
|
||||
multicastInterface["Listen"] = true
|
||||
multicastInterface["Port"] = 0
|
||||
(config["MulticastInterfaces"] as MutableList<Any>)[0] = multicastInterface
|
||||
//(config["SessionFirewall"] as MutableMap<Any, Any>)["AllowFromDirect"] = true
|
||||
//(config["SessionFirewall"] as MutableMap<Any, Any>)["AllowFromRemote"] = true
|
||||
//(config["SessionFirewall"] as MutableMap<Any, Any>)["AlwaysAllowOutbound"] = true
|
||||
//(config["SessionFirewall"] as MutableMap<Any, Any>)["WhitelistEncryptionPublicKeys"] = whiteList
|
||||
//(config["SessionFirewall"] as MutableMap<Any, Any>)["BlacklistEncryptionPublicKeys"] = blackList
|
||||
|
||||
//(config["SwitchOptions"] as MutableMap<Any, Any>)["MaxTotalQueueSize"] = 4194304
|
||||
if (config["AutoStart"] == null) {
|
||||
val tmpMap = emptyMap<String, Boolean>().toMutableMap()
|
||||
@ -202,6 +211,8 @@ class YggdrasilTunService : VpnService() {
|
||||
ygg.send(buffer.sliceArray(IntRange(0, length - 1)))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,6 +223,8 @@ class YggdrasilTunService : VpnService() {
|
||||
tunOutputStream.write(buffer)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -267,6 +280,7 @@ class YggdrasilTunService : VpnService() {
|
||||
""
|
||||
}
|
||||
var intent = Intent(this, MainActivity::class.java)
|
||||
intent.putExtra(IS_VPN_SERVICE_STOPPED, isClosed);
|
||||
var stackBuilder = TaskStackBuilder.create(this)
|
||||
stackBuilder.addNextIntentWithParentStack(intent)
|
||||
var pi = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models
|
||||
package org.yggdrasil.app.crispa.models
|
||||
|
||||
import android.content.Context
|
||||
import com.hbb20.CCPCountry
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models
|
||||
package org.yggdrasil.app.crispa.models
|
||||
|
||||
class NodeInfo {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models
|
||||
package org.yggdrasil.app.crispa.models
|
||||
|
||||
import android.content.Context
|
||||
import com.hbb20.CCPCountry
|
@ -0,0 +1,5 @@
|
||||
package org.yggdrasil.app.crispa.models
|
||||
|
||||
class Status {
|
||||
var up: Boolean = false
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
@ -7,8 +7,8 @@ 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
|
||||
import org.yggdrasil.app.crispa.R
|
||||
import org.yggdrasil.app.crispa.models.DNSInfo
|
||||
|
||||
|
||||
class DNSInfoListAdapter(
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
@ -11,7 +11,7 @@ import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import io.github.chronosx88.yggdrasil.R
|
||||
import org.yggdrasil.app.crispa.R
|
||||
|
||||
|
||||
class DropDownAdapter(
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
@ -9,9 +9,10 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.chronosx88.yggdrasil.R
|
||||
import org.yggdrasil.app.crispa.R
|
||||
import org.yggdrasil.app.crispa.models.NodeInfo
|
||||
|
||||
class NodeInfoListAdapter(private val context: Context, private val infoSet: Array<Pair<String, String>>) :
|
||||
class NodeInfoListAdapter(private val context: Context, private val infoSet: Array<NodeInfo>) :
|
||||
RecyclerView.Adapter<NodeInfoListAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(private val context: Context, view: View) : RecyclerView.ViewHolder(view) {
|
||||
@ -50,8 +51,8 @@ class NodeInfoListAdapter(private val context: Context, private val infoSet: Arr
|
||||
|
||||
// Get element from your dataset at this position and replace the
|
||||
// contents of the view with that element
|
||||
viewHolder.key.text = infoSet[position].first
|
||||
viewHolder.value.text = infoSet[position].second
|
||||
viewHolder.key.text = infoSet[position].key
|
||||
viewHolder.value.text = infoSet[position].value
|
||||
}
|
||||
|
||||
// Return the size of your dataset (invoked by the layout manager)
|
@ -0,0 +1,13 @@
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Peer (
|
||||
//Example [{"Key":"JQZIX3KIamcp/6S9rycKiAGyg9MK7U6h8UUY5ej36fY=","Root":"AAABERGfllXfKNJshDs/8uzKEIFkFEccE16dmZV/cAo=","Coords":[2,4],"Port":1,"Remote":"tcp://[fe80::5207:4518:4378:7f1%wlan0]:57541","IP":"202:d7cd:bd04:6bbc:acc6:b002:da12:86c7"},{"Key":"DCNBiKAV1xr72JAFUgNrOYfY6Qm/f0Nq6ESZTSLn1eo=","Root":"AAABERGfllXfKNJshDs/8uzKEIFkFEccE16dmZV/cAo=","Coords":[2,4,1],"Port":2,"Remote":"tcp://[fe80::1c39:839:90a5:6ef%wlan0]:1108","IP":"204:7b97:ceeb:fd45:1ca0:84ed:ff55:bf92"}]
|
||||
@SerializedName("Key") var key : String,
|
||||
@SerializedName("Root") var root : String,
|
||||
//@SerializedName("Coords") var uptime : Long,
|
||||
@SerializedName("Port") var port : Int,
|
||||
@SerializedName("Remote") var remote : String,
|
||||
@SerializedName("IP") var ip : String
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
@ -7,8 +7,8 @@ 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
|
||||
import org.yggdrasil.app.crispa.R
|
||||
import org.yggdrasil.app.crispa.models.PeerInfo
|
||||
|
||||
|
||||
class PeerInfoListAdapter(
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
@ -9,8 +9,8 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import io.github.chronosx88.yggdrasil.R
|
||||
import io.github.chronosx88.yggdrasil.models.DNSInfo
|
||||
import org.yggdrasil.app.crispa.R
|
||||
import org.yggdrasil.app.crispa.models.DNSInfo
|
||||
|
||||
class SelectDNSInfoListAdapter(
|
||||
context: Context,
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
@ -9,8 +9,8 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import io.github.chronosx88.yggdrasil.R
|
||||
import io.github.chronosx88.yggdrasil.models.PeerInfo
|
||||
import org.yggdrasil.app.crispa.R
|
||||
import org.yggdrasil.app.crispa.models.PeerInfo
|
||||
|
||||
class SelectPeerInfoListAdapter(
|
||||
context: Context,
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
@ -1,8 +1,8 @@
|
||||
package io.github.chronosx88.yggdrasil.models.config
|
||||
package org.yggdrasil.app.crispa.models.config
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.github.chronosx88.yggdrasil.models.DNSInfo
|
||||
import io.github.chronosx88.yggdrasil.models.PeerInfo
|
||||
import org.yggdrasil.app.crispa.models.DNSInfo
|
||||
import org.yggdrasil.app.crispa.models.PeerInfo
|
||||
import org.acra.ACRA
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
@ -119,11 +119,8 @@ class Utils {
|
||||
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 fixWlanPart = p.remote.substring(p.remote.indexOf('%'), p.remote.indexOf(']'))
|
||||
var fixedUrlString = p.remote.replace(fixWlanPart, "")
|
||||
var url = URI(fixedUrlString)
|
||||
out.add(
|
||||
PeerInfo(
|
||||
@ -136,7 +133,6 @@ class Utils {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package io.github.chronosx88.yggdrasil
|
||||
package org.yggdrasil.app.crispa
|
||||
|
||||
import org.junit.Test
|
||||
|
@ -1,12 +1,12 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.5.10"
|
||||
ext.kotlin_version = "1.5.31"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.2.1"
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
|
@ -1,10 +1,7 @@
|
||||
GOPATH=$(shell go env GOPATH)
|
||||
export GO111MODULE=off
|
||||
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; \
|
||||
ANDROID=true ./build;
|
||||
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;
|
||||
cd yggdrasil-extras; \
|
||||
gomobile bind -v -target android -tags mobile -o yggdrasil.aar \
|
||||
github.com/yggdrasil-network/yggdrasil-extras/src/mobile \
|
||||
github.com/yggdrasil-network/yggdrasil-go/src/config
|
||||
mv -f yggdrasil-extras/yggdrasil.aar yggdrasil.aar;
|
||||
mv -f yggdrasil-extras/yggdrasil-sources.jar yggdrasil-sources.jar;
|
||||
|
1
yggdrasil/yggdrasil-extras
Submodule
1
yggdrasil/yggdrasil-extras
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 21d029d6a42d64ec169dfa5d8db039ada07226da
|
Loading…
Reference in New Issue
Block a user