diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt index fc1c3ef..8b2b5e7 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt @@ -108,7 +108,7 @@ class DnsActivity : AppCompatActivity() { view.findViewById(R.id.deletePeerButton).setOnClickListener { button -> val builder: AlertDialog.Builder = AlertDialog.Builder(this) - builder.setTitle("Remove ${server}?") + builder.setTitle(getString(R.string.dns_remove_title, server)) builder.setPositiveButton(getString(R.string.remove)) { dialog, _ -> servers.removeAt(button.tag as Int) preferences.edit().apply { diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt index b2af6e0..13fedff 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt @@ -1,25 +1,27 @@ package eu.neilalexander.yggdrasil import android.app.Application -import android.content.IntentFilter -import androidx.localbroadcastmanager.content.LocalBroadcastManager class GlobalApplication: Application() { - private var state = PacketTunnelState private lateinit var config: ConfigurationProxy + var updaterConnections: Int = 0 override fun onCreate() { super.onCreate() config = ConfigurationProxy(applicationContext) - - LocalBroadcastManager.getInstance(this).registerReceiver( - state, IntentFilter(PacketTunnelProvider.RECEIVER_INTENT) - ) } - override fun onTerminate() { - super.onTerminate() + fun subscribe() { + updaterConnections++ + } - LocalBroadcastManager.getInstance(this).unregisterReceiver(state) + fun unsubscribe() { + if (updaterConnections > 0) { + updaterConnections-- + } + } + + fun needUiUpdates(): Boolean { + return updaterConnections > 0 } } \ 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 82fb296..d33d4ac 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt @@ -12,12 +12,12 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.localbroadcastmanager.content.LocalBroadcastManager +import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT import mobile.Mobile +import org.json.JSONArray class MainActivity : AppCompatActivity() { - private var state = PacketTunnelState - private lateinit var enabledSwitch: Switch private lateinit var enabledLabel: TextView private lateinit var ipAddressLabel: TextView @@ -116,20 +116,27 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() LocalBroadcastManager.getInstance(this).registerReceiver( - receiver, IntentFilter(PacketTunnelState.RECEIVER_INTENT) + receiver, IntentFilter(STATE_INTENT) ) val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext) val serverString = preferences.getString(KEY_DNS_SERVERS, "") if (serverString!!.isNotEmpty()) { val servers = serverString.split(",") dnsLabel.text = when (servers.size) { - 0 -> "No servers" - 1 -> "1 server" - else -> "${servers.size} servers" + 0 -> getString(R.string.dns_no_servers) + 1 -> getString(R.string.dns_one_server) + else -> getString(R.string.dns_many_servers, servers.size) } } else { - dnsLabel.text = "No servers" + dnsLabel.text = getString(R.string.dns_no_servers) } + (application as GlobalApplication).subscribe() + } + + override fun onPause() { + super.onPause() + (application as GlobalApplication).unsubscribe() + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } private val receiver: BroadcastReceiver = object : BroadcastReceiver() { @@ -138,33 +145,39 @@ class MainActivity : AppCompatActivity() { "state" -> { enabledLabel.text = if (intent.getBooleanExtra("started", false)) { enabledSwitch.isChecked = true - if (state.dhtCount() == 0) { + var count = 0 + if (intent.hasExtra("dht")) { + val dht = intent.getStringExtra("dht") + if (dht != null && dht != "null") { + val dhtState = JSONArray(dht) + count = dhtState.length() + } + } + if (count == 0) { enabledLabel.setTextColor(Color.RED) - "No connectivity" + getString(R.string.main_no_connectivity) } else { enabledLabel.setTextColor(Color.GREEN) - "Enabled" + getString(R.string.main_enabled) } } else { enabledSwitch.isChecked = false enabledLabel.setTextColor(Color.GRAY) - "Not enabled" + getString(R.string.main_disabled) } ipAddressLabel.text = intent.getStringExtra("ip") ?: "N/A" subnetLabel.text = intent.getStringExtra("subnet") ?: "N/A" coordinatesLabel.text = intent.getStringExtra("coords") ?: "[]" - peersLabel.text = when (val count = state.peerCount()) { - 0 -> "No peers" - 1 -> "1 peer" - else -> "$count peers" + if (intent.hasExtra("peers")) { + val peerState = JSONArray(intent.getStringExtra("peers") ?: "[]") + peersLabel.text = when (val count = peerState.length()) { + 0 -> getString(R.string.main_no_peers) + 1 -> getString(R.string.main_one_peer) + else -> getString(R.string.main_many_peers, count) + } } } } } } - - override fun onPause() { - super.onPause() - LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) - } } diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt index 4289165..76342a9 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt @@ -17,7 +17,7 @@ private const val TAG = "PacketTunnelProvider" class PacketTunnelProvider: VpnService() { companion object { - const val RECEIVER_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.MESSAGE" + const val STATE_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STATE_MESSAGE" const val ACTION_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START" const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP" @@ -130,7 +130,7 @@ class PacketTunnelProvider: VpnService() { updater() } - val intent = Intent(RECEIVER_INTENT) + val intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", true) intent.putExtra("ip", yggdrasil.addressString) @@ -173,7 +173,7 @@ class PacketTunnelProvider: VpnService() { updateThread = null } - val intent = Intent(RECEIVER_INTENT) + val intent = Intent(STATE_INTENT) intent.putExtra("type", "state") intent.putExtra("started", false) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) @@ -183,15 +183,24 @@ class PacketTunnelProvider: VpnService() { private fun updater() { updates@ while (started.get()) { - val intent = Intent(RECEIVER_INTENT) - intent.putExtra("type", "state") - intent.putExtra("started", true) - intent.putExtra("ip", yggdrasil.addressString) - intent.putExtra("subnet", yggdrasil.subnetString) - intent.putExtra("coords", yggdrasil.coordsString) - intent.putExtra("peers", yggdrasil.peersJSON) - intent.putExtra("dht", yggdrasil.dhtjson) - LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + if ((application as GlobalApplication).needUiUpdates()) { + Log.d(TAG, "Sending stats to UI...") + val intent = Intent(STATE_INTENT) + intent.putExtra("type", "state") + intent.putExtra("started", true) + intent.putExtra("ip", yggdrasil.addressString) + intent.putExtra("subnet", yggdrasil.subnetString) + intent.putExtra("coords", yggdrasil.coordsString) + intent.putExtra("peers", yggdrasil.peersJSON) + intent.putExtra("dht", yggdrasil.dhtjson) + LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + } else { + try { + Thread.sleep(1000) + } catch (e: InterruptedException) { + return + } + } if (Thread.currentThread().isInterrupted) { break@updates } diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelState.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelState.kt deleted file mode 100644 index 5d05231..0000000 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelState.kt +++ /dev/null @@ -1,53 +0,0 @@ -package eu.neilalexander.yggdrasil - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import org.json.JSONArray - -object PacketTunnelState: BroadcastReceiver() { - var dhtState: JSONArray? = null - private set - - var peersState: JSONArray? = null - private set - - const val RECEIVER_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelState.MESSAGE" - - fun peerCount(): Int { - if (peersState == null) { - return 0 - } - return peersState!!.length() - } - - fun dhtCount(): Int { - if (dhtState == null) { - return 0 - } - return dhtState!!.length() - } - - override fun onReceive(context: Context?, intent: Intent) { - when (intent.getStringExtra("type")) { - "state" -> { - var dht = intent.getStringExtra("dht") - var peers = intent.getStringExtra("peers") - - if (dht == null || dht == "null") { - dht = "[]" - } - if (peers == null || peers == "null") { - peers = "[]" - } - - peersState = JSONArray(peers) - dhtState = JSONArray(dht) - - intent.action = RECEIVER_INTENT - LocalBroadcastManager.getInstance(context!!).sendBroadcast(intent) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt index b9ec651..5664fdc 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt @@ -1,19 +1,25 @@ package eu.neilalexander.yggdrasil import android.app.AlertDialog +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.widget.* +import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.textfield.TextInputEditText import org.json.JSONArray +import org.json.JSONObject class PeersActivity : AppCompatActivity() { - private var state = PacketTunnelState private lateinit var config: ConfigurationProxy private lateinit var inflater: LayoutInflater + private lateinit var peers: Array private lateinit var connectedTableLayout: TableLayout private lateinit var connectedTableLabel: TextView @@ -29,6 +35,7 @@ class PeersActivity : AppCompatActivity() { config = ConfigurationProxy(applicationContext) inflater = LayoutInflater.from(this) + peers = emptyArray() connectedTableLayout = findViewById(R.id.connectedPeersTableLayout) connectedTableLabel = findViewById(R.id.connectedPeersLabel) @@ -52,16 +59,16 @@ class PeersActivity : AppCompatActivity() { val view = inflater.inflate(R.layout.dialog_addpeer, null) val input = view.findViewById(R.id.addPeerInput) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog)) - builder.setTitle("Add Configured Peer") + builder.setTitle(getString(R.string.peers_add_peer)) builder.setView(view) - builder.setPositiveButton("Add") { dialog, _ -> + builder.setPositiveButton(getString(R.string.peers_add)) { dialog, _ -> config.updateJSON { json -> json.getJSONArray("Peers").put(input.text.toString().trim()) } dialog.dismiss() updateConfiguredPeers() } - builder.setNegativeButton("Cancel") { dialog, _ -> + builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() @@ -70,22 +77,32 @@ class PeersActivity : AppCompatActivity() { override fun onResume() { super.onResume() + LocalBroadcastManager.getInstance(this).registerReceiver( + receiver, IntentFilter(PacketTunnelProvider.STATE_INTENT) + ) + (application as GlobalApplication).subscribe() updateConfiguredPeers() updateConnectedPeers() } + override fun onPause() { + super.onPause() + (application as GlobalApplication).unsubscribe() + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) + } + private fun updateConfiguredPeers() { val peers = config.getJSON().getJSONArray("Peers") when (peers.length()) { 0 -> { configuredTableLayout.visibility = View.GONE - configuredTableLabel.text = "No peers currently configured" + configuredTableLabel.text = getString(R.string.peers_no_configured_title) } else -> { configuredTableLayout.visibility = View.VISIBLE - configuredTableLabel.text = "Configured Peers" + configuredTableLabel.text = getString(R.string.peers_configured_title) configuredTableLayout.removeAllViewsInLayout() for (i in 0 until peers.length()) { @@ -96,15 +113,15 @@ class PeersActivity : AppCompatActivity() { view.findViewById(R.id.deletePeerButton).setOnClickListener { button -> val builder: AlertDialog.Builder = AlertDialog.Builder(this) - builder.setTitle("Remove ${peer}?") - builder.setPositiveButton("Remove") { dialog, _ -> + builder.setTitle(getString(R.string.peers_remove_title, peer)) + builder.setPositiveButton(getString(R.string.peers_remove)) { dialog, _ -> config.updateJSON { json -> json.getJSONArray("Peers").remove(button.tag as Int) } dialog.dismiss() updateConfiguredPeers() } - builder.setNegativeButton("Cancel") { dialog, _ -> + builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() @@ -116,20 +133,17 @@ class PeersActivity : AppCompatActivity() { } private fun updateConnectedPeers() { - val peers = state.peersState ?: JSONArray("[]") - - when (peers.length()) { + when (peers.size) { 0 -> { connectedTableLayout.visibility = View.GONE - connectedTableLabel.text = "No peers currently connected" + connectedTableLabel.text = getString(R.string.peers_no_connected_title) } else -> { connectedTableLayout.visibility = View.VISIBLE - connectedTableLabel.text = "Connected Peers" + connectedTableLabel.text = getString(R.string.peers_connected_title) connectedTableLayout.removeAllViewsInLayout() - for (i in 0 until peers.length()) { - val peer = peers.getJSONObject(i) + for (peer in peers) { val view = inflater.inflate(R.layout.peers_connected, null) val ip = peer.getString("IP") view.findViewById(R.id.addressLabel).text = ip @@ -139,4 +153,23 @@ class PeersActivity : AppCompatActivity() { } } } + + private val receiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + when (intent.getStringExtra("type")) { + "state" -> { + if (intent.hasExtra("peers")) { + val peersArray = JSONArray(intent.getStringExtra("peers") ?: "[]") + val array = Array(peersArray.length()) { i -> + peersArray.getJSONObject(i) + } + array.sortWith(compareBy { it.getString("IP") }) + peers = array + + updateConnectedPeers() + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/SettingsActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/SettingsActivity.kt index 4e4fbcc..11f9b32 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/SettingsActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/SettingsActivity.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.widget.* -import androidx.core.view.setPadding import androidx.core.widget.doOnTextChanged import org.json.JSONObject @@ -33,7 +32,7 @@ class SettingsActivity : AppCompatActivity() { deviceNameEntry.doOnTextChanged { text, _, _, _ -> config.updateJSON { cfg -> - var nodeinfo = cfg.optJSONObject("NodeInfo") + val nodeinfo = cfg.optJSONObject("NodeInfo") if (nodeinfo == null) { cfg.put("NodeInfo", JSONObject("{}")) } @@ -42,16 +41,16 @@ class SettingsActivity : AppCompatActivity() { } resetConfigurationRow.setOnClickListener { - var view = inflater.inflate(R.layout.dialog_resetconfig, null) + val view = inflater.inflate(R.layout.dialog_resetconfig, null) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog)) - builder.setTitle("Warning") + builder.setTitle(getString(R.string.settings_warning_title)) builder.setView(view) - builder.setPositiveButton("Reset") { dialog, _ -> + builder.setPositiveButton(getString(R.string.settings_reset)) { dialog, _ -> config.resetJSON() updateView() dialog.dismiss() } - builder.setNegativeButton("Cancel") { dialog, _ -> + builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.cancel() } builder.show() @@ -68,7 +67,7 @@ class SettingsActivity : AppCompatActivity() { updateView() } - fun updateView() { + private fun updateView() { val nodeinfo = config.getJSON().optJSONObject("NodeInfo") if (nodeinfo != null) { deviceNameEntry.setText(nodeinfo.getString("name"), TextView.BufferType.EDITABLE) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 242d96b..a159e7d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,4 +18,24 @@ Fix Chrome-based browsers If you do not have IPv6 internet connectivity, this option should help Chrome-based browsers to resolve Yggdrasil domain names correctly. DNS fixes + No servers + 1 server + %d server + Remove %s? + No connectivity + Enabled + Not enabled + No peers + 1 peer + %d peers + Add Configured Peer + Add + Remove %s? + Remove + No peers currently configured + Configured Peers + No peers currently connected + Connected Peers + Warning + Reset \ No newline at end of file