Implement send notification

This commit is contained in:
世界 2024-11-07 12:48:48 +08:00
parent e38f352c48
commit 108f8b05af
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
15 changed files with 116 additions and 45 deletions

View file

@ -10,7 +10,7 @@ plugins {
android {
namespace "io.nekohasekai.sfa"
compileSdk 34
compileSdk 35
ndkVersion "27.2.12479018"
@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId "io.nekohasekai.sfa"
minSdk 21
targetSdk 34
targetSdk 35
versionCode getVersionProps("VERSION_CODE").toInteger()
versionName getVersionProps("VERSION_NAME")
setProperty("archivesBaseName", "SFA-" + versionName)
@ -90,23 +90,23 @@ android {
dependencies {
implementation(fileTree("libs"))
implementation "androidx.core:core-ktx:1.13.1"
implementation "androidx.core:core-ktx:1.15.0"
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "com.google.android.material:material:1.12.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6"
implementation "androidx.constraintlayout:constraintlayout:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.7"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7"
implementation "androidx.navigation:navigation-fragment-ktx:2.8.3"
implementation "androidx.navigation:navigation-ui-ktx:2.8.3"
implementation "com.google.zxing:core:3.5.3"
implementation "androidx.room:room-runtime:2.6.1"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.camera:camera-view:1.3.4"
implementation "androidx.camera:camera-lifecycle:1.3.4"
implementation "androidx.camera:camera-camera2:1.3.4"
implementation "androidx.camera:camera-view:1.4.0"
implementation "androidx.camera:camera-lifecycle:1.4.0"
implementation "androidx.camera:camera-camera2:1.4.0"
ksp "androidx.room:room-compiler:2.6.1"
implementation "androidx.work:work-runtime-ktx:2.9.1"
implementation "androidx.work:work-runtime-ktx:2.10.0"
implementation "androidx.browser:browser:1.8.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
@ -120,6 +120,8 @@ dependencies {
implementation "com.google.guava:guava:33.0.0-android"
playImplementation "com.google.android.play:app-update-ktx:2.1.0"
playImplementation "com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1"
implementation "com.github.tiann:FreeReflection:3.1.0"
}
def playCredentialsJSON = rootProject.file("service-account-credentials.json")

View file

@ -16,13 +16,14 @@ import io.nekohasekai.sfa.bg.UpdateProfileWork
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import me.weishu.reflection.Reflection
import io.nekohasekai.sfa.Application as BoxApplication
class Application : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
Reflection.unseal(base)
application = this
}

View file

@ -1,15 +1,20 @@
package io.nekohasekai.sfa.bg
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.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.os.PowerManager
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import go.Seq
@ -17,6 +22,7 @@ import io.nekohasekai.libbox.BoxService
import io.nekohasekai.libbox.CommandServer
import io.nekohasekai.libbox.CommandServerHandler
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.Notification
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.SystemProxyStatus
import io.nekohasekai.sfa.Application
@ -27,6 +33,7 @@ import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.hasPermission
import io.nekohasekai.sfa.ui.MainActivity
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -36,8 +43,7 @@ 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 {
@ -101,8 +107,7 @@ class BoxService(
}
private fun startCommandServer() {
val commandServer =
CommandServer(this, 300)
val commandServer = CommandServer(this, 300)
commandServer.start()
this.commandServer = commandServer
}
@ -328,4 +333,45 @@ class BoxService(
commandServer?.writeMessage(message)
}
internal fun sendNotification(notification: Notification) {
val builder =
NotificationCompat.Builder(service, notification.identifier).setShowWhen(false)
.setContentTitle(notification.title)
.setContentText(notification.body)
.setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.ic_menu)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
if (!notification.subtitle.isNullOrBlank()) {
builder.setContentInfo(notification.subtitle)
}
if (!notification.openURL.isNullOrBlank()) {
builder.setContentIntent(
PendingIntent.getActivity(
service,
0,
Intent(
service, MainActivity::class.java
).apply {
setAction(Action.OPEN_URL).setData(Uri.parse(notification.openURL))
setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
},
ServiceNotification.flags,
)
)
}
GlobalScope.launch(Dispatchers.Main) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel(
NotificationChannel(
notification.identifier,
notification.typeName,
NotificationManager.IMPORTANCE_HIGH
)
)
}
Application.notification.notify(notification.typeID, builder.build())
}
}
}

View file

