diff --git a/app/src/main/java/io/nekohasekai/sfa/Application.kt b/app/src/main/java/io/nekohasekai/sfa/Application.kt index 2f9cd6f..efd60fd 100644 --- a/app/src/main/java/io/nekohasekai/sfa/Application.kt +++ b/app/src/main/java/io/nekohasekai/sfa/Application.kt @@ -45,6 +45,7 @@ class Application : Application() { val connectivity by lazy { application.getSystemService()!! } val packageManager by lazy { application.packageManager } val powerManager by lazy { application.getSystemService()!! } + val notificationManager by lazy { application.getSystemService()!! } } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt index d8c0e31..c78172a 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt @@ -86,7 +86,7 @@ class BoxService( private val status = MutableLiveData(Status.Stopped) private val binder = ServiceBinder(status) - private val notification = ServiceNotification(service) + private val notification = ServiceNotification(status, service) private var boxService: BoxService? = null private var commandServer: CommandServer? = null private var pprofServer: PProfServer? = null @@ -118,6 +118,7 @@ class BoxService( this.commandServer = commandServer } + private var lastProfileName = "" private suspend fun startService(delayStart: Boolean = false) { try { val selectedProfileId = Settings.selectedProfile @@ -138,6 +139,7 @@ class BoxService( return } + lastProfileName = profile.name withContext(Dispatchers.Main) { binder.broadcast { it.onServiceResetLogs(listOf()) @@ -163,6 +165,10 @@ class BoxService( boxService = newService commandServer?.setService(boxService) status.postValue(Status.Started) + + withContext(Dispatchers.Main) { + notification.show(lastProfileName) + } } catch (e: Exception) { stopAndAlert(Alert.StartService, e.message) return @@ -170,23 +176,24 @@ class BoxService( } override fun serviceReload() { + notification.close() 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 { - 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) } } @@ -283,7 +290,6 @@ class BoxService( receiverRegistered = true } - notification.show() GlobalScope.launch(Dispatchers.IO) { Settings.startedByUser = true initialize() diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt index 0c50003..0d58383 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt @@ -4,16 +4,30 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat +import androidx.lifecycle.MutableLiveData +import io.nekohasekai.libbox.Libbox +import io.nekohasekai.libbox.StatusMessage import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.R import io.nekohasekai.sfa.constant.Action +import io.nekohasekai.sfa.constant.Status +import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.ui.MainActivity +import io.nekohasekai.sfa.utils.CommandClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.withContext -class ServiceNotification(private val service: Service) { +class ServiceNotification( + private val status: MutableLiveData, private val service: Service +) : BroadcastReceiver(), CommandClient.Handler { companion object { private const val notificationId = 1 private const val notificationChannel = "service" @@ -28,11 +42,12 @@ class ServiceNotification(private val service: Service) { } } + private val commandClient = + CommandClient(GlobalScope, CommandClient.ConnectionType.Status, this) - private val notification by lazy { - NotificationCompat.Builder(service, notificationChannel).setWhen(0) - .setContentTitle("sing-box") - .setContentText("service started").setOnlyAlertOnce(true) + private val notificationBuilder by lazy { + NotificationCompat.Builder(service, notificationChannel).setShowWhen(false).setOngoing(true) + .setContentTitle("sing-box").setOnlyAlertOnce(true) .setSmallIcon(R.drawable.ic_menu) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setContentIntent( @@ -60,7 +75,7 @@ class ServiceNotification(private val service: Service) { } } - fun show() { + suspend fun show(lastProfileName: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Application.notification.createNotificationChannel( NotificationChannel( @@ -68,10 +83,52 @@ class ServiceNotification(private val service: Service) { ) ) } - service.startForeground(notificationId, notification.build()) + service.startForeground( + notificationId, notificationBuilder + .setContentTitle(lastProfileName.takeIf { it.isNotBlank() } ?: "sing-box") + .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) + }) + } + + 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() { + commandClient.disconnect() ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE) + service.unregisterReceiver(this) } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt b/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt index 88a0952..25a7cc6 100644 --- a/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt +++ b/app/src/main/java/io/nekohasekai/sfa/constant/SettingsKey.kt @@ -6,6 +6,7 @@ object SettingsKey { const val SERVICE_MODE = "service_mode" const val CHECK_UPDATE_ENABLED = "check_update_enabled" const val DISABLE_MEMORY_LIMIT = "disable_memory_limit" + const val DYNAMIC_NOTIFICATION = "dynamic_notification" const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled" const val PER_APP_PROXY_MODE = "per_app_proxy_mode" diff --git a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt index 44622f6..cc83056 100644 --- a/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt +++ b/app/src/main/java/io/nekohasekai/sfa/database/Settings.kt @@ -40,6 +40,7 @@ object Settings { var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { true } var disableMemoryLimit by dataStore.boolean(SettingsKey.DISABLE_MEMORY_LIMIT) + var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true } const val PER_APP_PROXY_DISABLED = 0 diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt index 64fbc48..6e53fb7 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/main/SettingsFragment.kt @@ -61,7 +61,8 @@ class SettingsFragment : Fragment() { } } if (!Vendor.checkUpdateAvailable()) { - binding.appSettingsCard.isVisible = false + binding.checkUpdateEnabled.isVisible = false + binding.checkUpdateButton.isVisible = false } binding.checkUpdateEnabled.addTextChangedListener { lifecycleScope.launch(Dispatchers.IO) { @@ -78,6 +79,13 @@ class SettingsFragment : Fragment() { Settings.disableMemoryLimit = !newValue } } + binding.dynamicNotificationEnabled.addTextChangedListener { + lifecycleScope.launch(Dispatchers.IO) { + val newValue = EnabledType.valueOf(it).boolValue + Settings.dynamicNotification = newValue + } + } + binding.dontKillMyAppButton.setOnClickListener { it.context.launchCustomTab("https://dontkillmyapp.com/") } @@ -119,6 +127,7 @@ class SettingsFragment : Fragment() { } else { true } + val dynamicNotification = Settings.dynamicNotification withContext(Dispatchers.Main) { binding.dataSizeText.text = dataSize binding.checkUpdateEnabled.text = EnabledType.from(checkUpdateEnabled).name @@ -126,6 +135,8 @@ class SettingsFragment : Fragment() { binding.disableMemoryLimit.text = EnabledType.from(!Settings.disableMemoryLimit).name binding.disableMemoryLimit.setSimpleItems(R.array.enabled) binding.backgroundPermissionCard.isGone = removeBackgroundPermissionPage + binding.dynamicNotificationEnabled.text = EnabledType.from(dynamicNotification).name + binding.dynamicNotificationEnabled.setSimpleItems(R.array.enabled) } } diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 2df3d20..76e2ef3 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -16,7 +16,6 @@ + + + + + + Profile Version Core + Display realtime speed in notification Data Size Atomic Check Update Check Update - App Settings + App About Android client for sing-box, the universal proxy platform. Documentation