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 gen
make translate make translate
- name: Get Geo Assets
run: |
make get-geo-assets
- name: Get Libs ${{ matrix.platform }} - name: Get Libs ${{ matrix.platform }}
run: | run: |
make ${{ matrix.platform }}-libs make ${{ matrix.platform }}-libs

3
.gitignore vendored
View File

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

6
.gitmodules vendored
View File

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

View File

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

3
android/.gitignore vendored
View File

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

View File

@@ -60,6 +60,11 @@ android {
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
} }
buildFeatures {
viewBinding true
aidl true
}
} }
flutter { flutter {
@@ -67,7 +72,11 @@ flutter {
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.window:window:1.0.0' implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java: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.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.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 <application
android:name=".Application"
android:label="hiddify" android:label="hiddify"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"> android:roundIcon="@mipmap/ic_launcher_round"
tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -39,15 +49,14 @@
<data android:host="install-config"/> <data android:host="install-config"/>
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
android:name=".HiddifyVpnService" android:name=".bg.VPNService"
android:permission="android.permission.BIND_VPN_SERVICE" android:exported="false"
android:stopWithTask="false" android:permission="android.permission.BIND_VPN_SERVICE">
android:exported="false"> <intent-filter>
<intent-filter> <action android:name="android.net.VpnService" />
<action android:name="android.net.VpnService"/> </intent-filter>
</intent-filter> </service>
</service>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <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 package com.hiddify.hiddify
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService import android.net.VpnService
import android.util.Log 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.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel import kotlinx.coroutines.Dispatchers
import io.flutter.plugin.common.MethodCall import kotlinx.coroutines.launch
import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.withContext
import java.util.LinkedList
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
class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
companion object { companion object {
private const val TAG = "ANDROID/MyActivity"
lateinit var instance: MainActivity
const val VPN_PERMISSION_REQUEST_CODE = 1001 const val VPN_PERMISSION_REQUEST_CODE = 1001
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1010
enum class Action(val method: String) {
GrantPermission("grant_permission"),
StartProxy("start"),
StopProxy("stop"),
RefreshStatus("refresh_status"),
SetPrefs("set_prefs")
}
} }
private fun registerBroadcastReceiver() { private val connection = ServiceConnection(this, this)
Log.d(HiddifyVpnService.TAG, "registering broadcast receiver")
vpnBroadcastReceiver = VpnState()
val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS)
registerReceiver(vpnBroadcastReceiver, intentFilter)
}
private fun unregisterBroadcastReceiver() { val logList = LinkedList<String>()
Log.d(HiddifyVpnService.TAG, "unregistering broadcast receiver") var logCallback: ((Boolean) -> Unit)? = null
if (vpnBroadcastReceiver != null) { val serviceStatus = MutableLiveData(Status.Stopped)
unregisterReceiver(vpnBroadcastReceiver) val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
vpnBroadcastReceiver = null val serviceLogs = MutableLiveData<String?>(null)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
methodChannel = instance = this
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.TAG) reconnect()
methodChannel.setMethodCallHandler(this) flutterEngine.plugins.add(MethodHandler())
flutterEngine.plugins.add(EventHandler())
eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.EVENT_TAG) flutterEngine.plugins.add(LogHandler())
registerBroadcastReceiver()
eventChannel.setStreamHandler(vpnBroadcastReceiver)
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { fun reconnect() {
methodResult = result connection.reconnect()
@Suppress("UNCHECKED_CAST") }
when (call.method) {
Action.GrantPermission.method -> { fun startService() {
grantVpnPermission() 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 -> { val intent = Intent(Application.application, VPNService::class.java)
VpnServiceManager.startVpnService(this) withContext(Dispatchers.Main) {
result.success(true) ContextCompat.startForegroundService(Application.application, intent)
}
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()
} }
} }
} }
override fun onDestroy() {
super.onDestroy() override fun onServiceStatusChanged(status: Status) {
unregisterBroadcastReceiver() Log.d(TAG, "service status changed: $status")
serviceStatus.postValue(status)
} }
private fun grantVpnPermission() {
val vpnPermissionIntent = VpnService.prepare(this) override fun onServiceAlert(type: Alert, message: String?) {
if (vpnPermissionIntent == null) { Log.d(TAG, "service alert: $type")
onActivityResult(VPN_PERMISSION_REQUEST_CODE, RESULT_OK, null) serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
} else { }
startActivityForResult(vpnPermissionIntent, VPN_PERMISSION_REQUEST_CODE)
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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (requestCode == VPN_PERMISSION_REQUEST_CODE) { if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
methodResult.success(resultCode == RESULT_OK) if (resultCode == RESULT_OK) startService()
} else if (requestCode == 101010) { else onServiceAlert(Alert.RequestVPNPermission, null)
methodResult.success(resultCode == RESULT_OK) } 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.useAndroidX=true
android.enableJetifier=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", "unexpected": "unexpected failure",
"core": "clash failure ${reason}" "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": { "connectivity": {
"unexpected": "unexpected failure" "unexpected": "unexpected failure",
"missingVpnPermission": "Missing VPN permission",
"missingNotificationPermission": "Missing Notification permission",
"core": "Core failure"
}, },
"profiles": { "profiles": {
"unexpected": "unexpected failure", "unexpected": "unexpected failure",

View File

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

1
core

Submodule core deleted from 1149e93363

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,9 +20,9 @@ part 'mobile_routes.g.dart';
TypedGoRoute<LogsRoute>(path: LogsRoute.path), TypedGoRoute<LogsRoute>(path: LogsRoute.path),
TypedGoRoute<SettingsRoute>( TypedGoRoute<SettingsRoute>(
path: SettingsRoute.path, path: SettingsRoute.path,
routes: [ // routes: [
TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path), // TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
], // ],
), ),
TypedGoRoute<AboutRoute>(path: AboutRoute.path), TypedGoRoute<AboutRoute>(path: AboutRoute.path),
], ],
@@ -69,20 +69,20 @@ class SettingsRoute extends GoRouteData {
} }
} }
class ClashOverridesRoute extends GoRouteData { // class ClashOverridesRoute extends GoRouteData {
const ClashOverridesRoute(); // const ClashOverridesRoute();
static const path = 'clash'; // static const path = 'clash';
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey; // static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
@override // @override
Page<void> buildPage(BuildContext context, GoRouterState state) { // Page<void> buildPage(BuildContext context, GoRouterState state) {
return const MaterialPage( // return const MaterialPage(
fullscreenDialog: true, // fullscreenDialog: true,
child: ClashOverridesPage(), // child: ClashOverridesPage(),
); // );
} // }
} // }
class AboutRoute extends GoRouteData { class AboutRoute extends GoRouteData {
const AboutRoute(); 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:dio/dio.dart';
import 'package:hiddify/data/api/clash_api.dart';
import 'package:hiddify/data/local/dao/dao.dart'; import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/data/local/database.dart';
import 'package:hiddify/data/repository/repository.dart'; import 'package:hiddify/data/repository/repository.dart';
import 'package:hiddify/data/repository/update_repository_impl.dart'; import 'package:hiddify/data/repository/update_repository_impl.dart';
import 'package:hiddify/domain/app/app.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/domain/profiles/profiles.dart';
import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/services/service_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -28,21 +30,26 @@ ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao(
ref.watch(appDatabaseProvider), ref.watch(appDatabaseProvider),
); );
@Riverpod(keepAlive: true)
ClashFacade clashFacade(ClashFacadeRef ref) => ClashFacadeImpl(
clashService: ref.watch(clashServiceProvider),
filesEditor: ref.watch(filesEditorServiceProvider),
);
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) => ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) =>
ProfilesRepositoryImpl( ProfilesRepositoryImpl(
profilesDao: ref.watch(profilesDaoProvider), profilesDao: ref.watch(profilesDaoProvider),
filesEditor: ref.watch(filesEditorServiceProvider), filesEditor: ref.watch(filesEditorServiceProvider),
clashFacade: ref.watch(clashFacadeProvider), singbox: ref.watch(coreFacadeProvider),
dio: ref.watch(dioProvider), dio: ref.watch(dioProvider),
); );
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
UpdateRepository updateRepository(UpdateRepositoryRef ref) => UpdateRepository updateRepository(UpdateRepositoryRef ref) =>
UpdateRepositoryImpl(ref.watch(dioProvider)); 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/dao/dao.dart';
import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/tables.dart';
import 'package:hiddify/data/local/type_converters.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/path.dart' as p;
import 'package:path_provider/path_provider.dart';
part 'database.g.dart'; part 'database.g.dart';
@@ -22,8 +22,8 @@ class AppDatabase extends _$AppDatabase {
LazyDatabase _openConnection() { LazyDatabase _openConnection() {
return LazyDatabase(() async { return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory(); final dbDir = await FilesEditorService.getDatabaseDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite')); final file = File(p.join(dbDir.path, 'db.sqlite'));
return NativeDatabase.createInBackground(file); 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:fpdart/fpdart.dart';
import 'package:hiddify/data/local/dao/dao.dart'; import 'package:hiddify/data/local/dao/dao.dart';
import 'package:hiddify/data/repository/exception_handlers.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/enums.dart';
import 'package:hiddify/domain/profiles/profiles.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/services/files_editor_service.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@@ -18,13 +18,13 @@ class ProfilesRepositoryImpl
ProfilesRepositoryImpl({ ProfilesRepositoryImpl({
required this.profilesDao, required this.profilesDao,
required this.filesEditor, required this.filesEditor,
required this.clashFacade, required this.singbox,
required this.dio, required this.dio,
}); });
final ProfilesDao profilesDao; final ProfilesDao profilesDao;
final FilesEditorService filesEditor; final FilesEditorService filesEditor;
final ClashFacade clashFacade; final SingboxFacade singbox;
final Dio dio; final Dio dio;
@override @override
@@ -166,20 +166,17 @@ class ProfilesRepositoryImpl
() async { () async {
final path = filesEditor.configPath(fileName); final path = filesEditor.configPath(fileName);
final response = await dio.download(url, path); final response = await dio.download(url, path);
if (response.statusCode != 200) { final parseResult = await singbox.parseConfig(path).run();
await File(path).delete(); return parseResult.fold(
return left(const ProfileUnexpectedFailure()); (l) async {
} await File(path).delete();
final isValid = await clashFacade return left(ProfileFailure.invalidConfig(l.msg));
.validateConfig(fileName) },
.getOrElse((_) => false) (_) {
.run(); final profile = Profile.fromResponse(url, response.headers.map);
if (!isValid) { return right(profile);
await File(path).delete(); },
return left(const ProfileFailure.invalidConfig()); );
}
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'; export 'profiles_repository_impl.dart';

View File

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

View File

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

View File

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

View File

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

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 // TODO: test and improve
@Freezed(fromJson: true) @Freezed(fromJson: true)
class ClashProxy with _$ClashProxy { sealed class ClashProxy with _$ClashProxy {
const ClashProxy._(); const ClashProxy._();
const factory ClashProxy.group({ const factory ClashProxy.group({
@@ -15,6 +15,7 @@ class ClashProxy with _$ClashProxy {
@JsonKey(fromJson: _typeFromJson) required ProxyType type, @JsonKey(fromJson: _typeFromJson) required ProxyType type,
required List<String> all, required List<String> all,
required String now, required String now,
@Default(false) bool udp,
List<ClashHistory>? history, List<ClashHistory>? history,
@JsonKey(includeFromJson: false, includeToJson: false) int? delay, @JsonKey(includeFromJson: false, includeToJson: false) int? delay,
}) = ClashProxyGroup; }) = ClashProxyGroup;
@@ -22,6 +23,7 @@ class ClashProxy with _$ClashProxy {
const factory ClashProxy.item({ const factory ClashProxy.item({
required String name, required String name,
@JsonKey(fromJson: _typeFromJson) required ProxyType type, @JsonKey(fromJson: _typeFromJson) required ProxyType type,
@Default(false) bool udp,
List<ClashHistory>? history, List<ClashHistory>? history,
@JsonKey(includeFromJson: false, includeToJson: false) int? delay, @JsonKey(includeFromJson: false, includeToJson: false) int? delay,
}) = ClashProxyItem; }) = 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:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.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'; part 'connection_status.freezed.dart';
@@ -9,24 +9,13 @@ sealed class ConnectionStatus with _$ConnectionStatus {
const ConnectionStatus._(); const ConnectionStatus._();
const factory ConnectionStatus.disconnected([ const factory ConnectionStatus.disconnected([
ConnectivityFailure? connectFailure, ConnectionFailure? connectionFailure,
]) = Disconnected; ]) = Disconnected;
const factory ConnectionStatus.connecting() = Connecting; const factory ConnectionStatus.connecting() = Connecting;
const factory ConnectionStatus.connected([ const factory ConnectionStatus.connected() = Connected;
ConnectivityFailure? disconnectFailure,
]) = Connected;
const factory ConnectionStatus.disconnecting() = Disconnecting; const factory ConnectionStatus.disconnecting() = Disconnecting;
factory ConnectionStatus.fromBool(bool connected) { bool get isConnected => switch (this) { Connected() => true, _ => false };
return connected
? const ConnectionStatus.connected()
: const Disconnected();
}
bool get isConnected => switch (this) {
Connected() => true,
_ => false,
};
bool get isSwitching => switch (this) { bool get isSwitching => switch (this) {
Connecting() => true, Connecting() => true,

View File

@@ -1,4 +1,5 @@
export 'connection_facade.dart';
export 'connection_failure.dart';
export 'connection_status.dart'; export 'connection_status.dart';
export 'connectivity_failure.dart';
export 'network_prefs.dart'; export 'network_prefs.dart';
export 'traffic.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 { abstract class Constants {
static const localHost = '127.0.0.1'; static const geoipFileName = "geoip.db";
static const clashFolderName = "clash"; static const geositeFileName = "geosite.db";
static const delayTestUrl = "https://www.google.com"; static const configsFolderName = "configs";
static const configFileName = "config"; static const localHost = "127.0.0.1";
static const countryMMDBFileName = "Country";
static const githubUrl = "https://github.com/hiddify/hiddify-next"; static const githubUrl = "https://github.com/hiddify/hiddify-next";
static const githubReleasesApiUrl = static const githubReleasesApiUrl =
"https://api.github.com/repos/hiddify/hiddify-next/releases"; "https://api.github.com/repos/hiddify/hiddify-next/releases";
@@ -11,3 +10,9 @@ abstract class Constants {
"https://github.com/hiddify/hiddify-next/releases/latest"; "https://github.com/hiddify/hiddify-next/releases/latest";
static const telegramChannelUrl = "https://t.me/hiddify"; 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.notFound() = ProfileNotFoundFailure;
const factory ProfileFailure.invalidConfig() = ProfileInvalidConfigFailure; const factory ProfileFailure.invalidConfig([String? message]) =
ProfileInvalidConfigFailure;
@override @override
String present(TranslationsEn t) { 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/core/prefs/prefs.dart';
import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.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:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -11,13 +11,16 @@ part 'clash_mode.g.dart';
class ClashMode extends _$ClashMode with AppLogger { class ClashMode extends _$ClashMode with AppLogger {
@override @override
Future<TunnelMode?> build() async { Future<TunnelMode?> build() async {
final clash = ref.watch(clashFacadeProvider); final clash = ref.watch(coreFacadeProvider);
await ref.watch(clashControllerProvider.future); if (!await ref.watch(serviceRunningProvider.future)) {
return null;
}
ref.watch(prefsControllerProvider.select((value) => value.clash.mode)); ref.watch(prefsControllerProvider.select((value) => value.clash.mode));
return clash return clash.getConfigs().map((r) => r.mode).getOrElse(
.getConfigs() (l) {
.map((r) => r.mode) loggy.warning("fetching clash mode: $l");
.getOrElse((l) => throw l) throw l;
.run(); },
).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/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/common/window/window_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/features/system_tray/controller/system_tray_controller.dart';
import 'package:hiddify/utils/platform_utils.dart'; import 'package:hiddify/utils/platform_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -12,9 +12,8 @@ part 'common_controllers.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
void commonControllers(CommonControllersRef ref) { void commonControllers(CommonControllersRef ref) {
ref.listen( ref.listen(
clashControllerProvider, logsNotifierProvider,
(previous, next) {}, (previous, next) {},
fireImmediately: true,
); );
ref.listen( ref.listen(
connectivityControllerProvider, 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/domain/connectivity/connectivity.dart';
import 'package:hiddify/services/connectivity/connectivity.dart'; import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'connectivity_controller.g.dart'; part 'connectivity_controller.g.dart';
// TODO: test and improve
// TODO: abort connection on clash error
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class ConnectivityController extends _$ConnectivityController with AppLogger { class ConnectivityController extends _$ConnectivityController with AppLogger {
@override @override
ConnectionStatus build() { Stream<ConnectionStatus> build() {
state = const Disconnected();
final connection = _connectivity
.watchConnectionStatus()
.map(ConnectionStatus.fromBool)
.listen((event) => state = event);
// currently changes wont take effect while connected
ref.listen( ref.listen(
prefsControllerProvider.select((value) => value.network), activeProfileProvider.select((value) => value.asData?.value),
(_, next) => _networkPrefs = next, (previous, next) async {
fireImmediately: true, if (previous == null) return;
final shouldReconnect = previous != next;
if (shouldReconnect) {
loggy.debug("active profile modified, reconnect");
await reconnect();
}
},
); );
ref.listen( return _connectivity.watchConnectionStatus();
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;
} }
ConnectivityService get _connectivity => CoreFacade get _connectivity => ref.watch(coreFacadeProvider);
ref.watch(connectivityServiceProvider);
late ({int http, int socks}) _ports;
// ignore: unused_field
late NetworkPrefs _networkPrefs;
Future<void> toggleConnection() async { Future<void> toggleConnection() async {
switch (state) { if (state case AsyncError()) {
case Disconnected(): await _connect();
if (!await _connectivity.grantVpnPermission()) { } else if (state case AsyncData(:final value)) {
state = const Disconnected(ConnectivityFailure.unexpected()); switch (value) {
return; case Disconnected():
} await _connect();
await _connectivity.connect( case Connected():
httpPort: _ports.http, await _disconnect();
socksPort: _ports.socks, default:
); loggy.warning("switching status, debounce");
case Connected(): }
await _connectivity.disconnect();
default:
} }
} }
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) { switch (asyncTraffics) {
case AsyncData(value: final traffics): case AsyncData(value: final traffics):
final latest = return _Chart(traffics, chartSteps);
traffics.lastOrNull ?? const Traffic(upload: 0, download: 0); case AsyncLoading(:final value):
final latestUploadData = formatByteSpeed(latest.upload); if (value == null) return const SizedBox();
final latestDownloadData = formatByteSpeed(latest.download); return _Chart(value, chartSteps);
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
default: default:
return const SizedBox(); 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/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.dart'; import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.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:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -13,28 +14,37 @@ class TrafficNotifier extends _$TrafficNotifier with AppLogger {
int get _steps => 100; int get _steps => 100;
@override @override
Stream<List<Traffic>> build() { Stream<List<Traffic>> build() async* {
return Stream.periodic(const Duration(seconds: 1)).asyncMap( final serviceRunning = await ref.watch(serviceRunningProvider.future);
(_) async { if (serviceRunning) {
return ref.read(clashFacadeProvider).getTraffic().match( yield* ref.watch(coreFacadeProvider).watchTraffic().map(
(f) { (event) => _mapToState(
loggy.warning('failed to watch clash traffic: $f'); event
return const ClashTraffic(upload: 0, download: 0); .getOrElse((_) => const ClashTraffic(upload: 0, download: 0)),
}, ),
(traffic) => traffic, );
).run(); } else {
}, yield* Stream.periodic(const Duration(seconds: 1)).asyncMap(
).map( (_) async {
(event) => switch (state) { return const ClashTraffic(upload: 0, download: 0);
AsyncData(:final value) => [ },
...value.takeLast(_steps - 1), ).map(_mapToState);
Traffic(upload: event.upload, download: event.download), }
], }
_ => List.generate(
_steps, List<Traffic> _mapToState(ClashTraffic event) {
(index) => const Traffic(upload: 0, download: 0), 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(); await windowManager.close();
} }
Future<void> quit() async {
loggy.debug("quitting");
await windowManager.close();
await windowManager.destroy();
}
@override @override
Future<void> onWindowClose() async { Future<void> onWindowClose() async {
await windowManager.hide(); 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/domain/failures.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.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/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/common/common.dart';
import 'package:hiddify/features/home/widgets/widgets.dart'; import 'package:hiddify/features/home/widgets/widgets.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
@@ -22,18 +21,6 @@ class HomePage extends HookConsumerWidget {
final hasAnyProfile = ref.watch(hasAnyProfileProvider); final hasAnyProfile = ref.watch(hasAnyProfileProvider);
final activeProfile = ref.watch(activeProfileProvider); 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( return Scaffold(
body: Stack( body: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,

View File

@@ -3,8 +3,11 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/theme/theme.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/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/alerts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
@@ -17,12 +20,71 @@ class ConnectionButton extends HookConsumerWidget {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final connectionStatus = ref.watch(connectivityControllerProvider); final connectionStatus = ref.watch(connectivityControllerProvider);
final Color connectionLogoColor = connectionStatus.isConnected ref.listen(
? ConnectionButtonColor.connected connectivityControllerProvider,
: ConnectionButtonColor.disconnected; (_, 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( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -33,7 +95,7 @@ class ConnectionButton extends HookConsumerWidget {
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
blurRadius: 16, blurRadius: 16,
color: connectionLogoColor.withOpacity(0.5), color: buttonColor.withOpacity(0.5),
), ),
], ],
), ),
@@ -43,26 +105,24 @@ class ConnectionButton extends HookConsumerWidget {
shape: const CircleBorder(), shape: const CircleBorder(),
color: Colors.white, color: Colors.white,
child: InkWell( child: InkWell(
onTap: () async { onTap: onTap,
await ref
.read(connectivityControllerProvider.notifier)
.toggleConnection();
},
child: Padding( child: Padding(
padding: const EdgeInsets.all(36), padding: const EdgeInsets.all(36),
child: Assets.images.logo.svg( child: Assets.images.logo.svg(
colorFilter: ColorFilter.mode( colorFilter: ColorFilter.mode(
connectionLogoColor, buttonColor,
BlendMode.srcIn, BlendMode.srcIn,
), ),
), ),
), ),
), ),
).animate(target: intractable ? 0 : 1).blurXY(end: 1), ).animate(target: enabled ? 0 : 1).blurXY(end: 1),
).animate(target: intractable ? 0 : 1).scaleXY(end: .88), )
.animate(target: enabled ? 0 : 1)
.scaleXY(end: .88, curve: Curves.easeIn),
const Gap(16), const Gap(16),
Text( Text(
connectionStatus.present(t).sentenceCase, label.sentenceCase,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
], ],

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import 'package:hiddify/features/logs/notifier/notifier.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
import 'package:tint/tint.dart';
class LogsPage extends HookConsumerWidget { class LogsPage extends HookConsumerWidget {
const LogsPage({super.key}); const LogsPage({super.key});
@@ -80,19 +81,7 @@ class LogsPage extends HookConsumerWidget {
children: [ children: [
ListTile( ListTile(
dense: true, dense: true,
title: Text.rich( subtitle: Text(log.strip()),
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),
), ),
if (index != 0) if (index != 0)
const Divider( const Divider(

View File

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

View File

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

View File

@@ -3,8 +3,9 @@ import 'dart:async';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/domain/clash/clash.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/clash/clash_mode.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
import 'package:hiddify/features/proxies/model/model.dart'; import 'package:hiddify/features/proxies/model/model.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -16,23 +17,23 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger {
@override @override
Future<List<GroupWithProxies>> build() async { Future<List<GroupWithProxies>> build() async {
loggy.debug('building'); loggy.debug('building');
await ref.watch(clashControllerProvider.future); if (!await ref.watch(serviceRunningProvider.future)) {
throw const CoreServiceNotRunning();
}
final mode = await ref.watch(clashModeProvider.future); final mode = await ref.watch(clashModeProvider.future);
return _clash return _clash.getProxies().flatMap(
.getProxies() (proxies) {
.flatMap( return TaskEither(
(proxies) { () async => right(await GroupWithProxies.fromProxies(proxies, mode)),
return TaskEither( );
() async => },
right(await GroupWithProxies.fromProxies(proxies, mode)), ).getOrElse((l) {
); loggy.warning("failed receiving proxies: $l");
}, throw l;
) }).run();
.getOrElse((l) => throw l)
.run();
} }
ClashFacade get _clash => ref.read(clashFacadeProvider); ClashFacade get _clash => ref.read(coreFacadeProvider);
Future<void> changeProxy(String selectorName, String proxyName) async { Future<void> changeProxy(String selectorName, String proxyName) async {
loggy.debug("changing proxy, selector: $selectorName - proxy: $proxyName "); 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 notifier = ref.watch(proxiesNotifierProvider.notifier);
final asyncProxies = ref.watch(proxiesNotifierProvider); final asyncProxies = ref.watch(proxiesNotifierProvider);
final proxies = asyncProxies.value ?? []; final proxies = asyncProxies.asData?.value ?? [];
final delays = ref.watch(proxiesDelayNotifierProvider); final delays = ref.watch(proxiesDelayNotifierProvider);
final selectActiveProxyMutation = useMutation( final selectActiveProxyMutation = useMutation(
@@ -163,7 +163,10 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
NestedTabAppBar( NestedTabAppBar(
title: Text(t.proxies.pageTitle.titleCase), 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return ListTile( return ListTile(
title: Text( title: Text(
proxy.name, switch (proxy) {
ClashProxyGroup(:final name) => name.toUpperCase(),
ClashProxyItem(:final name) => name,
},
overflow: TextOverflow.ellipsis, 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, trailing: delay != null ? Text(delay.toString()) : null,
selected: selected, selected: selected,
onTap: onSelect, onTap: onSelect,
horizontalTitleGap: 4,
); );
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/core_providers.dart';
import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/core/prefs/prefs.dart';
@@ -27,7 +25,7 @@ class SystemTrayController extends _$SystemTrayController
_initialized = true; _initialized = true;
} }
final connection = ref.watch(connectivityControllerProvider); final connection = await ref.watch(connectivityControllerProvider.future);
final mode = final mode =
ref.watch(clashModeProvider.select((value) => value.valueOrNull)); ref.watch(clashModeProvider.select((value) => value.valueOrNull));
@@ -104,8 +102,9 @@ class SystemTrayController extends _$SystemTrayController
return ref.read(connectivityControllerProvider.notifier).toggleConnection(); return ref.read(connectivityControllerProvider.notifier).toggleConnection();
} }
// TODO rewrite
Future<void> handleClickExitApp(MenuItem menuItem) async { 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 // ignore_for_file: type=lint
import 'dart:ffi' as ffi; import 'dart:ffi' as ffi;
/// Bindings to Clash /// Bindings to Singbox
class ClashNativeLibrary { class SingboxNativeLibrary {
/// Holds the symbol lookup function. /// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup; _lookup;
/// The symbols are looked up in [dynamicLibrary]. /// The symbols are looked up in [dynamicLibrary].
ClashNativeLibrary(ffi.DynamicLibrary dynamicLibrary) SingboxNativeLibrary(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup; : _lookup = dynamicLibrary.lookup;
/// The symbols are looked up with [lookup]. /// The symbols are looked up with [lookup].
ClashNativeLibrary.fromLookup( SingboxNativeLibrary.fromLookup(
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
lookup) lookup)
: _lookup = lookup; : _lookup = lookup;
@@ -857,206 +857,69 @@ class ClashNativeLibrary {
late final __FCmulcr = late final __FCmulcr =
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>(); __FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
void getConfigs( void setup(
int port, ffi.Pointer<ffi.Char> baseDir,
ffi.Pointer<ffi.Char> workingDir,
ffi.Pointer<ffi.Char> tempDir,
) { ) {
return _getConfigs( return _setup(
port, baseDir,
workingDir,
tempDir,
); );
} }
late final _getConfigsPtr = late final _setupPtr = _lookup<
_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<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function( ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong, ffi.Pointer<ffi.Char>)>>('patchConfigs'); ffi.Pointer<ffi.Char>)>>('setup');
late final _patchConfigs = late final _setup = _setupPtr.asFunction<
_patchConfigsPtr.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>(); void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>();
void updateConfigs( ffi.Pointer<ffi.Char> parse(
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> path, ffi.Pointer<ffi.Char> path,
) { ) {
return _validateConfig( return _parse(
port,
path, path,
); );
} }
late final _validateConfigPtr = _lookup< late final _parsePtr = _lookup<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function( ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('parse');
ffi.LongLong, ffi.Pointer<ffi.Char>)>>('validateConfig'); late final _parse = _parsePtr
late final _validateConfig = _validateConfigPtr .asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
void initNativeDartBridge( ffi.Pointer<ffi.Char> create(
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> configPath, ffi.Pointer<ffi.Char> configPath,
) { ) {
return _setOptions( return _create(
port,
dir,
configPath, configPath,
); );
} }
late final _setOptionsPtr = _lookup< late final _createPtr = _lookup<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('create');
ffi.Pointer<ffi.Char>)>>('setOptions'); late final _create = _createPtr
late final _setOptions = _setOptionsPtr.asFunction< .asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>();
void start( ffi.Pointer<ffi.Char> start() {
int port, return _start();
) {
return _start(
port,
);
} }
late final _startPtr = late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('start'); _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('start');
late final _start = _startPtr.asFunction<void Function(int)>(); late final _start = _startPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void startLog( ffi.Pointer<ffi.Char> stop() {
int port, return _stop();
ffi.Pointer<ffi.Char> levelStr,
) {
return _startLog(
port,
levelStr,
);
} }
late final _startLogPtr = _lookup< late final _stopPtr =
ffi.NativeFunction< _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('stop');
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>)>>('startLog'); late final _stop = _stopPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
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)>();
} }
typedef va_list = ffi.Pointer<ffi.Char>; typedef va_list = ffi.Pointer<ffi.Char>;
@@ -1136,7 +999,6 @@ final class GoSlice extends ffi.Struct {
typedef GoInt = GoInt64; typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong; typedef GoInt64 = ffi.LongLong;
typedef GoUint8 = ffi.UnsignedChar;
const int _VCRT_COMPILER_PREPROCESSOR = 1; 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