mirror of
https://github.com/yggdrasil-network/yggdrasil-android.git
synced 2025-04-27 21:55:08 +03:00
* Fixes #30, added quick settings icon. * Added saving of enabled state, added some fixes and refactorings. * Fixed a bug with turning on/off the VPN. * Fixed possibility to add duplicate servers in DNS settings.
This commit is contained in:
parent
ee81f4e902
commit
aa94ccad26
12 changed files with 441 additions and 60 deletions
|
@ -4,6 +4,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".GlobalApplication"
|
android:name=".GlobalApplication"
|
||||||
|
@ -13,10 +14,6 @@
|
||||||
android:roundIcon="@drawable/ic_launcher_round"
|
android:roundIcon="@drawable/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Yggdrasil">
|
android:theme="@style/Theme.Yggdrasil">
|
||||||
<activity android:name=".SettingsActivity"
|
|
||||||
android:parentActivityName=".MainActivity" />
|
|
||||||
<activity android:name=".PeersActivity"
|
|
||||||
android:parentActivityName=".MainActivity" />
|
|
||||||
<activity android:name=".MainActivity" android:exported="true">
|
<activity android:name=".MainActivity" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
@ -24,7 +21,21 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".SettingsActivity" android:parentActivityName=".MainActivity" />
|
||||||
|
<activity android:name=".PeersActivity" android:parentActivityName=".MainActivity" />
|
||||||
<activity android:name=".DnsActivity" android:exported="false" />
|
<activity android:name=".DnsActivity" android:exported="false" />
|
||||||
|
<activity android:name=".TileServiceActivity" android:theme="@android:style/Theme.NoDisplay"
|
||||||
|
android:allowTaskReparenting="true"
|
||||||
|
android:alwaysRetainTaskState="false"
|
||||||
|
android:clearTaskOnLaunch="true"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:finishOnTaskLaunch="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".PacketTunnelProvider"
|
android:name=".PacketTunnelProvider"
|
||||||
|
@ -34,6 +45,18 @@
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".YggTileService"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:icon="@drawable/ic_tile_icon"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:exported="true">
|
||||||
|
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE" android:value="true" />
|
||||||
|
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -9,6 +9,7 @@ import android.view.ContextThemeWrapper
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
||||||
const val KEY_DNS_SERVERS = "dns_servers"
|
const val KEY_DNS_SERVERS = "dns_servers"
|
||||||
|
@ -59,13 +60,17 @@ class DnsActivity : AppCompatActivity() {
|
||||||
builder.setTitle(getString(R.string.dns_add_server_dialog_title))
|
builder.setTitle(getString(R.string.dns_add_server_dialog_title))
|
||||||
builder.setView(view)
|
builder.setView(view)
|
||||||
builder.setPositiveButton(getString(R.string.add)) { dialog, _ ->
|
builder.setPositiveButton(getString(R.string.add)) { dialog, _ ->
|
||||||
servers.add(input.text.toString())
|
val server = input.text.toString()
|
||||||
preferences.edit().apply {
|
if (!servers.contains(server)) {
|
||||||
putString(KEY_DNS_SERVERS, servers.joinToString(","))
|
servers.add(server)
|
||||||
commit()
|
preferences.edit().apply {
|
||||||
|
putString(KEY_DNS_SERVERS, servers.joinToString(","))
|
||||||
|
commit()
|
||||||
|
}
|
||||||
|
updateConfiguredServers()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, R.string.dns_already_added_server, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
|
||||||
updateConfiguredServers()
|
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||||
dialog.cancel()
|
dialog.cancel()
|
||||||
|
@ -85,7 +90,7 @@ class DnsActivity : AppCompatActivity() {
|
||||||
enableChromeFix.toggle()
|
enableChromeFix.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
|
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
|
||||||
servers = if (serverString!!.isNotEmpty()) {
|
servers = if (serverString!!.isNotEmpty()) {
|
||||||
serverString.split(",").toMutableList()
|
serverString.split(",").toMutableList()
|
||||||
|
@ -157,12 +162,17 @@ class DnsActivity : AppCompatActivity() {
|
||||||
addButton.tag = server
|
addButton.tag = server
|
||||||
|
|
||||||
addButton.setOnClickListener { button ->
|
addButton.setOnClickListener { button ->
|
||||||
servers.add(button.tag as String)
|
val serverString = button.tag as String
|
||||||
preferences.edit().apply {
|
if (!servers.contains(serverString)) {
|
||||||
this.putString(KEY_DNS_SERVERS, servers.joinToString(","))
|
servers.add(serverString)
|
||||||
this.commit()
|
preferences.edit().apply {
|
||||||
|
this.putString(KEY_DNS_SERVERS, servers.joinToString(","))
|
||||||
|
this.commit()
|
||||||
|
}
|
||||||
|
updateConfiguredServers()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, R.string.dns_already_added_server, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
updateConfiguredServers()
|
|
||||||
}
|
}
|
||||||
view.setOnLongClickListener {
|
view.setOnLongClickListener {
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
package eu.neilalexander.yggdrasil
|
package eu.neilalexander.yggdrasil
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.*
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
||||||
class GlobalApplication: Application() {
|
const val PREF_KEY_ENABLED = "enabled"
|
||||||
|
|
||||||
|
class GlobalApplication: Application(), YggStateReceiver.StateReceiver {
|
||||||
private lateinit var config: ConfigurationProxy
|
private lateinit var config: ConfigurationProxy
|
||||||
|
private var currentState: State = State.Disabled
|
||||||
var updaterConnections: Int = 0
|
var updaterConnections: Int = 0
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
config = ConfigurationProxy(applicationContext)
|
config = ConfigurationProxy(applicationContext)
|
||||||
val callback = NetworkStateCallback(this)
|
val callback = NetworkStateCallback(this)
|
||||||
|
callback.register()
|
||||||
|
val receiver = YggStateReceiver(this)
|
||||||
|
receiver.register(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe() {
|
fun subscribe() {
|
||||||
|
@ -25,4 +38,63 @@ class GlobalApplication: Application() {
|
||||||
fun needUiUpdates(): Boolean {
|
fun needUiUpdates(): Boolean {
|
||||||
return updaterConnections > 0
|
return updaterConnections > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCurrentState(): State {
|
||||||
|
return currentState
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
override fun onStateChange(state: State) {
|
||||||
|
if (state != currentState) {
|
||||||
|
val componentName = ComponentName(this, YggTileService::class.java)
|
||||||
|
TileService.requestListeningState(this, componentName)
|
||||||
|
|
||||||
|
if (state != State.Disabled) {
|
||||||
|
val notification = createServiceNotification(this, state)
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.notify(SERVICE_NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createServiceNotification(context: Context, state: State): Notification {
|
||||||
|
// Create the NotificationChannel, but only on API 26+ because
|
||||||
|
// the NotificationChannel class is new and not in the support library
|
||||||
|
val channelId = "Foreground Service"
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name = context.getString(R.string.channel_name)
|
||||||
|
val descriptionText = context.getString(R.string.channel_description)
|
||||||
|
val importance = NotificationManager.IMPORTANCE_MIN
|
||||||
|
val channel = NotificationChannel(channelId, name, importance).apply {
|
||||||
|
description = descriptionText
|
||||||
|
}
|
||||||
|
// Register the channel with the system
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
|
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
|
||||||
|
val text = when (state) {
|
||||||
|
State.Disabled -> context.getText(R.string.tile_disabled)
|
||||||
|
State.Enabled -> context.getText(R.string.tile_enabled)
|
||||||
|
State.Connected -> context.getText(R.string.tile_connected)
|
||||||
|
else -> context.getText(R.string.tile_disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotificationCompat.Builder(context, channelId)
|
||||||
|
.setContentTitle(context.getText(R.string.app_name))
|
||||||
|
.setContentText(text)
|
||||||
|
.setSmallIcon(R.drawable.ic_tile_icon)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
|
.build()
|
||||||
}
|
}
|
|
@ -11,7 +11,9 @@ import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT
|
import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT
|
||||||
import mobile.Mobile
|
import mobile.Mobile
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
@ -76,6 +78,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
startService(intent)
|
startService(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
|
preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, isChecked) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val enableYggdrasilPanel = findViewById<TableRow>(R.id.enableYggdrasilPanel)
|
val enableYggdrasilPanel = findViewById<TableRow>(R.id.enableYggdrasilPanel)
|
||||||
|
@ -123,7 +127,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||||
receiver, IntentFilter(STATE_INTENT)
|
receiver, IntentFilter(STATE_INTENT)
|
||||||
)
|
)
|
||||||
val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
|
enabledSwitch.isChecked = preferences.getBoolean(PREF_KEY_ENABLED, false)
|
||||||
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
|
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
|
||||||
if (serverString!!.isNotEmpty()) {
|
if (serverString!!.isNotEmpty()) {
|
||||||
val servers = serverString.split(",")
|
val servers = serverString.split(",")
|
||||||
|
@ -149,7 +154,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
when (intent.getStringExtra("type")) {
|
when (intent.getStringExtra("type")) {
|
||||||
"state" -> {
|
"state" -> {
|
||||||
enabledLabel.text = if (intent.getBooleanExtra("started", false)) {
|
enabledLabel.text = if (intent.getBooleanExtra("started", false)) {
|
||||||
enabledSwitch.isChecked = true
|
|
||||||
var count = 0
|
var count = 0
|
||||||
if (intent.hasExtra("dht")) {
|
if (intent.hasExtra("dht")) {
|
||||||
val dht = intent.getStringExtra("dht")
|
val dht = intent.getStringExtra("dht")
|
||||||
|
@ -166,7 +170,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
getString(R.string.main_enabled)
|
getString(R.string.main_enabled)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enabledSwitch.isChecked = false
|
|
||||||
enabledLabel.setTextColor(Color.GRAY)
|
enabledLabel.setTextColor(Color.GRAY)
|
||||||
getString(R.string.main_disabled)
|
getString(R.string.main_disabled)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,43 @@ package eu.neilalexander.yggdrasil
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.*
|
import android.net.*
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
|
||||||
|
|
||||||
private const val TAG = "Network"
|
private const val TAG = "Network"
|
||||||
|
|
||||||
class NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCallback() {
|
class NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCallback() {
|
||||||
|
|
||||||
init {
|
override fun onAvailable(network: Network) {
|
||||||
|
super.onAvailable(network)
|
||||||
|
Log.d(TAG, "onAvailable")
|
||||||
|
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
if (preferences.getBoolean(PREF_KEY_ENABLED, false)) {
|
||||||
|
Thread {
|
||||||
|
// The message often arrives before the connection is fully established
|
||||||
|
Thread.sleep(1000)
|
||||||
|
val intent = Intent(context, PacketTunnelProvider::class.java)
|
||||||
|
intent.action = PacketTunnelProvider.ACTION_CONNECT
|
||||||
|
try {
|
||||||
|
context.startService(intent)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
super.onLost(network)
|
||||||
|
Log.d(TAG, "onLost")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register() {
|
||||||
val request = NetworkRequest.Builder()
|
val request = NetworkRequest.Builder()
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
@ -20,22 +49,4 @@ class NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCa
|
||||||
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
manager.registerNetworkCallback(request, this)
|
manager.registerNetworkCallback(request, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAvailable(network: Network) {
|
|
||||||
super.onAvailable(network)
|
|
||||||
Log.d(TAG, "onAvailable")
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
// The message often arrives before the connection is fully established
|
|
||||||
Thread.sleep(1000)
|
|
||||||
val intent = Intent(context, PacketTunnelProvider::class.java)
|
|
||||||
intent.action = PacketTunnelProvider.ACTION_CONNECT
|
|
||||||
context.startService(intent)
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
|
||||||
super.onLost(network)
|
|
||||||
Log.d(TAG, "onLost")
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -2,11 +2,15 @@ package eu.neilalexander.yggdrasil
|
||||||
|
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.system.OsConstants
|
import android.system.OsConstants
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import eu.neilalexander.yggdrasil.YggStateReceiver.Companion.YGG_STATE_INTENT
|
||||||
import mobile.Yggdrasil
|
import mobile.Yggdrasil
|
||||||
|
import org.json.JSONArray
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
@ -14,6 +18,7 @@ import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
|
||||||
private const val TAG = "PacketTunnelProvider"
|
private const val TAG = "PacketTunnelProvider"
|
||||||
|
const val SERVICE_NOTIFICATION_ID = 1000
|
||||||
|
|
||||||
class PacketTunnelProvider: VpnService() {
|
class PacketTunnelProvider: VpnService() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -21,6 +26,7 @@ class PacketTunnelProvider: VpnService() {
|
||||||
|
|
||||||
const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START"
|
const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START"
|
||||||
const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP"
|
const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP"
|
||||||
|
const val ACTION_TOGGLE = "eu.neilalexander.yggdrasil.PacketTunnelProvider.TOGGLE"
|
||||||
const val ACTION_CONNECT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.CONNECT"
|
const val ACTION_CONNECT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.CONNECT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +58,8 @@ class PacketTunnelProvider: VpnService() {
|
||||||
Log.d(TAG, "Intent is null")
|
Log.d(TAG, "Intent is null")
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
|
val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)
|
||||||
return when (intent.action ?: ACTION_STOP) {
|
return when (intent.action ?: ACTION_STOP) {
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
Log.d(TAG, "Stopping...")
|
Log.d(TAG, "Stopping...")
|
||||||
|
@ -59,9 +67,26 @@ class PacketTunnelProvider: VpnService() {
|
||||||
}
|
}
|
||||||
ACTION_CONNECT -> {
|
ACTION_CONNECT -> {
|
||||||
Log.d(TAG, "Connecting...")
|
Log.d(TAG, "Connecting...")
|
||||||
connect(); START_STICKY
|
if (started.get()) {
|
||||||
|
connect();
|
||||||
|
} else {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
START_STICKY
|
||||||
|
}
|
||||||
|
ACTION_TOGGLE -> {
|
||||||
|
Log.d(TAG, "Toggling...")
|
||||||
|
if (started.get()) {
|
||||||
|
stop(); START_NOT_STICKY
|
||||||
|
} else {
|
||||||
|
start(); START_STICKY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
if (!enabled) {
|
||||||
|
Log.d(TAG, "Service is disabled")
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
Log.d(TAG, "Starting...")
|
Log.d(TAG, "Starting...")
|
||||||
start(); START_STICKY
|
start(); START_STICKY
|
||||||
}
|
}
|
||||||
|
@ -73,6 +98,9 @@ class PacketTunnelProvider: VpnService() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val notification = createServiceNotification(this, State.Enabled)
|
||||||
|
startForeground(SERVICE_NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
Log.d(TAG, config.getJSON().toString())
|
Log.d(TAG, config.getJSON().toString())
|
||||||
yggdrasil.startJSON(config.getJSONByteArray())
|
yggdrasil.startJSON(config.getJSONByteArray())
|
||||||
|
|
||||||
|
@ -96,11 +124,11 @@ class PacketTunnelProvider: VpnService() {
|
||||||
// If we don't set metered status of VPN it is considered as metered.
|
// If we don't set metered status of VPN it is considered as metered.
|
||||||
// If we set it to false, then it will inherit this status from underlying network.
|
// If we set it to false, then it will inherit this status from underlying network.
|
||||||
// See: https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean)
|
// See: https://developer.android.com/reference/android/net/VpnService.Builder#setMetered(boolean)
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
builder.setMetered(false)
|
builder.setMetered(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
|
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
|
||||||
if (serverString!!.isNotEmpty()) {
|
if (serverString!!.isNotEmpty()) {
|
||||||
val servers = serverString.split(",")
|
val servers = serverString.split(",")
|
||||||
|
@ -135,7 +163,7 @@ class PacketTunnelProvider: VpnService() {
|
||||||
updater()
|
updater()
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = Intent(STATE_INTENT)
|
var intent = Intent(STATE_INTENT)
|
||||||
intent.putExtra("type", "state")
|
intent.putExtra("type", "state")
|
||||||
intent.putExtra("started", true)
|
intent.putExtra("started", true)
|
||||||
intent.putExtra("ip", yggdrasil.addressString)
|
intent.putExtra("ip", yggdrasil.addressString)
|
||||||
|
@ -143,6 +171,10 @@ class PacketTunnelProvider: VpnService() {
|
||||||
intent.putExtra("coords", yggdrasil.coordsString)
|
intent.putExtra("coords", yggdrasil.coordsString)
|
||||||
intent.putExtra("peers", yggdrasil.peersJSON)
|
intent.putExtra("peers", yggdrasil.peersJSON)
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
|
||||||
|
intent = Intent(YGG_STATE_INTENT)
|
||||||
|
intent.putExtra("state", STATE_ENABLED)
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stop() {
|
private fun stop() {
|
||||||
|
@ -178,11 +210,16 @@ class PacketTunnelProvider: VpnService() {
|
||||||
updateThread = null
|
updateThread = null
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = Intent(STATE_INTENT)
|
var intent = Intent(STATE_INTENT)
|
||||||
intent.putExtra("type", "state")
|
intent.putExtra("type", "state")
|
||||||
intent.putExtra("started", false)
|
intent.putExtra("started", false)
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
|
||||||
|
intent = Intent(YGG_STATE_INTENT)
|
||||||
|
intent.putExtra("state", STATE_DISABLED)
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
|
||||||
|
stopForeground(true)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +231,7 @@ class PacketTunnelProvider: VpnService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updater() {
|
private fun updater() {
|
||||||
|
var lastStateUpdate = System.currentTimeMillis()
|
||||||
updates@ while (started.get()) {
|
updates@ while (started.get()) {
|
||||||
if ((application as GlobalApplication).needUiUpdates()) {
|
if ((application as GlobalApplication).needUiUpdates()) {
|
||||||
val intent = Intent(STATE_INTENT)
|
val intent = Intent(STATE_INTENT)
|
||||||
|
@ -205,24 +243,39 @@ class PacketTunnelProvider: VpnService() {
|
||||||
intent.putExtra("peers", yggdrasil.peersJSON)
|
intent.putExtra("peers", yggdrasil.peersJSON)
|
||||||
intent.putExtra("dht", yggdrasil.dhtjson)
|
intent.putExtra("dht", yggdrasil.dhtjson)
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val curTime = System.currentTimeMillis()
|
||||||
|
if (lastStateUpdate + 10000 < curTime) {
|
||||||
|
val intent = Intent(YGG_STATE_INTENT)
|
||||||
|
var state = STATE_ENABLED
|
||||||
|
val dht = yggdrasil.dhtjson
|
||||||
|
if (dht != null && dht != "null") {
|
||||||
|
val dhtState = JSONArray(dht)
|
||||||
|
val count = dhtState.length()
|
||||||
|
if (count > 1)
|
||||||
|
state = STATE_CONNECTED
|
||||||
|
}
|
||||||
|
intent.putExtra("state", state)
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
|
||||||
|
lastStateUpdate = curTime
|
||||||
|
}
|
||||||
|
|
||||||
if (Thread.currentThread().isInterrupted) {
|
if (Thread.currentThread().isInterrupted) {
|
||||||
break@updates
|
break@updates
|
||||||
}
|
}
|
||||||
try {
|
if (sleep()) return
|
||||||
Thread.sleep(1000)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sleep(): Boolean {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun writer() {
|
private fun writer() {
|
||||||
val buf = ByteArray(65535)
|
val buf = ByteArray(65535)
|
||||||
writes@ while (started.get()) {
|
writes@ while (started.get()) {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package eu.neilalexander.yggdrasil
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
class TileServiceActivity : Activity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Just starting MainActivity
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
startService(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package eu.neilalexander.yggdrasil
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
|
||||||
|
const val STATE_ENABLED = "enabled"
|
||||||
|
const val STATE_DISABLED = "disabled"
|
||||||
|
const val STATE_CONNECTED = "connected"
|
||||||
|
const val STATE_RECONNECTING = "reconnecting"
|
||||||
|
|
||||||
|
class YggStateReceiver(var receiver: StateReceiver): BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val YGG_STATE_INTENT = "eu.neilalexander.yggdrasil.YggStateReceiver.STATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
if (context == null) return
|
||||||
|
|
||||||
|
val state = when (intent?.getStringExtra("state")) {
|
||||||
|
STATE_ENABLED -> State.Enabled
|
||||||
|
STATE_DISABLED -> State.Disabled
|
||||||
|
STATE_CONNECTED -> State.Connected
|
||||||
|
STATE_RECONNECTING -> State.Reconnecting
|
||||||
|
else -> State.Unknown
|
||||||
|
}
|
||||||
|
receiver.onStateChange(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(context: Context) {
|
||||||
|
LocalBroadcastManager.getInstance(context).registerReceiver(
|
||||||
|
this, IntentFilter(YGG_STATE_INTENT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister(context: Context) {
|
||||||
|
LocalBroadcastManager.getInstance(context).unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateReceiver {
|
||||||
|
fun onStateChange(state: State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class-supporter with an Yggdrasil state
|
||||||
|
*/
|
||||||
|
enum class State {
|
||||||
|
Unknown, Disabled, Enabled, Connected, Reconnecting;
|
||||||
|
}
|
113
app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt
Normal file
113
app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package eu.neilalexander.yggdrasil
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.service.quicksettings.Tile
|
||||||
|
import android.service.quicksettings.TileService
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
|
||||||
|
private const val TAG = "TileService"
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
class YggTileService: TileService(), YggStateReceiver.StateReceiver {
|
||||||
|
|
||||||
|
private lateinit var receiver: YggStateReceiver
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
receiver = YggStateReceiver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to override the method onBind to avoid crashes that were detected on Android 8
|
||||||
|
*
|
||||||
|
* The possible reason of crashes is described here:
|
||||||
|
* https://github.com/aosp-mirror/platform_frameworks_base/commit/ee68fd889c2dfcd895b8e73fc39d7b97826dc3d8
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
return try {
|
||||||
|
super.onBind(intent)
|
||||||
|
} catch (th: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileAdded() {
|
||||||
|
super.onTileAdded()
|
||||||
|
updateTileState((application as GlobalApplication).getCurrentState())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTileRemoved() {
|
||||||
|
super.onTileRemoved()
|
||||||
|
updateTileState((application as GlobalApplication).getCurrentState())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartListening() {
|
||||||
|
super.onStartListening()
|
||||||
|
receiver.register(this)
|
||||||
|
updateTileState((application as GlobalApplication).getCurrentState())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopListening() {
|
||||||
|
super.onStopListening()
|
||||||
|
receiver.unregister(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
receiver.unregister(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
super.onClick()
|
||||||
|
// Saving new state
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
|
val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)
|
||||||
|
preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, !enabled) }
|
||||||
|
// Starting or stopping VPN service
|
||||||
|
val intent = Intent(this, PacketTunnelProvider::class.java)
|
||||||
|
intent.action = PacketTunnelProvider.ACTION_TOGGLE
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTileState(state: State) {
|
||||||
|
val tile = qsTile ?: return
|
||||||
|
val oldState = tile.state
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
|
||||||
|
val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)
|
||||||
|
tile.state = when (enabled) {
|
||||||
|
false -> Tile.STATE_INACTIVE
|
||||||
|
true -> Tile.STATE_ACTIVE
|
||||||
|
}
|
||||||
|
var changed = oldState != tile.state
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val oldText = tile.subtitle
|
||||||
|
tile.subtitle = when (state) {
|
||||||
|
State.Enabled -> getText(R.string.tile_enabled)
|
||||||
|
State.Connected -> getText(R.string.tile_connected)
|
||||||
|
else -> getText(R.string.tile_disabled)
|
||||||
|
}
|
||||||
|
changed = changed || (oldText != tile.subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tile if changed state
|
||||||
|
if (changed) {
|
||||||
|
Log.i(TAG, "Updating tile, old state: $oldState, new state: ${tile.state}")
|
||||||
|
/*
|
||||||
|
Force set the icon in the tile, because there is a problem on icon tint in the Android Oreo.
|
||||||
|
Issue: https://github.com/AdguardTeam/AdguardForAndroid/issues/1996
|
||||||
|
*/
|
||||||
|
tile.icon = Icon.createWithResource(applicationContext, R.drawable.ic_tile_icon)
|
||||||
|
tile.updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChange(state: State) {
|
||||||
|
updateTileState(state)
|
||||||
|
}
|
||||||
|
}
|
14
app/src/main/res/drawable/ic_tile_icon.xml
Normal file
14
app/src/main/res/drawable/ic_tile_icon.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="480dp"
|
||||||
|
android:height="480dp"
|
||||||
|
android:viewportWidth="480"
|
||||||
|
android:viewportHeight="480">
|
||||||
|
<path
|
||||||
|
android:pathData="m58.51,474.95c1.37,-5.62 17.69,-45.43 32.05,-78.2c14.7,-33.54 14.42,-32.23 10.32,-49.01c-5.11,-20.95 -4.8,-55.06 0.67,-73.17c19.73,-65.38 70.97,-109.69 182.24,-157.59c36.24,-15.6 56.14,-25.61 71.24,-35.83c26.61,-18.01 54.3,-49.27 63.15,-71.29c1.87,-4.65 3.96,-8.4 4.66,-8.35c2.18,0.16 1.1,66.01 -1.46,88.95c-15.82,142.09 -64.01,234.52 -143.35,274.93c-45.79,23.32 -117.97,31.97 -151.59,18.15c-4.75,-1.95 -9.76,-3.55 -11.12,-3.55c-5.12,0 -23.48,49.02 -28.75,76.76c-1.64,8.61 -4.12,20.42 -4.48,22.23c-8.2,-0.06 -0.53,-0.02 -12.19,-0.02l-12.36,0z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="1.49"
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt"/>
|
||||||
|
</vector>
|
|
@ -22,8 +22,9 @@
|
||||||
<string name="dns_one_server">1 сервер</string>
|
<string name="dns_one_server">1 сервер</string>
|
||||||
<string name="dns_many_servers">%d сервера/серверов</string>
|
<string name="dns_many_servers">%d сервера/серверов</string>
|
||||||
<string name="dns_remove_title">Убрать %s?</string>
|
<string name="dns_remove_title">Убрать %s?</string>
|
||||||
<string name="main_no_connectivity">Нет подключения</string>
|
<string name="dns_already_added_server">Этот сервер уже добавлен.</string>
|
||||||
<string name="main_enabled">Включено</string>
|
<string name="main_no_connectivity">Включено (Нет подключения)</string>
|
||||||
|
<string name="main_enabled">Подключено</string>
|
||||||
<string name="main_disabled">Выключено</string>
|
<string name="main_disabled">Выключено</string>
|
||||||
<string name="main_no_peers">Нет пиров</string>
|
<string name="main_no_peers">Нет пиров</string>
|
||||||
<string name="main_one_peer">1 пир</string>
|
<string name="main_one_peer">1 пир</string>
|
||||||
|
@ -65,8 +66,13 @@
|
||||||
<string name="public_key_hint">Ваш публичный ключ идентифицирует вас в сети. Его распространение безопасно.</string>
|
<string name="public_key_hint">Ваш публичный ключ идентифицирует вас в сети. Его распространение безопасно.</string>
|
||||||
<string name="reset_configuration">Сбросить настройки</string>
|
<string name="reset_configuration">Сбросить настройки</string>
|
||||||
<string name="reset_configuration_hint">Сброс создаст полностью новые настройки. Это изменит ваш публичный ключ и адрес IP.</string>
|
<string name="reset_configuration_hint">Сброс создаст полностью новые настройки. Это изменит ваш публичный ключ и адрес IP.</string>
|
||||||
|
<string name="tile_disabled">Выключено</string>
|
||||||
|
<string name="tile_enabled">Включено (Нет подключения)</string>
|
||||||
|
<string name="tile_connected">Подключено</string>
|
||||||
<string name="location_amsterdam">Амстердам, Нидерланды</string>
|
<string name="location_amsterdam">Амстердам, Нидерланды</string>
|
||||||
<string name="location_prague">Прага, Чехия</string>
|
<string name="location_prague">Прага, Чехия</string>
|
||||||
<string name="location_bratislava">Братислава, Словакия</string>
|
<string name="location_bratislava">Братислава, Словакия</string>
|
||||||
<string name="location_buffalo">Баффало, США</string>
|
<string name="location_buffalo">Баффало, США</string>
|
||||||
|
<string name="channel_name">Сервис VPN</string>
|
||||||
|
<string name="channel_description">Главный канал нотификаций сервиса</string>
|
||||||
</resources>
|
</resources>
|
|
@ -22,8 +22,9 @@
|
||||||
<string name="dns_one_server">1 server</string>
|
<string name="dns_one_server">1 server</string>
|
||||||
<string name="dns_many_servers">%d server</string>
|
<string name="dns_many_servers">%d server</string>
|
||||||
<string name="dns_remove_title">Remove %s?</string>
|
<string name="dns_remove_title">Remove %s?</string>
|
||||||
<string name="main_no_connectivity">No connectivity</string>
|
<string name="dns_already_added_server">This server is already added.</string>
|
||||||
<string name="main_enabled">Enabled</string>
|
<string name="main_no_connectivity">Enabled (No connectivity)</string>
|
||||||
|
<string name="main_enabled">Connected</string>
|
||||||
<string name="main_disabled">Not enabled</string>
|
<string name="main_disabled">Not enabled</string>
|
||||||
<string name="main_no_peers">No peers</string>
|
<string name="main_no_peers">No peers</string>
|
||||||
<string name="main_one_peer">1 peer</string>
|
<string name="main_one_peer">1 peer</string>
|
||||||
|
@ -65,8 +66,13 @@
|
||||||
<string name="public_key_hint">Your public key forms your identity on the network. It is safe to be shared.</string>
|
<string name="public_key_hint">Your public key forms your identity on the network. It is safe to be shared.</string>
|
||||||
<string name="reset_configuration">Reset configuration</string>
|
<string name="reset_configuration">Reset configuration</string>
|
||||||
<string name="reset_configuration_hint">Resetting will overwrite with newly generated configuration. Your public keys and IP address on the network will change.</string>
|
<string name="reset_configuration_hint">Resetting will overwrite with newly generated configuration. Your public keys and IP address on the network will change.</string>
|
||||||
|
<string name="tile_disabled">Disabled</string>
|
||||||
|
<string name="tile_enabled">Enabled (No connectivity)</string>
|
||||||
|
<string name="tile_connected">Connected</string>
|
||||||
<string name="location_amsterdam">Amsterdam, NL</string>
|
<string name="location_amsterdam">Amsterdam, NL</string>
|
||||||
<string name="location_prague">Prague, CZ</string>
|
<string name="location_prague">Prague, CZ</string>
|
||||||
<string name="location_bratislava">Bratislava, SK</string>
|
<string name="location_bratislava">Bratislava, SK</string>
|
||||||
<string name="location_buffalo">Buffalo, US</string>
|
<string name="location_buffalo">Buffalo, US</string>
|
||||||
|
<string name="channel_name">VPN Service</string>
|
||||||
|
<string name="channel_description">Main channel for foreground notification</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue