Fixes #30, added quick settings icon. (#32)

* 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:
Revertron 2022-11-19 20:49:34 +01:00 committed by GitHub
parent ee81f4e902
commit aa94ccad26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 441 additions and 60 deletions

View file

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

View file

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

View file

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

View file

@ -3,14 +3,43 @@ 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 {
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()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
@ -20,22 +49,4 @@ class NetworkStateCallback(val context: Context) : ConnectivityManager.NetworkCa
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()
}
override fun onLost(network: Network) {
super.onLost(network)
Log.d(TAG, "onLost")
}
}

View file

@ -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,24 +243,39 @@ 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() {
val buf = ByteArray(65535)
writes@ while (started.get()) {

View file

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

View file

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

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