Optimized UI refresh to save battery. Extracted all strings to xml to enable localizations.

This commit is contained in:
Revertron 2022-11-07 17:14:51 +01:00
parent 8615d43761
commit a0fd3854bc
8 changed files with 142 additions and 119 deletions

View file

@ -108,7 +108,7 @@ class DnsActivity : AppCompatActivity() {
view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button -> view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button ->
val builder: AlertDialog.Builder = AlertDialog.Builder(this) 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, _ -> builder.setPositiveButton(getString(R.string.remove)) { dialog, _ ->
servers.removeAt(button.tag as Int) servers.removeAt(button.tag as Int)
preferences.edit().apply { preferences.edit().apply {

View file

@ -1,25 +1,27 @@
package eu.neilalexander.yggdrasil package eu.neilalexander.yggdrasil
import android.app.Application import android.app.Application
import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
class GlobalApplication: Application() { class GlobalApplication: Application() {
private var state = PacketTunnelState
private lateinit var config: ConfigurationProxy private lateinit var config: ConfigurationProxy
var updaterConnections: Int = 0
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
config = ConfigurationProxy(applicationContext) config = ConfigurationProxy(applicationContext)
LocalBroadcastManager.getInstance(this).registerReceiver(
state, IntentFilter(PacketTunnelProvider.RECEIVER_INTENT)
)
} }
override fun onTerminate() { fun subscribe() {
super.onTerminate() updaterConnections++
}
LocalBroadcastManager.getInstance(this).unregisterReceiver(state) fun unsubscribe() {
if (updaterConnections > 0) {
updaterConnections--
}
}
fun needUiUpdates(): Boolean {
return updaterConnections > 0
} }
} }

View file

@ -12,12 +12,12 @@ 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.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT
import mobile.Mobile import mobile.Mobile
import org.json.JSONArray
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private var state = PacketTunnelState
private lateinit var enabledSwitch: Switch private lateinit var enabledSwitch: Switch
private lateinit var enabledLabel: TextView private lateinit var enabledLabel: TextView
private lateinit var ipAddressLabel: TextView private lateinit var ipAddressLabel: TextView
@ -116,20 +116,27 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
LocalBroadcastManager.getInstance(this).registerReceiver( LocalBroadcastManager.getInstance(this).registerReceiver(
receiver, IntentFilter(PacketTunnelState.RECEIVER_INTENT) receiver, IntentFilter(STATE_INTENT)
) )
val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext) val preferences = androidx.preference.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(",")
dnsLabel.text = when (servers.size) { dnsLabel.text = when (servers.size) {
0 -> "No servers" 0 -> getString(R.string.dns_no_servers)
1 -> "1 server" 1 -> getString(R.string.dns_one_server)
else -> "${servers.size} servers" else -> getString(R.string.dns_many_servers, servers.size)
} }
} else { } 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() { private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
@ -138,33 +145,39 @@ class MainActivity : AppCompatActivity() {
"state" -> { "state" -> {
enabledLabel.text = if (intent.getBooleanExtra("started", false)) { enabledLabel.text = if (intent.getBooleanExtra("started", false)) {
enabledSwitch.isChecked = true 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) enabledLabel.setTextColor(Color.RED)
"No connectivity" getString(R.string.main_no_connectivity)
} else { } else {
enabledLabel.setTextColor(Color.GREEN) enabledLabel.setTextColor(Color.GREEN)
"Enabled" getString(R.string.main_enabled)
} }
} else { } else {
enabledSwitch.isChecked = false enabledSwitch.isChecked = false
enabledLabel.setTextColor(Color.GRAY) enabledLabel.setTextColor(Color.GRAY)
"Not enabled" getString(R.string.main_disabled)
} }
ipAddressLabel.text = intent.getStringExtra("ip") ?: "N/A" ipAddressLabel.text = intent.getStringExtra("ip") ?: "N/A"
subnetLabel.text = intent.getStringExtra("subnet") ?: "N/A" subnetLabel.text = intent.getStringExtra("subnet") ?: "N/A"
coordinatesLabel.text = intent.getStringExtra("coords") ?: "[]" coordinatesLabel.text = intent.getStringExtra("coords") ?: "[]"
peersLabel.text = when (val count = state.peerCount()) { if (intent.hasExtra("peers")) {
0 -> "No peers" val peerState = JSONArray(intent.getStringExtra("peers") ?: "[]")
1 -> "1 peer" peersLabel.text = when (val count = peerState.length()) {
else -> "$count peers" 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)
}
} }

