diff --git a/app/build.gradle b/app/build.gradle index 944ab62..b5c4cdb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") diff --git a/app/src/main/java/io/nekohasekai/sfa/Application.kt b/app/src/main/java/io/nekohasekai/sfa/Application.kt index bec7372..2b8311e 100644 --- a/app/src/main/java/io/nekohasekai/sfa/Application.kt +++ b/app/src/main/java/io/nekohasekai/sfa/Application.kt @@ -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 } 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 de5712c..c8361b8 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/BoxService.kt @@ -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()) + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt b/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt index a5b29be..c5bf700 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt @@ -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) : StringIterator { diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt index a84082f..6087d49 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ProxyService.kt @@ -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) + } \ No newline at end of file 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 db1a618..52a236f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/ServiceNotification.kt @@ -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 ) ) } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt b/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt index 50f838b..9817e95 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/VPNService.kt @@ -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) + } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/constant/Action.kt b/app/src/main/java/io/nekohasekai/sfa/constant/Action.kt index 5a65152..c0bb947 100644 --- a/app/src/main/java/io/nekohasekai/sfa/constant/Action.kt +++ b/app/src/main/java/io/nekohasekai/sfa/constant/Action.kt @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt index 187baf8..d3986a2 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/MainActivity.kt @@ -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(), 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()) diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt b/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt index 45c12db..75ac88c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/dashboard/OverviewFragment.kt @@ -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 { diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt index c1aab2e..f56717c 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/debug/VPNScanActivity.kt @@ -78,9 +78,9 @@ class VPNScanActivity : AbstractActivity() { 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() { } 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() { } 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() { } 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 { diff --git a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt index 2e6f392..990d304 100644 --- a/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt +++ b/app/src/main/java/io/nekohasekai/sfa/ui/profileoverride/PerAppProxyActivity.kt @@ -53,24 +53,25 @@ class PerAppProxyActivity : AbstractActivity() { 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() { val packages = mutableListOf() 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() @@ -699,6 +701,7 @@ class PerAppProxyActivity : AbstractActivity() { 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() { 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 } diff --git a/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt b/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt index 2e75c5b..4b6c3bb 100644 --- a/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/utils/CommandClient.kt @@ -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) {} @@ -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) } diff --git a/build.gradle b/build.gradle index 7a43853..9958810 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/settings.gradle b/settings.gradle index c7eb8d7..3195007 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url "https://jitpack.io" } } } rootProject.name = "sing-box"