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