diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index edec37d..5a2f858 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,10 +13,6 @@ android:roundIcon="@drawable/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Yggdrasil"> - - @@ -24,7 +20,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt index 58cda01..a0bde8b 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt @@ -1,15 +1,22 @@ package eu.neilalexander.yggdrasil 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 var currentState: State = State.Disabled var updaterConnections: Int = 0 override fun onCreate() { super.onCreate() config = ConfigurationProxy(applicationContext) val callback = NetworkStateCallback(this) + val receiver = YggStateReceiver(this) + receiver.register(this) } fun subscribe() { @@ -25,4 +32,17 @@ 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) + currentState = state + } + } } \ 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..e58f131 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt @@ -6,7 +6,9 @@ import android.os.ParcelFileDescriptor import android.system.OsConstants import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager +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 @@ -21,6 +23,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" } @@ -59,7 +62,20 @@ 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 -> { Log.d(TAG, "Starting...") @@ -135,7 +151,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 +159,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 +198,15 @@ 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) + stopSelf() } @@ -194,6 +218,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 +230,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 + 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()) { 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..e997ff9 --- /dev/null +++ b/app/src/main/java/eu/neilalexander/yggdrasil/YggTileService.kt @@ -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) + } +} \ 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..546e80d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -65,6 +65,9 @@ Ваш публичный ключ идентифицирует вас в сети. Его распространение безопасно. Сбросить настройки Сброс создаст полностью новые настройки. Это изменит ваш публичный ключ и адрес IP. + Выключено + Включено + Подключено Амстердам, Нидерланды Прага, Чехия Братислава, Словакия diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 109b144..db791e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -65,6 +65,9 @@ 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 + Connected Amsterdam, NL Prague, CZ Bratislava, SK