Migrate to singbox

This commit is contained in:
problematicconsumer
2023-08-19 22:27:23 +03:30
parent 14369d0a03
commit 684acc555d
124 changed files with 3408 additions and 2047 deletions

View File

@@ -98,6 +98,10 @@ jobs:
make gen
make translate
- name: Get Geo Assets
run: |
make get-geo-assets
- name: Get Libs ${{ matrix.platform }}
run: |
make ${{ matrix.platform }}-libs

3
.gitignore vendored
View File

@@ -42,6 +42,9 @@ migrate_working_dir/
**/*.dylib
/dist/
/assets/core/*
!/assets/core/.gitkeep
# Symbolication related
app.*.symbols

6
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "core"]
path = core
url = https://github.com/hiddify/hiddify-libclash
[submodule "libcore"]
path = libcore
url = https://github.com/hiddify/hiddify-next-core

View File

@@ -1,7 +1,8 @@
ANDROID_OUT=./android/app/src/main/jniLibs
DESKTOP_OUT=./core/bin
NDK_BIN=$(ANDROID_HOME)/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin
GOBUILD=CGO_ENABLED=1 go build -trimpath -tags with_gvisor,with_lwip -ldflags="-w -s" -buildmode=c-shared
BINDIR=./libcore/bin
ANDROID_OUT=./android/app/libs
DESKTOP_OUT=./libcore/bin
GEO_ASSETS_DIR=./assets/core
LIBS_DOWNLOAD_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft
get:
flutter pub get
@@ -20,40 +21,40 @@ windows-release:
linux-release:
flutter_distributor package --platform linux --targets appimage
macos-realase:
flutter build macos --release &&\
tree ./build/macos/Build &&\
create-dmg --app-drop-link 600 185 "hiddify-amd64.dmg" ./build/macos/Build/Products/Release/hiddify-clash.app
android-libs:
mkdir -p $(ANDROID_OUT)/x86_64 $(ANDROID_OUT)/arm64-v8a/ $(ANDROID_OUT)/armeabi-v7a/ &&\
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-android-amd64.so.gz | gunzip > $(ANDROID_OUT)/x86_64/libclash.so &&\
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-android-arm64.so.gz | gunzip > $(ANDROID_OUT)/arm64-v8a/libclash.so &&\
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-android-arm.so.gz | gunzip > $(ANDROID_OUT)/armeabi-v7a/libclash.so
mkdir -p $(ANDROID_OUT)
curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-android.aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar
windows-libs:
mkdir -p $(DESKTOP_OUT)/ &&\
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-windows-amd64.dll.gz | gunzip > $(DESKTOP_OUT)/libclash.dll
mkdir -p $(DESKTOP_OUT)
curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-windows-amd64.dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll
linux-libs:
mkdir -p $(DESKTOP_OUT)/ &&\
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-linux-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libclash.so
mkdir -p $(DESKTOP_OUT)
curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-linux-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libcore.so
macos-libs:
mkdir -p $(DESKTOP_OUT)/ &&\
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-macos-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libclash.dylib
get-geo-assets:
curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db
curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db
build-headers:
make -C libcore -f Makefile headers && mv $(BINDIR)/hiddify-libcore-headers.h $(BINDIR)/libcore.h
build-android-libs:
cd core &&\
mkdir -p .$(ANDROID_OUT)/x86_64/ .$(ANDROID_OUT)/arm64-v8a/ .$(ANDROID_OUT)/armeabi-v7a/ &&\
make android-amd64 && mv bin/hiddify-clashlib-android-amd64.so .$(ANDROID_OUT)/x86_64/libclash.so &&\
make android-arm && mv bin/hiddify-clashlib-android-arm.so .$(ANDROID_OUT)/armeabi-v7a/libclash.so &&\
make android-arm64 && mv bin/hiddify-clashlib-android-arm64.so .$(ANDROID_OUT)/arm64-v8a/libclash.so
make -C libcore -f Makefile android && mv $(BINDIR)/hiddify-libcore-android.aar $(ANDROID_OUT)/libcore.aar
build-windows-libs:
cd core &&\
make windows-amd64 && mv bin/hiddify-clashlib-windows-amd64.dll bin/libclash.dll
make -C libcore -f Makefile windows-amd64 && mv $(BINDIR)/hiddify-libcore-windows-amd64.dll $(DESKTOP_OUT)/libcore.dll
build-linux-libs:
cd core &&\
make linux-amd64 && mv bin/hiddify-clashlib-linux-amd64.dll bin/libclash.so
make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/hiddify-libcore-linux-amd64.dll $(DESKTOP_OUT)/libcore.so

3
android/.gitignore vendored
View File

@@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
/app/libs/*
!/app/libs/.gitkeep

View File

@@ -60,6 +60,11 @@ android {
signingConfig signingConfigs.debug
}
}
buildFeatures {
viewBinding true
aidl true
}
}
flutter {
@@ -67,7 +72,11 @@ flutter {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'

View File

View File

@@ -1,14 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".Application"
android:label="hiddify"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round">
android:roundIcon="@mipmap/ic_launcher_round"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -39,15 +49,14 @@
<data android:host="install-config"/>
</intent-filter>
</activity>
<service
android:name=".HiddifyVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:stopWithTask="false"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
</service>
<service
android:name=".bg.VPNService"
android:exported="false"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@@ -0,0 +1,9 @@
package com.hiddify.hiddify;
import com.hiddify.hiddify.IServiceCallback;
interface IService {
int getStatus();
void registerCallback(in IServiceCallback callback);
oneway void unregisterCallback(in IServiceCallback callback);
}

View File

@@ -0,0 +1,8 @@
package com.hiddify.hiddify;
interface IServiceCallback {
void onServiceStatusChanged(int status);
void onServiceAlert(int type, String message);
void onServiceWriteLog(String message);
void onServiceResetLogs(in List<String> messages);
}

View File

@@ -0,0 +1,42 @@
package com.hiddify.hiddify
import android.app.Application
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.PowerManager
import androidx.core.content.getSystemService
import com.hiddify.hiddify.bg.AppChangeReceiver
import go.Seq
import com.hiddify.hiddify.Application as BoxApplication
class Application : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
application = this
}
override fun onCreate() {
super.onCreate()
Seq.setContext(this)
registerReceiver(AppChangeReceiver(), IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addDataScheme("package")
})
}
companion object {
lateinit var application: BoxApplication
val notification by lazy { application.getSystemService<NotificationManager>()!! }
val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }
val packageManager by lazy { application.packageManager }
val powerManager by lazy { application.getSystemService<PowerManager>()!! }
}
}

View File

@@ -0,0 +1,77 @@
package com.hiddify.hiddify
import android.util.Log
import androidx.lifecycle.Observer
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
class EventHandler : FlutterPlugin {
companion object {
const val TAG = "A/EventHandler"
const val SERVICE_STATUS = "com.hiddify.app/service.status"
const val SERVICE_ALERTS = "com.hiddify.app/service.alerts"
}
private lateinit var statusChannel: EventChannel
private lateinit var alertsChannel: EventChannel
private lateinit var statusObserver: Observer<Status>
private lateinit var alertsObserver: Observer<ServiceEvent?>
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS)
alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS)
statusChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
statusObserver = Observer {
Log.d(TAG, "new status: $it")
val map = listOf(
Pair("status", it.name)
)
.toMap()
events?.success(map)
}
MainActivity.instance.serviceStatus.observeForever(statusObserver)
}
override fun onCancel(arguments: Any?) {
MainActivity.instance.serviceStatus.removeObserver(statusObserver)
}
})
alertsChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
alertsObserver = Observer {
if (it == null) return@Observer
Log.d(TAG, "new alert: $it")
val map = listOf(
Pair("status", it.status.name),
Pair("failure", it.alert?.name),
Pair("message", it.message)
)
.mapNotNull { p -> p.second?.let { Pair(p.first, p.second) } }
.toMap()
events?.success(map)
}
MainActivity.instance.serviceAlerts.observeForever(alertsObserver)
}
override fun onCancel(arguments: Any?) {
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver)
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
MainActivity.instance.serviceStatus.removeObserver(statusObserver)
statusChannel.setStreamHandler(null)
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver)
alertsChannel.setStreamHandler(null)
}
}
data class ServiceEvent(val status: Status, val alert: Alert? = null, val message: String? = null)

View File

@@ -1,317 +0,0 @@
package com.hiddify.hiddify
import android.app.PendingIntent
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
import android.os.StrictMode
import android.util.Log
import androidx.annotation.RequiresApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.lang.ref.SoftReference
class HiddifyVpnService : VpnService() {
companion object {
const val TAG = "Hiddify/VpnService"
const val EVENT_TAG = "Hiddify/VpnServiceEvents"
private const val TUN2SOCKS = "libtun2socks.so"
private const val TUN_MTU = 9000
private const val TUN_GATEWAY = "172.19.0.1"
private const val TUN_ROUTER = "172.19.0.2"
private const val TUN_SUBNET_PREFIX = 30
private const val NET_ANY = "0.0.0.0"
private val HTTP_PROXY_LOCAL_LIST = listOf(
"localhost",
"*.local",
"127.*",
"10.*",
"172.16.*",
"172.17.*",
"172.18.*",
"172.19.*",
"172.2*",
"172.30.*",
"172.31.*",
"192.168.*"
)
}
private var vpnBroadcastReceiver: VpnState? = null
private var conn: ParcelFileDescriptor? = null
private lateinit var process: Process
private var isRunning = false
// prefs
private var includeAppPackages: Set<String> = HashSet()
fun getService(): Service {
return this
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startVpnService()
return START_STICKY
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "creating vpn service")
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
registerBroadcastReceiver()
VpnServiceManager.vpnService = SoftReference(this)
}
override fun onRevoke() {
Log.d(TAG, "vpn service revoked")
super.onRevoke()
stopVpnService()
}
override fun onDestroy() {
Log.d(TAG, "vpn service destroyed")
super.onDestroy()
broadcastVpnStatus(false)
VpnServiceManager.cancelNotification()
unregisterBroadcastReceiver()
}
private fun registerBroadcastReceiver() {
Log.d(TAG, "registering receiver in service")
vpnBroadcastReceiver = VpnState()
val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS)
registerReceiver(vpnBroadcastReceiver, intentFilter)
}
private fun unregisterBroadcastReceiver() {
Log.d(TAG, "unregistering receiver in service")
if (vpnBroadcastReceiver != null) {
unregisterReceiver(vpnBroadcastReceiver)
vpnBroadcastReceiver = null
}
}
private fun broadcastVpnStatus(isVpnActive: Boolean) {
Log.d(TAG, "broadcasting status= $isVpnActive")
val intent = Intent(VpnState.ACTION_VPN_STATUS)
intent.putExtra(VpnState.IS_VPN_ACTIVE, isVpnActive)
sendBroadcast(intent)
}
@delegate:RequiresApi(Build.VERSION_CODES.P)
private val defaultNetworkRequest by lazy {
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.build()
}
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
@delegate:RequiresApi(Build.VERSION_CODES.P)
private val defaultNetworkCallback by lazy {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
setUnderlyingNetworks(arrayOf(network))
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
// it's a good idea to refresh capabilities
setUnderlyingNetworks(arrayOf(network))
}
override fun onLost(network: Network) {
setUnderlyingNetworks(null)
}
}
}
private fun startVpnService() {
val prepare = prepare(this)
if (prepare != null) {
return
}
with(Builder()) {
addAddress(TUN_GATEWAY, TUN_SUBNET_PREFIX)
setMtu(TUN_MTU)
addRoute(NET_ANY, 0)
addDnsServer(TUN_ROUTER)
allowBypass()
setBlocking(true)
setSession("Hiddify")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setMetered(false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && VpnServiceManager.prefs.systemProxy) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
VpnServiceManager.prefs.httpPort,
HTTP_PROXY_LOCAL_LIST,
)
)
}
if (includeAppPackages.isEmpty()) {
addDisallowedApplication(packageName)
} else {
includeAppPackages.forEach {
addAllowedApplication(it)
}
}
setConfigureIntent(
PendingIntent.getActivity(
this@HiddifyVpnService,
0,
Intent().setComponent(ComponentName(packageName, "$packageName.MainActivity")),
pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
)
)
try {
conn?.close()
} catch (ignored: Exception) {
// ignored
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
} catch (e: Exception) {
e.printStackTrace()
}
}
try {
conn = establish()
isRunning = true
runTun2socks()
VpnServiceManager.showNotification()
Log.d(TAG, "vpn connection established")
broadcastVpnStatus(true)
} catch (e: Exception) {
Log.w(TAG, "failed to start vpn service: $e")
e.printStackTrace()
stopVpnService()
broadcastVpnStatus(false)
}
}
}
fun stopVpnService(isForced: Boolean = true) {
Log.d(TAG, "stopping vpn service, forced: [$isForced]")
isRunning = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
} catch (ignored: Exception) {
// ignored
}
}
try {
Log.d(TAG, "destroying tun2socks")
process.destroy()
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
if(isForced) {
stopSelf()
try {
conn?.close()
conn = null
} catch (ignored: Exception) {
// ignored
}
}
Log.d(TAG, "vpn service stopped")
}
private fun runTun2socks() {
val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
"--netif-ipaddr", TUN_ROUTER,
"--netif-netmask", "255.255.255.252",
"--socks-server-addr", "127.0.0.1:${VpnServiceManager.prefs.socksPort}",
"--tunmtu", TUN_MTU.toString(),
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
"--enable-udprelay",
"--loglevel", "notice")
Log.d(TAG, cmd.toString())
protect(conn!!.fd) // not sure
try {
val proBuilder = ProcessBuilder(cmd)
proBuilder.redirectErrorStream(true)
process = proBuilder
.directory(applicationContext.filesDir)
.start()
Thread(Runnable {
Log.d(TAG,"$TUN2SOCKS check")
process.waitFor()
Log.d(TAG,"$TUN2SOCKS exited")
if (isRunning) {
Log.d(packageName,"$TUN2SOCKS restart")
runTun2socks()
}
}).start()
Log.d(TAG, process.toString())
sendFd()
} catch (e: Exception) {
Log.d(TAG, e.toString())
}
}
private fun sendFd() {
val fd = conn!!.fileDescriptor
val path = File(applicationContext.filesDir, "sock_path").absolutePath
Log.d(TAG, path)
GlobalScope.launch(Dispatchers.IO) {
var tries = 0
while (true) try {
Thread.sleep(50L shl tries)
Log.d(TAG, "sendFd tries: $tries")
LocalSocket().use { localSocket ->
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
localSocket.setFileDescriptorsForSend(arrayOf(fd))
localSocket.outputStream.write(42)
}
break
} catch (e: Exception) {
Log.d(TAG, e.toString())
if (tries > 5) break
tries += 1
}
}
}
private fun pendingIntentFlags(flags: Int, mutable: Boolean = false): Int {
return if (Build.VERSION.SDK_INT >= 24) {
if (Build.VERSION.SDK_INT > 30 && mutable) {
flags or PendingIntent.FLAG_MUTABLE
} else {
flags or PendingIntent.FLAG_IMMUTABLE
}
} else {
flags
}
}
}

View File

@@ -0,0 +1,35 @@
package com.hiddify.hiddify
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
class LogHandler : FlutterPlugin {
companion object {
const val TAG = "A/LogHandler"
const val SERVICE_LOGS = "com.hiddify.app/service.logs"
}
private lateinit var logsChannel: EventChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
logsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_LOGS)
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
MainActivity.instance.serviceLogs.observeForever { it ->
if (it == null) return@observeForever
events?.success(it)
}
}
override fun onCancel(arguments: Any?) {
}
})
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}
}

View File

@@ -1,116 +1,148 @@
package com.hiddify.hiddify
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import com.hiddify.hiddify.bg.ServiceConnection
import com.hiddify.hiddify.bg.ServiceNotification
import com.hiddify.hiddify.bg.VPNService
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
private lateinit var methodChannel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var methodResult: MethodChannel.Result
private var vpnBroadcastReceiver: VpnState? = null
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.LinkedList
class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
companion object {
private const val TAG = "ANDROID/MyActivity"
lateinit var instance: MainActivity
const val VPN_PERMISSION_REQUEST_CODE = 1001
enum class Action(val method: String) {
GrantPermission("grant_permission"),
StartProxy("start"),
StopProxy("stop"),
RefreshStatus("refresh_status"),
SetPrefs("set_prefs")
}
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1010
}
private fun registerBroadcastReceiver() {
Log.d(HiddifyVpnService.TAG, "registering broadcast receiver")
vpnBroadcastReceiver = VpnState()
val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS)
registerReceiver(vpnBroadcastReceiver, intentFilter)
}
private val connection = ServiceConnection(this, this)
private fun unregisterBroadcastReceiver() {
Log.d(HiddifyVpnService.TAG, "unregistering broadcast receiver")
if (vpnBroadcastReceiver != null) {
unregisterReceiver(vpnBroadcastReceiver)
vpnBroadcastReceiver = null
}
}
val logList = LinkedList<String>()
var logCallback: ((Boolean) -> Unit)? = null
val serviceStatus = MutableLiveData(Status.Stopped)
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
val serviceLogs = MutableLiveData<String?>(null)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
methodChannel =
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.TAG)
methodChannel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.EVENT_TAG)
registerBroadcastReceiver()
eventChannel.setStreamHandler(vpnBroadcastReceiver)
instance = this
reconnect()
flutterEngine.plugins.add(MethodHandler())
flutterEngine.plugins.add(EventHandler())
flutterEngine.plugins.add(LogHandler())
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
methodResult = result
@Suppress("UNCHECKED_CAST")
when (call.method) {
Action.GrantPermission.method -> {
grantVpnPermission()
fun reconnect() {
connection.reconnect()
}
fun startService() {
if (!ServiceNotification.checkPermission()) {
Log.d(TAG, "missing notification permission")
return
}
lifecycleScope.launch(Dispatchers.IO) {
// if (Settings.rebuildServiceMode()) {
// reconnect()
// }
if (prepare()) {
Log.d(TAG, "VPN permission required")
return@launch
}
Action.StartProxy.method -> {
VpnServiceManager.startVpnService(this)
result.success(true)
}
Action.StopProxy.method -> {
VpnServiceManager.stopVpnService()
result.success(true)
}
Action.RefreshStatus.method -> {
val statusIntent = Intent(VpnState.ACTION_VPN_STATUS)
statusIntent.putExtra(VpnState.IS_VPN_ACTIVE, VpnServiceManager.isRunning)
sendBroadcast(statusIntent)
result.success(true)
}
Action.SetPrefs.method -> {
val args = call.arguments as Map<String, Any>
VpnServiceManager.setPrefs(context, args)
result.success(true)
}
else -> {
result.notImplemented()
val intent = Intent(Application.application, VPNService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(Application.application, intent)
}
}
}
override fun onDestroy() {
super.onDestroy()
unregisterBroadcastReceiver()
override fun onServiceStatusChanged(status: Status) {
Log.d(TAG, "service status changed: $status")
serviceStatus.postValue(status)
}
private fun grantVpnPermission() {
val vpnPermissionIntent = VpnService.prepare(this)
if (vpnPermissionIntent == null) {
onActivityResult(VPN_PERMISSION_REQUEST_CODE, RESULT_OK, null)
} else {
startActivityForResult(vpnPermissionIntent, VPN_PERMISSION_REQUEST_CODE)
override fun onServiceAlert(type: Alert, message: String?) {
Log.d(TAG, "service alert: $type")
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
}
private var paused = false
override fun onPause() {
super.onPause()
paused = true
}
override fun onResume() {
super.onResume()
paused = false
logCallback?.invoke(true)
}
override fun onServiceWriteLog(message: String?) {
if (paused) {
if (logList.size > 300) {
logList.removeFirst()
}
}
logList.addLast(message)
if (!paused) {
logCallback?.invoke(false)
serviceLogs.postValue(message)
}
}
override fun onServiceResetLogs(messages: MutableList<String>) {
logList.clear()
logList.addAll(messages)
if (!paused) logCallback?.invoke(true)
}
override fun onDestroy() {
connection.disconnect()
super.onDestroy()
}
private suspend fun prepare() = withContext(Dispatchers.Main) {
try {
val intent = VpnService.prepare(this@MainActivity)
if (intent != null) {
// prepareLauncher.launch(intent)
startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
true
} else {
false
}
} catch (e: Exception) {
onServiceAlert(Alert.RequestVPNPermission, e.message)
false
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
methodResult.success(resultCode == RESULT_OK)
} else if (requestCode == 101010) {
methodResult.success(resultCode == RESULT_OK)
if (resultCode == RESULT_OK) startService()
else onServiceAlert(Alert.RequestVPNPermission, null)
} else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
if (resultCode == RESULT_OK) startService()
else onServiceAlert(Alert.RequestNotificationPermission, null)
}
}
}

View File

@@ -0,0 +1,67 @@
package com.hiddify.hiddify
import androidx.annotation.NonNull
import com.hiddify.hiddify.bg.BoxService
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.StandardMethodCodec
class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
companion object {
const val channelName = "com.hiddify.app/method"
enum class Trigger(val method: String) {
ParseConfig("parse_config"),
SetActiveConfigPath("set_active_config_path"),
Start("start"),
Stop("stop"),
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val taskQueue = flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
channel = MethodChannel(
flutterPluginBinding.binaryMessenger,
channelName,
StandardMethodCodec.INSTANCE,
taskQueue
)
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
Trigger.ParseConfig.method -> {
val args = call.arguments as Map<*, *>
val path = args["path"] as String? ?: ""
val msg = BoxService.parseConfig(path)
result.success(msg)
}
Trigger.SetActiveConfigPath.method -> {
val args = call.arguments as Map<*, *>
Settings.selectedConfigPath = args["path"] as String? ?: ""
result.success(true)
}
Trigger.Start.method -> {
MainActivity.instance.startService()
result.success(true)
}
Trigger.Stop.method -> {
BoxService.stop()
result.success(true)
}
else -> result.notImplemented()
}
}
}

View File

@@ -0,0 +1,33 @@
package com.hiddify.hiddify
import android.content.Context
import com.hiddify.hiddify.constant.SettingsKey
object Settings {
const val PER_APP_PROXY_DISABLED = 0
const val PER_APP_PROXY_EXCLUDE = 1
const val PER_APP_PROXY_INCLUDE = 2
private val preferences by lazy {
val context = Application.application.applicationContext
context.getSharedPreferences("preferences", Context.MODE_PRIVATE)
}
var disableMemoryLimit = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
var perAppProxyEnabled = preferences.getBoolean(SettingsKey.PER_APP_PROXY_ENABLED, false)
var perAppProxyMode = preferences.getInt(SettingsKey.PER_APP_PROXY_MODE, PER_APP_PROXY_EXCLUDE)
var perAppProxyList = preferences.getStringSet(SettingsKey.PER_APP_PROXY_LIST, emptySet())!!
var perAppProxyUpdateOnChange =
preferences.getInt(SettingsKey.PER_APP_PROXY_UPDATE_ON_CHANGE, PER_APP_PROXY_DISABLED)
var selectedConfigPath: String
get() = preferences.getString(SettingsKey.SELECTED_CONFIG_PATH, "") ?: ""
set(value) = preferences.edit().putString(SettingsKey.SELECTED_CONFIG_PATH, value).apply()
var startedByUser: Boolean
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)
set(value) = preferences.edit().putBoolean(SettingsKey.STARTED_BY_USER, value).apply()
}

View File

@@ -1,97 +0,0 @@
package com.hiddify.hiddify
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import java.lang.ref.SoftReference
data class VpnServiceConfigs(val httpPort: Int = 12346, val socksPort: Int = 12347, val systemProxy: Boolean = true)
object VpnServiceManager {
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_CHANNEL_ID = "hiddify_vpn"
private const val NOTIFICATION_CHANNEL_NAME = "Hiddify VPN"
var vpnService: SoftReference<HiddifyVpnService>? = null
var prefs = VpnServiceConfigs()
var isRunning = false
private var mBuilder: NotificationCompat.Builder? = null
private var mNotificationManager: NotificationManager? = null
fun startVpnService(context: Context) {
val intent = Intent(context.applicationContext, HiddifyVpnService::class.java)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stopVpnService() {
val service = vpnService?.get() ?: return
service.stopVpnService()
}
fun setPrefs(context: Context,args: Map<String, Any>) {
prefs = prefs.copy(
httpPort = args["httpPort"] as Int? ?: prefs.httpPort,
socksPort = args["socksPort"] as Int? ?: prefs.socksPort,
systemProxy = args["systemProxy"] as Boolean? ?: prefs.systemProxy,
)
if(isRunning) {
stopVpnService()
startVpnService(context)
}
}
fun showNotification() {
val service = vpnService?.get()?.getService() ?: return
val channelId = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
} else {
""
}
mBuilder = NotificationCompat.Builder(service, channelId)
.setSmallIcon(R.drawable.ic_stat_logo)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
.setShowWhen(false)
.setOnlyAlertOnce(true)
.setContentTitle("Hiddify")
.setContentText("Connected")
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
}
fun cancelNotification() {
val service = vpnService?.get()?.getService() ?: return
service.stopForeground(true)
mBuilder = null
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
getNotificationManager()?.createNotificationChannel(
channel
)
return NOTIFICATION_CHANNEL_ID
}
private fun getNotificationManager(): NotificationManager? {
if (mNotificationManager == null) {
val service = vpnService?.get()?.getService() ?: return null
mNotificationManager = service.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
return mNotificationManager
}
}

View File

@@ -1,34 +0,0 @@
package com.hiddify.hiddify
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import io.flutter.plugin.common.EventChannel
class VpnState : BroadcastReceiver(), EventChannel.StreamHandler{
companion object {
const val ACTION_VPN_STATUS = "Hiddify.VpnState.ACTION_VPN_STATUS"
const val IS_VPN_ACTIVE = "isVpnActive"
}
private var eventSink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_VPN_STATUS) {
val isVpnActive = intent.getBooleanExtra(IS_VPN_ACTIVE, false)
Log.d(HiddifyVpnService.TAG, "send to flutter: status= $isVpnActive")
VpnServiceManager.isRunning = isVpnActive
eventSink?.success(isVpnActive)
}
}
}

View File

@@ -0,0 +1,37 @@
package com.hiddify.hiddify.bg
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.hiddify.hiddify.Settings
class AppChangeReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "A/AppChangeReceiver"
}
override fun onReceive(context: Context, intent: Intent) {
checkUpdate(context, intent)
}
private fun checkUpdate(context: Context, intent: Intent) {
if (!Settings.perAppProxyEnabled) {
return
}
val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
return
}
val packageName = intent.dataString?.substringAfter("package:")
if (packageName.isNullOrBlank()) {
return
}
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
Settings.perAppProxyList = Settings.perAppProxyList + packageName
} else {
Settings.perAppProxyList = Settings.perAppProxyList - packageName
}
}
}

View File

@@ -0,0 +1,291 @@
package com.hiddify.hiddify.bg
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import go.Seq
import io.nekohasekai.libbox.BoxService
import io.nekohasekai.libbox.CommandServer
import io.nekohasekai.libbox.CommandServerHandler
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.PProfServer
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.mobile.Mobile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
class BoxService(
private val service: Service,
private val platformInterface: PlatformInterface
) : CommandServerHandler {
companion object {
private const val TAG = "A/BoxService"
private var initializeOnce = false
private fun initialize() {
if (initializeOnce) return
val baseDir = Application.application.filesDir
baseDir.mkdirs()
val workingDir = Application.application.getExternalFilesDir(null) ?: return
workingDir.mkdirs()
val tempDir = Application.application.cacheDir
tempDir.mkdirs()
Log.d(TAG, "base dir: ${baseDir.path}")
Log.d(TAG, "working dir: ${workingDir.path}")
Log.d(TAG, "temp dir: ${tempDir.path}")
Libbox.setup(baseDir.path, workingDir.path, tempDir.path, false)
Libbox.redirectStderr(File(workingDir, "stderr.log").path)
initializeOnce = true
return
}
fun parseConfig(path: String): String {
return try {
Mobile.parse(path)
""
} catch (e: Exception) {
Log.w(TAG, e)
e.message ?: "invalid config"
}
}
fun start() {
val intent = runBlocking {
withContext(Dispatchers.IO) {
Intent(Application.application, VPNService::class.java)
}
}
ContextCompat.startForegroundService(Application.application, intent)
}
fun stop() {
Application.application.sendBroadcast(
Intent(Action.SERVICE_CLOSE).setPackage(
Application.application.packageName
)
)
}
fun reload() {
Application.application.sendBroadcast(
Intent(Action.SERVICE_RELOAD).setPackage(
Application.application.packageName
)
)
}
}
var fileDescriptor: ParcelFileDescriptor? = null
private val status = MutableLiveData(Status.Stopped)
private val binder = ServiceBinder(status)
private val notification = ServiceNotification(service)
private var boxService: BoxService? = null
private var commandServer: CommandServer? = null
private var pprofServer: PProfServer? = null
private var receiverRegistered = false
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Action.SERVICE_CLOSE -> {
stopService()
}
Action.SERVICE_RELOAD -> {
serviceReload()
}
}
}
}
private fun startCommandServer() {
val commandServer =
CommandServer(this, 300)
commandServer.start()
this.commandServer = commandServer
}
private suspend fun startService() {
try {
Log.d(TAG, "starting service")
val selectedConfigPath = Settings.selectedConfigPath
if (selectedConfigPath.isBlank()) {
stopAndAlert(Alert.EmptyConfiguration)
return
}
val content = try {
Mobile.applyOverrides(selectedConfigPath)
} catch (e: Exception) {
Log.w(TAG, e)
stopAndAlert(Alert.EmptyConfiguration)
return
}
withContext(Dispatchers.Main) {
binder.broadcast {
it.onServiceResetLogs(listOf())
}
}
DefaultNetworkMonitor.start()
Libbox.registerLocalDNSTransport(LocalResolver)
Libbox.setMemoryLimit(!Settings.disableMemoryLimit)
val newService = try {
Libbox.newService(content, platformInterface)
} catch (e: Exception) {
stopAndAlert(Alert.CreateService, e.message)
return
}
newService.start()
boxService = newService
commandServer?.setService(boxService)
status.postValue(Status.Started)
} catch (e: Exception) {
stopAndAlert(Alert.StartService, e.message)
return
}
}
override fun serviceReload() {
GlobalScope.launch(Dispatchers.IO) {
val pfd = fileDescriptor
if (pfd != null) {
pfd.close()
fileDescriptor = null
}
commandServer?.setService(null)
boxService?.apply {
runCatching {
close()
}.onFailure {
writeLog("service: error when closing: $it")
}
Seq.destroyRef(refnum)
}
boxService = null
startService()
}
}
private fun stopService() {
if (status.value != Status.Started) return
status.value = Status.Stopping
if (receiverRegistered) {
service.unregisterReceiver(receiver)
receiverRegistered = false
}
notification.close()
GlobalScope.launch(Dispatchers.IO) {
val pfd = fileDescriptor
if (pfd != null) {
pfd.close()
fileDescriptor = null
}
commandServer?.setService(null)
boxService?.apply {
runCatching {
close()
}.onFailure {
writeLog("service: error when closing: $it")
}
Seq.destroyRef(refnum)
}
boxService = null
Libbox.registerLocalDNSTransport(null)
DefaultNetworkMonitor.stop()
commandServer?.apply {
close()
Seq.destroyRef(refnum)
}
commandServer = null
Settings.startedByUser = false
withContext(Dispatchers.Main) {
status.value = Status.Stopped
service.stopSelf()
}
}
}
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
Settings.startedByUser = false
withContext(Dispatchers.Main) {
if (receiverRegistered) {
service.unregisterReceiver(receiver)
receiverRegistered = false
}
notification.close()
binder.broadcast { callback ->
callback.onServiceAlert(type.ordinal, message)
}
status.value = Status.Stopped
}
}
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
status.value = Status.Starting
if (!receiverRegistered) {
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {
addAction(Action.SERVICE_CLOSE)
addAction(Action.SERVICE_RELOAD)
}, ContextCompat.RECEIVER_NOT_EXPORTED)
receiverRegistered = true
}
notification.show()
GlobalScope.launch(Dispatchers.IO) {
Settings.startedByUser = true
initialize()
try {
startCommandServer()
} catch (e: Exception) {
stopAndAlert(Alert.StartCommandServer, e.message)
return@launch
}
startService()
}
return Service.START_NOT_STICKY
}
fun onBind(intent: Intent): IBinder {
return binder
}
fun onDestroy() {
binder.close()
}
fun onRevoke() {
stopService()
}
fun writeLog(message: String) {
binder.broadcast {
it.onServiceWriteLog(message)
}
}
}

View File

@@ -0,0 +1,176 @@
package com.hiddify.hiddify.bg
import android.annotation.TargetApi
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.hiddify.hiddify.Application
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.runBlocking
import java.net.UnknownHostException
object DefaultNetworkListener {
private sealed class NetworkMessage {
class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()
class Get : NetworkMessage() {
val response = CompletableDeferred<Network>()
}
class Stop(val key: Any) : NetworkMessage()
class Put(val network: Network) : NetworkMessage()
class Update(val network: Network) : NetworkMessage()
class Lost(val network: Network) : NetworkMessage()
}
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null
val pendingRequests = arrayListOf<NetworkMessage.Get>()
for (message in channel) when (message) {
is NetworkMessage.Start -> {
if (listeners.isEmpty()) register()
listeners[message.key] = message.listener
if (network != null) message.listener(network)
}
is NetworkMessage.Get -> {
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
if (network == null) pendingRequests += message else message.response.complete(
network
)
}
is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
listeners.remove(message.key) != null && listeners.isEmpty()
) {
network = null
unregister()
}
is NetworkMessage.Put -> {
network = message.network
pendingRequests.forEach { it.response.complete(message.network) }
pendingRequests.clear()
listeners.values.forEach { it(network) }
}
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach {
it(
network
)
}
is NetworkMessage.Lost -> if (network == message.network) {
network = null
listeners.values.forEach { it(null) }
}
}
}
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(
NetworkMessage.Start(
key,
listener
)
)
suspend fun get() = if (fallback) @TargetApi(23) {
Application.connectivity.activeNetwork
?: throw UnknownHostException() // failed to listen, return current if available
} else NetworkMessage.Get().run {
networkActor.send(this)
response.await()
}
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
private object Callback : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) = runBlocking {
networkActor.send(
NetworkMessage.Put(
network
)
)
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
// it's a good idea to refresh capabilities
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
}
override fun onLost(network: Network) = runBlocking {
networkActor.send(
NetworkMessage.Lost(
network
)
)
}
}
private var fallback = false
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
}
}.build()
private val mainHandler = Handler(Looper.getMainLooper())
/**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
*
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
* satisfies default network capabilities but only THE default network. Unfortunately, we need to have
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
*
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/
private fun register() {
when (Build.VERSION.SDK_INT) {
in 31..Int.MAX_VALUE -> @TargetApi(31) {
Application.connectivity.registerBestMatchingNetworkCallback(
request,
Callback,
mainHandler
)
}
in 28 until 31 -> @TargetApi(28) { // we want REQUEST here instead of LISTEN
Application.connectivity.requestNetwork(request, Callback, mainHandler)
}
in 26 until 28 -> @TargetApi(26) {
Application.connectivity.registerDefaultNetworkCallback(Callback, mainHandler)
}
in 24 until 26 -> @TargetApi(24) {
Application.connectivity.registerDefaultNetworkCallback(Callback)
}
else -> try {
fallback = false
Application.connectivity.requestNetwork(request, Callback)
} catch (e: RuntimeException) {
fallback =
true // known bug on API 23: https://stackoverflow.com/a/33509180/2245107
}
}
}
private fun unregister() = Application.connectivity.unregisterNetworkCallback(Callback)
}

View File

@@ -0,0 +1,43 @@
package com.hiddify.hiddify.bg
import android.net.Network
import android.os.Build
import com.hiddify.hiddify.Application
import io.nekohasekai.libbox.InterfaceUpdateListener
object DefaultNetworkMonitor {
var defaultNetwork: Network? = null
private var listener: InterfaceUpdateListener? = null
suspend fun start() {
DefaultNetworkListener.start(this) {
defaultNetwork = it
checkDefaultInterfaceUpdate(it)
}
defaultNetwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Application.connectivity.activeNetwork
} else {
DefaultNetworkListener.get()
}
}
suspend fun stop() {
DefaultNetworkListener.stop(this)
}
fun setListener(listener: InterfaceUpdateListener?) {
this.listener = listener
checkDefaultInterfaceUpdate(defaultNetwork)
}
private fun checkDefaultInterfaceUpdate(
newNetwork: Network?
) {
val listener = listener ?: return
val link = Application.connectivity.getLinkProperties(newNetwork ?: return) ?: return
listener.updateDefaultInterface(link.interfaceName, -1)
}
}

View File

@@ -0,0 +1,134 @@
package com.hiddify.hiddify.bg
import android.net.DnsResolver
import android.os.Build
import android.os.CancellationSignal
import android.system.ErrnoException
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.ktx.tryResumeWithException
import io.nekohasekai.libbox.ExchangeContext
import io.nekohasekai.libbox.LocalDNSTransport
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.runBlocking
import java.net.InetAddress
import java.net.UnknownHostException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object LocalResolver : LocalDNSTransport {
private const val RCODE_NXDOMAIN = 3
override fun raw(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
return runBlocking {
suspendCoroutine { continuation ->
val signal = CancellationSignal()
ctx.onCancel(signal::cancel)
val callback = object : DnsResolver.Callback<ByteArray> {
override fun onAnswer(answer: ByteArray, rcode: Int) {
if (rcode == 0) {
ctx.rawSuccess(answer)
} else {
ctx.errorCode(rcode)
}
continuation.resume(Unit)
}
override fun onError(error: DnsResolver.DnsException) {
when (val cause = error.cause) {
is ErrnoException -> {
ctx.errnoCode(cause.errno)
continuation.resume(Unit)
return
}
}
continuation.tryResumeWithException(error)
}
}
DnsResolver.getInstance().rawQuery(
DefaultNetworkMonitor.defaultNetwork,
message,
DnsResolver.FLAG_NO_RETRY,
Dispatchers.IO.asExecutor(),
signal,
callback
)
}
}
}
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return runBlocking {
suspendCoroutine { continuation ->
val signal = CancellationSignal()
ctx.onCancel(signal::cancel)
val callback = object : DnsResolver.Callback<Collection<InetAddress>> {
@Suppress("ThrowableNotThrown")
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
if (rcode == 0) {
ctx.success((answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
.joinToString("\n"))
} else {
ctx.errorCode(rcode)
}
continuation.resume(Unit)
}
override fun onError(error: DnsResolver.DnsException) {
when (val cause = error.cause) {
is ErrnoException -> {
ctx.errnoCode(cause.errno)
continuation.resume(Unit)
return
}
}
continuation.tryResumeWithException(error)
}
}
val type = when {
network.endsWith("4") -> DnsResolver.TYPE_A
network.endsWith("6") -> DnsResolver.TYPE_AAAA
else -> null
}
if (type != null) {
DnsResolver.getInstance().query(
DefaultNetworkMonitor.defaultNetwork,
domain,
type,
DnsResolver.FLAG_NO_RETRY,
Dispatchers.IO.asExecutor(),
signal,
callback
)
} else {
DnsResolver.getInstance().query(
DefaultNetworkMonitor.defaultNetwork,
domain,
DnsResolver.FLAG_NO_RETRY,
Dispatchers.IO.asExecutor(),
signal,
callback
)
}
}
}
} else {
val underlyingNetwork =
DefaultNetworkMonitor.defaultNetwork ?: error("upstream network not found")
val answer = try {
underlyingNetwork.getAllByName(domain)
} catch (e: UnknownHostException) {
ctx.errorCode(RCODE_NXDOMAIN)
return
}
ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
}
}
}

View File

@@ -0,0 +1,144 @@
package com.hiddify.hiddify.bg
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
import androidx.annotation.RequiresApi
import com.hiddify.hiddify.Application
import io.nekohasekai.libbox.InterfaceUpdateListener
import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.StringIterator
import io.nekohasekai.libbox.TunOptions
import java.net.Inet6Address
import java.net.InetSocketAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.util.Enumeration
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
interface PlatformInterfaceWrapper : PlatformInterface {
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
return true
}
override fun autoDetectInterfaceControl(fd: Int) {
}
override fun openTun(options: TunOptions): Int {
error("invalid argument")
}
override fun useProcFS(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun findConnectionOwner(
ipProtocol: Int,
sourceAddress: String,
sourcePort: Int,
destinationAddress: String,
destinationPort: Int
): Int {
val uid = Application.connectivity.getConnectionOwnerUid(
ipProtocol,
InetSocketAddress(sourceAddress, sourcePort),
InetSocketAddress(destinationAddress, destinationPort)
)
if (uid == Process.INVALID_UID) error("android: connection owner not found")
return uid
}
override fun packageNameByUid(uid: Int): String {
val packages = Application.packageManager.getPackagesForUid(uid)
if (packages.isNullOrEmpty()) error("android: package not found")
return packages[0]
}
@Suppress("DEPRECATION")
override fun uidByPackageName(packageName: String): Int {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Application.packageManager.getPackageUid(
packageName, PackageManager.PackageInfoFlags.of(0)
)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Application.packageManager.getPackageUid(packageName, 0)
} else {
Application.packageManager.getApplicationInfo(packageName, 0).uid
}
} catch (e: PackageManager.NameNotFoundException) {
error("android: package not found")
}
}
override fun usePlatformDefaultInterfaceMonitor(): Boolean {
return true
}
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(listener)
}
override fun closeDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(null)
}
override fun usePlatformInterfaceGetter(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
override fun getInterfaces(): NetworkInterfaceIterator {
return InterfaceArray(NetworkInterface.getNetworkInterfaces())
}
override fun underNetworkExtension(): Boolean {
return false
}
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
NetworkInterfaceIterator {
override fun hasNext(): Boolean {
return iterator.hasMoreElements()
}
override fun next(): LibboxNetworkInterface {
val element = iterator.nextElement()
return LibboxNetworkInterface().apply {
name = element.name
index = element.index
runCatching {
mtu = element.mtu
}
addresses =
StringArray(
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
.iterator()
)
}
}
private fun InterfaceAddress.toPrefix(): String {
return if (address is Inet6Address) {
"${Inet6Address.getByAddress(address.address).hostAddress}/${networkPrefixLength}"
} else {
"${address.hostAddress}/${networkPrefixLength}"
}
}
}
private class StringArray(private val iterator: Iterator<String>) : StringIterator {
override fun hasNext(): Boolean {
return iterator.hasNext()
}
override fun next(): String {
return iterator.next()
}
}
}

View File

@@ -0,0 +1,59 @@
package com.hiddify.hiddify.bg
import android.os.RemoteCallbackList
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.IService
import com.hiddify.hiddify.IServiceCallback
import com.hiddify.hiddify.constant.Status
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub() {
private val callbacks = RemoteCallbackList<IServiceCallback>()
private val broadcastLock = Mutex()
init {
status.observeForever {
broadcast { callback ->
callback.onServiceStatusChanged(it.ordinal)
}
}
}
fun broadcast(work: (IServiceCallback) -> Unit) {
GlobalScope.launch(Dispatchers.Main) {
broadcastLock.withLock {
val count = callbacks.beginBroadcast()
try {
repeat(count) {
try {
work(callbacks.getBroadcastItem(it))
} catch (_: Exception) {
}
}
} finally {
callbacks.finishBroadcast()
}
}
}
}
override fun getStatus(): Int {
return (status.value ?: Status.Stopped).ordinal
}
override fun registerCallback(callback: IServiceCallback) {
callbacks.register(callback)
}
override fun unregisterCallback(callback: IServiceCallback?) {
callbacks.unregister(callback)
}
fun close() {
callbacks.kill()
}
}

View File

@@ -0,0 +1,109 @@
package com.hiddify.hiddify.bg
import com.hiddify.hiddify.IService
import com.hiddify.hiddify.IServiceCallback
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class ServiceConnection(
private val context: Context,
callback: Callback,
private val register: Boolean = true,
) : ServiceConnection {
companion object {
private const val TAG = "ServiceConnection"
}
private val callback = ServiceCallback(callback)
private var service: IService? = null
val status get() = service?.status?.let { Status.values()[it] } ?: Status.Stopped
fun connect() {
val intent = runBlocking {
withContext(Dispatchers.IO) {
Intent(context, VPNService::class.java).setAction(Action.SERVICE)
}
}
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
}
fun disconnect() {
try {
context.unbindService(this)
} catch (_: IllegalArgumentException) {
}
}
fun reconnect() {
try {
context.unbindService(this)
} catch (_: IllegalArgumentException) {
}
val intent = runBlocking {
withContext(Dispatchers.IO) {
Intent(context, VPNService::class.java).setAction(Action.SERVICE)
}
}
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
}
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IService.Stub.asInterface(binder)
this.service = service
try {
if (register) service.registerCallback(callback)
callback.onServiceStatusChanged(service.status)
} catch (e: RemoteException) {
Log.e(TAG, "initialize service connection", e)
}
}
override fun onServiceDisconnected(name: ComponentName?) {
try {
service?.unregisterCallback(callback)
} catch (e: RemoteException) {
Log.e(TAG, "cleanup service connection", e)
}
}
override fun onBindingDied(name: ComponentName?) {
reconnect()
}
interface Callback {
fun onServiceStatusChanged(status: Status)
fun onServiceAlert(type: Alert, message: String?) {}
fun onServiceWriteLog(message: String?) {}
fun onServiceResetLogs(messages: MutableList<String>) {}
}
class ServiceCallback(private val callback: Callback) : IServiceCallback.Stub() {
override fun onServiceStatusChanged(status: Int) {
callback.onServiceStatusChanged(Status.values()[status])
}
override fun onServiceAlert(type: Int, message: String?) {
callback.onServiceAlert(Alert.values()[type], message)
}
override fun onServiceWriteLog(message: String?) = callback.onServiceWriteLog(message)
override fun onServiceResetLogs(messages: MutableList<String>) =
callback.onServiceResetLogs(messages)
}
}

View File

@@ -0,0 +1,80 @@
package com.hiddify.hiddify.bg
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.hiddify.hiddify.Application
import com.hiddify.hiddify.MainActivity
import com.hiddify.hiddify.R
import com.hiddify.hiddify.constant.Action
class ServiceNotification(private val service: Service) {
companion object {
private const val notificationId = 1
private const val notificationChannel = "service"
private val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
fun checkPermission(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
return true
}
if (Application.notification.areNotificationsEnabled()) {
return true
}
return false
}
}
private val notification by lazy {
NotificationCompat.Builder(service, notificationChannel).setWhen(0)
.setContentTitle("hiddify next")
.setContentText("service running").setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.ic_stat_logo)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentIntent(
PendingIntent.getActivity(
service,
0,
Intent(
service,
MainActivity::class.java
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
flags
)
)
.setPriority(NotificationCompat.PRIORITY_LOW).apply {
addAction(
NotificationCompat.Action.Builder(
0, service.getText(R.string.stop), PendingIntent.getBroadcast(
service,
0,
Intent(Action.SERVICE_CLOSE).setPackage(service.packageName),
flags
)
).build()
)
}
}
fun show() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel(
NotificationChannel(
notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
)
)
}
service.startForeground(notificationId, notification.build())
}
fun close() {
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
}
}

View File

@@ -0,0 +1,147 @@
package com.hiddify.hiddify.bg
import com.hiddify.hiddify.Settings
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import io.nekohasekai.libbox.TunOptions
class VPNService : VpnService(), PlatformInterfaceWrapper {
companion object {
private const val TAG = "A/VPNService"
}
private val service = BoxService(this, this)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
service.onStartCommand(intent, flags, startId)
override fun onBind(intent: Intent) = service.onBind(intent)
override fun onDestroy() {
service.onDestroy()
}
override fun onRevoke() {
service.onRevoke()
}
override fun autoDetectInterfaceControl(fd: Int) {
protect(fd)
}
override fun openTun(options: TunOptions): Int {
if (prepare(this) != null) error("android: missing vpn permission")
val builder = Builder()
.setSession("sing-box")
.setMtu(options.mtu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setMetered(false)
}
val inet4Address = options.inet4Address
if (inet4Address.hasNext()) {
while (inet4Address.hasNext()) {
val address = inet4Address.next()
builder.addAddress(address.address, address.prefix)
}
}
val inet6Address = options.inet6Address
if (inet6Address.hasNext()) {
while (inet6Address.hasNext()) {
val address = inet6Address.next()
builder.addAddress(address.address, address.prefix)
}
}
if (options.autoRoute) {
builder.addDnsServer(options.dnsServerAddress)
val inet4RouteAddress = options.inet4RouteAddress
if (inet4RouteAddress.hasNext()) {
while (inet4RouteAddress.hasNext()) {
val address = inet4RouteAddress.next()
builder.addRoute(address.address, address.prefix)
}
} else {
builder.addRoute("0.0.0.0", 0)
}
val inet6RouteAddress = options.inet6RouteAddress
if (inet6RouteAddress.hasNext()) {
while (inet6RouteAddress.hasNext()) {
val address = inet6RouteAddress.next()
builder.addRoute(address.address, address.prefix)
}
} else {
builder.addRoute("::", 0)
}
if (Settings.perAppProxyEnabled) {
val appList = Settings.perAppProxyList
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
appList.forEach {
try {
builder.addAllowedApplication(it)
} catch (_: NameNotFoundException) {
}
}
builder.addAllowedApplication(packageName)
} else {
appList.forEach {
try {
builder.addDisallowedApplication(it)
} catch (_: NameNotFoundException) {
}
}
}
} else {
val includePackage = options.includePackage
if (includePackage.hasNext()) {
while (includePackage.hasNext()) {
try {
builder.addAllowedApplication(includePackage.next())
} catch (_: NameNotFoundException) {
}
}
}
val excludePackage = options.excludePackage
if (excludePackage.hasNext()) {
while (excludePackage.hasNext()) {
try {
builder.addDisallowedApplication(excludePackage.next())
} catch (_: NameNotFoundException) {
}
}
}
}
}
if (options.isHTTPProxyEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
builder.setHttpProxy(
ProxyInfo.buildDirectProxy(
options.httpProxyServer,
options.httpProxyServerPort
)
)
} else {
error("android: tun.platform.http_proxy requires android 10 or higher")
}
}
val pfd =
builder.establish() ?: error("android: the application is not prepared or is revoked")
service.fileDescriptor = pfd
return pfd.fd
}
override fun writeLog(message: String) = service.writeLog(message)
}

View File

@@ -0,0 +1,7 @@
package com.hiddify.hiddify.constant
object Action {
const val SERVICE = "com.hiddify.app.SERVICE"
const val SERVICE_CLOSE = "com.hiddify.app.SERVICE_CLOSE"
const val SERVICE_RELOAD = "com.hiddify.app.sfa.SERVICE_RELOAD"
}

View File

@@ -0,0 +1,10 @@
package com.hiddify.hiddify.constant
enum class Alert {
RequestVPNPermission,
RequestNotificationPermission,
EmptyConfiguration,
StartCommandServer,
CreateService,
StartService
}

View File

@@ -0,0 +1,16 @@
package com.hiddify.hiddify.constant
object SettingsKey {
const val SELECTED_CONFIG_PATH = "selected_config_path"
const val DISABLE_MEMORY_LIMIT = "disable_memory_limit"
const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled"
const val PER_APP_PROXY_MODE = "per_app_proxy_mode"
const val PER_APP_PROXY_LIST = "per_app_proxy_list"
const val PER_APP_PROXY_UPDATE_ON_CHANGE = "per_app_proxy_update_on_change"
// cache
const val STARTED_BY_USER = "started_by_user"
}

View File

@@ -0,0 +1,8 @@
package com.hiddify.hiddify.constant
enum class Status {
Stopped,
Starting,
Started,
Stopping,
}

View File

@@ -0,0 +1,18 @@
package com.hiddify.hiddify.ktx
import kotlin.coroutines.Continuation
fun <T> Continuation<T>.tryResume(value: T) {
try {
resumeWith(Result.success(value))
} catch (ignored: IllegalStateException) {
}
}
fun <T> Continuation<T>.tryResumeWithException(exception: Throwable) {
try {
resumeWith(Result.failure(exception))
} catch (ignored: IllegalStateException) {
}
}

View File

@@ -1,80 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- exclude 127.0.0.0/8 169.254.0.0/16 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12 -->
<string-array name="bypass_private_route" translatable="false">
<item>1.0.0.0/8</item>
<item>2.0.0.0/7</item>
<item>4.0.0.0/6</item>
<item>8.0.0.0/7</item>
<item>11.0.0.0/8</item>
<item>12.0.0.0/6</item>
<item>16.0.0.0/4</item>
<item>32.0.0.0/3</item>
<item>64.0.0.0/3</item>
<item>96.0.0.0/4</item>
<item>112.0.0.0/5</item>
<item>120.0.0.0/6</item>
<item>124.0.0.0/7</item>
<item>126.0.0.0/8</item>
<item>128.0.0.0/3</item>
<item>160.0.0.0/5</item>
<item>168.0.0.0/8</item>
<item>169.0.0.0/9</item>
<item>169.128.0.0/10</item>
<item>169.192.0.0/11</item>
<item>169.224.0.0/12</item>
<item>169.240.0.0/13</item>
<item>169.248.0.0/14</item>
<item>169.252.0.0/15</item>
<item>169.255.0.0/16</item>
<item>170.0.0.0/7</item>
<item>172.0.0.0/12</item>
<item>172.32.0.0/11</item>
<item>172.64.0.0/10</item>
<item>172.128.0.0/9</item>
<item>173.0.0.0/8</item>
<item>174.0.0.0/7</item>
<item>176.0.0.0/4</item>
<item>192.0.0.0/9</item>
<item>192.128.0.0/11</item>
<item>192.160.0.0/13</item>
<item>192.169.0.0/16</item>
<item>192.170.0.0/15</item>
<item>192.172.0.0/14</item>
<item>192.176.0.0/12</item>
<item>192.192.0.0/10</item>
<item>193.0.0.0/8</item>
<item>194.0.0.0/7</item>
<item>196.0.0.0/6</item>
<item>200.0.0.0/5</item>
<item>208.0.0.0/4</item>
<item>240.0.0.0/5</item>
<item>248.0.0.0/6</item>
<item>252.0.0.0/7</item>
<item>254.0.0.0/8</item>
<item>255.0.0.0/9</item>
<item>255.128.0.0/10</item>
<item>255.192.0.0/11</item>
<item>255.224.0.0/12</item>
<item>255.240.0.0/13</item>
<item>255.248.0.0/14</item>
<item>255.252.0.0/15</item>
<item>255.254.0.0/16</item>
<item>255.255.0.0/17</item>
<item>255.255.128.0/18</item>
<item>255.255.192.0/19</item>
<item>255.255.224.0/20</item>
<item>255.255.240.0/21</item>
<item>255.255.248.0/22</item>
<item>255.255.252.0/23</item>
<item>255.255.254.0/24</item>
<item>255.255.255.0/25</item>
<item>255.255.255.128/26</item>
<item>255.255.255.192/27</item>
<item>255.255.255.224/28</item>
<item>255.255.255.240/29</item>
<item>255.255.255.248/30</item>
<item>255.255.255.252/31</item>
<item>255.255.255.254/32</item>
</string-array>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="stop">Stop</string>
</resources>

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true

0
assets/core/.gitkeep Normal file
View File

Binary file not shown.

View File

@@ -1,16 +0,0 @@
external-controller: 127.0.0.1:22345
mixed-port: 22346
rules:
# localhost rule
- DOMAIN-KEYWORD,announce,DIRECT
- DOMAIN-KEYWORD,torrent,DIRECT
- DOMAIN-KEYWORD,tracker,DIRECT
- DOMAIN-SUFFIX,smtp,DIRECT
- DOMAIN-SUFFIX,local,DIRECT
- IP-CIDR,192.168.0.0/16,DIRECT
- IP-CIDR,10.0.0.0/8,DIRECT
- IP-CIDR,172.16.0.0/12,DIRECT
- IP-CIDR,127.0.0.0/8,DIRECT
- IP-CIDR,100.64.0.0/10,DIRECT

View File

@@ -145,8 +145,18 @@
"unexpected": "unexpected failure",
"core": "clash failure ${reason}"
},
"singbox": {
"unexpected": "unexpected service failure",
"serviceNotRunning": "Service not running",
"invalidConfig": "Configuration is not valid",
"create": "Error creating service",
"start": "Error starting service"
},
"connectivity": {
"unexpected": "unexpected failure"
"unexpected": "unexpected failure",
"missingVpnPermission": "Missing VPN permission",
"missingNotificationPermission": "Missing Notification permission",
"core": "Core failure"
},
"profiles": {
"unexpected": "unexpected failure",

View File

@@ -145,8 +145,18 @@
"unexpected": "خطایی رخ داده",
"core": "خطای کلش ${reason}"
},
"singbox": {
"unexpected": "خطایی غیر منتظره در سرویس رخ داد",
"serviceNotRunning": "سرویس در حال اجرا نیست",
"invalidConfig": "کانفیگ غیر معتبر",
"create": "در ایجاد سرویس خطایی رخ داده",
"start": "در راه‌اندازی سرویس خطایی رخ داده"
},
"connectivity": {
"unexpected": "خطایی رخ داده"
"unexpected": "خطایی رخ داده",
"missingVpnPermission": "نیازمند دسترسی VPN",
"missingNotificationPermission": "نیازمند دسترسی اعلانات",
"core": "خطای هسته"
},
"profiles": {
"unexpected": "خطایی رخ داده",

1
core

Submodule core deleted from 1149e93363

View File

@@ -39,7 +39,15 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
overrides: [sharedPreferencesProvider.overrideWithValue(sharedPreferences)],
);
Loggy.initLoggy(logPrinter: const PrettyPrinter());
// Loggy.initLoggy(logPrinter: const PrettyPrinter());
final filesEditor = container.read(filesEditorServiceProvider);
await filesEditor.init();
Loggy.initLoggy(
logPrinter: MultiLogPrinter(
const PrettyPrinter(),
FileLogPrinter(filesEditor.appLogsPath),
),
);
final silentStart =
container.read(prefsControllerProvider).general.silentStart;
@@ -68,12 +76,10 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
Future<void> initAppServices(
Result Function<Result>(ProviderListenable<Result>) read,
) async {
await read(filesEditorServiceProvider).init();
// await read(filesEditorServiceProvider).init();
await Future.wait(
[
read(connectivityServiceProvider).init(),
read(clashServiceProvider).init(),
read(clashServiceProvider).start(),
read(notificationServiceProvider).init(),
],
);
@@ -83,6 +89,7 @@ Future<void> initAppServices(
Future<void> initControllers(
Result Function<Result>(ProviderListenable<Result>) read,
) async {
_loggy.debug("initializing controllers");
await Future.wait(
[
read(activeProfileProvider.future),

View File

@@ -36,7 +36,7 @@ class PrefsController extends _$PrefsController with AppLogger {
ClashConfig _getClashPrefs() {
final persisted = _prefs.getString(_overridesKey);
if (persisted == null) return ClashConfig.initial;
if (persisted == null) return const ClashConfig();
return ClashConfig.fromJson(jsonDecode(persisted) as Map<String, dynamic>);
}

View File

@@ -33,11 +33,11 @@ GoRouter router(RouterRef ref) {
int getCurrentIndex(BuildContext context) {
final String location = GoRouterState.of(context).location;
if (location == HomeRoute.path) return 0;
if (location.startsWith(ProxiesRoute.path)) return 1;
if (location.startsWith(LogsRoute.path)) return 2;
if (location.startsWith(SettingsRoute.path)) return 3;
if (location.startsWith(AboutRoute.path)) return 4;
if (location == const HomeRoute().location) return 0;
if (location.startsWith(const ProxiesRoute().location)) return 1;
if (location.startsWith(const LogsRoute().location)) return 2;
if (location.startsWith(const SettingsRoute().location)) return 3;
if (location.startsWith(const AboutRoute().location)) return 4;
return 0;
}

View File

@@ -23,9 +23,9 @@ part 'desktop_routes.g.dart';
TypedGoRoute<LogsRoute>(path: LogsRoute.path),
TypedGoRoute<SettingsRoute>(
path: SettingsRoute.path,
routes: [
TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
],
// routes: [
// TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
// ],
),
TypedGoRoute<AboutRoute>(path: AboutRoute.path),
],
@@ -59,18 +59,18 @@ class SettingsRoute extends GoRouteData {
}
}
class ClashOverridesRoute extends GoRouteData {
const ClashOverridesRoute();
static const path = 'clash';
// class ClashOverridesRoute extends GoRouteData {
// const ClashOverridesRoute();
// static const path = 'clash';
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage(
fullscreenDialog: true,
child: ClashOverridesPage(),
);
}
}
// @override
// Page<void> buildPage(BuildContext context, GoRouterState state) {
// return const MaterialPage(
// fullscreenDialog: true,
// child: ClashOverridesPage(),
// );
// }
// }
class AboutRoute extends GoRouteData {
const AboutRoute();

View File

@@ -20,9 +20,9 @@ part 'mobile_routes.g.dart';
TypedGoRoute<LogsRoute>(path: LogsRoute.path),
TypedGoRoute<SettingsRoute>(
path: SettingsRoute.path,
routes: [
TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
],
// routes: [
// TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
// ],
),
TypedGoRoute<AboutRoute>(path: AboutRoute.path),
],
@@ -69,20 +69,20 @@ class SettingsRoute extends GoRouteData {
}
}
class ClashOverridesRoute extends GoRouteData {
const ClashOverridesRoute();
static const path = 'clash';
// class ClashOverridesRoute extends GoRouteData {
// const ClashOverridesRoute();
// static const path = 'clash';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
// static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage(
fullscreenDialog: true,
child: ClashOverridesPage(),
);
}
}
// @override
// Page<void> buildPage(BuildContext context, GoRouterState state) {
// return const MaterialPage(
// fullscreenDialog: true,
// child: ClashOverridesPage(),
// );
// }
// }
class AboutRoute extends GoRouteData {
const AboutRoute();

145
lib/data/api/clash_api.dart Normal file
View File

@@ -0,0 +1,145 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class ClashApi with InfraLogger {
ClashApi(int port) : address = "${Constants.localHost}:$port";
final String address;
late final _clashDio = Dio(
BaseOptions(
baseUrl: "http://$address",
connectTimeout: const Duration(seconds: 3),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 3),
),
);
TaskEither<String, List<ClashProxy>> getProxies() {
return TaskEither(
() async {
final response = await _clashDio.get("/proxies");
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
final proxies = (jsonDecode(response.data! as String)["proxies"]
as Map<String, dynamic>)
.entries
.map(
(e) {
final proxyMap = (e.value as Map<String, dynamic>)
..putIfAbsent('name', () => e.key);
return ClashProxy.fromJson(proxyMap);
},
);
return right(proxies.toList());
},
);
}
TaskEither<String, Unit> changeProxy(String selectorName, String proxyName) {
return TaskEither(
() async {
final response = await _clashDio.put(
"/proxies/$selectorName",
data: {"name": proxyName},
);
if (response.statusCode != HttpStatus.noContent) {
return left(response.statusMessage ?? "");
}
return right(unit);
},
);
}
TaskEither<String, int> getProxyDelay(
String name,
String url, {
Duration timeout = const Duration(seconds: 10),
}) {
return TaskEither(
() async {
final response = await _clashDio.get<Map>(
"/proxies/$name/delay",
queryParameters: {
"timeout": timeout.inMilliseconds,
"url": url,
},
);
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
return right(response.data!["delay"] as int);
},
);
}
TaskEither<String, ClashConfig> getConfigs() {
return TaskEither(
() async {
final response = await _clashDio.get("/configs");
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
final config =
ClashConfig.fromJson(response.data as Map<String, dynamic>);
return right(config);
},
);
}
TaskEither<String, Unit> updateConfigs(String path) {
return TaskEither.of(unit);
}
TaskEither<String, Unit> patchConfigs(ClashConfig config) {
return TaskEither(
() async {
final response = await _clashDio.patch(
"/configs",
data: config.toJson(),
);
if (response.statusCode != HttpStatus.noContent) {
return left(response.statusMessage ?? "");
}
return right(unit);
},
);
}
Stream<ClashLog> watchLogs(LogLevel level) {
return const Stream.empty();
}
Stream<ClashTraffic> watchTraffic() {
final channel = WebSocketChannel.connect(
Uri.parse("ws://$address/traffic"),
);
return channel.stream.map(
(event) {
return ClashTraffic.fromJson(
jsonDecode(event as String) as Map<String, dynamic>,
);
},
);
}
TaskEither<String, ClashTraffic> getTraffic() {
return TaskEither(
() async {
final response = await _clashDio.get<Map<String, dynamic>>("/traffic");
if (response.statusCode != 200 || response.data == null) {
return left(response.statusMessage ?? "");
}
return right(ClashTraffic.fromJson(response.data!));
},
);
}
}

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:hiddify/data/api/clash_api.dart';
import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/local/database.dart';
import 'package:hiddify/data/repository/repository.dart';
import 'package:hiddify/data/repository/update_repository_impl.dart';
import 'package:hiddify/domain/app/app.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -28,21 +30,26 @@ ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao(
ref.watch(appDatabaseProvider),
);
@Riverpod(keepAlive: true)
ClashFacade clashFacade(ClashFacadeRef ref) => ClashFacadeImpl(
clashService: ref.watch(clashServiceProvider),
filesEditor: ref.watch(filesEditorServiceProvider),
);
@Riverpod(keepAlive: true)
ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) =>
ProfilesRepositoryImpl(
profilesDao: ref.watch(profilesDaoProvider),
filesEditor: ref.watch(filesEditorServiceProvider),
clashFacade: ref.watch(clashFacadeProvider),
singbox: ref.watch(coreFacadeProvider),
dio: ref.watch(dioProvider),
);
@Riverpod(keepAlive: true)
UpdateRepository updateRepository(UpdateRepositoryRef ref) =>
UpdateRepositoryImpl(ref.watch(dioProvider));
@Riverpod(keepAlive: true)
ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort);
@Riverpod(keepAlive: true)
CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
ref.watch(singboxServiceProvider),
ref.watch(filesEditorServiceProvider),
ref.watch(clashApiProvider),
ref.watch(connectivityServiceProvider),
);

View File

@@ -5,8 +5,8 @@ import 'package:drift/native.dart';
import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/local/tables.dart';
import 'package:hiddify/data/local/type_converters.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
part 'database.g.dart';
@@ -22,8 +22,8 @@ class AppDatabase extends _$AppDatabase {
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
final dbDir = await FilesEditorService.getDatabaseDirectory();
final file = File(p.join(dbDir.path, 'db.sqlite'));
return NativeDatabase.createInBackground(file);
});
}

View File

@@ -1,116 +0,0 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/services/clash/clash.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/utils/utils.dart';
class ClashFacadeImpl
with ExceptionHandler, InfraLogger
implements ClashFacade {
ClashFacadeImpl({
required ClashService clashService,
required FilesEditorService filesEditor,
}) : _clash = clashService,
_filesEditor = filesEditor;
final ClashService _clash;
final FilesEditorService _filesEditor;
@override
TaskEither<ClashFailure, ClashConfig> getConfigs() {
return exceptionHandler(
() async => _clash.getConfigs().mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, bool> validateConfig(String configFileName) {
return exceptionHandler(
() async {
final path = _filesEditor.configPath(configFileName);
return _clash.validateConfig(path).mapLeft(ClashFailure.core).run();
},
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, Unit> changeConfigs(String configFileName) {
return exceptionHandler(
() async {
loggy.debug("changing config, file name: [$configFileName]");
final path = _filesEditor.configPath(configFileName);
return _clash.updateConfigs(path).mapLeft(ClashFailure.core).run();
},
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, Unit> patchOverrides(ClashConfig overrides) {
return exceptionHandler(
() async =>
_clash.patchConfigs(overrides).mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, List<ClashProxy>> getProxies() {
return exceptionHandler(
() async => _clash.getProxies().mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, Unit> changeProxy(
String selectorName,
String proxyName,
) {
return exceptionHandler(
() async => _clash
.changeProxy(selectorName, proxyName)
.mapLeft(ClashFailure.core)
.run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, ClashTraffic> getTraffic() {
return exceptionHandler(
() async => _clash.getTraffic().mapLeft(ClashFailure.core).run(),
ClashFailure.unexpected,
);
}
@override
TaskEither<ClashFailure, int> testDelay(
String proxyName, {
String testUrl = Constants.delayTestUrl,
}) {
return exceptionHandler(
() async {
final result = _clash
.getProxyDelay(proxyName, testUrl)
.mapLeft(ClashFailure.core)
.run();
return result;
},
ClashFailure.unexpected,
);
}
@override
Stream<Either<ClashFailure, ClashLog>> watchLogs() {
return _clash
.watchLogs(LogLevel.info)
.handleExceptions(ClashFailure.unexpected);
}
}

View File

@@ -0,0 +1,187 @@
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/api/clash_api.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connection_status.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/services/connectivity/connectivity.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/services/singbox/singbox_service.dart';
import 'package:hiddify/utils/utils.dart';
class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
CoreFacadeImpl(this.singbox, this.filesEditor, this.clash, this.connectivity);
final SingboxService singbox;
final FilesEditorService filesEditor;
final ClashApi clash;
final ConnectivityService connectivity;
bool _initialized = false;
@override
TaskEither<CoreServiceFailure, Unit> setup() {
if (_initialized) return TaskEither.of(unit);
return exceptionHandler(
() {
loggy.debug("setting up singbox");
return singbox
.setup(
filesEditor.baseDir.path,
filesEditor.workingDir.path,
filesEditor.tempDir.path,
)
.map((r) {
loggy.debug("setup complete");
_initialized = true;
return r;
})
.mapLeft(CoreServiceFailure.other)
.run();
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> parseConfig(String path) {
return exceptionHandler(
() {
return singbox
.parseConfig(path)
.mapLeft(CoreServiceFailure.invalidConfig)
.run();
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> changeConfig(String fileName) {
return exceptionHandler(
() {
final configPath = filesEditor.configPath(fileName);
loggy.debug("changing config to: $configPath");
return setup()
.andThen(
() =>
singbox.create(configPath).mapLeft(CoreServiceFailure.create),
)
.run();
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> start() {
return exceptionHandler(
() => singbox.start().mapLeft(CoreServiceFailure.start).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> stop() {
return exceptionHandler(
() => singbox.stop().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
Stream<Either<CoreServiceFailure, String>> watchLogs() {
return singbox
.watchLogs(filesEditor.logsPath)
.handleExceptions(CoreServiceFailure.unexpected);
}
@override
TaskEither<CoreServiceFailure, ClashConfig> getConfigs() {
return exceptionHandler(
() async => clash.getConfigs().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> patchOverrides(ClashConfig overrides) {
return exceptionHandler(
() async =>
clash.patchConfigs(overrides).mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, List<ClashProxy>> getProxies() {
return exceptionHandler(
() async => clash.getProxies().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> changeProxy(
String selectorName,
String proxyName,
) {
return exceptionHandler(
() async => clash
.changeProxy(selectorName, proxyName)
.mapLeft(CoreServiceFailure.other)
.run(),
CoreServiceFailure.unexpected,
);
}
@override
Stream<Either<CoreServiceFailure, ClashTraffic>> watchTraffic() {
return clash.watchTraffic().handleExceptions(CoreServiceFailure.unexpected);
}
@override
TaskEither<CoreServiceFailure, int> testDelay(
String proxyName, {
String testUrl = Defaults.delayTestUrl,
}) {
return exceptionHandler(
() async {
final result = clash
.getProxyDelay(proxyName, testUrl)
.mapLeft(CoreServiceFailure.other)
.run();
return result;
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> connect() {
return exceptionHandler(
() async {
await connectivity.connect();
return right(unit);
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> disconnect() {
return exceptionHandler(
() async {
await connectivity.disconnect();
return right(unit);
},
CoreServiceFailure.unexpected,
);
}
@override
Stream<ConnectionStatus> watchConnectionStatus() =>
connectivity.watchConnectionStatus();
}

View File

@@ -4,9 +4,9 @@ import 'package:dio/dio.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/repository/exception_handlers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/enums.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/services/files_editor_service.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:meta/meta.dart';
@@ -18,13 +18,13 @@ class ProfilesRepositoryImpl
ProfilesRepositoryImpl({
required this.profilesDao,
required this.filesEditor,
required this.clashFacade,
required this.singbox,
required this.dio,
});
final ProfilesDao profilesDao;
final FilesEditorService filesEditor;
final ClashFacade clashFacade;
final SingboxFacade singbox;
final Dio dio;
@override
@@ -166,20 +166,17 @@ class ProfilesRepositoryImpl
() async {
final path = filesEditor.configPath(fileName);
final response = await dio.download(url, path);
if (response.statusCode != 200) {
await File(path).delete();
return left(const ProfileUnexpectedFailure());
}
final isValid = await clashFacade
.validateConfig(fileName)
.getOrElse((_) => false)
.run();
if (!isValid) {
await File(path).delete();
return left(const ProfileFailure.invalidConfig());
}
final profile = Profile.fromResponse(url, response.headers.map);
return right(profile);
final parseResult = await singbox.parseConfig(path).run();
return parseResult.fold(
(l) async {
await File(path).delete();
return left(ProfileFailure.invalidConfig(l.msg));
},
(_) {
final profile = Profile.fromResponse(url, response.headers.map);
return right(profile);
},
);
},
);
}

View File

@@ -1,2 +1,2 @@
export 'clash_facade_impl.dart';
export 'core_facade_impl.dart';
export 'profiles_repository_impl.dart';

View File

@@ -1,7 +1,6 @@
export 'clash_config.dart';
export 'clash_enums.dart';
export 'clash_facade.dart';
export 'clash_failures.dart';
export 'clash_log.dart';
export 'clash_proxy.dart';
export 'clash_traffic.dart';

View File

@@ -24,12 +24,6 @@ class ClashConfig with _$ClashConfig {
bool? ipv6,
}) = _ClashConfig;
static const initial = ClashConfig(
httpPort: 12346,
socksPort: 12347,
mixedPort: 12348,
);
ClashConfig patch(ClashConfigPatch patch) {
return copyWith(
httpPort: (patch.httpPort ?? optionOf(httpPort)).toNullable(),

View File

@@ -38,6 +38,7 @@ enum ProxyType {
hysteria("Hysteria"),
wireGuard("WireGuard"),
tuic("Tuic"),
ssh("SSH"),
relay("Relay"),
selector("Selector"),
fallback("Fallback"),

View File

@@ -1,32 +1,24 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/core_service_failure.dart';
abstract class ClashFacade {
TaskEither<ClashFailure, ClashConfig> getConfigs();
TaskEither<CoreServiceFailure, ClashConfig> getConfigs();
TaskEither<ClashFailure, bool> validateConfig(String configFileName);
TaskEither<CoreServiceFailure, Unit> patchOverrides(ClashConfig overrides);
/// change active configuration file by [configFileName]
TaskEither<ClashFailure, Unit> changeConfigs(String configFileName);
TaskEither<CoreServiceFailure, List<ClashProxy>> getProxies();
TaskEither<ClashFailure, Unit> patchOverrides(ClashConfig overrides);
TaskEither<ClashFailure, List<ClashProxy>> getProxies();
TaskEither<ClashFailure, Unit> changeProxy(
TaskEither<CoreServiceFailure, Unit> changeProxy(
String selectorName,
String proxyName,
);
TaskEither<ClashFailure, int> testDelay(
TaskEither<CoreServiceFailure, int> testDelay(
String proxyName, {
String testUrl = Constants.delayTestUrl,
String testUrl = Defaults.delayTestUrl,
});
TaskEither<ClashFailure, ClashTraffic> getTraffic();
Stream<Either<ClashFailure, ClashLog>> watchLogs();
Stream<Either<CoreServiceFailure, ClashTraffic>> watchTraffic();
}

View File

@@ -1,27 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/failures.dart';
part 'clash_failures.freezed.dart';
// TODO: rewrite
@freezed
sealed class ClashFailure with _$ClashFailure, Failure {
const ClashFailure._();
const factory ClashFailure.unexpected(
Object error,
StackTrace stackTrace,
) = ClashUnexpectedFailure;
const factory ClashFailure.core([String? error]) = ClashCoreFailure;
@override
String present(TranslationsEn t) {
return switch (this) {
ClashUnexpectedFailure() => t.failure.clash.unexpected,
ClashCoreFailure(:final error) =>
t.failure.clash.core(reason: error ?? ""),
};
}
}

View File

@@ -7,7 +7,7 @@ part 'clash_proxy.g.dart';
// TODO: test and improve
@Freezed(fromJson: true)
class ClashProxy with _$ClashProxy {
sealed class ClashProxy with _$ClashProxy {
const ClashProxy._();
const factory ClashProxy.group({
@@ -15,6 +15,7 @@ class ClashProxy with _$ClashProxy {
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
required List<String> all,
required String now,
@Default(false) bool udp,
List<ClashHistory>? history,
@JsonKey(includeFromJson: false, includeToJson: false) int? delay,
}) = ClashProxyGroup;
@@ -22,6 +23,7 @@ class ClashProxy with _$ClashProxy {
const factory ClashProxy.item({
required String name,
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
@Default(false) bool udp,
List<ClashHistory>? history,
@JsonKey(includeFromJson: false, includeToJson: false) int? delay,
}) = ClashProxyItem;

View File

@@ -0,0 +1,11 @@
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/connectivity/connection_status.dart';
import 'package:hiddify/domain/core_service_failure.dart';
abstract interface class ConnectionFacade {
TaskEither<CoreServiceFailure, Unit> connect();
TaskEither<CoreServiceFailure, Unit> disconnect();
Stream<ConnectionStatus> watchConnectionStatus();
}

View File

@@ -0,0 +1,40 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/domain/failures.dart';
part 'connection_failure.freezed.dart';
@freezed
sealed class ConnectionFailure with _$ConnectionFailure, Failure {
const ConnectionFailure._();
const factory ConnectionFailure.unexpected([
Object? error,
StackTrace? stackTrace,
]) = UnexpectedConnectionFailure;
const factory ConnectionFailure.missingVpnPermission([String? message]) =
MissingVpnPermission;
const factory ConnectionFailure.missingNotificationPermission([
String? message,
]) = MissingNotificationPermission;
const factory ConnectionFailure.core(CoreServiceFailure failure) =
CoreConnectionFailure;
@override
String present(TranslationsEn t) {
return switch (this) {
UnexpectedConnectionFailure() => t.failure.connectivity.unexpected,
MissingVpnPermission(:final message) =>
t.failure.connectivity.missingVpnPermission +
(message == null ? "" : ": $message"),
MissingNotificationPermission(:final message) =>
t.failure.connectivity.missingNotificationPermission +
(message == null ? "" : ": $message"),
CoreConnectionFailure(:final failure) => failure.present(t),
};
}
}

View File

@@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/connectivity/connectivity_failure.dart';
import 'package:hiddify/domain/connectivity/connection_failure.dart';
part 'connection_status.freezed.dart';
@@ -9,24 +9,13 @@ sealed class ConnectionStatus with _$ConnectionStatus {
const ConnectionStatus._();
const factory ConnectionStatus.disconnected([
ConnectivityFailure? connectFailure,
ConnectionFailure? connectionFailure,
]) = Disconnected;
const factory ConnectionStatus.connecting() = Connecting;
const factory ConnectionStatus.connected([
ConnectivityFailure? disconnectFailure,
]) = Connected;
const factory ConnectionStatus.connected() = Connected;
const factory ConnectionStatus.disconnecting() = Disconnecting;
factory ConnectionStatus.fromBool(bool connected) {
return connected
? const ConnectionStatus.connected()
: const Disconnected();
}
bool get isConnected => switch (this) {
Connected() => true,
_ => false,
};
bool get isConnected => switch (this) { Connected() => true, _ => false };
bool get isSwitching => switch (this) {
Connecting() => true,

View File

@@ -1,4 +1,5 @@
export 'connection_facade.dart';
export 'connection_failure.dart';
export 'connection_status.dart';
export 'connectivity_failure.dart';
export 'network_prefs.dart';
export 'traffic.dart';

View File

@@ -1,21 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/failures.dart';
part 'connectivity_failure.freezed.dart';
// TODO: rewrite
@freezed
sealed class ConnectivityFailure with _$ConnectivityFailure, Failure {
const ConnectivityFailure._();
const factory ConnectivityFailure.unexpected([
Object? error,
StackTrace? stackTrace,
]) = ConnectivityUnexpectedFailure;
@override
String present(TranslationsEn t) {
return t.failure.connectivity.unexpected;
}
}

View File

@@ -1,9 +1,8 @@
abstract class Constants {
static const localHost = '127.0.0.1';
static const clashFolderName = "clash";
static const delayTestUrl = "https://www.google.com";
static const configFileName = "config";
static const countryMMDBFileName = "Country";
static const geoipFileName = "geoip.db";
static const geositeFileName = "geosite.db";
static const configsFolderName = "configs";
static const localHost = "127.0.0.1";
static const githubUrl = "https://github.com/hiddify/hiddify-next";
static const githubReleasesApiUrl =
"https://api.github.com/repos/hiddify/hiddify-next/releases";
@@ -11,3 +10,9 @@ abstract class Constants {
"https://github.com/hiddify/hiddify-next/releases/latest";
static const telegramChannelUrl = "https://t.me/hiddify";
}
abstract class Defaults {
static const clashApiPort = 9090;
static const mixedPort = 2334;
static const delayTestUrl = "https://www.gstatic.com/generate_204";
}

View File

@@ -0,0 +1,6 @@
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/singbox/singbox.dart';
abstract interface class CoreFacade
implements SingboxFacade, ClashFacade, ConnectionFacade {}

View File

@@ -0,0 +1,60 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/failures.dart';
part 'core_service_failure.freezed.dart';
@freezed
sealed class CoreServiceFailure with _$CoreServiceFailure, Failure {
const CoreServiceFailure._();
const factory CoreServiceFailure.unexpected(
Object error,
StackTrace stackTrace,
) = UnexpectedCoreServiceFailure;
const factory CoreServiceFailure.serviceNotRunning([String? message]) =
CoreServiceNotRunning;
const factory CoreServiceFailure.invalidConfig([
String? message,
]) = InvalidConfig;
const factory CoreServiceFailure.create([
String? message,
]) = CoreServiceCreateFailure;
const factory CoreServiceFailure.start([
String? message,
]) = CoreServiceStartFailure;
const factory CoreServiceFailure.other([
String? message,
]) = CoreServiceOtherFailure;
String? get msg => switch (this) {
UnexpectedCoreServiceFailure() => null,
CoreServiceNotRunning(:final message) => message,
InvalidConfig(:final message) => message,
CoreServiceCreateFailure(:final message) => message,
CoreServiceStartFailure(:final message) => message,
CoreServiceOtherFailure(:final message) => message,
};
@override
String present(TranslationsEn t) {
return switch (this) {
UnexpectedCoreServiceFailure() => t.failure.singbox.unexpected,
CoreServiceNotRunning(:final message) =>
t.failure.singbox.serviceNotRunning +
(message == null ? "" : ": $message"),
InvalidConfig(:final message) =>
t.failure.singbox.invalidConfig + (message == null ? "" : ": $message"),
CoreServiceCreateFailure(:final message) =>
t.failure.singbox.create + (message == null ? "" : ": $message"),
CoreServiceStartFailure(:final message) =>
t.failure.singbox.start + (message == null ? "" : ": $message"),
CoreServiceOtherFailure(:final message) => message ?? "",
};
}
}

View File

@@ -15,7 +15,8 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
const factory ProfileFailure.invalidConfig() = ProfileInvalidConfigFailure;
const factory ProfileFailure.invalidConfig([String? message]) =
ProfileInvalidConfigFailure;
@override
String present(TranslationsEn t) {

View File

@@ -0,0 +1 @@
export 'singbox_facade.dart';

View File

@@ -0,0 +1,16 @@
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/core_service_failure.dart';
abstract interface class SingboxFacade {
TaskEither<CoreServiceFailure, Unit> setup();
TaskEither<CoreServiceFailure, Unit> parseConfig(String path);
TaskEither<CoreServiceFailure, Unit> changeConfig(String fileName);
TaskEither<CoreServiceFailure, Unit> start();
TaskEither<CoreServiceFailure, Unit> stop();
Stream<Either<CoreServiceFailure, String>> watchLogs();
}

View File

@@ -1,54 +0,0 @@
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'clash_controller.g.dart';
@Riverpod(keepAlive: true)
class ClashController extends _$ClashController with AppLogger {
Profile? _oldProfile;
@override
Future<void> build() async {
final clash = ref.watch(clashFacadeProvider);
final overridesListener = ref.listen(
prefsControllerProvider.select((value) => value.clash),
(_, overrides) async {
loggy.debug("new clash overrides received, patching...");
await clash.patchOverrides(overrides).getOrElse((l) => throw l).run();
},
);
final overrides = overridesListener.read();
final activeProfile = await ref.watch(activeProfileProvider.future);
final oldProfile = _oldProfile;
_oldProfile = activeProfile;
if (activeProfile != null) {
if (oldProfile == null ||
oldProfile.id != activeProfile.id ||
oldProfile.lastUpdate != activeProfile.lastUpdate) {
loggy.debug("profile changed or updated, updating clash core");
await clash
.changeConfigs(activeProfile.id)
.call(clash.patchOverrides(overrides))
.getOrElse((error) {
loggy.warning("failed to change or patch configs, $error");
throw error;
}).run();
}
} else {
if (oldProfile != null) {
loggy.debug("active profile removed, resetting clash");
await clash
.changeConfigs(Constants.configFileName)
.getOrElse((l) => throw l)
.run();
}
}
}
}

View File

@@ -1,7 +1,7 @@
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/features/common/clash/clash_controller.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -11,13 +11,16 @@ part 'clash_mode.g.dart';
class ClashMode extends _$ClashMode with AppLogger {
@override
Future<TunnelMode?> build() async {
final clash = ref.watch(clashFacadeProvider);
await ref.watch(clashControllerProvider.future);
final clash = ref.watch(coreFacadeProvider);
if (!await ref.watch(serviceRunningProvider.future)) {
return null;
}
ref.watch(prefsControllerProvider.select((value) => value.clash.mode));
return clash
.getConfigs()
.map((r) => r.mode)
.getOrElse((l) => throw l)
.run();
return clash.getConfigs().map((r) => r.mode).getOrElse(
(l) {
loggy.warning("fetching clash mode: $l");
throw l;
},
).run();
}
}

View File

@@ -1,6 +1,6 @@
import 'package:hiddify/features/common/clash/clash_controller.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/logs/notifier/notifier.dart';
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -12,9 +12,8 @@ part 'common_controllers.g.dart';
@Riverpod(keepAlive: true)
void commonControllers(CommonControllersRef ref) {
ref.listen(
clashControllerProvider,
logsNotifierProvider,
(previous, next) {},
fireImmediately: true,
);
ref.listen(
connectivityControllerProvider,

View File

@@ -1,62 +1,90 @@
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/services/connectivity/connectivity.dart';
import 'package:hiddify/services/service_providers.dart';
import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'connectivity_controller.g.dart';
// TODO: test and improve
// TODO: abort connection on clash error
@Riverpod(keepAlive: true)
class ConnectivityController extends _$ConnectivityController with AppLogger {
@override
ConnectionStatus build() {
state = const Disconnected();
final connection = _connectivity
.watchConnectionStatus()
.map(ConnectionStatus.fromBool)
.listen((event) => state = event);
// currently changes wont take effect while connected
Stream<ConnectionStatus> build() {
ref.listen(
prefsControllerProvider.select((value) => value.network),
(_, next) => _networkPrefs = next,
fireImmediately: true,
activeProfileProvider.select((value) => value.asData?.value),
(previous, next) async {
if (previous == null) return;
final shouldReconnect = previous != next;
if (shouldReconnect) {
loggy.debug("active profile modified, reconnect");
await reconnect();
}
},
);
ref.listen(
prefsControllerProvider
.select((value) => (value.clash.httpPort!, value.clash.socksPort!)),
(_, next) => _ports = (http: next.$1, socks: next.$2),
fireImmediately: true,
);
ref.onDispose(connection.cancel);
return state;
return _connectivity.watchConnectionStatus();
}
ConnectivityService get _connectivity =>
ref.watch(connectivityServiceProvider);
late ({int http, int socks}) _ports;
// ignore: unused_field
late NetworkPrefs _networkPrefs;
CoreFacade get _connectivity => ref.watch(coreFacadeProvider);
Future<void> toggleConnection() async {
switch (state) {
case Disconnected():
if (!await _connectivity.grantVpnPermission()) {
state = const Disconnected(ConnectivityFailure.unexpected());
return;
}
await _connectivity.connect(
httpPort: _ports.http,
socksPort: _ports.socks,
);
case Connected():
await _connectivity.disconnect();
default:
if (state case AsyncError()) {
await _connect();
} else if (state case AsyncData(:final value)) {
switch (value) {
case Disconnected():
await _connect();
case Connected():
await _disconnect();
default:
loggy.warning("switching status, debounce");
}
}
}
Future<void> reconnect() async {
if (state case AsyncData(:final value)) {
if (value case Connected()) {
loggy.debug("reconnecting");
await _disconnect();
await _connect();
}
}
}
Future<void> abortConnection() async {
if (state case AsyncData(:final value)) {
switch (value) {
case Connected() || Connecting():
loggy.debug("aborting connection");
await _disconnect();
default:
}
}
}
Future<void> _connect() async {
final activeProfile = await ref.read(activeProfileProvider.future);
await _connectivity
.changeConfig(activeProfile!.id)
.andThen(_connectivity.connect)
.mapLeft((l) {
loggy.warning("error connecting: $l");
state = AsyncError(l, StackTrace.current);
}).run();
}
Future<void> _disconnect() async {
await _connectivity.disconnect().mapLeft((l) {
loggy.warning("error disconnecting: $l");
state = AsyncError(l, StackTrace.current);
}).run();
}
}
@Riverpod(keepAlive: true)
Future<bool> serviceRunning(ServiceRunningRef ref) => ref
.watch(
connectivityControllerProvider.selectAsync((data) => data.isConnected),
)
.onError((error, stackTrace) => false);

View File

@@ -22,73 +22,85 @@ class TrafficChart extends HookConsumerWidget {
switch (asyncTraffics) {
case AsyncData(value: final traffics):
final latest =
traffics.lastOrNull ?? const Traffic(upload: 0, download: 0);
final latestUploadData = formatByteSpeed(latest.upload);
final latestDownloadData = formatByteSpeed(latest.download);
final uploadChartSpots = traffics.takeLast(chartSteps).mapIndexed(
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
);
final downloadChartSpots = traffics.takeLast(chartSteps).mapIndexed(
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
);
return Column(
mainAxisSize: MainAxisSize.min,
// mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 68,
child: LineChart(
LineChartData(
minY: 0,
borderData: FlBorderData(show: false),
titlesData: const FlTitlesData(show: false),
gridData: const FlGridData(show: false),
lineTouchData: const LineTouchData(enabled: false),
lineBarsData: [
LineChartBarData(
isCurved: true,
preventCurveOverShooting: true,
dotData: const FlDotData(show: false),
spots: uploadChartSpots.toList(),
),
LineChartBarData(
color: Theme.of(context).colorScheme.tertiary,
isCurved: true,
preventCurveOverShooting: true,
dotData: const FlDotData(show: false),
spots: downloadChartSpots.toList(),
),
],
),
duration: Duration.zero,
),
),
const Gap(16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text(""),
Text(latestUploadData.size),
Text(latestUploadData.unit),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text(""),
Text(latestDownloadData.size),
Text(latestDownloadData.unit),
],
),
const Gap(16),
],
);
// TODO: handle loading and error
return _Chart(traffics, chartSteps);
case AsyncLoading(:final value):
if (value == null) return const SizedBox();
return _Chart(value, chartSteps);
default:
return const SizedBox();
}
}
}
class _Chart extends StatelessWidget {
const _Chart(this.records, this.steps);
final List<Traffic> records;
final int steps;
@override
Widget build(BuildContext context) {
final latest = records.lastOrNull ?? const Traffic(upload: 0, download: 0);
final latestUploadData = formatByteSpeed(latest.upload);
final latestDownloadData = formatByteSpeed(latest.download);
final uploadChartSpots = records.takeLast(steps).mapIndexed(
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
);
final downloadChartSpots = records.takeLast(steps).mapIndexed(
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 68,
child: LineChart(
LineChartData(
minY: 0,
borderData: FlBorderData(show: false),
titlesData: const FlTitlesData(show: false),
gridData: const FlGridData(show: false),
lineTouchData: const LineTouchData(enabled: false),
lineBarsData: [
LineChartBarData(
isCurved: true,
preventCurveOverShooting: true,
dotData: const FlDotData(show: false),
spots: uploadChartSpots.toList(),
),
LineChartBarData(
color: Theme.of(context).colorScheme.tertiary,
isCurved: true,
preventCurveOverShooting: true,
dotData: const FlDotData(show: false),
spots: downloadChartSpots.toList(),
),
],
),
duration: Duration.zero,
),
),
const Gap(16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text(""),
Text(latestUploadData.size),
Text(latestUploadData.unit),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text(""),
Text(latestDownloadData.size),
Text(latestDownloadData.unit),
],
),
const Gap(16),
],
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:dartx/dartx.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -13,28 +14,37 @@ class TrafficNotifier extends _$TrafficNotifier with AppLogger {
int get _steps => 100;
@override
Stream<List<Traffic>> build() {
return Stream.periodic(const Duration(seconds: 1)).asyncMap(
(_) async {
return ref.read(clashFacadeProvider).getTraffic().match(
(f) {
loggy.warning('failed to watch clash traffic: $f');
return const ClashTraffic(upload: 0, download: 0);
},
(traffic) => traffic,
).run();
},
).map(
(event) => switch (state) {
AsyncData(:final value) => [
...value.takeLast(_steps - 1),
Traffic(upload: event.upload, download: event.download),
],
_ => List.generate(
_steps,
(index) => const Traffic(upload: 0, download: 0),
)
},
);
Stream<List<Traffic>> build() async* {
final serviceRunning = await ref.watch(serviceRunningProvider.future);
if (serviceRunning) {
yield* ref.watch(coreFacadeProvider).watchTraffic().map(
(event) => _mapToState(
event
.getOrElse((_) => const ClashTraffic(upload: 0, download: 0)),
),
);
} else {
yield* Stream.periodic(const Duration(seconds: 1)).asyncMap(
(_) async {
return const ClashTraffic(upload: 0, download: 0);
},
).map(_mapToState);
}
}
List<Traffic> _mapToState(ClashTraffic event) {
final previous = state.valueOrNull ??
List.generate(
_steps,
(index) => const Traffic(upload: 0, download: 0),
);
while (previous.length < _steps) {
loggy.debug("previous short, adding");
previous.insert(0, const Traffic(upload: 0, download: 0));
}
return [
...previous.takeLast(_steps - 1),
Traffic(upload: event.upload, download: event.download),
];
}
}

View File

@@ -46,6 +46,12 @@ class WindowController extends _$WindowController
await windowManager.close();
}
Future<void> quit() async {
loggy.debug("quitting");
await windowManager.close();
await windowManager.destroy();
}
@override
Future<void> onWindowClose() async {
await windowManager.hide();

View File

@@ -5,7 +5,6 @@ import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart';
import 'package:hiddify/features/common/clash/clash_controller.dart';
import 'package:hiddify/features/common/common.dart';
import 'package:hiddify/features/home/widgets/widgets.dart';
import 'package:hiddify/utils/utils.dart';
@@ -22,18 +21,6 @@ class HomePage extends HookConsumerWidget {
final hasAnyProfile = ref.watch(hasAnyProfileProvider);
final activeProfile = ref.watch(activeProfileProvider);
ref.listen(
clashControllerProvider,
(_, next) {
if (next case AsyncError(:final error)) {
CustomToast.error(
t.presentError(error),
duration: const Duration(seconds: 10),
).show(context);
}
},
);
return Scaffold(
body: Stack(
alignment: Alignment.bottomCenter,

View File

@@ -3,8 +3,11 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/theme/theme.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/alerts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
@@ -17,12 +20,71 @@ class ConnectionButton extends HookConsumerWidget {
final t = ref.watch(translationsProvider);
final connectionStatus = ref.watch(connectivityControllerProvider);
final Color connectionLogoColor = connectionStatus.isConnected
? ConnectionButtonColor.connected
: ConnectionButtonColor.disconnected;
ref.listen(
connectivityControllerProvider,
(_, next) {
if (next case AsyncError(:final error)) {
CustomToast.error(t.presentError(error)).show(context);
}
if (next
case AsyncData(value: Disconnected(:final connectionFailure?))) {
CustomAlertDialog(
message: connectionFailure.present(t),
).show(context);
}
},
);
final bool intractable = !connectionStatus.isSwitching;
switch (connectionStatus) {
case AsyncData(value: final status):
final Color connectionLogoColor = status.isConnected
? ConnectionButtonColor.connected
: ConnectionButtonColor.disconnected;
return _ConnectionButton(
onTap: () => ref
.read(connectivityControllerProvider.notifier)
.toggleConnection(),
enabled: !status.isSwitching,
label: status.present(t),
buttonColor: connectionLogoColor,
);
case AsyncError():
return _ConnectionButton(
onTap: () => ref
.read(connectivityControllerProvider.notifier)
.toggleConnection(),
enabled: true,
label: const Disconnected().present(t),
buttonColor: ConnectionButtonColor.disconnected,
);
default:
// HACK
return _ConnectionButton(
onTap: () {},
enabled: false,
label: "",
buttonColor: Colors.red,
);
}
}
}
class _ConnectionButton extends StatelessWidget {
const _ConnectionButton({
required this.onTap,
required this.enabled,
required this.label,
required this.buttonColor,
});
final VoidCallback onTap;
final bool enabled;
final String label;
final Color buttonColor;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -33,7 +95,7 @@ class ConnectionButton extends HookConsumerWidget {
boxShadow: [
BoxShadow(
blurRadius: 16,
color: connectionLogoColor.withOpacity(0.5),
color: buttonColor.withOpacity(0.5),
),
],
),
@@ -43,26 +105,24 @@ class ConnectionButton extends HookConsumerWidget {
shape: const CircleBorder(),
color: Colors.white,
child: InkWell(
onTap: () async {
await ref
.read(connectivityControllerProvider.notifier)
.toggleConnection();
},
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(36),
child: Assets.images.logo.svg(
colorFilter: ColorFilter.mode(
connectionLogoColor,
buttonColor,
BlendMode.srcIn,
),
),
),
),
).animate(target: intractable ? 0 : 1).blurXY(end: 1),
).animate(target: intractable ? 0 : 1).scaleXY(end: .88),
).animate(target: enabled ? 0 : 1).blurXY(end: 1),
)
.animate(target: enabled ? 0 : 1)
.scaleXY(end: .88, curve: Curves.easeIn),
const Gap(16),
Text(
connectionStatus.present(t).sentenceCase,
label.sentenceCase,
style: Theme.of(context).textTheme.bodyLarge,
),
],

View File

@@ -10,14 +10,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'logs_notifier.g.dart';
// TODO: rewrite
@riverpod
@Riverpod(keepAlive: true)
class LogsNotifier extends _$LogsNotifier with AppLogger {
static const maxLength = 1000;
@override
Stream<LogsState> build() {
state = const AsyncData(LogsState());
return ref.read(clashFacadeProvider).watchLogs().asyncMap(
return ref.read(coreFacadeProvider).watchLogs().asyncMap(
(event) async {
_logs = [
event.getOrElse((l) => throw l),
@@ -32,16 +32,15 @@ class LogsNotifier extends _$LogsNotifier with AppLogger {
);
}
var _logs = <ClashLog>[];
var _logs = <String>[];
final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200));
LogLevel? _levelFilter;
String _filter = "";
Future<List<ClashLog>> _computeLogs() async {
Future<List<String>> _computeLogs() async {
if (_levelFilter == null && _filter.isEmpty) return _logs;
return _logs.where((e) {
return (_filter.isEmpty || e.message.contains(_filter)) &&
(_levelFilter == null || e.level == _levelFilter);
return _filter.isEmpty || e.contains(_filter);
}).toList();
}

View File

@@ -8,7 +8,7 @@ class LogsState with _$LogsState {
const LogsState._();
const factory LogsState({
@Default([]) List<ClashLog> logs,
@Default([]) List<String> logs,
@Default("") String filter,
LogLevel? levelFilter,
}) = _LogsState;

View File

@@ -10,6 +10,7 @@ import 'package:hiddify/features/logs/notifier/notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
import 'package:tint/tint.dart';
class LogsPage extends HookConsumerWidget {
const LogsPage({super.key});
@@ -80,19 +81,7 @@ class LogsPage extends HookConsumerWidget {
children: [
ListTile(
dense: true,
title: Text.rich(
TextSpan(
children: [
TextSpan(text: log.timeStamp),
const TextSpan(text: " "),
TextSpan(
text: log.level.name.toUpperCase(),
style: TextStyle(color: log.level.color),
),
],
),
),
subtitle: Text(log.message),
subtitle: Text(log.strip()),
),
if (index != 0)
const Divider(

View File

@@ -24,7 +24,7 @@ class GroupWithProxies with _$GroupWithProxies {
final result = <GroupWithProxies>[];
for (final proxy in proxies) {
if (proxy is ClashProxyGroup) {
if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue;
// if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue;
final current = <ClashProxy>[];
for (final name in proxy.all) {
current.addAll(proxies.where((e) => e.name == name).toList());

View File

@@ -32,7 +32,7 @@ class ProxiesDelayNotifier extends _$ProxiesDelayNotifier with AppLogger {
return {};
}
ClashFacade get _clash => ref.read(clashFacadeProvider);
ClashFacade get _clash => ref.read(coreFacadeProvider);
StreamSubscription? _currentTest;
Future<void> testDelay(Iterable<String> proxies) async {

View File

@@ -3,8 +3,9 @@ import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/features/common/clash/clash_controller.dart';
import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/features/common/clash/clash_mode.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/proxies/model/model.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -16,23 +17,23 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger {
@override
Future<List<GroupWithProxies>> build() async {
loggy.debug('building');
await ref.watch(clashControllerProvider.future);
if (!await ref.watch(serviceRunningProvider.future)) {
throw const CoreServiceNotRunning();
}
final mode = await ref.watch(clashModeProvider.future);
return _clash
.getProxies()
.flatMap(
(proxies) {
return TaskEither(
() async =>
right(await GroupWithProxies.fromProxies(proxies, mode)),
);
},
)
.getOrElse((l) => throw l)
.run();
return _clash.getProxies().flatMap(
(proxies) {
return TaskEither(
() async => right(await GroupWithProxies.fromProxies(proxies, mode)),
);
},
).getOrElse((l) {
loggy.warning("failed receiving proxies: $l");
throw l;
}).run();
}
ClashFacade get _clash => ref.read(clashFacadeProvider);
ClashFacade get _clash => ref.read(coreFacadeProvider);
Future<void> changeProxy(String selectorName, String proxyName) async {
loggy.debug("changing proxy, selector: $selectorName - proxy: $proxyName ");

View File

@@ -20,7 +20,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
final notifier = ref.watch(proxiesNotifierProvider.notifier);
final asyncProxies = ref.watch(proxiesNotifierProvider);
final proxies = asyncProxies.value ?? [];
final proxies = asyncProxies.asData?.value ?? [];
final delays = ref.watch(proxiesDelayNotifierProvider);
final selectActiveProxyMutation = useMutation(
@@ -163,7 +163,10 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase),
),
SliverErrorBodyPlaceholder(t.presentError(error)),
SliverErrorBodyPlaceholder(
t.presentError(error),
icon: null,
),
],
),
);

View File

@@ -19,15 +19,61 @@ class ProxyTile extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return ListTile(
title: Text(
proxy.name,
switch (proxy) {
ClashProxyGroup(:final name) => name.toUpperCase(),
ClashProxyItem(:final name) => name,
},
overflow: TextOverflow.ellipsis,
),
subtitle: Text(proxy.type.label),
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Container(
width: 6,
height: double.maxFinite,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: selected ? theme.colorScheme.primary : Colors.transparent,
),
),
),
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(text: proxy.type.label),
if (proxy.udp)
WidgetSpan(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: theme.colorScheme.tertiaryContainer,
),
borderRadius: BorderRadius.circular(6),
),
child: Text(
" UDP ",
style: TextStyle(
fontSize: theme.textTheme.labelSmall?.fontSize,
),
),
),
),
),
if (proxy case ClashProxyGroup(:final now)) ...[
TextSpan(text: " ($now)"),
],
],
),
),
trailing: delay != null ? Text(delay.toString()) : null,
selected: selected,
onTap: onSelect,
horizontalTitleGap: 4,
);
}
}

View File

@@ -1,103 +1,100 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/features/settings/widgets/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
// import 'package:flutter/material.dart';
// import 'package:hiddify/core/core_providers.dart';
// import 'package:hiddify/core/prefs/prefs.dart';
// import 'package:hiddify/domain/clash/clash.dart';
// import 'package:hiddify/features/settings/widgets/widgets.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:recase/recase.dart';
class ClashOverridesPage extends HookConsumerWidget {
const ClashOverridesPage({super.key});
// class ClashOverridesPage extends HookConsumerWidget {
// const ClashOverridesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final t = ref.watch(translationsProvider);
final overrides =
ref.watch(prefsControllerProvider.select((value) => value.clash));
final notifier = ref.watch(prefsControllerProvider.notifier);
// final overrides =
// ref.watch(prefsControllerProvider.select((value) => value.clash));
// final notifier = ref.watch(prefsControllerProvider.notifier);
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text(t.settings.clash.sectionTitle.titleCase),
pinned: true,
),
SliverList.list(
children: [
InputOverrideTile(
title: t.settings.clash.overrides.httpPort,
value: overrides.httpPort,
resetValue: ClashConfig.initial.httpPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(httpPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.socksPort,
value: overrides.socksPort,
resetValue: ClashConfig.initial.socksPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(socksPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.redirPort,
value: overrides.redirPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(redirPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.tproxyPort,
value: overrides.tproxyPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(tproxyPort: value),
),
),
InputOverrideTile(
title: t.settings.clash.overrides.mixedPort,
value: overrides.mixedPort,
resetValue: ClashConfig.initial.mixedPort,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(mixedPort: value),
),
),
ToggleOverrideTile(
title: t.settings.clash.overrides.allowLan,
value: overrides.allowLan,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(allowLan: value),
),
),
ToggleOverrideTile(
title: t.settings.clash.overrides.ipv6,
value: overrides.ipv6,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(ipv6: value),
),
),
ChoiceOverrideTile(
title: t.settings.clash.overrides.mode,
value: overrides.mode,
options: TunnelMode.values,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(mode: value),
),
),
ChoiceOverrideTile(
title: t.settings.clash.overrides.logLevel,
value: overrides.logLevel,
options: LogLevel.values,
onChange: (value) => notifier.patchClashOverrides(
ClashConfigPatch(logLevel: value),
),
),
],
),
],
),
);
}
}
// return Scaffold(
// body: CustomScrollView(
// slivers: [
// SliverAppBar(
// title: Text(t.settings.clash.sectionTitle.titleCase),
// pinned: true,
// ),
// SliverList.list(
// children: [
// InputOverrideTile(
// title: t.settings.clash.overrides.httpPort,
// value: overrides.httpPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(httpPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.socksPort,
// value: overrides.socksPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(socksPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.redirPort,
// value: overrides.redirPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(redirPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.tproxyPort,
// value: overrides.tproxyPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(tproxyPort: value),
// ),
// ),
// InputOverrideTile(
// title: t.settings.clash.overrides.mixedPort,
// value: overrides.mixedPort,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(mixedPort: value),
// ),
// ),
// ToggleOverrideTile(
// title: t.settings.clash.overrides.allowLan,
// value: overrides.allowLan,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(allowLan: value),
// ),
// ),
// ToggleOverrideTile(
// title: t.settings.clash.overrides.ipv6,
// value: overrides.ipv6,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(ipv6: value),
// ),
// ),
// ChoiceOverrideTile(
// title: t.settings.clash.overrides.mode,
// value: overrides.mode,
// options: TunnelMode.values,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(mode: value),
// ),
// ),
// ChoiceOverrideTile(
// title: t.settings.clash.overrides.logLevel,
// value: overrides.logLevel,
// options: LogLevel.values,
// onChange: (value) => notifier.patchClashOverrides(
// ClashConfigPatch(logLevel: value),
// ),
// ),
// ],
// ),
// ],
// ),
// );
// }
// }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/settings/widgets/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
@@ -13,7 +12,7 @@ class SettingsPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
const divider = Divider(indent: 16, endIndent: 16);
// const divider = Divider(indent: 16, endIndent: 16);
return Scaffold(
appBar: AppBar(
@@ -29,18 +28,18 @@ class SettingsPage extends HookConsumerWidget {
t.settings.general.sectionTitle.titleCase,
),
const AppearanceSettingTiles(),
divider,
_SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
const NetworkSettingTiles(),
divider,
ListTile(
title: Text(t.settings.clash.sectionTitle.titleCase),
leading: const Icon(Icons.edit_document),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () async {
await const ClashOverridesRoute().push(context);
},
),
// divider,
// _SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
// const NetworkSettingTiles(),
// divider,
// ListTile(
// title: Text(t.settings.clash.sectionTitle.titleCase),
// leading: const Icon(Icons.edit_document),
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
// onTap: () async {
// await const ClashOverridesRoute().push(context);
// },
// ),
const Gap(16),
],
),

View File

@@ -1,36 +1,36 @@
import 'package:flutter/material.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart';
// import 'package:flutter/material.dart';
// import 'package:hiddify/core/core_providers.dart';
// import 'package:hiddify/core/prefs/prefs.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:recase/recase.dart';
class NetworkSettingTiles extends HookConsumerWidget {
const NetworkSettingTiles({super.key});
// class NetworkSettingTiles extends HookConsumerWidget {
// const NetworkSettingTiles({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final t = ref.watch(translationsProvider);
final prefs =
ref.watch(prefsControllerProvider.select((value) => value.network));
final notifier = ref.watch(prefsControllerProvider.notifier);
// final prefs =
// ref.watch(prefsControllerProvider.select((value) => value.network));
// final notifier = ref.watch(prefsControllerProvider.notifier);
return Column(
children: [
SwitchListTile(
title: Text(t.settings.network.systemProxy.titleCase),
subtitle: Text(t.settings.network.systemProxyMsg),
value: prefs.systemProxy,
onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
),
SwitchListTile(
title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
value: prefs.bypassPrivateNetworks,
onChanged: (value) =>
notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
),
],
);
}
}
// return Column(
// children: [
// SwitchListTile(
// title: Text(t.settings.network.systemProxy.titleCase),
// subtitle: Text(t.settings.network.systemProxyMsg),
// value: prefs.systemProxy,
// onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
// ),
// SwitchListTile(
// title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
// subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
// value: prefs.bypassPrivateNetworks,
// onChanged: (value) =>
// notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
// ),
// ],
// );
// }
// }

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart';
@@ -27,7 +25,7 @@ class SystemTrayController extends _$SystemTrayController
_initialized = true;
}
final connection = ref.watch(connectivityControllerProvider);
final connection = await ref.watch(connectivityControllerProvider.future);
final mode =
ref.watch(clashModeProvider.select((value) => value.valueOrNull));
@@ -104,8 +102,9 @@ class SystemTrayController extends _$SystemTrayController
return ref.read(connectivityControllerProvider.notifier).toggleConnection();
}
// TODO rewrite
Future<void> handleClickExitApp(MenuItem menuItem) async {
exit(0);
await ref.read(connectivityControllerProvider.notifier).abortConnection();
await trayManager.destroy();
return ref.read(windowControllerProvider.notifier).quit();
}
}

View File

@@ -4,18 +4,18 @@
// ignore_for_file: type=lint
import 'dart:ffi' as ffi;
/// Bindings to Clash
class ClashNativeLibrary {
/// Bindings to Singbox
class SingboxNativeLibrary {
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
ClashNativeLibrary(ffi.DynamicLibrary dynamicLibrary)
SingboxNativeLibrary(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
/// The symbols are looked up with [lookup].
ClashNativeLibrary.fromLookup(
SingboxNativeLibrary.fromLookup(
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
lookup)
: _lookup = lookup;
@@ -857,206 +857,69 @@ class ClashNativeLibrary {
late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
void getConfigs(
int port,
void setup(
ffi.Pointer<ffi.Char> baseDir,
ffi.Pointer<ffi.Char> workingDir,
ffi.Pointer<ffi.Char> tempDir,
) {
return _getConfigs(
port,
return _setup(
baseDir,
workingDir,
tempDir,
);
}
late final _getConfigsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'getConfigs');
late final _getConfigs = _getConfigsPtr.asFunction<void Function(int)>();
void patchConfigs(
int port,
ffi.Pointer<ffi.Char> patchStr,
) {
return _patchConfigs(
port,
patchStr,
);
}
late final _patchConfigsPtr = _lookup<
late final _setupPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.LongLong, ffi.Pointer<ffi.Char>)>>('patchConfigs');
late final _patchConfigs =
_patchConfigsPtr.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>>('setup');
late final _setup = _setupPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>();
void updateConfigs(
int port,
ffi.Pointer<ffi.Char> pathStr,
int force,
) {
return _updateConfigs(
port,
pathStr,
force,
);
}
late final _updateConfigsPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.LongLong, ffi.Pointer<ffi.Char>, GoUint8)>>('updateConfigs');
late final _updateConfigs = _updateConfigsPtr
.asFunction<void Function(int, ffi.Pointer<ffi.Char>, int)>();
void validateConfig(
int port,
ffi.Pointer<ffi.Char> parse(
ffi.Pointer<ffi.Char> path,
) {
return _validateConfig(
port,
return _parse(
path,
);
}
late final _validateConfigPtr = _lookup<
late final _parsePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.LongLong, ffi.Pointer<ffi.Char>)>>('validateConfig');
late final _validateConfig = _validateConfigPtr
.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('parse');
late final _parse = _parsePtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void initNativeDartBridge(
ffi.Pointer<ffi.Void> api,
) {
return _initNativeDartBridge(
api,
);
}
late final _initNativeDartBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeDartBridge');
late final _initNativeDartBridge = _initNativeDartBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void setOptions(
int port,
ffi.Pointer<ffi.Char> dir,
ffi.Pointer<ffi.Char> create(
ffi.Pointer<ffi.Char> configPath,
) {
return _setOptions(
port,
dir,
return _create(
configPath,
);
}
late final _setOptionsPtr = _lookup<
late final _createPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>>('setOptions');
late final _setOptions = _setOptionsPtr.asFunction<
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('create');
late final _create = _createPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void start(
int port,
) {
return _start(
port,
);
ffi.Pointer<ffi.Char> start() {
return _start();
}
late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('start');
late final _start = _startPtr.asFunction<void Function(int)>();
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('start');
late final _start = _startPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void startLog(
int port,
ffi.Pointer<ffi.Char> levelStr,
) {
return _startLog(
port,
levelStr,
);
ffi.Pointer<ffi.Char> stop() {
return _stop();
}
late final _startLogPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>)>>('startLog');
late final _startLog =
_startLogPtr.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
void stopLog() {
return _stopLog();
}
late final _stopLogPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
void getProxies(
int port,
) {
return _getProxies(
port,
);
}
late final _getProxiesPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'getProxies');
late final _getProxies = _getProxiesPtr.asFunction<void Function(int)>();
void updateProxy(
int port,
ffi.Pointer<ffi.Char> selectorName,
ffi.Pointer<ffi.Char> proxyName,
) {
return _updateProxy(
port,
selectorName,
proxyName,
);
}
late final _updateProxyPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>>('updateProxy');
late final _updateProxy = _updateProxyPtr.asFunction<
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>();
void getProxyDelay(
int port,
ffi.Pointer<ffi.Char> name,
ffi.Pointer<ffi.Char> url,
int timeout,
) {
return _getProxyDelay(
port,
name,
url,
timeout,
);
}
late final _getProxyDelayPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>, ffi.Long)>>('getProxyDelay');
late final _getProxyDelay = _getProxyDelayPtr.asFunction<
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void getTraffic(
int port,
) {
return _getTraffic(
port,
);
}
late final _getTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'getTraffic');
late final _getTraffic = _getTrafficPtr.asFunction<void Function(int)>();
late final _stopPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('stop');
late final _stop = _stopPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
}
typedef va_list = ffi.Pointer<ffi.Char>;
@@ -1136,7 +999,6 @@ final class GoSlice extends ffi.Struct {
typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong;
typedef GoUint8 = ffi.UnsignedChar;
const int _VCRT_COMPILER_PREPROCESSOR = 1;

View File

@@ -1,28 +0,0 @@
import 'dart:convert';
import 'dart:ffi';
import 'dart:isolate';
import 'package:hiddify/services/clash/async_ffi_response.dart';
import 'package:hiddify/utils/utils.dart';
// TODO: add timeout
// TODO: test and improve
mixin AsyncFFI implements LoggerMixin {
Future<AsyncFfiResponse> runAsync(void Function(int port) run) async {
final receivePort = ReceivePort();
final responseFuture = receivePort.map(
(event) {
if (event is String) {
receivePort.close();
return AsyncFfiResponse.fromJson(
jsonDecode(event) as Map<String, dynamic>,
);
}
receivePort.close();
throw Exception("unexpected data type[${event.runtimeType}]");
},
).first;
run(receivePort.sendPort.nativePort);
return responseFuture;
}
}

View File

@@ -1,18 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'async_ffi_response.freezed.dart';
part 'async_ffi_response.g.dart';
@freezed
class AsyncFfiResponse with _$AsyncFfiResponse {
const AsyncFfiResponse._();
const factory AsyncFfiResponse({
@JsonKey(name: 'success') required bool success,
@JsonKey(name: 'message') String? message,
@JsonKey(name: 'data') String? data,
}) = _AsyncFfiResponse;
factory AsyncFfiResponse.fromJson(Map<String, dynamic> json) =>
_$AsyncFfiResponseFromJson(json);
}

View File

@@ -1,2 +0,0 @@
export 'clash_service.dart';
export 'clash_service_impl.dart';

Some files were not shown because too many files have changed in this diff Show More