diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4607b16..c132174 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -297,6 +297,18 @@
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt b/app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt
new file mode 100644
index 0000000..fd037ad
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/BootReceiver.kt
@@ -0,0 +1,42 @@
+package io.nekohasekai.sagernet
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import io.nekohasekai.sagernet.bg.SubscriptionUpdater
+import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.ktx.app
+import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
+
+class BootReceiver : BroadcastReceiver() {
+ companion object {
+ private val componentName by lazy { ComponentName(app, BootReceiver::class.java) }
+ var enabled: Boolean
+ get() = app.packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ set(value) = app.packageManager.setComponentEnabledSetting(
+ componentName, if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
+ )
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ runOnDefaultDispatcher {
+ SubscriptionUpdater.reconfigureUpdater()
+ }
+
+ if (!DataStore.persistAcrossReboot) { // sanity check
+ enabled = false
+ return
+ }
+
+ val doStart = when (intent.action) {
+ Intent.ACTION_LOCKED_BOOT_COMPLETED -> false // DataStore.directBootAware
+ else -> Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked
+ } && DataStore.selectedProxy > 0
+
+ if (doStart) SagerNet.startService()
+ }
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
index d84be13..992f817 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
@@ -7,6 +7,8 @@ object Key {
const val DB_PUBLIC = "configuration.db"
const val DB_PROFILE = "sager_net.db"
+ const val PERSIST_ACROSS_REBOOT = "isAutoConnect"
+
const val APP_EXPERT = "isExpert"
const val APP_THEME = "appTheme"
const val NIGHT_THEME = "nightTheme"
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
index 127ef5e..7d4be57 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
@@ -7,6 +7,7 @@ import android.content.IntentFilter
import android.os.*
import android.widget.Toast
import io.nekohasekai.sagernet.Action
+import io.nekohasekai.sagernet.BootReceiver
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.ISagerNetService
@@ -314,6 +315,7 @@ class BaseService {
val proxy = ProxyInstance(profile, this)
data.proxy = proxy
+ BootReceiver.enabled = DataStore.persistAcrossReboot
if (!data.closeReceiverRegistered) {
registerReceiver(data.receiver, IntentFilter().apply {
addAction(Action.RELOAD)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt
index 54239e9..c1b0bb4 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt
@@ -60,7 +60,7 @@ class TileService : BaseTileService(), SagerConnection.Callback {
}
override fun onClick() {
- toggle()
+ if (isLocked) unlockAndRun(this::toggle) else toggle()
}
private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {
@@ -72,15 +72,18 @@ class TileService : BaseTileService(), SagerConnection.Callback {
icon = iconBusy
state = Tile.STATE_ACTIVE
}
+
BaseService.State.Connected -> {
icon = iconConnected
label = profileName()
state = Tile.STATE_ACTIVE
}
+
BaseService.State.Stopping -> {
icon = iconBusy
state = Tile.STATE_UNAVAILABLE
}
+
BaseService.State.Stopped -> {
icon = iconIdle
state = Tile.STATE_INACTIVE
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
index c413ee6..db248f4 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
@@ -151,6 +151,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var individual by configurationStore.string(Key.INDIVIDUAL)
var showDirectSpeed by configurationStore.boolean(Key.SHOW_DIRECT_SPEED) { true }
+ val persistAcrossReboot by configurationStore.boolean(Key.PERSIST_ACROSS_REBOOT) { false }
+
var appendHttpProxy by configurationStore.boolean(Key.APPEND_HTTP_PROXY)
var requireTransproxy by configurationStore.boolean(Key.REQUIRE_TRANSPROXY)
var transproxyMode by configurationStore.stringToInt(Key.TRANSPROXY_MODE)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
index ef40f08..13271a8 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
@@ -98,6 +98,14 @@ class MainActivity : ThemedActivity(),
)
}
}
+
+ refreshNavMenu(DataStore.enableClashAPI)
+ }
+
+ fun refreshNavMenu(clashApi: Boolean) {
+ if (::navigation.isInitialized) {
+ navigation.menu.findItem(R.id.nav_traffic)?.isVisible = clashApi
+ }
}
override fun onNewIntent(intent: Intent) {
@@ -347,17 +355,6 @@ class MainActivity : ThemedActivity(),
binding.fab.changeState(state, DataStore.serviceState, animate)
binding.stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
-
- when (state) {
- BaseService.State.Stopped -> {
- runOnDefaultDispatcher {
- // refresh view
- ProfileManager.postUpdate(DataStore.currentProfile)
- }
- }
-
- else -> {}
- }
}
override fun snackbarInternal(text: CharSequence): Snackbar {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
index 118b176..465a561 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
@@ -189,6 +189,11 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
val resolveDestination = findPreference(Key.RESOLVE_DESTINATION)!!
val acquireWakeLock = findPreference(Key.ACQUIRE_WAKE_LOCK)!!
val enableClashAPI = findPreference(Key.ENABLE_CLASH_API)!!
+ enableClashAPI.setOnPreferenceChangeListener { _, newValue ->
+ (activity as MainActivity?)?.refreshNavMenu(newValue as Boolean)
+ needReload()
+ true
+ }
serviceMode.onPreferenceChangeListener = reloadListener
mixedPort.onPreferenceChangeListener = reloadListener
@@ -218,7 +223,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
resolveDestination.onPreferenceChangeListener = reloadListener
tunImplementation.onPreferenceChangeListener = reloadListener
acquireWakeLock.onPreferenceChangeListener = reloadListener
- enableClashAPI.onPreferenceChangeListener = reloadListener
}
diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml
index d5f61bd..9c3578f 100644
--- a/app/src/main/res/xml/global_preferences.xml
+++ b/app/src/main/res/xml/global_preferences.xml
@@ -6,6 +6,12 @@
app:icon="@drawable/ic_baseline_android_24"
app:key="nekoPlugins"
app:title="@string/neko_plugin" />
+