View file

@ -17,7 +17,7 @@ private const val TAG = "PacketTunnelProvider"
class PacketTunnelProvider: VpnService() { class PacketTunnelProvider: VpnService() {
companion object { 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_START = "eu.neilalexander.yggdrasil.PacketTunnelProvider.START"
const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP" const val ACTION_STOP = "eu.neilalexander.yggdrasil.PacketTunnelProvider.STOP"
@ -130,7 +130,7 @@ class PacketTunnelProvider: VpnService() {
updater() updater()
} }
val intent = Intent(RECEIVER_INTENT) val 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)
@ -173,7 +173,7 @@ class PacketTunnelProvider: VpnService() {
updateThread = null updateThread = null
} }
val intent = Intent(RECEIVER_INTENT) val 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)
@ -183,15 +183,24 @@ class PacketTunnelProvider: VpnService() {
private fun updater() { private fun updater() {
updates@ while (started.get()) { updates@ while (started.get()) {
val intent = Intent(RECEIVER_INTENT) if ((application as GlobalApplication).needUiUpdates()) {
intent.putExtra("type", "state") Log.d(TAG, "Sending stats to UI...")
intent.putExtra("started", true) val intent = Intent(STATE_INTENT)
intent.putExtra("ip", yggdrasil.addressString) intent.putExtra("type", "state")
intent.putExtra("subnet", yggdrasil.subnetString) intent.putExtra("started", true)
intent.putExtra("coords", yggdrasil.coordsString) intent.putExtra("ip", yggdrasil.addressString)
intent.putExtra("peers", yggdrasil.peersJSON) intent.putExtra("subnet", yggdrasil.subnetString)
intent.putExtra("dht", yggdrasil.dhtjson) intent.putExtra("coords", yggdrasil.coordsString)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent) 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) { if (Thread.currentThread().isInterrupted) {
break@updates break@updates
} }

View file

@ -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)
}
}
}
}

View file

