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

View File

@@ -51,6 +51,10 @@ object Settings {
get() = preferences.getString(SettingsKey.ACTIVE_CONFIG_PATH, "")!!
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
get() = preferences.getString(SettingsKey.SERVICE_MODE, ServiceMode.NORMAL)!!
set(value) = preferences.edit().putString(SettingsKey.SERVICE_MODE, value).apply()
@@ -71,6 +75,11 @@ object Settings {
set(value) =
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
get() = preferences.getBoolean(SettingsKey.SYSTEM_PROXY_ENABLED, true)
set(value) =

View File

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

View File

@@ -4,21 +4,33 @@ 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 com.hiddify.hiddify.Application
import com.hiddify.hiddify.MainActivity
import com.hiddify.hiddify.R
import com.hiddify.hiddify.Settings
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 {
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
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) {
@@ -29,49 +41,102 @@ class ServiceNotification(private val service: Service) {
}
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
private val commandClient =
CommandClient(GlobalScope, CommandClient.ConnectionType.Status, this)
private var receiverRegistered = false
private val notificationBuilder by lazy {
NotificationCompat.Builder(service, notificationChannel)
.setShowWhen(false)
.setOngoing(true)
.setContentTitle("Hiddify Next")
.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
)
).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) {
Application.notification.createNotificationChannel(
NotificationChannel(
notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
)
NotificationChannel(
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() {
commandClient.disconnect()
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."
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 CONFIG_OPTIONS = "config_options_json"
@@ -15,6 +16,7 @@ object SettingsKey {
const val DEBUG_MODE = "${KEY_PREFIX}debug_mode"
const val ENABLE_TUN = "${KEY_PREFIX}enable-tun"
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"
// cache