Migrate to singbox
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -98,6 +98,10 @@ jobs:
|
||||
make gen
|
||||
make translate
|
||||
|
||||
- name: Get Geo Assets
|
||||
run: |
|
||||
make get-geo-assets
|
||||
|
||||
- name: Get Libs ${{ matrix.platform }}
|
||||
run: |
|
||||
make ${{ matrix.platform }}-libs
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,6 +42,9 @@ migrate_working_dir/
|
||||
**/*.dylib
|
||||
/dist/
|
||||
|
||||
/assets/core/*
|
||||
!/assets/core/.gitkeep
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "core"]
|
||||
path = core
|
||||
url = https://github.com/hiddify/hiddify-libclash
|
||||
[submodule "libcore"]
|
||||
path = libcore
|
||||
url = https://github.com/hiddify/hiddify-next-core
|
||||
|
||||
45
Makefile
45
Makefile
@@ -1,7 +1,8 @@
|
||||
ANDROID_OUT=./android/app/src/main/jniLibs
|
||||
DESKTOP_OUT=./core/bin
|
||||
NDK_BIN=$(ANDROID_HOME)/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin
|
||||
GOBUILD=CGO_ENABLED=1 go build -trimpath -tags with_gvisor,with_lwip -ldflags="-w -s" -buildmode=c-shared
|
||||
BINDIR=./libcore/bin
|
||||
ANDROID_OUT=./android/app/libs
|
||||
DESKTOP_OUT=./libcore/bin
|
||||
GEO_ASSETS_DIR=./assets/core
|
||||
LIBS_DOWNLOAD_URL=https://github.com/hiddify/hiddify-next-core/releases/download/draft
|
||||
|
||||
get:
|
||||
flutter pub get
|
||||
@@ -20,40 +21,40 @@ windows-release:
|
||||
|
||||
linux-release:
|
||||
flutter_distributor package --platform linux --targets appimage
|
||||
|
||||
macos-realase:
|
||||
flutter build macos --release &&\
|
||||
tree ./build/macos/Build &&\
|
||||
create-dmg --app-drop-link 600 185 "hiddify-amd64.dmg" ./build/macos/Build/Products/Release/hiddify-clash.app
|
||||
|
||||
android-libs:
|
||||
mkdir -p $(ANDROID_OUT)/x86_64 $(ANDROID_OUT)/arm64-v8a/ $(ANDROID_OUT)/armeabi-v7a/ &&\
|
||||
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-android-amd64.so.gz | gunzip > $(ANDROID_OUT)/x86_64/libclash.so &&\
|
||||
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-android-arm64.so.gz | gunzip > $(ANDROID_OUT)/arm64-v8a/libclash.so &&\
|
||||
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-android-arm.so.gz | gunzip > $(ANDROID_OUT)/armeabi-v7a/libclash.so
|
||||
mkdir -p $(ANDROID_OUT)
|
||||
curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-android.aar.gz | gunzip > $(ANDROID_OUT)/libcore.aar
|
||||
|
||||
windows-libs:
|
||||
mkdir -p $(DESKTOP_OUT)/ &&\
|
||||
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-windows-amd64.dll.gz | gunzip > $(DESKTOP_OUT)/libclash.dll
|
||||
mkdir -p $(DESKTOP_OUT)
|
||||
curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-windows-amd64.dll.gz | gunzip > $(DESKTOP_OUT)/libcore.dll
|
||||
|
||||
linux-libs:
|
||||
mkdir -p $(DESKTOP_OUT)/ &&\
|
||||
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-linux-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libclash.so
|
||||
mkdir -p $(DESKTOP_OUT)
|
||||
curl -L $(LIBS_DOWNLOAD_URL)/hiddify-libcore-linux-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libcore.so
|
||||
|
||||
macos-libs:
|
||||
mkdir -p $(DESKTOP_OUT)/ &&\
|
||||
curl -L https://github.com/hiddify/hiddify-libclash/releases/latest/download/hiddify-clashlib-macos-amd64.so.gz | gunzip > $(DESKTOP_OUT)/libclash.dylib
|
||||
|
||||
get-geo-assets:
|
||||
curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db
|
||||
curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db
|
||||
|
||||
build-headers:
|
||||
make -C libcore -f Makefile headers && mv $(BINDIR)/hiddify-libcore-headers.h $(BINDIR)/libcore.h
|
||||
|
||||
build-android-libs:
|
||||
cd core &&\
|
||||
mkdir -p .$(ANDROID_OUT)/x86_64/ .$(ANDROID_OUT)/arm64-v8a/ .$(ANDROID_OUT)/armeabi-v7a/ &&\
|
||||
make android-amd64 && mv bin/hiddify-clashlib-android-amd64.so .$(ANDROID_OUT)/x86_64/libclash.so &&\
|
||||
make android-arm && mv bin/hiddify-clashlib-android-arm.so .$(ANDROID_OUT)/armeabi-v7a/libclash.so &&\
|
||||
make android-arm64 && mv bin/hiddify-clashlib-android-arm64.so .$(ANDROID_OUT)/arm64-v8a/libclash.so
|
||||
make -C libcore -f Makefile android && mv $(BINDIR)/hiddify-libcore-android.aar $(ANDROID_OUT)/libcore.aar
|
||||
|
||||
build-windows-libs:
|
||||
cd core &&\
|
||||
make windows-amd64 && mv bin/hiddify-clashlib-windows-amd64.dll bin/libclash.dll
|
||||
make -C libcore -f Makefile windows-amd64 && mv $(BINDIR)/hiddify-libcore-windows-amd64.dll $(DESKTOP_OUT)/libcore.dll
|
||||
|
||||
build-linux-libs:
|
||||
cd core &&\
|
||||
make linux-amd64 && mv bin/hiddify-clashlib-linux-amd64.dll bin/libclash.so
|
||||
|
||||
make -C libcore -f Makefile linux-amd64 && mv $(BINDIR)/hiddify-libcore-linux-amd64.dll $(DESKTOP_OUT)/libcore.so
|
||||
3
android/.gitignore
vendored
3
android/.gitignore
vendored
@@ -11,3 +11,6 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
/app/libs/*
|
||||
!/app/libs/.gitkeep
|
||||
@@ -60,6 +60,11 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
aidl true
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
@@ -67,7 +72,11 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
implementation 'androidx.window:window:1.0.0'
|
||||
implementation 'androidx.window:window-java:1.0.0'
|
||||
|
||||
0
android/app/libs/.gitkeep
Normal file
0
android/app/libs/.gitkeep
Normal file
@@ -1,14 +1,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:label="hiddify"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round">
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -39,15 +49,14 @@
|
||||
<data android:host="install-config"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".HiddifyVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:stopWithTask="false"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".bg.VPNService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>()!! }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,148 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.hiddify.hiddify.bg.ServiceConnection
|
||||
import com.hiddify.hiddify.bg.ServiceNotification
|
||||
import com.hiddify.hiddify.bg.VPNService
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
private lateinit var eventChannel: EventChannel
|
||||
private lateinit var methodResult: MethodChannel.Result
|
||||
private var vpnBroadcastReceiver: VpnState? = null
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.LinkedList
|
||||
|
||||
class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
|
||||
companion object {
|
||||
private const val TAG = "ANDROID/MyActivity"
|
||||
lateinit var instance: MainActivity
|
||||
|
||||
const val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
enum class Action(val method: String) {
|
||||
GrantPermission("grant_permission"),
|
||||
StartProxy("start"),
|
||||
StopProxy("stop"),
|
||||
RefreshStatus("refresh_status"),
|
||||
SetPrefs("set_prefs")
|
||||
}
|
||||
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1010
|
||||
}
|
||||
|
||||
private fun registerBroadcastReceiver() {
|
||||
Log.d(HiddifyVpnService.TAG, "registering broadcast receiver")
|
||||
vpnBroadcastReceiver = VpnState()
|
||||
val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS)
|
||||
registerReceiver(vpnBroadcastReceiver, intentFilter)
|
||||
}
|
||||
private val connection = ServiceConnection(this, this)
|
||||
|
||||
private fun unregisterBroadcastReceiver() {
|
||||
Log.d(HiddifyVpnService.TAG, "unregistering broadcast receiver")
|
||||
if (vpnBroadcastReceiver != null) {
|
||||
unregisterReceiver(vpnBroadcastReceiver)
|
||||
vpnBroadcastReceiver = null
|
||||
}
|
||||
}
|
||||
val logList = LinkedList<String>()
|
||||
var logCallback: ((Boolean) -> Unit)? = null
|
||||
val serviceStatus = MutableLiveData(Status.Stopped)
|
||||
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
|
||||
val serviceLogs = MutableLiveData<String?>(null)
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
methodChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.TAG)
|
||||
methodChannel.setMethodCallHandler(this)
|
||||
|
||||
eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.EVENT_TAG)
|
||||
registerBroadcastReceiver()
|
||||
eventChannel.setStreamHandler(vpnBroadcastReceiver)
|
||||
instance = this
|
||||
reconnect()
|
||||
flutterEngine.plugins.add(MethodHandler())
|
||||
flutterEngine.plugins.add(EventHandler())
|
||||
flutterEngine.plugins.add(LogHandler())
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
methodResult = result
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (call.method) {
|
||||
Action.GrantPermission.method -> {
|
||||
grantVpnPermission()
|
||||
fun reconnect() {
|
||||
connection.reconnect()
|
||||
}
|
||||
|
||||
fun startService() {
|
||||
if (!ServiceNotification.checkPermission()) {
|
||||
Log.d(TAG, "missing notification permission")
|
||||
return
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
// if (Settings.rebuildServiceMode()) {
|
||||
// reconnect()
|
||||
// }
|
||||
if (prepare()) {
|
||||
Log.d(TAG, "VPN permission required")
|
||||
return@launch
|
||||
}
|
||||
|
||||
Action.StartProxy.method -> {
|
||||
VpnServiceManager.startVpnService(this)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Action.StopProxy.method -> {
|
||||
VpnServiceManager.stopVpnService()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Action.RefreshStatus.method -> {
|
||||
val statusIntent = Intent(VpnState.ACTION_VPN_STATUS)
|
||||
statusIntent.putExtra(VpnState.IS_VPN_ACTIVE, VpnServiceManager.isRunning)
|
||||
sendBroadcast(statusIntent)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Action.SetPrefs.method -> {
|
||||
val args = call.arguments as Map<String, Any>
|
||||
VpnServiceManager.setPrefs(context, args)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
val intent = Intent(Application.application, VPNService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(Application.application, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterBroadcastReceiver()
|
||||
|
||||
override fun onServiceStatusChanged(status: Status) {
|
||||
Log.d(TAG, "service status changed: $status")
|
||||
serviceStatus.postValue(status)
|
||||
}
|
||||
|
||||
private fun grantVpnPermission() {
|
||||
val vpnPermissionIntent = VpnService.prepare(this)
|
||||
if (vpnPermissionIntent == null) {
|
||||
onActivityResult(VPN_PERMISSION_REQUEST_CODE, RESULT_OK, null)
|
||||
} else {
|
||||
startActivityForResult(vpnPermissionIntent, VPN_PERMISSION_REQUEST_CODE)
|
||||
|
||||
override fun onServiceAlert(type: Alert, message: String?) {
|
||||
Log.d(TAG, "service alert: $type")
|
||||
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
|
||||
}
|
||||
|
||||
private var paused = false
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
paused = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
paused = false
|
||||
logCallback?.invoke(true)
|
||||
}
|
||||
|
||||
override fun onServiceWriteLog(message: String?) {
|
||||
if (paused) {
|
||||
if (logList.size > 300) {
|
||||
logList.removeFirst()
|
||||
}
|
||||
}
|
||||
logList.addLast(message)
|
||||
if (!paused) {
|
||||
logCallback?.invoke(false)
|
||||
serviceLogs.postValue(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceResetLogs(messages: MutableList<String>) {
|
||||
logList.clear()
|
||||
logList.addAll(messages)
|
||||
if (!paused) logCallback?.invoke(true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
connection.disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private suspend fun prepare() = withContext(Dispatchers.Main) {
|
||||
try {
|
||||
val intent = VpnService.prepare(this@MainActivity)
|
||||
if (intent != null) {
|
||||
// prepareLauncher.launch(intent)
|
||||
startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onServiceAlert(Alert.RequestVPNPermission, e.message)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
methodResult.success(resultCode == RESULT_OK)
|
||||
} else if (requestCode == 101010) {
|
||||
methodResult.success(resultCode == RESULT_OK)
|
||||
if (resultCode == RESULT_OK) startService()
|
||||
else onServiceAlert(Alert.RequestVPNPermission, null)
|
||||
} else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_OK) startService()
|
||||
else onServiceAlert(Alert.RequestNotificationPermission, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
33
android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
Normal file
33
android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
Normal 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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
291
android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
Normal file
291
android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
147
android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
Normal file
147
android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
Normal 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)
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Alert {
|
||||
RequestVPNPermission,
|
||||
RequestNotificationPermission,
|
||||
EmptyConfiguration,
|
||||
StartCommandServer,
|
||||
CreateService,
|
||||
StartService
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Status {
|
||||
Stopped,
|
||||
Starting,
|
||||
Started,
|
||||
Stopping,
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
4
android/app/src/main/res/values/strings.xml
Normal file
4
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="stop">Stop</string>
|
||||
</resources>
|
||||
@@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
0
assets/core/.gitkeep
Normal file
0
assets/core/.gitkeep
Normal file
Binary file not shown.
@@ -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
|
||||
@@ -145,8 +145,18 @@
|
||||
"unexpected": "unexpected failure",
|
||||
"core": "clash failure ${reason}"
|
||||
},
|
||||
"singbox": {
|
||||
"unexpected": "unexpected service failure",
|
||||
"serviceNotRunning": "Service not running",
|
||||
"invalidConfig": "Configuration is not valid",
|
||||
"create": "Error creating service",
|
||||
"start": "Error starting service"
|
||||
},
|
||||
"connectivity": {
|
||||
"unexpected": "unexpected failure"
|
||||
"unexpected": "unexpected failure",
|
||||
"missingVpnPermission": "Missing VPN permission",
|
||||
"missingNotificationPermission": "Missing Notification permission",
|
||||
"core": "Core failure"
|
||||
},
|
||||
"profiles": {
|
||||
"unexpected": "unexpected failure",
|
||||
|
||||
@@ -145,8 +145,18 @@
|
||||
"unexpected": "خطایی رخ داده",
|
||||
"core": "خطای کلش ${reason}"
|
||||
},
|
||||
"singbox": {
|
||||
"unexpected": "خطایی غیر منتظره در سرویس رخ داد",
|
||||
"serviceNotRunning": "سرویس در حال اجرا نیست",
|
||||
"invalidConfig": "کانفیگ غیر معتبر",
|
||||
"create": "در ایجاد سرویس خطایی رخ داده",
|
||||
"start": "در راهاندازی سرویس خطایی رخ داده"
|
||||
},
|
||||
"connectivity": {
|
||||
"unexpected": "خطایی رخ داده"
|
||||
"unexpected": "خطایی رخ داده",
|
||||
"missingVpnPermission": "نیازمند دسترسی VPN",
|
||||
"missingNotificationPermission": "نیازمند دسترسی اعلانات",
|
||||
"core": "خطای هسته"
|
||||
},
|
||||
"profiles": {
|
||||
"unexpected": "خطایی رخ داده",
|
||||
|
||||
1
core
1
core
Submodule core deleted from 1149e93363
@@ -39,7 +39,15 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
|
||||
overrides: [sharedPreferencesProvider.overrideWithValue(sharedPreferences)],
|
||||
);
|
||||
|
||||
Loggy.initLoggy(logPrinter: const PrettyPrinter());
|
||||
// Loggy.initLoggy(logPrinter: const PrettyPrinter());
|
||||
final filesEditor = container.read(filesEditorServiceProvider);
|
||||
await filesEditor.init();
|
||||
Loggy.initLoggy(
|
||||
logPrinter: MultiLogPrinter(
|
||||
const PrettyPrinter(),
|
||||
FileLogPrinter(filesEditor.appLogsPath),
|
||||
),
|
||||
);
|
||||
|
||||
final silentStart =
|
||||
container.read(prefsControllerProvider).general.silentStart;
|
||||
@@ -68,12 +76,10 @@ Future<void> lazyBootstrap(WidgetsBinding widgetsBinding) async {
|
||||
Future<void> initAppServices(
|
||||
Result Function<Result>(ProviderListenable<Result>) read,
|
||||
) async {
|
||||
await read(filesEditorServiceProvider).init();
|
||||
// await read(filesEditorServiceProvider).init();
|
||||
await Future.wait(
|
||||
[
|
||||
read(connectivityServiceProvider).init(),
|
||||
read(clashServiceProvider).init(),
|
||||
read(clashServiceProvider).start(),
|
||||
read(notificationServiceProvider).init(),
|
||||
],
|
||||
);
|
||||
@@ -83,6 +89,7 @@ Future<void> initAppServices(
|
||||
Future<void> initControllers(
|
||||
Result Function<Result>(ProviderListenable<Result>) read,
|
||||
) async {
|
||||
_loggy.debug("initializing controllers");
|
||||
await Future.wait(
|
||||
[
|
||||
read(activeProfileProvider.future),
|
||||
|
||||
@@ -36,7 +36,7 @@ class PrefsController extends _$PrefsController with AppLogger {
|
||||
|
||||
ClashConfig _getClashPrefs() {
|
||||
final persisted = _prefs.getString(_overridesKey);
|
||||
if (persisted == null) return ClashConfig.initial;
|
||||
if (persisted == null) return const ClashConfig();
|
||||
return ClashConfig.fromJson(jsonDecode(persisted) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ GoRouter router(RouterRef ref) {
|
||||
|
||||
int getCurrentIndex(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).location;
|
||||
if (location == HomeRoute.path) return 0;
|
||||
if (location.startsWith(ProxiesRoute.path)) return 1;
|
||||
if (location.startsWith(LogsRoute.path)) return 2;
|
||||
if (location.startsWith(SettingsRoute.path)) return 3;
|
||||
if (location.startsWith(AboutRoute.path)) return 4;
|
||||
if (location == const HomeRoute().location) return 0;
|
||||
if (location.startsWith(const ProxiesRoute().location)) return 1;
|
||||
if (location.startsWith(const LogsRoute().location)) return 2;
|
||||
if (location.startsWith(const SettingsRoute().location)) return 3;
|
||||
if (location.startsWith(const AboutRoute().location)) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ part 'desktop_routes.g.dart';
|
||||
TypedGoRoute<LogsRoute>(path: LogsRoute.path),
|
||||
TypedGoRoute<SettingsRoute>(
|
||||
path: SettingsRoute.path,
|
||||
routes: [
|
||||
TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
|
||||
],
|
||||
// routes: [
|
||||
// TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
|
||||
// ],
|
||||
),
|
||||
TypedGoRoute<AboutRoute>(path: AboutRoute.path),
|
||||
],
|
||||
@@ -59,18 +59,18 @@ class SettingsRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
class ClashOverridesRoute extends GoRouteData {
|
||||
const ClashOverridesRoute();
|
||||
static const path = 'clash';
|
||||
// class ClashOverridesRoute extends GoRouteData {
|
||||
// const ClashOverridesRoute();
|
||||
// static const path = 'clash';
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return const MaterialPage(
|
||||
fullscreenDialog: true,
|
||||
child: ClashOverridesPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
// return const MaterialPage(
|
||||
// fullscreenDialog: true,
|
||||
// child: ClashOverridesPage(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
class AboutRoute extends GoRouteData {
|
||||
const AboutRoute();
|
||||
|
||||
@@ -20,9 +20,9 @@ part 'mobile_routes.g.dart';
|
||||
TypedGoRoute<LogsRoute>(path: LogsRoute.path),
|
||||
TypedGoRoute<SettingsRoute>(
|
||||
path: SettingsRoute.path,
|
||||
routes: [
|
||||
TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
|
||||
],
|
||||
// routes: [
|
||||
// TypedGoRoute<ClashOverridesRoute>(path: ClashOverridesRoute.path),
|
||||
// ],
|
||||
),
|
||||
TypedGoRoute<AboutRoute>(path: AboutRoute.path),
|
||||
],
|
||||
@@ -69,20 +69,20 @@ class SettingsRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
class ClashOverridesRoute extends GoRouteData {
|
||||
const ClashOverridesRoute();
|
||||
static const path = 'clash';
|
||||
// class ClashOverridesRoute extends GoRouteData {
|
||||
// const ClashOverridesRoute();
|
||||
// static const path = 'clash';
|
||||
|
||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||
// static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return const MaterialPage(
|
||||
fullscreenDialog: true,
|
||||
child: ClashOverridesPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// @override
|
||||
// Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
// return const MaterialPage(
|
||||
// fullscreenDialog: true,
|
||||
// child: ClashOverridesPage(),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
class AboutRoute extends GoRouteData {
|
||||
const AboutRoute();
|
||||
|
||||
145
lib/data/api/clash_api.dart
Normal file
145
lib/data/api/clash_api.dart
Normal 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!));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hiddify/data/api/clash_api.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/data/repository/repository.dart';
|
||||
import 'package:hiddify/data/repository/update_repository_impl.dart';
|
||||
import 'package:hiddify/domain/app/app.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/domain/core_facade.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -28,21 +30,26 @@ ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao(
|
||||
ref.watch(appDatabaseProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ClashFacade clashFacade(ClashFacadeRef ref) => ClashFacadeImpl(
|
||||
clashService: ref.watch(clashServiceProvider),
|
||||
filesEditor: ref.watch(filesEditorServiceProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) =>
|
||||
ProfilesRepositoryImpl(
|
||||
profilesDao: ref.watch(profilesDaoProvider),
|
||||
filesEditor: ref.watch(filesEditorServiceProvider),
|
||||
clashFacade: ref.watch(clashFacadeProvider),
|
||||
singbox: ref.watch(coreFacadeProvider),
|
||||
dio: ref.watch(dioProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
UpdateRepository updateRepository(UpdateRepositoryRef ref) =>
|
||||
UpdateRepositoryImpl(ref.watch(dioProvider));
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
|
||||
ref.watch(singboxServiceProvider),
|
||||
ref.watch(filesEditorServiceProvider),
|
||||
ref.watch(clashApiProvider),
|
||||
ref.watch(connectivityServiceProvider),
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:drift/native.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/local/tables.dart';
|
||||
import 'package:hiddify/data/local/type_converters.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@@ -22,8 +22,8 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
final dbDir = await FilesEditorService.getDatabaseDirectory();
|
||||
final file = File(p.join(dbDir.path, 'db.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
187
lib/data/repository/core_facade_impl.dart
Normal file
187
lib/data/repository/core_facade_impl.dart
Normal 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();
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import 'package:dio/dio.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/repository/exception_handlers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/enums.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
@@ -18,13 +18,13 @@ class ProfilesRepositoryImpl
|
||||
ProfilesRepositoryImpl({
|
||||
required this.profilesDao,
|
||||
required this.filesEditor,
|
||||
required this.clashFacade,
|
||||
required this.singbox,
|
||||
required this.dio,
|
||||
});
|
||||
|
||||
final ProfilesDao profilesDao;
|
||||
final FilesEditorService filesEditor;
|
||||
final ClashFacade clashFacade;
|
||||
final SingboxFacade singbox;
|
||||
final Dio dio;
|
||||
|
||||
@override
|
||||
@@ -166,20 +166,17 @@ class ProfilesRepositoryImpl
|
||||
() async {
|
||||
final path = filesEditor.configPath(fileName);
|
||||
final response = await dio.download(url, path);
|
||||
if (response.statusCode != 200) {
|
||||
await File(path).delete();
|
||||
return left(const ProfileUnexpectedFailure());
|
||||
}
|
||||
final isValid = await clashFacade
|
||||
.validateConfig(fileName)
|
||||
.getOrElse((_) => false)
|
||||
.run();
|
||||
if (!isValid) {
|
||||
await File(path).delete();
|
||||
return left(const ProfileFailure.invalidConfig());
|
||||
}
|
||||
final profile = Profile.fromResponse(url, response.headers.map);
|
||||
return right(profile);
|
||||
final parseResult = await singbox.parseConfig(path).run();
|
||||
return parseResult.fold(
|
||||
(l) async {
|
||||
await File(path).delete();
|
||||
return left(ProfileFailure.invalidConfig(l.msg));
|
||||
},
|
||||
(_) {
|
||||
final profile = Profile.fromResponse(url, response.headers.map);
|
||||
return right(profile);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export 'clash_facade_impl.dart';
|
||||
export 'core_facade_impl.dart';
|
||||
export 'profiles_repository_impl.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export 'clash_config.dart';
|
||||
export 'clash_enums.dart';
|
||||
export 'clash_facade.dart';
|
||||
export 'clash_failures.dart';
|
||||
export 'clash_log.dart';
|
||||
export 'clash_proxy.dart';
|
||||
export 'clash_traffic.dart';
|
||||
|
||||
@@ -24,12 +24,6 @@ class ClashConfig with _$ClashConfig {
|
||||
bool? ipv6,
|
||||
}) = _ClashConfig;
|
||||
|
||||
static const initial = ClashConfig(
|
||||
httpPort: 12346,
|
||||
socksPort: 12347,
|
||||
mixedPort: 12348,
|
||||
);
|
||||
|
||||
ClashConfig patch(ClashConfigPatch patch) {
|
||||
return copyWith(
|
||||
httpPort: (patch.httpPort ?? optionOf(httpPort)).toNullable(),
|
||||
|
||||
@@ -38,6 +38,7 @@ enum ProxyType {
|
||||
hysteria("Hysteria"),
|
||||
wireGuard("WireGuard"),
|
||||
tuic("Tuic"),
|
||||
ssh("SSH"),
|
||||
relay("Relay"),
|
||||
selector("Selector"),
|
||||
fallback("Fallback"),
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/domain/core_service_failure.dart';
|
||||
|
||||
abstract class ClashFacade {
|
||||
TaskEither<ClashFailure, ClashConfig> getConfigs();
|
||||
TaskEither<CoreServiceFailure, ClashConfig> getConfigs();
|
||||
|
||||
TaskEither<ClashFailure, bool> validateConfig(String configFileName);
|
||||
TaskEither<CoreServiceFailure, Unit> patchOverrides(ClashConfig overrides);
|
||||
|
||||
/// change active configuration file by [configFileName]
|
||||
TaskEither<ClashFailure, Unit> changeConfigs(String configFileName);
|
||||
TaskEither<CoreServiceFailure, List<ClashProxy>> getProxies();
|
||||
|
||||
TaskEither<ClashFailure, Unit> patchOverrides(ClashConfig overrides);
|
||||
|
||||
TaskEither<ClashFailure, List<ClashProxy>> getProxies();
|
||||
|
||||
TaskEither<ClashFailure, Unit> changeProxy(
|
||||
TaskEither<CoreServiceFailure, Unit> changeProxy(
|
||||
String selectorName,
|
||||
String proxyName,
|
||||
);
|
||||
|
||||
TaskEither<ClashFailure, int> testDelay(
|
||||
TaskEither<CoreServiceFailure, int> testDelay(
|
||||
String proxyName, {
|
||||
String testUrl = Constants.delayTestUrl,
|
||||
String testUrl = Defaults.delayTestUrl,
|
||||
});
|
||||
|
||||
TaskEither<ClashFailure, ClashTraffic> getTraffic();
|
||||
|
||||
Stream<Either<ClashFailure, ClashLog>> watchLogs();
|
||||
Stream<Either<CoreServiceFailure, ClashTraffic>> watchTraffic();
|
||||
}
|
||||
|
||||
@@ -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 ?? ""),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ part 'clash_proxy.g.dart';
|
||||
|
||||
// TODO: test and improve
|
||||
@Freezed(fromJson: true)
|
||||
class ClashProxy with _$ClashProxy {
|
||||
sealed class ClashProxy with _$ClashProxy {
|
||||
const ClashProxy._();
|
||||
|
||||
const factory ClashProxy.group({
|
||||
@@ -15,6 +15,7 @@ class ClashProxy with _$ClashProxy {
|
||||
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
|
||||
required List<String> all,
|
||||
required String now,
|
||||
@Default(false) bool udp,
|
||||
List<ClashHistory>? history,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) int? delay,
|
||||
}) = ClashProxyGroup;
|
||||
@@ -22,6 +23,7 @@ class ClashProxy with _$ClashProxy {
|
||||
const factory ClashProxy.item({
|
||||
required String name,
|
||||
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
|
||||
@Default(false) bool udp,
|
||||
List<ClashHistory>? history,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) int? delay,
|
||||
}) = ClashProxyItem;
|
||||
|
||||
11
lib/domain/connectivity/connection_facade.dart
Normal file
11
lib/domain/connectivity/connection_facade.dart
Normal 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();
|
||||
}
|
||||
40
lib/domain/connectivity/connection_failure.dart
Normal file
40
lib/domain/connectivity/connection_failure.dart
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/locale/locale.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity_failure.dart';
|
||||
import 'package:hiddify/domain/connectivity/connection_failure.dart';
|
||||
|
||||
part 'connection_status.freezed.dart';
|
||||
|
||||
@@ -9,24 +9,13 @@ sealed class ConnectionStatus with _$ConnectionStatus {
|
||||
const ConnectionStatus._();
|
||||
|
||||
const factory ConnectionStatus.disconnected([
|
||||
ConnectivityFailure? connectFailure,
|
||||
ConnectionFailure? connectionFailure,
|
||||
]) = Disconnected;
|
||||
const factory ConnectionStatus.connecting() = Connecting;
|
||||
const factory ConnectionStatus.connected([
|
||||
ConnectivityFailure? disconnectFailure,
|
||||
]) = Connected;
|
||||
const factory ConnectionStatus.connected() = Connected;
|
||||
const factory ConnectionStatus.disconnecting() = Disconnecting;
|
||||
|
||||
factory ConnectionStatus.fromBool(bool connected) {
|
||||
return connected
|
||||
? const ConnectionStatus.connected()
|
||||
: const Disconnected();
|
||||
}
|
||||
|
||||
bool get isConnected => switch (this) {
|
||||
Connected() => true,
|
||||
_ => false,
|
||||
};
|
||||
bool get isConnected => switch (this) { Connected() => true, _ => false };
|
||||
|
||||
bool get isSwitching => switch (this) {
|
||||
Connecting() => true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export 'connection_facade.dart';
|
||||
export 'connection_failure.dart';
|
||||
export 'connection_status.dart';
|
||||
export 'connectivity_failure.dart';
|
||||
export 'network_prefs.dart';
|
||||
export 'traffic.dart';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
abstract class Constants {
|
||||
static const localHost = '127.0.0.1';
|
||||
static const clashFolderName = "clash";
|
||||
static const delayTestUrl = "https://www.google.com";
|
||||
static const configFileName = "config";
|
||||
static const countryMMDBFileName = "Country";
|
||||
static const geoipFileName = "geoip.db";
|
||||
static const geositeFileName = "geosite.db";
|
||||
static const configsFolderName = "configs";
|
||||
static const localHost = "127.0.0.1";
|
||||
static const githubUrl = "https://github.com/hiddify/hiddify-next";
|
||||
static const githubReleasesApiUrl =
|
||||
"https://api.github.com/repos/hiddify/hiddify-next/releases";
|
||||
@@ -11,3 +10,9 @@ abstract class Constants {
|
||||
"https://github.com/hiddify/hiddify-next/releases/latest";
|
||||
static const telegramChannelUrl = "https://t.me/hiddify";
|
||||
}
|
||||
|
||||
abstract class Defaults {
|
||||
static const clashApiPort = 9090;
|
||||
static const mixedPort = 2334;
|
||||
static const delayTestUrl = "https://www.gstatic.com/generate_204";
|
||||
}
|
||||
|
||||
6
lib/domain/core_facade.dart
Normal file
6
lib/domain/core_facade.dart
Normal 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 {}
|
||||
60
lib/domain/core_service_failure.dart
Normal file
60
lib/domain/core_service_failure.dart
Normal 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 ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
|
||||
|
||||
const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
|
||||
|
||||
const factory ProfileFailure.invalidConfig() = ProfileInvalidConfigFailure;
|
||||
const factory ProfileFailure.invalidConfig([String? message]) =
|
||||
ProfileInvalidConfigFailure;
|
||||
|
||||
@override
|
||||
String present(TranslationsEn t) {
|
||||
|
||||
1
lib/domain/singbox/singbox.dart
Normal file
1
lib/domain/singbox/singbox.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'singbox_facade.dart';
|
||||
16
lib/domain/singbox/singbox_facade.dart
Normal file
16
lib/domain/singbox/singbox_facade.dart
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/features/common/clash/clash_controller.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -11,13 +11,16 @@ part 'clash_mode.g.dart';
|
||||
class ClashMode extends _$ClashMode with AppLogger {
|
||||
@override
|
||||
Future<TunnelMode?> build() async {
|
||||
final clash = ref.watch(clashFacadeProvider);
|
||||
await ref.watch(clashControllerProvider.future);
|
||||
final clash = ref.watch(coreFacadeProvider);
|
||||
if (!await ref.watch(serviceRunningProvider.future)) {
|
||||
return null;
|
||||
}
|
||||
ref.watch(prefsControllerProvider.select((value) => value.clash.mode));
|
||||
return clash
|
||||
.getConfigs()
|
||||
.map((r) => r.mode)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
return clash.getConfigs().map((r) => r.mode).getOrElse(
|
||||
(l) {
|
||||
loggy.warning("fetching clash mode: $l");
|
||||
throw l;
|
||||
},
|
||||
).run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hiddify/features/common/clash/clash_controller.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/features/common/window/window_controller.dart';
|
||||
import 'package:hiddify/features/logs/notifier/notifier.dart';
|
||||
import 'package:hiddify/features/system_tray/controller/system_tray_controller.dart';
|
||||
import 'package:hiddify/utils/platform_utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -12,9 +12,8 @@ part 'common_controllers.g.dart';
|
||||
@Riverpod(keepAlive: true)
|
||||
void commonControllers(CommonControllersRef ref) {
|
||||
ref.listen(
|
||||
clashControllerProvider,
|
||||
logsNotifierProvider,
|
||||
(previous, next) {},
|
||||
fireImmediately: true,
|
||||
);
|
||||
ref.listen(
|
||||
connectivityControllerProvider,
|
||||
|
||||
@@ -1,62 +1,90 @@
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/services/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:hiddify/domain/core_facade.dart';
|
||||
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'connectivity_controller.g.dart';
|
||||
|
||||
// TODO: test and improve
|
||||
// TODO: abort connection on clash error
|
||||
@Riverpod(keepAlive: true)
|
||||
class ConnectivityController extends _$ConnectivityController with AppLogger {
|
||||
@override
|
||||
ConnectionStatus build() {
|
||||
state = const Disconnected();
|
||||
final connection = _connectivity
|
||||
.watchConnectionStatus()
|
||||
.map(ConnectionStatus.fromBool)
|
||||
.listen((event) => state = event);
|
||||
|
||||
// currently changes wont take effect while connected
|
||||
Stream<ConnectionStatus> build() {
|
||||
ref.listen(
|
||||
prefsControllerProvider.select((value) => value.network),
|
||||
(_, next) => _networkPrefs = next,
|
||||
fireImmediately: true,
|
||||
activeProfileProvider.select((value) => value.asData?.value),
|
||||
(previous, next) async {
|
||||
if (previous == null) return;
|
||||
final shouldReconnect = previous != next;
|
||||
if (shouldReconnect) {
|
||||
loggy.debug("active profile modified, reconnect");
|
||||
await reconnect();
|
||||
}
|
||||
},
|
||||
);
|
||||
ref.listen(
|
||||
prefsControllerProvider
|
||||
.select((value) => (value.clash.httpPort!, value.clash.socksPort!)),
|
||||
(_, next) => _ports = (http: next.$1, socks: next.$2),
|
||||
fireImmediately: true,
|
||||
);
|
||||
|
||||
ref.onDispose(connection.cancel);
|
||||
return state;
|
||||
return _connectivity.watchConnectionStatus();
|
||||
}
|
||||
|
||||
ConnectivityService get _connectivity =>
|
||||
ref.watch(connectivityServiceProvider);
|
||||
|
||||
late ({int http, int socks}) _ports;
|
||||
// ignore: unused_field
|
||||
late NetworkPrefs _networkPrefs;
|
||||
CoreFacade get _connectivity => ref.watch(coreFacadeProvider);
|
||||
|
||||
Future<void> toggleConnection() async {
|
||||
switch (state) {
|
||||
case Disconnected():
|
||||
if (!await _connectivity.grantVpnPermission()) {
|
||||
state = const Disconnected(ConnectivityFailure.unexpected());
|
||||
return;
|
||||
}
|
||||
await _connectivity.connect(
|
||||
httpPort: _ports.http,
|
||||
socksPort: _ports.socks,
|
||||
);
|
||||
case Connected():
|
||||
await _connectivity.disconnect();
|
||||
default:
|
||||
if (state case AsyncError()) {
|
||||
await _connect();
|
||||
} else if (state case AsyncData(:final value)) {
|
||||
switch (value) {
|
||||
case Disconnected():
|
||||
await _connect();
|
||||
case Connected():
|
||||
await _disconnect();
|
||||
default:
|
||||
loggy.warning("switching status, debounce");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconnect() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
if (value case Connected()) {
|
||||
loggy.debug("reconnecting");
|
||||
await _disconnect();
|
||||
await _connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> abortConnection() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
switch (value) {
|
||||
case Connected() || Connecting():
|
||||
loggy.debug("aborting connection");
|
||||
await _disconnect();
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connect() async {
|
||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||
await _connectivity
|
||||
.changeConfig(activeProfile!.id)
|
||||
.andThen(_connectivity.connect)
|
||||
.mapLeft((l) {
|
||||
loggy.warning("error connecting: $l");
|
||||
state = AsyncError(l, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
|
||||
Future<void> _disconnect() async {
|
||||
await _connectivity.disconnect().mapLeft((l) {
|
||||
loggy.warning("error disconnecting: $l");
|
||||
state = AsyncError(l, StackTrace.current);
|
||||
}).run();
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<bool> serviceRunning(ServiceRunningRef ref) => ref
|
||||
.watch(
|
||||
connectivityControllerProvider.selectAsync((data) => data.isConnected),
|
||||
)
|
||||
.onError((error, stackTrace) => false);
|
||||
|
||||
@@ -22,73 +22,85 @@ class TrafficChart extends HookConsumerWidget {
|
||||
|
||||
switch (asyncTraffics) {
|
||||
case AsyncData(value: final traffics):
|
||||
final latest =
|
||||
traffics.lastOrNull ?? const Traffic(upload: 0, download: 0);
|
||||
final latestUploadData = formatByteSpeed(latest.upload);
|
||||
final latestDownloadData = formatByteSpeed(latest.download);
|
||||
|
||||
final uploadChartSpots = traffics.takeLast(chartSteps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
|
||||
);
|
||||
final downloadChartSpots = traffics.takeLast(chartSteps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
// mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 68,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
lineTouchData: const LineTouchData(enabled: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: uploadChartSpots.toList(),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: downloadChartSpots.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↑"),
|
||||
Text(latestUploadData.size),
|
||||
Text(latestUploadData.unit),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↓"),
|
||||
Text(latestDownloadData.size),
|
||||
Text(latestDownloadData.unit),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
// TODO: handle loading and error
|
||||
return _Chart(traffics, chartSteps);
|
||||
case AsyncLoading(:final value):
|
||||
if (value == null) return const SizedBox();
|
||||
return _Chart(value, chartSteps);
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Chart extends StatelessWidget {
|
||||
const _Chart(this.records, this.steps);
|
||||
|
||||
final List<Traffic> records;
|
||||
final int steps;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final latest = records.lastOrNull ?? const Traffic(upload: 0, download: 0);
|
||||
final latestUploadData = formatByteSpeed(latest.upload);
|
||||
final latestDownloadData = formatByteSpeed(latest.download);
|
||||
|
||||
final uploadChartSpots = records.takeLast(steps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.upload.toDouble()),
|
||||
);
|
||||
final downloadChartSpots = records.takeLast(steps).mapIndexed(
|
||||
(index, p) => FlSpot(index.toDouble(), p.download.toDouble()),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 68,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
borderData: FlBorderData(show: false),
|
||||
titlesData: const FlTitlesData(show: false),
|
||||
gridData: const FlGridData(show: false),
|
||||
lineTouchData: const LineTouchData(enabled: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: uploadChartSpots.toList(),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
spots: downloadChartSpots.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
duration: Duration.zero,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↑"),
|
||||
Text(latestUploadData.size),
|
||||
Text(latestUploadData.unit),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Text("↓"),
|
||||
Text(latestDownloadData.size),
|
||||
Text(latestDownloadData.unit),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -13,28 +14,37 @@ class TrafficNotifier extends _$TrafficNotifier with AppLogger {
|
||||
int get _steps => 100;
|
||||
|
||||
@override
|
||||
Stream<List<Traffic>> build() {
|
||||
return Stream.periodic(const Duration(seconds: 1)).asyncMap(
|
||||
(_) async {
|
||||
return ref.read(clashFacadeProvider).getTraffic().match(
|
||||
(f) {
|
||||
loggy.warning('failed to watch clash traffic: $f');
|
||||
return const ClashTraffic(upload: 0, download: 0);
|
||||
},
|
||||
(traffic) => traffic,
|
||||
).run();
|
||||
},
|
||||
).map(
|
||||
(event) => switch (state) {
|
||||
AsyncData(:final value) => [
|
||||
...value.takeLast(_steps - 1),
|
||||
Traffic(upload: event.upload, download: event.download),
|
||||
],
|
||||
_ => List.generate(
|
||||
_steps,
|
||||
(index) => const Traffic(upload: 0, download: 0),
|
||||
)
|
||||
},
|
||||
);
|
||||
Stream<List<Traffic>> build() async* {
|
||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||
if (serviceRunning) {
|
||||
yield* ref.watch(coreFacadeProvider).watchTraffic().map(
|
||||
(event) => _mapToState(
|
||||
event
|
||||
.getOrElse((_) => const ClashTraffic(upload: 0, download: 0)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
yield* Stream.periodic(const Duration(seconds: 1)).asyncMap(
|
||||
(_) async {
|
||||
return const ClashTraffic(upload: 0, download: 0);
|
||||
},
|
||||
).map(_mapToState);
|
||||
}
|
||||
}
|
||||
|
||||
List<Traffic> _mapToState(ClashTraffic event) {
|
||||
final previous = state.valueOrNull ??
|
||||
List.generate(
|
||||
_steps,
|
||||
(index) => const Traffic(upload: 0, download: 0),
|
||||
);
|
||||
while (previous.length < _steps) {
|
||||
loggy.debug("previous short, adding");
|
||||
previous.insert(0, const Traffic(upload: 0, download: 0));
|
||||
}
|
||||
return [
|
||||
...previous.takeLast(_steps - 1),
|
||||
Traffic(upload: event.upload, download: event.download),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,12 @@ class WindowController extends _$WindowController
|
||||
await windowManager.close();
|
||||
}
|
||||
|
||||
Future<void> quit() async {
|
||||
loggy.debug("quitting");
|
||||
await windowManager.close();
|
||||
await windowManager.destroy();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onWindowClose() async {
|
||||
await windowManager.hide();
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:hiddify/core/router/router.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
|
||||
import 'package:hiddify/features/common/active_profile/has_any_profile_notifier.dart';
|
||||
import 'package:hiddify/features/common/clash/clash_controller.dart';
|
||||
import 'package:hiddify/features/common/common.dart';
|
||||
import 'package:hiddify/features/home/widgets/widgets.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -22,18 +21,6 @@ class HomePage extends HookConsumerWidget {
|
||||
final hasAnyProfile = ref.watch(hasAnyProfileProvider);
|
||||
final activeProfile = ref.watch(activeProfileProvider);
|
||||
|
||||
ref.listen(
|
||||
clashControllerProvider,
|
||||
(_, next) {
|
||||
if (next case AsyncError(:final error)) {
|
||||
CustomToast.error(
|
||||
t.presentError(error),
|
||||
duration: const Duration(seconds: 10),
|
||||
).show(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
|
||||
@@ -3,8 +3,11 @@ import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/theme/theme.dart';
|
||||
import 'package:hiddify/domain/connectivity/connectivity.dart';
|
||||
import 'package:hiddify/domain/failures.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/gen/assets.gen.dart';
|
||||
import 'package:hiddify/utils/alerts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
@@ -17,12 +20,71 @@ class ConnectionButton extends HookConsumerWidget {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final connectionStatus = ref.watch(connectivityControllerProvider);
|
||||
|
||||
final Color connectionLogoColor = connectionStatus.isConnected
|
||||
? ConnectionButtonColor.connected
|
||||
: ConnectionButtonColor.disconnected;
|
||||
ref.listen(
|
||||
connectivityControllerProvider,
|
||||
(_, next) {
|
||||
if (next case AsyncError(:final error)) {
|
||||
CustomToast.error(t.presentError(error)).show(context);
|
||||
}
|
||||
if (next
|
||||
case AsyncData(value: Disconnected(:final connectionFailure?))) {
|
||||
CustomAlertDialog(
|
||||
message: connectionFailure.present(t),
|
||||
).show(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final bool intractable = !connectionStatus.isSwitching;
|
||||
switch (connectionStatus) {
|
||||
case AsyncData(value: final status):
|
||||
final Color connectionLogoColor = status.isConnected
|
||||
? ConnectionButtonColor.connected
|
||||
: ConnectionButtonColor.disconnected;
|
||||
|
||||
return _ConnectionButton(
|
||||
onTap: () => ref
|
||||
.read(connectivityControllerProvider.notifier)
|
||||
.toggleConnection(),
|
||||
enabled: !status.isSwitching,
|
||||
label: status.present(t),
|
||||
buttonColor: connectionLogoColor,
|
||||
);
|
||||
case AsyncError():
|
||||
return _ConnectionButton(
|
||||
onTap: () => ref
|
||||
.read(connectivityControllerProvider.notifier)
|
||||
.toggleConnection(),
|
||||
enabled: true,
|
||||
label: const Disconnected().present(t),
|
||||
buttonColor: ConnectionButtonColor.disconnected,
|
||||
);
|
||||
default:
|
||||
// HACK
|
||||
return _ConnectionButton(
|
||||
onTap: () {},
|
||||
enabled: false,
|
||||
label: "",
|
||||
buttonColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ConnectionButton extends StatelessWidget {
|
||||
const _ConnectionButton({
|
||||
required this.onTap,
|
||||
required this.enabled,
|
||||
required this.label,
|
||||
required this.buttonColor,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final bool enabled;
|
||||
final String label;
|
||||
final Color buttonColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -33,7 +95,7 @@ class ConnectionButton extends HookConsumerWidget {
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 16,
|
||||
color: connectionLogoColor.withOpacity(0.5),
|
||||
color: buttonColor.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -43,26 +105,24 @@ class ConnectionButton extends HookConsumerWidget {
|
||||
shape: const CircleBorder(),
|
||||
color: Colors.white,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(connectivityControllerProvider.notifier)
|
||||
.toggleConnection();
|
||||
},
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(36),
|
||||
child: Assets.images.logo.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
connectionLogoColor,
|
||||
buttonColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).animate(target: intractable ? 0 : 1).blurXY(end: 1),
|
||||
).animate(target: intractable ? 0 : 1).scaleXY(end: .88),
|
||||
).animate(target: enabled ? 0 : 1).blurXY(end: 1),
|
||||
)
|
||||
.animate(target: enabled ? 0 : 1)
|
||||
.scaleXY(end: .88, curve: Curves.easeIn),
|
||||
const Gap(16),
|
||||
Text(
|
||||
connectionStatus.present(t).sentenceCase,
|
||||
label.sentenceCase,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -10,14 +10,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'logs_notifier.g.dart';
|
||||
|
||||
// TODO: rewrite
|
||||
@riverpod
|
||||
@Riverpod(keepAlive: true)
|
||||
class LogsNotifier extends _$LogsNotifier with AppLogger {
|
||||
static const maxLength = 1000;
|
||||
|
||||
@override
|
||||
Stream<LogsState> build() {
|
||||
state = const AsyncData(LogsState());
|
||||
return ref.read(clashFacadeProvider).watchLogs().asyncMap(
|
||||
return ref.read(coreFacadeProvider).watchLogs().asyncMap(
|
||||
(event) async {
|
||||
_logs = [
|
||||
event.getOrElse((l) => throw l),
|
||||
@@ -32,16 +32,15 @@ class LogsNotifier extends _$LogsNotifier with AppLogger {
|
||||
);
|
||||
}
|
||||
|
||||
var _logs = <ClashLog>[];
|
||||
var _logs = <String>[];
|
||||
final _debouncer = CallbackDebouncer(const Duration(milliseconds: 200));
|
||||
LogLevel? _levelFilter;
|
||||
String _filter = "";
|
||||
|
||||
Future<List<ClashLog>> _computeLogs() async {
|
||||
Future<List<String>> _computeLogs() async {
|
||||
if (_levelFilter == null && _filter.isEmpty) return _logs;
|
||||
return _logs.where((e) {
|
||||
return (_filter.isEmpty || e.message.contains(_filter)) &&
|
||||
(_levelFilter == null || e.level == _levelFilter);
|
||||
return _filter.isEmpty || e.contains(_filter);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class LogsState with _$LogsState {
|
||||
const LogsState._();
|
||||
|
||||
const factory LogsState({
|
||||
@Default([]) List<ClashLog> logs,
|
||||
@Default([]) List<String> logs,
|
||||
@Default("") String filter,
|
||||
LogLevel? levelFilter,
|
||||
}) = _LogsState;
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:hiddify/features/logs/notifier/notifier.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:tint/tint.dart';
|
||||
|
||||
class LogsPage extends HookConsumerWidget {
|
||||
const LogsPage({super.key});
|
||||
@@ -80,19 +81,7 @@ class LogsPage extends HookConsumerWidget {
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: log.timeStamp),
|
||||
const TextSpan(text: " "),
|
||||
TextSpan(
|
||||
text: log.level.name.toUpperCase(),
|
||||
style: TextStyle(color: log.level.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Text(log.message),
|
||||
subtitle: Text(log.strip()),
|
||||
),
|
||||
if (index != 0)
|
||||
const Divider(
|
||||
|
||||
@@ -24,7 +24,7 @@ class GroupWithProxies with _$GroupWithProxies {
|
||||
final result = <GroupWithProxies>[];
|
||||
for (final proxy in proxies) {
|
||||
if (proxy is ClashProxyGroup) {
|
||||
if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue;
|
||||
// if (mode != TunnelMode.global && proxy.name == "GLOBAL") continue;
|
||||
final current = <ClashProxy>[];
|
||||
for (final name in proxy.all) {
|
||||
current.addAll(proxies.where((e) => e.name == name).toList());
|
||||
|
||||
@@ -32,7 +32,7 @@ class ProxiesDelayNotifier extends _$ProxiesDelayNotifier with AppLogger {
|
||||
return {};
|
||||
}
|
||||
|
||||
ClashFacade get _clash => ref.read(clashFacadeProvider);
|
||||
ClashFacade get _clash => ref.read(coreFacadeProvider);
|
||||
StreamSubscription? _currentTest;
|
||||
|
||||
Future<void> testDelay(Iterable<String> proxies) async {
|
||||
|
||||
@@ -3,8 +3,9 @@ import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/data/data_providers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/features/common/clash/clash_controller.dart';
|
||||
import 'package:hiddify/domain/core_service_failure.dart';
|
||||
import 'package:hiddify/features/common/clash/clash_mode.dart';
|
||||
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
|
||||
import 'package:hiddify/features/proxies/model/model.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -16,23 +17,23 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger {
|
||||
@override
|
||||
Future<List<GroupWithProxies>> build() async {
|
||||
loggy.debug('building');
|
||||
await ref.watch(clashControllerProvider.future);
|
||||
if (!await ref.watch(serviceRunningProvider.future)) {
|
||||
throw const CoreServiceNotRunning();
|
||||
}
|
||||
final mode = await ref.watch(clashModeProvider.future);
|
||||
return _clash
|
||||
.getProxies()
|
||||
.flatMap(
|
||||
(proxies) {
|
||||
return TaskEither(
|
||||
() async =>
|
||||
right(await GroupWithProxies.fromProxies(proxies, mode)),
|
||||
);
|
||||
},
|
||||
)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
return _clash.getProxies().flatMap(
|
||||
(proxies) {
|
||||
return TaskEither(
|
||||
() async => right(await GroupWithProxies.fromProxies(proxies, mode)),
|
||||
);
|
||||
},
|
||||
).getOrElse((l) {
|
||||
loggy.warning("failed receiving proxies: $l");
|
||||
throw l;
|
||||
}).run();
|
||||
}
|
||||
|
||||
ClashFacade get _clash => ref.read(clashFacadeProvider);
|
||||
ClashFacade get _clash => ref.read(coreFacadeProvider);
|
||||
|
||||
Future<void> changeProxy(String selectorName, String proxyName) async {
|
||||
loggy.debug("changing proxy, selector: $selectorName - proxy: $proxyName ");
|
||||
|
||||
@@ -20,7 +20,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
|
||||
|
||||
final notifier = ref.watch(proxiesNotifierProvider.notifier);
|
||||
final asyncProxies = ref.watch(proxiesNotifierProvider);
|
||||
final proxies = asyncProxies.value ?? [];
|
||||
final proxies = asyncProxies.asData?.value ?? [];
|
||||
final delays = ref.watch(proxiesDelayNotifierProvider);
|
||||
|
||||
final selectActiveProxyMutation = useMutation(
|
||||
@@ -163,7 +163,10 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
|
||||
NestedTabAppBar(
|
||||
title: Text(t.proxies.pageTitle.titleCase),
|
||||
),
|
||||
SliverErrorBodyPlaceholder(t.presentError(error)),
|
||||
SliverErrorBodyPlaceholder(
|
||||
t.presentError(error),
|
||||
icon: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -19,15 +19,61 @@ class ProxyTile extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
proxy.name,
|
||||
switch (proxy) {
|
||||
ClashProxyGroup(:final name) => name.toUpperCase(),
|
||||
ClashProxyItem(:final name) => name,
|
||||
},
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(proxy.type.label),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Container(
|
||||
width: 6,
|
||||
height: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: selected ? theme.colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: proxy.type.label),
|
||||
if (proxy.udp)
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.tertiaryContainer,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
" UDP ",
|
||||
style: TextStyle(
|
||||
fontSize: theme.textTheme.labelSmall?.fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (proxy case ClashProxyGroup(:final now)) ...[
|
||||
TextSpan(text: " ($now)"),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: delay != null ? Text(delay.toString()) : null,
|
||||
selected: selected,
|
||||
onTap: onSelect,
|
||||
horizontalTitleGap: 4,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:hiddify/core/core_providers.dart';
|
||||
// import 'package:hiddify/core/prefs/prefs.dart';
|
||||
// import 'package:hiddify/domain/clash/clash.dart';
|
||||
// import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
// import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
// import 'package:recase/recase.dart';
|
||||
|
||||
class ClashOverridesPage extends HookConsumerWidget {
|
||||
const ClashOverridesPage({super.key});
|
||||
// class ClashOverridesPage extends HookConsumerWidget {
|
||||
// const ClashOverridesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
// @override
|
||||
// Widget build(BuildContext context, WidgetRef ref) {
|
||||
// final t = ref.watch(translationsProvider);
|
||||
|
||||
final overrides =
|
||||
ref.watch(prefsControllerProvider.select((value) => value.clash));
|
||||
final notifier = ref.watch(prefsControllerProvider.notifier);
|
||||
// final overrides =
|
||||
// ref.watch(prefsControllerProvider.select((value) => value.clash));
|
||||
// final notifier = ref.watch(prefsControllerProvider.notifier);
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
title: Text(t.settings.clash.sectionTitle.titleCase),
|
||||
pinned: true,
|
||||
),
|
||||
SliverList.list(
|
||||
children: [
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.httpPort,
|
||||
value: overrides.httpPort,
|
||||
resetValue: ClashConfig.initial.httpPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(httpPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.socksPort,
|
||||
value: overrides.socksPort,
|
||||
resetValue: ClashConfig.initial.socksPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(socksPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.redirPort,
|
||||
value: overrides.redirPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(redirPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.tproxyPort,
|
||||
value: overrides.tproxyPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(tproxyPort: value),
|
||||
),
|
||||
),
|
||||
InputOverrideTile(
|
||||
title: t.settings.clash.overrides.mixedPort,
|
||||
value: overrides.mixedPort,
|
||||
resetValue: ClashConfig.initial.mixedPort,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(mixedPort: value),
|
||||
),
|
||||
),
|
||||
ToggleOverrideTile(
|
||||
title: t.settings.clash.overrides.allowLan,
|
||||
value: overrides.allowLan,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(allowLan: value),
|
||||
),
|
||||
),
|
||||
ToggleOverrideTile(
|
||||
title: t.settings.clash.overrides.ipv6,
|
||||
value: overrides.ipv6,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(ipv6: value),
|
||||
),
|
||||
),
|
||||
ChoiceOverrideTile(
|
||||
title: t.settings.clash.overrides.mode,
|
||||
value: overrides.mode,
|
||||
options: TunnelMode.values,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(mode: value),
|
||||
),
|
||||
),
|
||||
ChoiceOverrideTile(
|
||||
title: t.settings.clash.overrides.logLevel,
|
||||
value: overrides.logLevel,
|
||||
options: LogLevel.values,
|
||||
onChange: (value) => notifier.patchClashOverrides(
|
||||
ClashConfigPatch(logLevel: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// return Scaffold(
|
||||
// body: CustomScrollView(
|
||||
// slivers: [
|
||||
// SliverAppBar(
|
||||
// title: Text(t.settings.clash.sectionTitle.titleCase),
|
||||
// pinned: true,
|
||||
// ),
|
||||
// SliverList.list(
|
||||
// children: [
|
||||
// InputOverrideTile(
|
||||
// title: t.settings.clash.overrides.httpPort,
|
||||
// value: overrides.httpPort,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(httpPort: value),
|
||||
// ),
|
||||
// ),
|
||||
// InputOverrideTile(
|
||||
// title: t.settings.clash.overrides.socksPort,
|
||||
// value: overrides.socksPort,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(socksPort: value),
|
||||
// ),
|
||||
// ),
|
||||
// InputOverrideTile(
|
||||
// title: t.settings.clash.overrides.redirPort,
|
||||
// value: overrides.redirPort,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(redirPort: value),
|
||||
// ),
|
||||
// ),
|
||||
// InputOverrideTile(
|
||||
// title: t.settings.clash.overrides.tproxyPort,
|
||||
// value: overrides.tproxyPort,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(tproxyPort: value),
|
||||
// ),
|
||||
// ),
|
||||
// InputOverrideTile(
|
||||
// title: t.settings.clash.overrides.mixedPort,
|
||||
// value: overrides.mixedPort,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(mixedPort: value),
|
||||
// ),
|
||||
// ),
|
||||
// ToggleOverrideTile(
|
||||
// title: t.settings.clash.overrides.allowLan,
|
||||
// value: overrides.allowLan,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(allowLan: value),
|
||||
// ),
|
||||
// ),
|
||||
// ToggleOverrideTile(
|
||||
// title: t.settings.clash.overrides.ipv6,
|
||||
// value: overrides.ipv6,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(ipv6: value),
|
||||
// ),
|
||||
// ),
|
||||
// ChoiceOverrideTile(
|
||||
// title: t.settings.clash.overrides.mode,
|
||||
// value: overrides.mode,
|
||||
// options: TunnelMode.values,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(mode: value),
|
||||
// ),
|
||||
// ),
|
||||
// ChoiceOverrideTile(
|
||||
// title: t.settings.clash.overrides.logLevel,
|
||||
// value: overrides.logLevel,
|
||||
// options: LogLevel.values,
|
||||
// onChange: (value) => notifier.patchClashOverrides(
|
||||
// ClashConfigPatch(logLevel: value),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/router/router.dart';
|
||||
import 'package:hiddify/features/settings/widgets/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
@@ -13,7 +12,7 @@ class SettingsPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
const divider = Divider(indent: 16, endIndent: 16);
|
||||
// const divider = Divider(indent: 16, endIndent: 16);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -29,18 +28,18 @@ class SettingsPage extends HookConsumerWidget {
|
||||
t.settings.general.sectionTitle.titleCase,
|
||||
),
|
||||
const AppearanceSettingTiles(),
|
||||
divider,
|
||||
_SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
|
||||
const NetworkSettingTiles(),
|
||||
divider,
|
||||
ListTile(
|
||||
title: Text(t.settings.clash.sectionTitle.titleCase),
|
||||
leading: const Icon(Icons.edit_document),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onTap: () async {
|
||||
await const ClashOverridesRoute().push(context);
|
||||
},
|
||||
),
|
||||
// divider,
|
||||
// _SettingsSectionHeader(t.settings.network.sectionTitle.titleCase),
|
||||
// const NetworkSettingTiles(),
|
||||
// divider,
|
||||
// ListTile(
|
||||
// title: Text(t.settings.clash.sectionTitle.titleCase),
|
||||
// leading: const Icon(Icons.edit_document),
|
||||
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
// onTap: () async {
|
||||
// await const ClashOverridesRoute().push(context);
|
||||
// },
|
||||
// ),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:hiddify/core/core_providers.dart';
|
||||
// import 'package:hiddify/core/prefs/prefs.dart';
|
||||
// import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
// import 'package:recase/recase.dart';
|
||||
|
||||
class NetworkSettingTiles extends HookConsumerWidget {
|
||||
const NetworkSettingTiles({super.key});
|
||||
// class NetworkSettingTiles extends HookConsumerWidget {
|
||||
// const NetworkSettingTiles({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
// @override
|
||||
// Widget build(BuildContext context, WidgetRef ref) {
|
||||
// final t = ref.watch(translationsProvider);
|
||||
|
||||
final prefs =
|
||||
ref.watch(prefsControllerProvider.select((value) => value.network));
|
||||
final notifier = ref.watch(prefsControllerProvider.notifier);
|
||||
// final prefs =
|
||||
// ref.watch(prefsControllerProvider.select((value) => value.network));
|
||||
// final notifier = ref.watch(prefsControllerProvider.notifier);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.network.systemProxy.titleCase),
|
||||
subtitle: Text(t.settings.network.systemProxyMsg),
|
||||
value: prefs.systemProxy,
|
||||
onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
|
||||
subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
|
||||
value: prefs.bypassPrivateNetworks,
|
||||
onChanged: (value) =>
|
||||
notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
// return Column(
|
||||
// children: [
|
||||
// SwitchListTile(
|
||||
// title: Text(t.settings.network.systemProxy.titleCase),
|
||||
// subtitle: Text(t.settings.network.systemProxyMsg),
|
||||
// value: prefs.systemProxy,
|
||||
// onChanged: (value) => notifier.patchNetworkPrefs(systemProxy: value),
|
||||
// ),
|
||||
// SwitchListTile(
|
||||
// title: Text(t.settings.network.bypassPrivateNetworks.titleCase),
|
||||
// subtitle: Text(t.settings.network.bypassPrivateNetworksMsg),
|
||||
// value: prefs.bypassPrivateNetworks,
|
||||
// onChanged: (value) =>
|
||||
// notifier.patchNetworkPrefs(bypassPrivateNetworks: value),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/core_providers.dart';
|
||||
import 'package:hiddify/core/prefs/prefs.dart';
|
||||
@@ -27,7 +25,7 @@ class SystemTrayController extends _$SystemTrayController
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
final connection = ref.watch(connectivityControllerProvider);
|
||||
final connection = await ref.watch(connectivityControllerProvider.future);
|
||||
final mode =
|
||||
ref.watch(clashModeProvider.select((value) => value.valueOrNull));
|
||||
|
||||
@@ -104,8 +102,9 @@ class SystemTrayController extends _$SystemTrayController
|
||||
return ref.read(connectivityControllerProvider.notifier).toggleConnection();
|
||||
}
|
||||
|
||||
// TODO rewrite
|
||||
Future<void> handleClickExitApp(MenuItem menuItem) async {
|
||||
exit(0);
|
||||
await ref.read(connectivityControllerProvider.notifier).abortConnection();
|
||||
await trayManager.destroy();
|
||||
return ref.read(windowControllerProvider.notifier).quit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
// ignore_for_file: type=lint
|
||||
import 'dart:ffi' as ffi;
|
||||
|
||||
/// Bindings to Clash
|
||||
class ClashNativeLibrary {
|
||||
/// Bindings to Singbox
|
||||
class SingboxNativeLibrary {
|
||||
/// Holds the symbol lookup function.
|
||||
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||
_lookup;
|
||||
|
||||
/// The symbols are looked up in [dynamicLibrary].
|
||||
ClashNativeLibrary(ffi.DynamicLibrary dynamicLibrary)
|
||||
SingboxNativeLibrary(ffi.DynamicLibrary dynamicLibrary)
|
||||
: _lookup = dynamicLibrary.lookup;
|
||||
|
||||
/// The symbols are looked up with [lookup].
|
||||
ClashNativeLibrary.fromLookup(
|
||||
SingboxNativeLibrary.fromLookup(
|
||||
ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||
lookup)
|
||||
: _lookup = lookup;
|
||||
@@ -857,206 +857,69 @@ class ClashNativeLibrary {
|
||||
late final __FCmulcr =
|
||||
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
|
||||
|
||||
void getConfigs(
|
||||
int port,
|
||||
void setup(
|
||||
ffi.Pointer<ffi.Char> baseDir,
|
||||
ffi.Pointer<ffi.Char> workingDir,
|
||||
ffi.Pointer<ffi.Char> tempDir,
|
||||
) {
|
||||
return _getConfigs(
|
||||
port,
|
||||
return _setup(
|
||||
baseDir,
|
||||
workingDir,
|
||||
tempDir,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getConfigsPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'getConfigs');
|
||||
late final _getConfigs = _getConfigsPtr.asFunction<void Function(int)>();
|
||||
|
||||
void patchConfigs(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> patchStr,
|
||||
) {
|
||||
return _patchConfigs(
|
||||
port,
|
||||
patchStr,
|
||||
);
|
||||
}
|
||||
|
||||
late final _patchConfigsPtr = _lookup<
|
||||
late final _setupPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.LongLong, ffi.Pointer<ffi.Char>)>>('patchConfigs');
|
||||
late final _patchConfigs =
|
||||
_patchConfigsPtr.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
|
||||
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>)>>('setup');
|
||||
late final _setup = _setupPtr.asFunction<
|
||||
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void updateConfigs(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> pathStr,
|
||||
int force,
|
||||
) {
|
||||
return _updateConfigs(
|
||||
port,
|
||||
pathStr,
|
||||
force,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateConfigsPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.LongLong, ffi.Pointer<ffi.Char>, GoUint8)>>('updateConfigs');
|
||||
late final _updateConfigs = _updateConfigsPtr
|
||||
.asFunction<void Function(int, ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void validateConfig(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> parse(
|
||||
ffi.Pointer<ffi.Char> path,
|
||||
) {
|
||||
return _validateConfig(
|
||||
port,
|
||||
return _parse(
|
||||
path,
|
||||
);
|
||||
}
|
||||
|
||||
late final _validateConfigPtr = _lookup<
|
||||
late final _parsePtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.LongLong, ffi.Pointer<ffi.Char>)>>('validateConfig');
|
||||
late final _validateConfig = _validateConfigPtr
|
||||
.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('parse');
|
||||
late final _parse = _parsePtr
|
||||
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void initNativeDartBridge(
|
||||
ffi.Pointer<ffi.Void> api,
|
||||
) {
|
||||
return _initNativeDartBridge(
|
||||
api,
|
||||
);
|
||||
}
|
||||
|
||||
late final _initNativeDartBridgePtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'initNativeDartBridge');
|
||||
late final _initNativeDartBridge = _initNativeDartBridgePtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void setOptions(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> dir,
|
||||
ffi.Pointer<ffi.Char> create(
|
||||
ffi.Pointer<ffi.Char> configPath,
|
||||
) {
|
||||
return _setOptions(
|
||||
port,
|
||||
dir,
|
||||
return _create(
|
||||
configPath,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setOptionsPtr = _lookup<
|
||||
late final _createPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>)>>('setOptions');
|
||||
late final _setOptions = _setOptionsPtr.asFunction<
|
||||
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>();
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('create');
|
||||
late final _create = _createPtr
|
||||
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void start(
|
||||
int port,
|
||||
) {
|
||||
return _start(
|
||||
port,
|
||||
);
|
||||
ffi.Pointer<ffi.Char> start() {
|
||||
return _start();
|
||||
}
|
||||
|
||||
late final _startPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('start');
|
||||
late final _start = _startPtr.asFunction<void Function(int)>();
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('start');
|
||||
late final _start = _startPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
void startLog(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> levelStr,
|
||||
) {
|
||||
return _startLog(
|
||||
port,
|
||||
levelStr,
|
||||
);
|
||||
ffi.Pointer<ffi.Char> stop() {
|
||||
return _stop();
|
||||
}
|
||||
|
||||
late final _startLogPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>)>>('startLog');
|
||||
late final _startLog =
|
||||
_startLogPtr.asFunction<void Function(int, ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void stopLog() {
|
||||
return _stopLog();
|
||||
}
|
||||
|
||||
late final _stopLogPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
|
||||
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
|
||||
|
||||
void getProxies(
|
||||
int port,
|
||||
) {
|
||||
return _getProxies(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getProxiesPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'getProxies');
|
||||
late final _getProxies = _getProxiesPtr.asFunction<void Function(int)>();
|
||||
|
||||
void updateProxy(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> selectorName,
|
||||
ffi.Pointer<ffi.Char> proxyName,
|
||||
) {
|
||||
return _updateProxy(
|
||||
port,
|
||||
selectorName,
|
||||
proxyName,
|
||||
);
|
||||
}
|
||||
|
||||
late final _updateProxyPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>)>>('updateProxy');
|
||||
late final _updateProxy = _updateProxyPtr.asFunction<
|
||||
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void getProxyDelay(
|
||||
int port,
|
||||
ffi.Pointer<ffi.Char> name,
|
||||
ffi.Pointer<ffi.Char> url,
|
||||
int timeout,
|
||||
) {
|
||||
return _getProxyDelay(
|
||||
port,
|
||||
name,
|
||||
url,
|
||||
timeout,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getProxyDelayPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(ffi.LongLong, ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>, ffi.Long)>>('getProxyDelay');
|
||||
late final _getProxyDelay = _getProxyDelayPtr.asFunction<
|
||||
void Function(int, ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
|
||||
|
||||
void getTraffic(
|
||||
int port,
|
||||
) {
|
||||
return _getTraffic(
|
||||
port,
|
||||
);
|
||||
}
|
||||
|
||||
late final _getTrafficPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
|
||||
'getTraffic');
|
||||
late final _getTraffic = _getTrafficPtr.asFunction<void Function(int)>();
|
||||
late final _stopPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('stop');
|
||||
late final _stop = _stopPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
}
|
||||
|
||||
typedef va_list = ffi.Pointer<ffi.Char>;
|
||||
@@ -1136,7 +999,6 @@ final class GoSlice extends ffi.Struct {
|
||||
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
typedef GoUint8 = ffi.UnsignedChar;
|
||||
|
||||
const int _VCRT_COMPILER_PREPROCESSOR = 1;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user