Add dynamic notification

Co-authored-by: Moe <moe@example.com>
Co-authored-by: 世界 <i@sekai.icu>
This commit is contained in:
世界 2023-11-09 14:29:47 +08:00
parent 71acd29526
commit 4295c30503
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
8 changed files with 122 additions and 28 deletions

View file

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

View file

@ -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()

View file

@ -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<Status>, 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)
}
}

View file

@ -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"

View file

@ -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

View file

@ -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)
}
}

View file

@ -16,7 +16,6 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/appSettingsCard"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -38,11 +37,28 @@
android:textAppearance="?attr/textAppearanceTitleLarge" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/checkUpdateEnabled"
android:id="@+id/dynamicNotificationEnabled"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:hint="@string/dynamic_notification">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:text="@string/disabled"
app:simpleItems="@array/enabled" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/checkUpdateEnabled"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/check_update_atomic">
<AutoCompleteTextView

View file

@ -81,10 +81,11 @@
<string name="profile">Profile</string>
<string name="core_version">Version</string>
<string name="core">Core</string>
<string name="dynamic_notification">Display realtime speed in notification</string>
<string name="core_data_size">Data Size</string>
<string name="check_update_atomic">Atomic Check Update</string>
<string name="check_update">Check Update</string>
<string name="title_app_settings">App Settings</string>
<string name="title_app_settings">App</string>
<string name="about_title">About</string>
<string name="app_description">Android client for sing-box, the universal proxy platform.</string>
<string name="documentation_button">Documentation</string>