Fixes #30, added quick settings icon.

This commit is contained in:
Revertron 2022-11-17 18:45:35 +01:00
parent ee81f4e902
commit 1152070fac
9 changed files with 296 additions and 19 deletions

View file

@ -13,10 +13,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 +20,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 +44,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>

View file

@ -1,15 +1,22 @@
package eu.neilalexander.yggdrasil package eu.neilalexander.yggdrasil
import android.app.Application import android.app.Application
import android.content.ComponentName
import android.os.Build
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
class GlobalApplication: Application() { 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)
val receiver = YggStateReceiver(this)
receiver.register(this)
} }
fun subscribe() { fun subscribe() {
@ -25,4 +32,17 @@ 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)
currentState = state
}
}
} }

View file

@ -6,7 +6,9 @@ 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 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
@ -21,6 +23,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"
} }
@ -59,7 +62,20 @@ 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 -> {
Log.d(TAG, "Starting...") Log.d(TAG, "Starting...")
@ -135,7 +151,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 +159,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 +198,15 @@ 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)
stopSelf() stopSelf()
} }
@ -194,6 +218,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,22 +230,35 @@ 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
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
} }
if (sleep()) return
}
}
private fun sleep(): Boolean {
try { try {
Thread.sleep(1000) Thread.sleep(1000)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
return return true
}
} }
return false
} }
private fun writer() { private fun writer() {

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,107 @@
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
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()
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
tile.state = when (state) {
State.Unknown -> Tile.STATE_UNAVAILABLE
State.Disabled -> Tile.STATE_INACTIVE
State.Enabled -> Tile.STATE_ACTIVE
State.Connected -> Tile.STATE_ACTIVE
State.Reconnecting -> 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)
}
}

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

View file

@ -65,6 +65,9 @@
<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>

View file

@ -65,6 +65,9 @@
<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</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>