Add android dynamic notification

This commit is contained in:
problematicconsumer
2023-12-14 14:50:10 +03:30
parent b9f1e83473
commit af64efec00
19 changed files with 251 additions and 95 deletions

View File

@@ -37,6 +37,7 @@ class Application : Application() {
val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! } val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }
val packageManager by lazy { application.packageManager } val packageManager by lazy { application.packageManager }
val powerManager by lazy { application.getSystemService<PowerManager>()!! } val powerManager by lazy { application.getSystemService<PowerManager>()!! }
val notificationManager by lazy { application.getSystemService<NotificationManager>()!! }
} }
} }

View File

@@ -2,12 +2,10 @@ package com.hiddify.hiddify
import android.util.Log import android.util.Log
import com.hiddify.hiddify.bg.BoxService import com.hiddify.hiddify.bg.BoxService
import com.hiddify.hiddify.constant.Alert
import com.hiddify.hiddify.constant.Status import com.hiddify.hiddify.constant.Status
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.StandardMethodCodec
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -15,7 +13,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin, class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
MethodChannel.MethodCallHandler { MethodChannel.MethodCallHandler {
private var channel: MethodChannel? = null private var channel: MethodChannel? = null
companion object { companion object {
@@ -37,8 +35,8 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel( channel = MethodChannel(
flutterPluginBinding.binaryMessenger, flutterPluginBinding.binaryMessenger,
channelName, channelName,
) )
channel!!.setMethodCallHandler(this) channel!!.setMethodCallHandler(this)
} }
@@ -92,6 +90,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
result.runCatching { result.runCatching {
val args = call.arguments as Map<*, *> val args = call.arguments as Map<*, *>
Settings.activeConfigPath = args["path"] as String? ?: "" Settings.activeConfigPath = args["path"] as String? ?: ""
Settings.activeProfileName = args["name"] as String? ?: ""
val mainActivity = MainActivity.instance val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started val started = mainActivity.serviceStatus.value == Status.Started
if (started) { if (started) {
@@ -124,6 +123,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
result.runCatching { result.runCatching {
val args = call.arguments as Map<*, *> val args = call.arguments as Map<*, *>
Settings.activeConfigPath = args["path"] as String? ?: "" Settings.activeConfigPath = args["path"] as String? ?: ""
Settings.activeProfileName = args["name"] as String? ?: ""
val mainActivity = MainActivity.instance val mainActivity = MainActivity.instance
val started = mainActivity.serviceStatus.value == Status.Started val started = mainActivity.serviceStatus.value == Status.Started
if (!started) return@launch success(true) if (!started) return@launch success(true)
@@ -150,10 +150,10 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
result.runCatching { result.runCatching {
val args = call.arguments as Map<*, *> val args = call.arguments as Map<*, *>
Libbox.newStandaloneCommandClient() Libbox.newStandaloneCommandClient()
.selectOutbound( .selectOutbound(
args["groupTag"] as String, args["groupTag"] as String,
args["outboundTag"] as String args["outboundTag"] as String
) )
success(true) success(true)
} }
} }
@@ -164,9 +164,9 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
result.runCatching { result.runCatching {
val args = call.arguments as Map<*, *> val args = call.arguments as Map<*, *>
Libbox.newStandaloneCommandClient() Libbox.newStandaloneCommandClient()
.urlTest( .urlTest(
args["groupTag"] as String args["groupTag"] as String
) )
success(true) success(true)
} }
} }

View File

@@ -51,6 +51,10 @@ object Settings {
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!! get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply() set(value) = preferences.edit().putString(SettingsKey.ACTIVE_CONFIG_PATH, value).apply()
var activeProfileName: String
get() = preferences.getString(SettingsKey.ACTIVE_PROFILE_NAME, "")!!
set(value) = preferences.edit().putString(SettingsKey.ACTIVE_PROFILE_NAME, value).apply()
var serviceMode: String var serviceMode: String
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)!! get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)!!
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply() set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
@@ -71,6 +75,11 @@ object Settings {
set(value) = set(value) =
preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply() preferences.edit().putBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, value).apply()
var dynamicNotification: Boolean
get() = preferences.getBoolean(SettingsKey.DYNAMIC_NOTIFICATION, true)
set(value) =
preferences.edit().putBoolean(SettingsKey.DYNAMIC_NOTIFICATION, value).apply()
var systemProxyEnabled: Boolean var systemProxyEnabled: Boolean
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true) get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
set(value) = set(value) =

View File

@@ -23,7 +23,6 @@ import io.nekohasekai.libbox.BoxService
import io.nekohasekai.libbox.CommandServer import io.nekohasekai.libbox.CommandServer
import io.nekohasekai.libbox.CommandServerHandler import io.nekohasekai.libbox.CommandServerHandler
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.PProfServer
import io.nekohasekai.libbox.PlatformInterface import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.SystemProxyStatus import io.nekohasekai.libbox.SystemProxyStatus
import io.nekohasekai.mobile.Mobile import io.nekohasekai.mobile.Mobile
@@ -36,8 +35,8 @@ import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class BoxService( class BoxService(
private val service: Service, private val service: Service,
private val platformInterface: PlatformInterface private val platformInterface: PlatformInterface
) : CommandServerHandler { ) : CommandServerHandler {
companion object { companion object {
@@ -72,8 +71,8 @@ class BoxService(
} }
} }
fun buildConfig(path: String, options: String):String { fun buildConfig(path: String, options: String): String {
return Mobile.buildConfig(path, options) return Mobile.buildConfig(path, options)
} }
fun start() { fun start() {
@@ -87,17 +86,17 @@ class BoxService(
fun stop() { fun stop() {
Application.application.sendBroadcast( Application.application.sendBroadcast(
Intent(Action.SERVICE_CLOSE).setPackage( Intent(Action.SERVICE_CLOSE).setPackage(
Application.application.packageName Application.application.packageName
) )
) )
} }
fun reload() { fun reload() {
Application.application.sendBroadcast( Application.application.sendBroadcast(
Intent(Action.SERVICE_RELOAD).setPackage( Intent(Action.SERVICE_RELOAD).setPackage(
Application.application.packageName Application.application.packageName
) )
) )
} }
} }
@@ -106,10 +105,9 @@ class BoxService(
private val status = MutableLiveData(Status.Stopped) private val status = MutableLiveData(Status.Stopped)
private val binder = ServiceBinder(status) private val binder = ServiceBinder(status)
private val notification = ServiceNotification(service) private val notification = ServiceNotification(status, service)
private var boxService: BoxService? = null private var boxService: BoxService? = null
private var commandServer: CommandServer? = null private var commandServer: CommandServer? = null
private var pprofServer: PProfServer? = null
private var receiverRegistered = false private var receiverRegistered = false
private val receiver = object : BroadcastReceiver() { private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
@@ -133,11 +131,12 @@ class BoxService(
private fun startCommandServer() { private fun startCommandServer() {
val commandServer = val commandServer =
CommandServer(this, 300) CommandServer(this, 300)
commandServer.start() commandServer.start()
this.commandServer = commandServer this.commandServer = commandServer
} }
private var activeProfileName = ""
private suspend fun startService(delayStart: Boolean = false) { private suspend fun startService(delayStart: Boolean = false) {
try { try {
Log.d(TAG, "starting service") Log.d(TAG, "starting service")
@@ -148,6 +147,8 @@ class BoxService(
return return
} }
activeProfileName = Settings.activeProfileName
val configOptions = Settings.configOptions val configOptions = Settings.configOptions
if (configOptions.isBlank()) { if (configOptions.isBlank()) {
stopAndAlert(Alert.EmptyConfiguration) stopAndAlert(Alert.EmptyConfiguration)
@@ -191,6 +192,10 @@ class BoxService(
boxService = newService boxService = newService
commandServer?.setService(boxService) commandServer?.setService(boxService)
status.postValue(Status.Started) status.postValue(Status.Started)
withContext(Dispatchers.Main) {
notification.show(activeProfileName)
}
} catch (e: Exception) { } catch (e: Exception) {
stopAndAlert(Alert.StartService, e.message) stopAndAlert(Alert.StartService, e.message)
return return
@@ -198,23 +203,24 @@ class BoxService(
} }
override fun serviceReload() { override fun serviceReload() {
notification.close()
status.postValue(Status.Starting) status.postValue(Status.Starting)
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
runBlocking { runBlocking {
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(true) startService(true)
} }
} }
@@ -311,7 +317,6 @@ class BoxService(
receiverRegistered = true receiverRegistered = true
} }
notification.show()
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
Settings.startedByUser = true Settings.startedByUser = true
initialize() initialize()

View File

@@ -4,21 +4,33 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.MutableLiveData
import com.hiddify.hiddify.Application import com.hiddify.hiddify.Application
import com.hiddify.hiddify.MainActivity import com.hiddify.hiddify.MainActivity
import com.hiddify.hiddify.R import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
import com.hiddify.hiddify.constant.Action import com.hiddify.hiddify.constant.Action
import com.hiddify.hiddify.constant.Status
import com.hiddify.hiddify.utils.CommandClient
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.StatusMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext
class ServiceNotification(private val service: Service) { class ServiceNotification(private val status: MutableLiveData<Status>, private val service: Service) : BroadcastReceiver(), CommandClient.Handler {
companion object { companion object {
private const val notificationId = 1 private const val notificationId = 1
private const val notificationChannel = "service" private const val notificationChannel = "service"
private val flags = private val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
fun checkPermission(): Boolean { fun checkPermission(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@@ -29,49 +41,102 @@ class ServiceNotification(private val service: Service) {
} }
private val notification by lazy { private val commandClient =
NotificationCompat.Builder(service, notificationChannel).setWhen(0) CommandClient(GlobalScope, CommandClient.ConnectionType.Status, this)
.setContentTitle("Hiddify Next") private var receiverRegistered = false
.setContentText("service running").setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.ic_stat_logo)
.setCategory(NotificationCompat.CATEGORY_SERVICE) private val notificationBuilder by lazy {
.setContentIntent( NotificationCompat.Builder(service, notificationChannel)
PendingIntent.getActivity( .setShowWhen(false)
service, .setOngoing(true)
0, .setContentTitle("Hiddify Next")
Intent( .setOnlyAlertOnce(true)
service, .setSmallIcon(R.drawable.ic_stat_logo)
MainActivity::class.java .setCategory(NotificationCompat.CATEGORY_SERVICE)
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), .setContentIntent(
flags PendingIntent.getActivity(
) service,
) 0,
.setPriority(NotificationCompat.PRIORITY_LOW).apply { Intent(
addAction( service,
NotificationCompat.Action.Builder( MainActivity::class.java
0, service.getText(R.string.stop), PendingIntent.getBroadcast( ).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
service, flags
0,
Intent(Action.SERVICE_CLOSE).setPackage(service.packageName),
flags
) )
).build()
) )
} .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() { suspend fun show(profileName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel( Application.notification.createNotificationChannel(
NotificationChannel( NotificationChannel(
notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
) )
) )
} }
service.startForeground(notificationId, notification.build()) service.startForeground(
notificationId, notificationBuilder
.setContentTitle(profileName.takeIf { it.isNotBlank() } ?: "Hiddify Next")
.setContentText("service started").build()
)
withContext(Dispatchers.IO) {
if (Settings.dynamicNotification) {
commandClient.connect()
withContext(Dispatchers.Main) {
registerReceiver()
}
}
}
}
private fun registerReceiver() {
service.registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
})
receiverRegistered = true
}
override fun updateStatus(status: StatusMessage) {
val content =
Libbox.formatBytes(status.uplink) + "/s ↑\t" + Libbox.formatBytes(status.downlink) + "/s ↓"
Application.notificationManager.notify(
notificationId,
notificationBuilder.setContentText(content).build()
)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_SCREEN_ON -> {
commandClient.connect()
}
Intent.ACTION_SCREEN_OFF -> {
commandClient.disconnect()
}
}
} }
fun close() { fun close() {
commandClient.disconnect()
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
if (receiverRegistered) {
service.unregisterReceiver(this)
receiverRegistered = false
}
} }
} }

View File

@@ -4,6 +4,7 @@ object SettingsKey {
private const val KEY_PREFIX = "flutter." private const val KEY_PREFIX = "flutter."
const val ACTIVE_CONFIG_PATH = "${KEY_PREFIX}active_config_path" const val ACTIVE_CONFIG_PATH = "${KEY_PREFIX}active_config_path"
const val ACTIVE_PROFILE_NAME = "${KEY_PREFIX}active_profile_name"
const val SERVICE_MODE = "${KEY_PREFIX}service_mode" const val SERVICE_MODE = "${KEY_PREFIX}service_mode"
const val CONFIG_OPTIONS = "config_options_json" const val CONFIG_OPTIONS = "config_options_json"
@@ -15,6 +16,7 @@ object SettingsKey {
const val DEBUG_MODE = "${KEY_PREFIX}debug_mode" const val DEBUG_MODE = "${KEY_PREFIX}debug_mode"
const val ENABLE_TUN = "${KEY_PREFIX}enable-tun" const val ENABLE_TUN = "${KEY_PREFIX}enable-tun"
const val DISABLE_MEMORY_LIMIT = "${KEY_PREFIX}disable_memory_limit" const val DISABLE_MEMORY_LIMIT = "${KEY_PREFIX}disable_memory_limit"
const val DYNAMIC_NOTIFICATION = "${KEY_PREFIX}dynamic_notification"
const val SYSTEM_PROXY_ENABLED = "${KEY_PREFIX}system_proxy_enabled" const val SYSTEM_PROXY_ENABLED = "${KEY_PREFIX}system_proxy_enabled"
// cache // cache

View File

@@ -155,7 +155,8 @@
"silentStart": "Silent Start", "silentStart": "Silent Start",
"openWorkingDir": "Open Working Directory", "openWorkingDir": "Open Working Directory",
"ignoreBatteryOptimizations": "Disable Battery Optimization", "ignoreBatteryOptimizations": "Disable Battery Optimization",
"ignoreBatteryOptimizationsMsg": "Remove restrictions for optimal VPN performance" "ignoreBatteryOptimizationsMsg": "Remove restrictions for optimal VPN performance",
"dynamicNotification": "Display speed in notification"
}, },
"advanced": { "advanced": {
"sectionTitle": "Advanced", "sectionTitle": "Advanced",

View File

@@ -155,7 +155,8 @@
"silentStart": "اجرای ساکت", "silentStart": "اجرای ساکت",
"openWorkingDir": "باز کردن دایرکتوری کاری", "openWorkingDir": "باز کردن دایرکتوری کاری",
"ignoreBatteryOptimizations": "غیرفعال کردن بهینه‌سازی باتری", "ignoreBatteryOptimizations": "غیرفعال کردن بهینه‌سازی باتری",
"ignoreBatteryOptimizationsMsg": "حذف محدودیت‌ها برای عملکرد بهتر VPN" "ignoreBatteryOptimizationsMsg": "حذف محدودیت‌ها برای عملکرد بهتر VPN",
"dynamicNotification": "نمایش سرعت در نوتیفیکیشن"
}, },
"advanced": { "advanced": {
"sectionTitle": "پیشرفته", "sectionTitle": "پیشرفته",

View File

@@ -155,7 +155,8 @@
"silentStart": "Тихий запуск", "silentStart": "Тихий запуск",
"openWorkingDir": "Открыть рабочую папку", "openWorkingDir": "Открыть рабочую папку",
"ignoreBatteryOptimizations": "Отключить оптимизацию батареи", "ignoreBatteryOptimizations": "Отключить оптимизацию батареи",
"ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN." "ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN.",
"dynamicNotification": "Отображение скорости в уведомлении"
}, },
"advanced": { "advanced": {
"sectionTitle": "Расширенные", "sectionTitle": "Расширенные",

View File

@@ -155,7 +155,8 @@
"silentStart": "Sessiz Başlatma", "silentStart": "Sessiz Başlatma",
"openWorkingDir": "Çalışma Dizinini Aç", "openWorkingDir": "Çalışma Dizinini Aç",
"ignoreBatteryOptimizations": "Pil Optimizasyonunu Devre Dışı Bırak", "ignoreBatteryOptimizations": "Pil Optimizasyonunu Devre Dışı Bırak",
"ignoreBatteryOptimizationsMsg": "Optimum VPN performansı için kısıtlamaları kaldırın" "ignoreBatteryOptimizationsMsg": "Optimum VPN performansı için kısıtlamaları kaldırın",
"dynamicNotification": "Bildirimde hızı göster"
}, },
"advanced": { "advanced": {
"sectionTitle": "Gelişmiş", "sectionTitle": "Gelişmiş",

View File

@@ -155,7 +155,8 @@
"silentStart": "静默启动", "silentStart": "静默启动",
"openWorkingDir": "打开工作目录", "openWorkingDir": "打开工作目录",
"ignoreBatteryOptimizations": "禁用电池优化", "ignoreBatteryOptimizations": "禁用电池优化",
"ignoreBatteryOptimizationsMsg": "消除限制以获得最佳 VPN 性能" "ignoreBatteryOptimizationsMsg": "消除限制以获得最佳 VPN 性能",
"dynamicNotification": "在通知中显示速度"
}, },
"advanced": { "advanced": {
"sectionTitle": "高级选项", "sectionTitle": "高级选项",

View File

@@ -184,3 +184,20 @@ class MarkNewProfileActive extends _$MarkNewProfileActive {
return _pref.update(value); return _pref.update(value);
} }
} }
@riverpod
class DynamicNotification extends _$DynamicNotification {
late final _pref = Pref(
ref.watch(sharedPreferencesProvider).requireValue,
"dynamic_notification",
true,
);
@override
bool build() => _pref.getValue();
Future<void> update(bool value) {
state = value;
return _pref.update(value);
}
}

View File

@@ -19,11 +19,13 @@ abstract interface class ConnectionRepository {
Stream<ConnectionStatus> watchConnectionStatus(); Stream<ConnectionStatus> watchConnectionStatus();
TaskEither<ConnectionFailure, Unit> connect( TaskEither<ConnectionFailure, Unit> connect(
String fileName, String fileName,
String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
); );
TaskEither<ConnectionFailure, Unit> disconnect(); TaskEither<ConnectionFailure, Unit> disconnect();
TaskEither<ConnectionFailure, Unit> reconnect( TaskEither<ConnectionFailure, Unit> reconnect(
String fileName, String fileName,
String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
); );
} }
@@ -144,6 +146,7 @@ class ConnectionRepositoryImpl
@override @override
TaskEither<ConnectionFailure, Unit> connect( TaskEither<ConnectionFailure, Unit> connect(
String fileName, String fileName,
String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
) { ) {
return TaskEither<ConnectionFailure, Unit>.Do( return TaskEither<ConnectionFailure, Unit>.Do(
@@ -173,6 +176,7 @@ class ConnectionRepositoryImpl
singbox singbox
.start( .start(
profilePathResolver.file(fileName).path, profilePathResolver.file(fileName).path,
profileName,
disableMemoryLimit, disableMemoryLimit,
) )
.mapLeft(UnexpectedConnectionFailure.new), .mapLeft(UnexpectedConnectionFailure.new),
@@ -192,6 +196,7 @@ class ConnectionRepositoryImpl
@override @override
TaskEither<ConnectionFailure, Unit> reconnect( TaskEither<ConnectionFailure, Unit> reconnect(
String fileName, String fileName,
String profileName,
bool disableMemoryLimit, bool disableMemoryLimit,
) { ) {
return exceptionHandler( return exceptionHandler(
@@ -202,6 +207,7 @@ class ConnectionRepositoryImpl
() => singbox () => singbox
.restart( .restart(
profilePathResolver.file(fileName).path, profilePathResolver.file(fileName).path,
profileName,
disableMemoryLimit, disableMemoryLimit,
) )
.mapLeft(UnexpectedConnectionFailure.new), .mapLeft(UnexpectedConnectionFailure.new),

View File

@@ -3,6 +3,7 @@ import 'package:hiddify/core/preferences/service_preferences.dart';
import 'package:hiddify/features/connection/data/connection_data_providers.dart'; import 'package:hiddify/features/connection/data/connection_data_providers.dart';
import 'package:hiddify/features/connection/data/connection_repository.dart'; import 'package:hiddify/features/connection/data/connection_repository.dart';
import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/connection/model/connection_status.dart';
import 'package:hiddify/features/profile/model/profile_entity.dart';
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -20,7 +21,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
if (previous == null) return; if (previous == null) return;
final shouldReconnect = next == null || previous.id != next.id; final shouldReconnect = next == null || previous.id != next.id;
if (shouldReconnect) { if (shouldReconnect) {
await reconnect(next?.id); await reconnect(next);
} }
}, },
); );
@@ -59,16 +60,20 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
} }
} }
Future<void> reconnect(String? profileId) async { Future<void> reconnect(ProfileEntity? profile) async {
if (state case AsyncData(:final value) when value == const Connected()) { if (state case AsyncData(:final value) when value == const Connected()) {
if (profileId == null) { if (profile == null) {
loggy.info("no active profile, disconnecting"); loggy.info("no active profile, disconnecting");
return _disconnect(); return _disconnect();
} }
loggy.info("active profile changed, reconnecting"); loggy.info("active profile changed, reconnecting");
await ref.read(startedByUserProvider.notifier).update(true); await ref.read(startedByUserProvider.notifier).update(true);
await _connectionRepo await _connectionRepo
.reconnect(profileId, ref.read(disableMemoryLimitProvider)) .reconnect(
profile.id,
profile.name,
ref.read(disableMemoryLimitProvider),
)
.mapLeft((err) { .mapLeft((err) {
loggy.warning("error reconnecting", err); loggy.warning("error reconnecting", err);
state = AsyncError(err, StackTrace.current); state = AsyncError(err, StackTrace.current);
@@ -90,7 +95,11 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
Future<void> _connect() async { Future<void> _connect() async {
final activeProfile = await ref.read(activeProfileProvider.future); final activeProfile = await ref.read(activeProfileProvider.future);
await _connectionRepo await _connectionRepo
.connect(activeProfile!.id, ref.read(disableMemoryLimitProvider)) .connect(
activeProfile!.id,
activeProfile.name,
ref.read(disableMemoryLimitProvider),
)
.mapLeft((err) async { .mapLeft((err) async {
loggy.warning("error connecting", err); loggy.warning("error connecting", err);
await ref.read(startedByUserProvider.notifier).update(false); await ref.read(startedByUserProvider.notifier).update(false);

View File

@@ -128,7 +128,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger {
if (active != null && active.id == profile.id) { if (active != null && active.id == profile.id) {
await ref await ref
.read(connectionNotifierProvider.notifier) .read(connectionNotifierProvider.notifier)
.reconnect(profile.id); .reconnect(profile);
} }
}); });
return unit; return unit;

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/localization/translations.dart';
@@ -73,6 +75,17 @@ class GeneralSettingTiles extends HookConsumerWidget {
} }
}, },
), ),
if (Platform.isAndroid)
SwitchListTile(
title: Text(t.settings.general.dynamicNotification),
secondary: const Icon(Icons.speed),
value: ref.watch(dynamicNotificationProvider),
onChanged: (value) async {
await ref
.read(dynamicNotificationProvider.notifier)
.update(value);
},
),
if (PlatformUtils.isDesktop) ...[ if (PlatformUtils.isDesktop) ...[
SwitchListTile( SwitchListTile(
title: Text(t.settings.general.autoStart), title: Text(t.settings.general.autoStart),

View File

@@ -158,7 +158,11 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
@override @override
TaskEither<String, Unit> start(String configPath, bool disableMemoryLimit) { TaskEither<String, Unit> start(
String configPath,
String name,
bool disableMemoryLimit,
) {
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
return TaskEither( return TaskEither(
() => CombineWorker().execute( () => CombineWorker().execute(
@@ -195,7 +199,11 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
@override @override
TaskEither<String, Unit> restart(String configPath, bool disableMemoryLimit) { TaskEither<String, Unit> restart(
String configPath,
String name,
bool disableMemoryLimit,
) {
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]"); loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
return TaskEither( return TaskEither(
() => CombineWorker().execute( () => CombineWorker().execute(

View File

@@ -89,13 +89,17 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
} }
@override @override
TaskEither<String, Unit> start(String path, bool disableMemoryLimit) { TaskEither<String, Unit> start(
String path,
String name,
bool disableMemoryLimit,
) {
return TaskEither( return TaskEither(
() async { () async {
loggy.debug("starting"); loggy.debug("starting");
await _methodChannel.invokeMethod( await _methodChannel.invokeMethod(
"start", "start",
{"path": path}, {"path": path, "name": name},
); );
return right(unit); return right(unit);
}, },
@@ -114,13 +118,17 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
} }
@override @override
TaskEither<String, Unit> restart(String path, bool disableMemoryLimit) { TaskEither<String, Unit> restart(
String path,
String name,
bool disableMemoryLimit,
) {
return TaskEither( return TaskEither(
() async { () async {
loggy.debug("restarting"); loggy.debug("restarting");
await _methodChannel.invokeMethod( await _methodChannel.invokeMethod(
"restart", "restart",
{"path": path}, {"path": path, "name": name},
); );
return right(unit); return right(unit);
}, },

View File

@@ -38,11 +38,19 @@ abstract interface class SingboxService {
String path, String path,
); );
TaskEither<String, Unit> start(String path, bool disableMemoryLimit); TaskEither<String, Unit> start(
String path,
String name,
bool disableMemoryLimit,
);
TaskEither<String, Unit> stop(); TaskEither<String, Unit> stop();
TaskEither<String, Unit> restart(String path, bool disableMemoryLimit); TaskEither<String, Unit> restart(
String path,
String name,
bool disableMemoryLimit,
);
Stream<List<SingboxOutboundGroup>> watchOutbounds(); Stream<List<SingboxOutboundGroup>> watchOutbounds();