@ -1,5 +1,6 @@
package io.nekohasekai.sfa.bg
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process
@ -136,6 +137,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
.iterator()
)
runCatching {
flags = element.flags
}
}
}
@ -146,6 +150,13 @@ interface PlatformInterfaceWrapper : PlatformInterface {
"${address.hostAddress}/${networkPrefixLength}"
}
}
private val NetworkInterface.flags: Int
@SuppressLint("SoonBlockedPrivateApi")
get() {
val getFlagsMethod = NetworkInterface::class.java.getDeclaredMethod("getFlags")
return getFlagsMethod.invoke(this) as Int
}
}
private class StringArray(private val iterator: Iterator<String>) : StringIterator {

View file

@ -2,6 +2,7 @@ package io.nekohasekai.sfa.bg
import android.app.Service
import android.content.Intent
import io.nekohasekai.libbox.Notification
class ProxyService : Service(), PlatformInterfaceWrapper {
@ -14,4 +15,8 @@ class ProxyService : Service(), PlatformInterfaceWrapper {
override fun onDestroy() = service.onDestroy()
override fun writeLog(message: String) = service.writeLog(message)
override fun sendNotification(notification: Notification) =
service.sendNotification(notification)
}

View file

@ -33,7 +33,7 @@ class ServiceNotification(
companion object {
private const val notificationId = 1
private const val notificationChannel = "service"
private val flags =
val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
fun checkPermission(): Boolean {
@ -83,7 +83,7 @@ class ServiceNotification(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Application.notification.createNotificationChannel(
NotificationChannel(
notificationChannel, "sing-box service", NotificationManager.IMPORTANCE_LOW
notificationChannel, "Service Notifications", NotificationManager.IMPORTANCE_LOW
)
)
}

View file

@ -6,6 +6,7 @@ import android.net.ProxyInfo
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import io.nekohasekai.libbox.Notification
import io.nekohasekai.libbox.TunOptions
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.ktx.toIpPrefix
@ -188,4 +189,7 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
override fun writeLog(message: String) = service.writeLog(message)
override fun sendNotification(notification: Notification) =
service.sendNotification(notification)
}

View file

@ -3,4 +3,5 @@ package io.nekohasekai.sfa.constant
object Action {
const val SERVICE = "io.nekohasekai.sfa.SERVICE"
const val SERVICE_CLOSE = "io.nekohasekai.sfa.SERVICE_CLOSE"
const val OPEN_URL = "io.nekohasekai.sfa.SERVICE_OPEN_URL"
}

View file

@ -33,6 +33,7 @@ import io.nekohasekai.sfa.Application
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.bg.ServiceConnection
import io.nekohasekai.sfa.bg.ServiceNotification
import io.nekohasekai.sfa.constant.Action
import io.nekohasekai.sfa.constant.Alert
import io.nekohasekai.sfa.constant.ServiceMode
import io.nekohasekai.sfa.constant.Status
@ -43,6 +44,7 @@ import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.databinding.ActivityMainBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ktx.hasPermission
import io.nekohasekai.sfa.ktx.launchCustomTab
import io.nekohasekai.sfa.ui.profile.NewProfileActivity
import io.nekohasekai.sfa.ui.settings.CoreFragment
import io.nekohasekai.sfa.ui.shared.AbstractActivity
@ -127,6 +129,12 @@ class MainActivity : AbstractActivity<ActivityMainBinding>(),
override public fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val uri = intent.data ?: return
when (intent.action) {
Action.OPEN_URL -> {
launchCustomTab(uri.toString())
return
}
}
if (uri.scheme == "sing-box" && uri.host == "import-remote-profile") {
val profile = try {
Libbox.parseRemoteProfileImportLink(uri.toString())

View file

@ -25,7 +25,6 @@ import io.nekohasekai.sfa.databinding.ViewClashModeButtonBinding
import io.nekohasekai.sfa.databinding.ViewProfileItemBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ktx.getAttrColor
import io.nekohasekai.sfa.ktx.launchCustomTab
import io.nekohasekai.sfa.ui.MainActivity
import io.nekohasekai.sfa.utils.CommandClient
import kotlinx.coroutines.CoroutineScope
@ -41,7 +40,7 @@ class OverviewFragment : Fragment() {
private val activity: MainActivity? get() = super.getActivity() as MainActivity?
private var binding: FragmentDashboardOverviewBinding? = null
private val statusClient =
CommandClient(lifecycleScope, CommandClient.ConnectionType.Status, StatusClient(), true)
CommandClient(lifecycleScope, CommandClient.ConnectionType.Status, StatusClient())
private val clashModeClient =
CommandClient(lifecycleScope, CommandClient.ConnectionType.ClashMode, ClashModeClient())
@ -172,10 +171,6 @@ class OverviewFragment : Fragment() {
}
}
override fun openURL(url: String) {
requireContext().launchCustomTab(url)
}
}
inner class ClashModeClient : CommandClient.Handler {

View file

@ -78,9 +78,9 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
RecyclerView.ViewHolder(binding.root) {
fun bind(element: AppInfo) {
binding.appIcon.setImageDrawable(element.packageInfo.applicationInfo.loadIcon(binding.root.context.packageManager))
binding.appIcon.setImageDrawable(element.packageInfo.applicationInfo!!.loadIcon(binding.root.context.packageManager))
binding.appName.text =
element.packageInfo.applicationInfo.loadLabel(binding.root.context.packageManager)
element.packageInfo.applicationInfo!!.loadLabel(binding.root.context.packageManager)
binding.packageName.text = element.packageInfo.packageName
val appType = element.vpnType.appType
if (appType != null) {
@ -129,7 +129,8 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
}
val vpnAppList =
installedPackages.filter {
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE } ?: false
it.services?.any { it.permission == Manifest.permission.BIND_VPN_SERVICE && it.applicationInfo != null }
?: false
}
for ((index, packageInfo) in vpnAppList.withIndex()) {
val appType = runCatching { getVPNAppType(packageInfo) }.getOrNull()
@ -181,7 +182,7 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
}
private fun getVPNAppType(packageInfo: PackageInfo): String? {
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use { packageFile ->
ZipFile(File(packageInfo.applicationInfo!!.publicSourceDir)).use { packageFile ->
for (packageEntry in packageFile.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
@ -235,8 +236,8 @@ class VPNScanActivity : AbstractActivity<ActivityVpnScanBinding>() {
}
private fun getVPNCoreType(packageInfo: PackageInfo): VPNCoreType? {
val packageFiles = mutableListOf(packageInfo.applicationInfo.publicSourceDir)
packageInfo.applicationInfo.splitPublicSourceDirs?.also {
val packageFiles = mutableListOf(packageInfo.applicationInfo!!.publicSourceDir)
packageInfo.applicationInfo!!.splitPublicSourceDirs?.also {
packageFiles.addAll(it)
}
val vpnType = try {

View file

@ -53,24 +53,25 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
inner class PackageCache(
private val packageInfo: PackageInfo,
private val appInfo: ApplicationInfo,
) {
val packageName: String get() = packageInfo.packageName
val uid get() = packageInfo.applicationInfo.uid
val uid get() = packageInfo.applicationInfo!!.uid
val installTime get() = packageInfo.firstInstallTime
val updateTime get() = packageInfo.lastUpdateTime
val isSystem get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
val isSystem get() = appInfo.flags and ApplicationInfo.FLAG_SYSTEM == 1
val isOffline get() = packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) != true
val isDisabled get() = packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0
val isDisabled get() = appInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0
val applicationIcon by lazy {
packageInfo.applicationInfo.loadIcon(packageManager)
appInfo.loadIcon(packageManager)
}
val applicationLabel by lazy {
packageInfo.applicationInfo.loadLabel(packageManager).toString()
appInfo.loadLabel(packageManager).toString()
}
}
@ -130,7 +131,8 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
val packages = mutableListOf<PackageCache>()
for (packageInfo in installedPackages) {
if (packageInfo.packageName == packageName) continue
packages.add(PackageCache(packageInfo))
val appInfo = packageInfo.applicationInfo ?: continue
packages.add(PackageCache(packageInfo, appInfo))
}
val selectedPackageNames = Settings.perAppProxyList.toMutableSet()
val selectedUIDs = mutableSetOf<Int>()
@ -699,6 +701,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
packageName, packageManagerFlags
)
}
val appInfo = packageInfo.applicationInfo ?: return false
packageInfo.services?.forEach {
if (it.name.matches(chinaAppRegex)) {
Log.d("PerAppProxyActivity", "Match service ${it.name} in $packageName")
@ -723,7 +726,7 @@ class PerAppProxyActivity : AbstractActivity<ActivityPerAppProxyBinding>() {
return true
}
}
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use {
ZipFile(File(appInfo.publicSourceDir)).use {
for (packageEntry in it.entries()) {
if (packageEntry.name.startsWith("firebase-")) return false
}

View file

@ -21,7 +21,6 @@ open class CommandClient(
private val scope: CoroutineScope,
private val connectionType: ConnectionType,
private val handler: Handler,
private val isMainClient: Boolean = false
) {
enum class ConnectionType {
@ -34,7 +33,6 @@ open class CommandClient(
fun onDisconnected() {}
fun updateStatus(status: StatusMessage) {}
fun openURL(url: String) {}
fun clearLogs() {}
fun appendLogs(message: List<String>) {}
@ -57,7 +55,6 @@ open class CommandClient(
ConnectionType.Log -> Libbox.CommandLog
ConnectionType.ClashMode -> Libbox.CommandClashMode
}
options.isMainClient = isMainClient
options.statusInterval = 1 * 1000 * 1000 * 1000
val commandClient = CommandClient(clientHandler, options)
scope.launch(Dispatchers.IO) {
@ -129,10 +126,6 @@ open class CommandClient(
handler.updateStatus(message)
}
override fun openURL(url: String) {
handler.openURL(url)
}
override fun initializeClashMode(modeList: StringIterator, currentMode: String) {
handler.initializeClashMode(modeList.toList(), currentMode)
}

View file

@ -5,8 +5,8 @@ buildscript {
}
plugins {
id 'com.android.application' version '8.7.1' apply false
id 'com.android.library' version '8.7.1' apply false
id 'com.android.application' version '8.7.2' apply false
id 'com.android.library' version '8.7.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
id 'com.google.devtools.ksp' version '1.9.23-1.0.20' apply false
id 'com.github.triplet.play' version '3.8.4' apply false

View file

@ -10,6 +10,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}
rootProject.name = "sing-box"