diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index edec37d..9cf3229 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
-
-
@@ -24,7 +21,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt
index 6262d7b..d95496b 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt
@@ -9,6 +9,7 @@ import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.widget.*
+import androidx.preference.PreferenceManager
import com.google.android.material.textfield.TextInputEditText
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.setView(view)
builder.setPositiveButton(getString(R.string.add)) { dialog, _ ->
- servers.add(input.text.toString())
- preferences.edit().apply {
- putString(KEY_DNS_SERVERS, servers.joinToString(","))
- commit()
+ val server = input.text.toString()
+ if (!servers.contains(server)) {
+ servers.add(server)
+ 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, _ ->
dialog.cancel()
@@ -85,7 +90,7 @@ class DnsActivity : AppCompatActivity() {
enableChromeFix.toggle()
}
- preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
servers = if (serverString!!.isNotEmpty()) {
serverString.split(",").toMutableList()
@@ -157,12 +162,17 @@ class DnsActivity : AppCompatActivity() {
addButton.tag = server
addButton.setOnClickListener { button ->
- servers.add(button.tag as String)
- preferences.edit().apply {
- this.putString(KEY_DNS_SERVERS, servers.joinToString(","))
- this.commit()
+ val serverString = button.tag as String
+ if (!servers.contains(serverString)) {
+ servers.add(serverString)
+ 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 {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt
index 58cda01..f5e247e 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt
@@ -1,15 +1,28 @@
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 var currentState: State = State.Disabled
var updaterConnections: Int = 0
override fun onCreate() {
super.onCreate()
config = ConfigurationProxy(applicationContext)
val callback = NetworkStateCallback(this)
+ callback.register()
+ val receiver = YggStateReceiver(this)
+ receiver.register(this)
}
fun subscribe() {
@@ -25,4 +38,63 @@ class GlobalApplication: Application() {
fun needUiUpdates(): Boolean {
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()
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
index 613d254..dc79acb 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
@@ -11,7 +11,9 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.preference.PreferenceManager
import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT
import mobile.Mobile
import org.json.JSONArray
@@ -76,6 +78,8 @@ class MainActivity : AppCompatActivity() {
startService(intent)
}
}
+ val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ preferences.edit(commit = true) { putBoolean(PREF_KEY_ENABLED, isChecked) }
}
val enableYggdrasilPanel = findViewById(R.id.enableYggdrasilPanel)
@@ -123,7 +127,8 @@ class MainActivity : AppCompatActivity() {
LocalBroadcastManager.getInstance(this).registerReceiver(
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, "")
if (serverString!!.isNotEmpty()) {
val servers = serverString.split(",")
@@ -149,7 +154,6 @@ class MainActivity : AppCompatActivity() {
when (intent.getStringExtra("type")) {
"state" -> {
enabledLabel.text = if (intent.getBooleanExtra("started", false)) {
- enabledSwitch.isChecked = true
var count = 0
if (intent.hasExtra("dht")) {
val dht = intent.getStringExtra("dht")
@@ -166,7 +170,6 @@ class MainActivity : AppCompatActivity() {
getString(R.string.main_enabled)
}
} else {
- enabledSwitch.isChecked = false
enabledLabel.setTextColor(Color.GRAY)
getString(R.string.main_disabled)
}
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/NetworkStateCallback.kt b/app/src/main/java/eu/neilalexander/yggdrasil/NetworkStateCallback.kt
index 404fd23..7775308 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/NetworkStateCallback.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/NetworkStateCallback.kt
@@ -3,39 +3,50 @@ package eu.neilalexander.yggdrasil
import android.content.Context
import android.content.Intent
import android.net.*
+import android.os.Build
import android.util.Log
+import androidx.preference.PreferenceManager
private const val TAG = "Network"
class NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCallback() {
- init {
- val request = NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build()
-
- val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- 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()
+ 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()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .build()
+
+ val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ manager.registerNetworkCallback(request, this)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt
index b761efc..4923a91 100644
--- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt
@@ -2,11 +2,15 @@ package eu.neilalexander.yggdrasil
import android.content.*
import android.net.VpnService
+import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.OsConstants
import android.util.Log
import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.preference.PreferenceManager
+import eu.neilalexander.yggdrasil.YggStateReceiver.Companion.YGG_STATE_INTENT
import mobile.Yggdrasil
+import org.json.JSONArray
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.concurrent.atomic.AtomicBoolean
@@ -14,6 +18,7 @@ import kotlin.concurrent.thread
private const val TAG = "PacketTunnelProvider"
+const val SERVICE_NOTIFICATION_ID = 1000
class PacketTunnelProvider: VpnService() {
companion object {
@@ -21,6 +26,7 @@ class PacketTunnelProvider: VpnService() {
const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START"
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"
}
@@ -52,6 +58,8 @@ class PacketTunnelProvider: VpnService() {
Log.d(TAG, "Intent is null")
return START_NOT_STICKY
}
+ val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ val enabled = preferences.getBoolean(PREF_KEY_ENABLED, false)
return when (intent.action ?: ACTION_STOP) {
ACTION_STOP -> {
Log.d(TAG, "Stopping...")
@@ -59,9 +67,26 @@ class PacketTunnelProvider: VpnService() {
}
ACTION_CONNECT -> {
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 -> {
+ if (!enabled) {
+ Log.d(TAG, "Service is disabled")
+ return START_NOT_STICKY
+ }
Log.d(TAG, "Starting...")
start(); START_STICKY
}
@@ -73,6 +98,9 @@ class PacketTunnelProvider: VpnService() {
return
}
+ val notification = createServiceNotification(this, State.Enabled)
+ startForeground(SERVICE_NOTIFICATION_ID, notification)
+
Log.d(TAG, config.getJSON().toString())
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 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)
- 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)
}
- val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext)
+ val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
val serverString = preferences.getString(KEY_DNS_SERVERS, "")
if (serverString!!.isNotEmpty()) {
val servers = serverString.split(",")
@@ -135,7 +163,7 @@ class PacketTunnelProvider: VpnService() {
updater()
}
- val intent = Intent(STATE_INTENT)
+ var intent = Intent(STATE_INTENT)
intent.putExtra("type", "state")
intent.putExtra("started", true)
intent.putExtra("ip", yggdrasil.addressString)
@@ -143,6 +171,10 @@ class PacketTunnelProvider: VpnService() {
intent.putExtra("coords", yggdrasil.coordsString)
intent.putExtra("peers", yggdrasil.peersJSON)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
+
+ intent = Intent(YGG_STATE_INTENT)
+ intent.putExtra("state", STATE_ENABLED)
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
private fun stop() {
@@ -178,11 +210,16 @@ class PacketTunnelProvider: VpnService() {
updateThread = null
}
- val intent = Intent(STATE_INTENT)
+ var intent = Intent(STATE_INTENT)
intent.putExtra("type", "state")
intent.putExtra("started", false)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
+ intent = Intent(YGG_STATE_INTENT)
+ intent.putExtra("state", STATE_DISABLED)
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
+
+ stopForeground(true)
stopSelf()
}
@@ -194,6 +231,7 @@ class PacketTunnelProvider: VpnService() {
}
private fun updater() {
+ var lastStateUpdate = System.currentTimeMillis()
updates@ while (started.get()) {
if ((application as GlobalApplication).needUiUpdates()) {
val intent = Intent(STATE_INTENT)
@@ -205,22 +243,37 @@ class PacketTunnelProvider: VpnService() {
intent.putExtra("peers", yggdrasil.peersJSON)
intent.putExtra("dht", yggdrasil.dhtjson)
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) {
break@updates
}
- try {
- Thread.sleep(1000)
- } catch (e: InterruptedException) {
- return
- }
+ if (sleep()) return
+ }
+ }
+
+ private fun sleep(): Boolean {
+ try {
+ Thread.sleep(1000)
+ } catch (e: InterruptedException) {
+ return true
}
+ return false
}
private fun writer() {
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/TileServiceActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/TileServiceActivity.kt
new file mode 100644
index 0000000..a4f71cc
--- /dev/null
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/TileServiceActivity.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/YggStateReceiver.kt b/app/src/main/java/eu/neilalexander/yggdrasil/YggStateReceiver.kt
new file mode 100644
index 0000000..f6745f8
--- /dev/null
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/YggStateReceiver.kt
@@ -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;
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt b/app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt
new file mode 100644
index 0000000..4725bd4
--- /dev/null
+++ b/app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_tile_icon.xml b/app/src/main/res/drawable/ic_tile_icon.xml
new file mode 100644
index 0000000..fae8232
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tile_icon.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 7ecf48d..e687b53 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -22,8 +22,9 @@
1 сервер
%d сервера/серверов
Убрать %s?
- Нет подключения
- Включено
+ Этот сервер уже добавлен.
+ Включено (Нет подключения)
+ Подключено
Выключено
Нет пиров
1 пир
@@ -65,8 +66,13 @@
Ваш публичный ключ идентифицирует вас в сети. Его распространение безопасно.
Сбросить настройки
Сброс создаст полностью новые настройки. Это изменит ваш публичный ключ и адрес IP.
+ Выключено
+ Включено (Нет подключения)
+ Подключено
Амстердам, Нидерланды
Прага, Чехия
Братислава, Словакия
Баффало, США
+ Сервис VPN
+ Главный канал нотификаций сервиса
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 109b144..387b9c9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -22,8 +22,9 @@
1 server
%d server
Remove %s?
- No connectivity
- Enabled
+ This server is already added.
+ Enabled (No connectivity)
+ Connected
Not enabled
No peers
1 peer
@@ -65,8 +66,13 @@
Your public key forms your identity on the network. It is safe to be shared.
Reset configuration
Resetting will overwrite with newly generated configuration. Your public keys and IP address on the network will change.
+ Disabled
+ Enabled (No connectivity)
+ Connected
Amsterdam, NL
Prague, CZ
Bratislava, SK
Buffalo, US
+ VPN Service
+ Main channel for foreground notification
\ No newline at end of file