diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 54b13ae..1431ddd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -88,6 +88,23 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/net/pangolin/Pangolin/MainActivity.kt b/app/src/main/java/net/pangolin/Pangolin/MainActivity.kt
index 61276dc..317a8b2 100644
--- a/app/src/main/java/net/pangolin/Pangolin/MainActivity.kt
+++ b/app/src/main/java/net/pangolin/Pangolin/MainActivity.kt
@@ -145,6 +145,17 @@ class MainActivity : BaseNavigationActivity() {
contentBinding.loadingOverlay.visibility = android.view.View.VISIBLE
contentBinding.mainContent.visibility = android.view.View.GONE
+ if (intent?.getBooleanExtra("auto_connect", false) == true) {
+ val currentState = tunnelManager.tunnelState.value
+ if (!currentState.isServiceRunning && !currentState.isConnecting) {
+ lifecycleScope.launch {
+ tunnelManager.connect()
+ }
+ }
+
+ intent.removeExtra("auto_connect")
+ }
+
// Setup toggle switch listener with helper function
fun setupToggleListener() {
contentBinding.toggleConnect.setOnCheckedChangeListener { _, isChecked ->
diff --git a/app/src/main/java/net/pangolin/Pangolin/tile/PangolinTileService.kt b/app/src/main/java/net/pangolin/Pangolin/tile/PangolinTileService.kt
new file mode 100644
index 0000000..f9afe91
--- /dev/null
+++ b/app/src/main/java/net/pangolin/Pangolin/tile/PangolinTileService.kt
@@ -0,0 +1,113 @@
+package net.pangolin.Pangolin.tile
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import net.pangolin.Pangolin.MainActivity
+import net.pangolin.Pangolin.util.TunnelManager
+
+class PangolinTileService : TileService() {
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
+ private var stateWatchJob: kotlinx.coroutines.Job? = null
+
+ override fun onStartListening() {
+ super.onStartListening()
+ watchState()
+ }
+
+ override fun onStopListening() {
+ super.onStopListening()
+ stateWatchJob?.cancel()
+ stateWatchJob = null
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ scope.cancel()
+ }
+
+ @SuppressLint("StartActivityAndCollapseDeprecated")
+ private fun watchState() {
+ val tunnelManager = TunnelManager.getInstance() ?: run {
+ setTileState(active = false, label = "Unavailable", clickable = false)
+
+ val intent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra("auto_connect", true)
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ startActivityAndCollapse(pendingIntent)
+ } else {
+ @Suppress("DEPRECATION")
+ startActivityAndCollapse(intent)
+ }
+
+ return
+ }
+
+ stateWatchJob?.cancel()
+ stateWatchJob = scope.launch {
+ tunnelManager.tunnelState.collect { state ->
+ setTileState(
+ active = state.isServiceRunning,
+ label = state.statusMessage,
+ clickable = state.canDisable || state.canEnable
+ )
+ }
+ }
+ }
+
+ override fun onClick() {
+ super.onClick()
+
+ val tunnelManager = TunnelManager.getInstance() ?: return
+ val state = tunnelManager.tunnelState.value
+
+ if (!state.canDisable && !state.canEnable) {
+ return
+ }
+
+ if (state.isServiceRunning || state.isConnecting) {
+ scope.launch {
+ tunnelManager.disconnect()
+ }
+ } else {
+ scope.launch {
+ tunnelManager.connect()
+ }
+ }
+ }
+
+ private fun setTileState(active: Boolean, label: String, clickable: Boolean) {
+ val tile = qsTile ?: return
+
+ tile.contentDescription = "Pangolin VPN"
+ tile.icon = Icon.createWithResource(this, net.pangolin.Pangolin.R.drawable.ic_tile)
+
+ tile.state = when {
+ !clickable -> Tile.STATE_UNAVAILABLE
+ active -> Tile.STATE_ACTIVE
+ else -> Tile.STATE_INACTIVE
+ }
+
+ tile.label = label
+
+ tile.updateTile()
+ }
+}
diff --git a/app/src/main/java/net/pangolin/Pangolin/util/TunnelManager.kt b/app/src/main/java/net/pangolin/Pangolin/util/TunnelManager.kt
index e4469d3..55e47d7 100644
--- a/app/src/main/java/net/pangolin/Pangolin/util/TunnelManager.kt
+++ b/app/src/main/java/net/pangolin/Pangolin/util/TunnelManager.kt
@@ -1,6 +1,8 @@
package net.pangolin.Pangolin.util
+import android.content.ComponentName
import android.content.Context
+import android.service.quicksettings.TileService
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -19,6 +21,7 @@ import net.pangolin.Pangolin.PacketTunnel.GoBackend
import net.pangolin.Pangolin.PacketTunnel.InitConfig
import net.pangolin.Pangolin.PacketTunnel.Tunnel
import net.pangolin.Pangolin.PacketTunnel.TunnelConfig
+import net.pangolin.Pangolin.tile.PangolinTileService
import java.io.File
/**
@@ -129,13 +132,13 @@ class TunnelManager private constructor(
val isConnected = status.connected && status.registered == true
val isRegistered = status.registered == true
- _tunnelState.value = currentState.copy(
+ updateState(currentState.copy(
isSocketConnected = status.connected,
isRegistered = isRegistered,
isConnecting = !isConnected && status.connected,
statusMessage = determineStatusMessage(status),
errorMessage = if (status.terminated) "Connection terminated" else null
- )
+ ))
}
/**
@@ -416,6 +419,8 @@ class TunnelManager private constructor(
*/
private fun updateState(newState: TunnelState) {
_tunnelState.value = newState
+
+ notifyTileUpdate()
}
/**
@@ -462,6 +467,13 @@ class TunnelManager private constructor(
scope.cancel()
}
+ private fun notifyTileUpdate() {
+ TileService.requestListeningState(
+ context,
+ ComponentName(context, PangolinTileService::class.java)
+ )
+ }
+
companion object {
@Volatile
private var instance: TunnelManager? = null
diff --git a/app/src/main/res/drawable/ic_tile.xml b/app/src/main/res/drawable/ic_tile.xml
new file mode 100644
index 0000000..729e102
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tile.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file