@ -1,19 +1,25 @@
package eu.neilalexander.yggdrasil package eu.neilalexander.yggdrasil
import android.app.AlertDialog 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 androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.view.ContextThemeWrapper 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.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject
class PeersActivity : AppCompatActivity() { class PeersActivity : AppCompatActivity() {
private var state = PacketTunnelState
private lateinit var config: ConfigurationProxy private lateinit var config: ConfigurationProxy
private lateinit var inflater: LayoutInflater private lateinit var inflater: LayoutInflater
private lateinit var peers: Array<JSONObject>
private lateinit var connectedTableLayout: TableLayout private lateinit var connectedTableLayout: TableLayout
private lateinit var connectedTableLabel: TextView private lateinit var connectedTableLabel: TextView
@ -29,6 +35,7 @@ class PeersActivity : AppCompatActivity() {
config = ConfigurationProxy(applicationContext) config = ConfigurationProxy(applicationContext)
inflater = LayoutInflater.from(this) inflater = LayoutInflater.from(this)
peers = emptyArray()
connectedTableLayout = findViewById(R.id.connectedPeersTableLayout) connectedTableLayout = findViewById(R.id.connectedPeersTableLayout)
connectedTableLabel = findViewById(R.id.connectedPeersLabel) connectedTableLabel = findViewById(R.id.connectedPeersLabel)
@ -52,16 +59,16 @@ class PeersActivity : AppCompatActivity() {
val view = inflater.inflate(R.layout.dialog_addpeer, null) val view = inflater.inflate(R.layout.dialog_addpeer, null)
val input = view.findViewById<TextInputEditText>(R.id.addPeerInput) val input = view.findViewById<TextInputEditText>(R.id.addPeerInput)
val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog)) 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.setView(view)
builder.setPositiveButton("Add") { dialog, _ -> builder.setPositiveButton(getString(R.string.peers_add)) { dialog, _ ->
config.updateJSON { json -> config.updateJSON { json ->
json.getJSONArray("Peers").put(input.text.toString().trim()) json.getJSONArray("Peers").put(input.text.toString().trim())
} }
dialog.dismiss() dialog.dismiss()
updateConfiguredPeers() updateConfiguredPeers()
} }
builder.setNegativeButton("Cancel") { dialog, _ -> builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
builder.show() builder.show()
@ -70,22 +77,32 @@ class PeersActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
LocalBroadcastManager.getInstance(this).registerReceiver(
receiver, IntentFilter(PacketTunnelProvider.STATE_INTENT)
)
(application as GlobalApplication).subscribe()
updateConfiguredPeers() updateConfiguredPeers()
updateConnectedPeers() updateConnectedPeers()
} }
override fun onPause() {
super.onPause()
(application as GlobalApplication).unsubscribe()
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
}
private fun updateConfiguredPeers() { private fun updateConfiguredPeers() {
val peers = config.getJSON().getJSONArray("Peers") val peers = config.getJSON().getJSONArray("Peers")
when (peers.length()) { when (peers.length()) {
0 -> { 0 -> {
configuredTableLayout.visibility = View.GONE configuredTableLayout.visibility = View.GONE
configuredTableLabel.text = "No peers currently configured" configuredTableLabel.text = getString(R.string.peers_no_configured_title)
} }
else -> { else -> {
configuredTableLayout.visibility = View.VISIBLE configuredTableLayout.visibility = View.VISIBLE
configuredTableLabel.text = "Configured Peers" configuredTableLabel.text = getString(R.string.peers_configured_title)
configuredTableLayout.removeAllViewsInLayout() configuredTableLayout.removeAllViewsInLayout()
for (i in 0 until peers.length()) { for (i in 0 until peers.length()) {
@ -96,15 +113,15 @@ class PeersActivity : AppCompatActivity() {
view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button -> view.findViewById<ImageButton>(R.id.deletePeerButton).setOnClickListener { button ->
val builder: AlertDialog.Builder = AlertDialog.Builder(this) val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle("Remove ${peer}?") builder.setTitle(getString(R.string.peers_remove_title, peer))
builder.setPositiveButton("Remove") { dialog, _ -> builder.setPositiveButton(getString(R.string.peers_remove)) { dialog, _ ->
config.updateJSON { json -> config.updateJSON { json ->
json.getJSONArray("Peers").remove(button.tag as Int) json.getJSONArray("Peers").remove(button.tag as Int)
} }
dialog.dismiss() dialog.dismiss()
updateConfiguredPeers() updateConfiguredPeers()
} }
builder.setNegativeButton("Cancel") { dialog, _ -> builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
builder.show() builder.show()
@ -116,20 +133,17 @@ class PeersActivity : AppCompatActivity() {
} }
private fun updateConnectedPeers() { private fun updateConnectedPeers() {
val peers = state.peersState ?: JSONArray("[]") when (peers.size) {
when (peers.length()) {
0 -> { 0 -> {
connectedTableLayout.visibility = View.GONE connectedTableLayout.visibility = View.GONE
connectedTableLabel.text = "No peers currently connected" connectedTableLabel.text = getString(R.string.peers_no_connected_title)
} }
else -> { else -> {
connectedTableLayout.visibility = View.VISIBLE connectedTableLayout.visibility = View.VISIBLE
connectedTableLabel.text = "Connected Peers" connectedTableLabel.text = getString(R.string.peers_connected_title)
connectedTableLayout.removeAllViewsInLayout() connectedTableLayout.removeAllViewsInLayout()
for (i in 0 until peers.length()) { for (peer in peers) {
val peer = peers.getJSONObject(i)
val view = inflater.inflate(R.layout.peers_connected, null) val view = inflater.inflate(R.layout.peers_connected, null)
val ip = peer.getString("IP") val ip = peer.getString("IP")
view.findViewById<TextView>(R.id.addressLabel).text = ip view.findViewById<TextView>(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()
}
}
}
}
}
} }

View file

@ -8,7 +8,6 @@ import android.os.Bundle
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.* import android.widget.*
import androidx.core.view.setPadding
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import org.json.JSONObject import org.json.JSONObject
@ -33,7 +32,7 @@ class SettingsActivity : AppCompatActivity() {
deviceNameEntry.doOnTextChanged { text, _, _, _ -> deviceNameEntry.doOnTextChanged { text, _, _, _ ->
config.updateJSON { cfg -> config.updateJSON { cfg ->
var nodeinfo = cfg.optJSONObject("NodeInfo") val nodeinfo = cfg.optJSONObject("NodeInfo")
if (nodeinfo == null) { if (nodeinfo == null) {
cfg.put("NodeInfo", JSONObject("{}")) cfg.put("NodeInfo", JSONObject("{}"))
} }
@ -42,16 +41,16 @@ class SettingsActivity : AppCompatActivity() {
} }
resetConfigurationRow.setOnClickListener { 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)) 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.setView(view)
builder.setPositiveButton("Reset") { dialog, _ -> builder.setPositiveButton(getString(R.string.settings_reset)) { dialog, _ ->
config.resetJSON() config.resetJSON()
updateView() updateView()
dialog.dismiss() dialog.dismiss()
} }
builder.setNegativeButton("Cancel") { dialog, _ -> builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.cancel() dialog.cancel()
} }
builder.show() builder.show()
@ -68,7 +67,7 @@ class SettingsActivity : AppCompatActivity() {
updateView() updateView()
} }
fun updateView() { private fun updateView() {
val nodeinfo = config.getJSON().optJSONObject("NodeInfo") val nodeinfo = config.getJSON().optJSONObject("NodeInfo")
if (nodeinfo != null) { if (nodeinfo != null) {
deviceNameEntry.setText(nodeinfo.getString("name"), TextView.BufferType.EDITABLE) deviceNameEntry.setText(nodeinfo.getString("name"), TextView.BufferType.EDITABLE)

View file

@ -18,4 +18,24 @@
<string name="dns_fix_chrome_based_browsers">Fix Chrome-based browsers</string> <string name="dns_fix_chrome_based_browsers">Fix Chrome-based browsers</string>
<string name="dns_fix_chrome_based_browsers_hint">If you do not have IPv6 internet connectivity, this option should help Chrome-based browsers to resolve Yggdrasil domain names correctly.</string> <string name="dns_fix_chrome_based_browsers_hint">If you do not have IPv6 internet connectivity, this option should help Chrome-based browsers to resolve Yggdrasil domain names correctly.</string>
<string name="dns_fixes">DNS fixes</string> <string name="dns_fixes">DNS fixes</string>
<string name="dns_no_servers">No servers</string>
<string name="dns_one_server">1 server</string>
<string name="dns_many_servers">%d server</string>
<string name="dns_remove_title">Remove %s?</string>
<string name="main_no_connectivity">No connectivity</string>
<string name="main_enabled">Enabled</string>
<string name="main_disabled">Not enabled</string>
<string name="main_no_peers">No peers</string>
<string name="main_one_peer">1 peer</string>
<string name="main_many_peers">%d peers</string>
<string name="peers_add_peer">Add Configured Peer</string>
<string name="peers_add">Add</string>
<string name="peers_remove_title">Remove %s?</string>
<string name="peers_remove">Remove</string>
<string name="peers_no_configured_title">No peers currently configured</string>
<string name="peers_configured_title">Configured Peers</string>
<string name="peers_no_connected_title">No peers currently connected</string>
<string name="peers_connected_title">Connected Peers</string>
<string name="settings_warning_title">Warning</string>
<string name="settings_reset">Reset</string>
</resources> </resources>