Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

<service
android:name=".tile.PangolinTileService"
android:exported="true"
android:icon="@drawable/ic_tile"
android:label="Pangolin"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
</application>

</manifest>
11 changes: 11 additions & 0 deletions app/src/main/java/net/pangolin/Pangolin/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
113 changes: 113 additions & 0 deletions app/src/main/java/net/pangolin/Pangolin/tile/PangolinTileService.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
16 changes: 14 additions & 2 deletions app/src/main/java/net/pangolin/Pangolin/util/TunnelManager.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

/**
Expand Down Expand Up @@ -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
)
))
}

/**
Expand Down Expand Up @@ -416,6 +419,8 @@ class TunnelManager private constructor(
*/
private fun updateState(newState: TunnelState) {
_tunnelState.value = newState

notifyTileUpdate()
}

/**
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/res/drawable/ic_tile.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="238.34"
android:viewportHeight="252.73">
<group android:scaleX="1.0"
android:scaleY="1.0"
android:translateX="0"
android:translateY="0">
<path
android:pathData="m200.54,84.15c4.96,0 8.97,4.02 8.97,8.97 0,4.96 -4.02,8.97 -8.97,8.97 -4.96,0 -8.97,-4.02 -8.97,-8.97 0,-4.96 4.02,-8.97 8.97,-8.97zM235.77,121.6c-0.9,29.81 -23.66,69.21 -54.51,79.34 -36.04,11.84 -63.41,-5.92 -72.08,-26.74 -6.76,-16.22 -1.65,-35.62 10.96,-43.84 10.65,-6.94 30.49,-8.77 47.15,2.19 -5.86,-15.34 -21.62,-25.43 -35.59,-28.49 -13.97,-3.07 -28.38,0.44 -38.75,5.7 13.29,-14.69 44.41,-28.95 78.24,-10.96 22.68,12.05 32.44,28.93 42.05,51.73C238.48,111.95 220.91,65.49 190.27,37.87 159.04,9.71 116.84,-1.59 84.55,0.18 95.21,6.75 107.73,16.14 119.29,27.13 68.18,20.77 26.06,32.39 0,67.82 14.56,64.95 33.75,63.15 52.86,64.09 15.62,90.33 -6.01,134.46 4.99,180.51c6.72,-11.74 16.77,-25.85 28.73,-38.63 -3.68,34.37 1.43,80.84 45.62,110.85 -2.26,-9.42 -4.08,-20.88 -4.91,-33.02 20.67,16.13 50.69,29.42 87.92,20.24 65.78,-16.22 83.35,-79.78 73.44,-118.36"
android:strokeWidth="0.0776283"
android:fillColor="#f36118"
android:strokeColor="#00000000"
android:fillType="nonZero"/>
</group>
</vector>