diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f6fc1ab655..9ab4f8857b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -152,6 +152,11 @@
android:configChanges="uiMode"
android:exported="false"
android:label="@string/app" />
+
(), Behavior {
}
}
design.requests.onReceive {
- ApplicationObserver.createdActivities.forEach {
- it.recreate()
+ when (it) {
+ AppSettingsDesign.Request.ReCreateAllActivities ->
+ ApplicationObserver.createdActivities.forEach { activity ->
+ activity.recreate()
+ }
+
+ AppSettingsDesign.Request.OpenAutoSwitchSettings ->
+ startActivity(AutoSwitchSettingsActivity::class.intent)
}
}
}
diff --git a/app/src/main/java/com/github/kr328/clash/AutoSwitchSettingsActivity.kt b/app/src/main/java/com/github/kr328/clash/AutoSwitchSettingsActivity.kt
new file mode 100644
index 0000000000..1ecd78ac65
--- /dev/null
+++ b/app/src/main/java/com/github/kr328/clash/AutoSwitchSettingsActivity.kt
@@ -0,0 +1,15 @@
+package com.github.kr328.clash
+
+import com.github.kr328.clash.design.AutoSwitchSettingsDesign
+import com.github.kr328.clash.service.store.ServiceStore
+
+class AutoSwitchSettingsActivity : BaseActivity() {
+ override suspend fun main() {
+ val design = AutoSwitchSettingsDesign(
+ this,
+ ServiceStore(this),
+ )
+
+ setContentDesign(design)
+ }
+}
diff --git a/common/src/main/java/com/github/kr328/clash/common/constants/Intents.kt b/common/src/main/java/com/github/kr328/clash/common/constants/Intents.kt
index b754dc701a..07b423a124 100644
--- a/common/src/main/java/com/github/kr328/clash/common/constants/Intents.kt
+++ b/common/src/main/java/com/github/kr328/clash/common/constants/Intents.kt
@@ -23,6 +23,8 @@ object Intents {
val ACTION_PROFILE_SCHEDULE_UPDATES = "$packageName.intent.action.SCHEDULE_UPDATES"
val ACTION_PROFILE_LOADED = "$packageName.intent.action.PROFILE_LOADED"
val ACTION_OVERRIDE_CHANGED = "$packageName.intent.action.OVERRIDE_CHANGED"
+ val ACTION_AUTO_SWITCH_RESCHEDULE = "$packageName.intent.action.AUTO_SWITCH_RESCHEDULE"
+ val ACTION_AUTO_SWITCH_TRIGGER = "$packageName.intent.action.AUTO_SWITCH_TRIGGER"
const val EXTRA_STOP_REASON = "stop_reason"
const val EXTRA_UUID = "uuid"
diff --git a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt
index fdef937d58..dd3deecdfc 100644
--- a/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt
+++ b/design/src/main/java/com/github/kr328/clash/design/AppSettingsDesign.kt
@@ -22,7 +22,8 @@ class AppSettingsDesign(
onHideIconChange: (hide: Boolean) -> Unit,
) : Design(context) {
enum class Request {
- ReCreateAllActivities
+ ReCreateAllActivities,
+ OpenAutoSwitchSettings,
}
private val binding = DesignSettingsCommonBinding
@@ -87,6 +88,16 @@ class AppSettingsDesign(
) {
enabled = !running
}
+
+ clickable(
+ title = R.string.auto_switch,
+ icon = R.drawable.ic_baseline_schedule,
+ summary = R.string.auto_switch_summary,
+ ) {
+ clicked {
+ requests.trySend(Request.OpenAutoSwitchSettings)
+ }
+ }
}
binding.content.addView(screen.root)
diff --git a/design/src/main/java/com/github/kr328/clash/design/AutoSwitchSettingsDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AutoSwitchSettingsDesign.kt
new file mode 100644
index 0000000000..060d33f78e
--- /dev/null
+++ b/design/src/main/java/com/github/kr328/clash/design/AutoSwitchSettingsDesign.kt
@@ -0,0 +1,275 @@
+package com.github.kr328.clash.design
+
+import android.app.TimePickerDialog
+import android.content.Context
+import android.text.format.DateFormat
+import android.view.View
+import com.github.kr328.clash.design.R
+import com.github.kr328.clash.design.databinding.DesignSettingsCommonBinding
+import com.github.kr328.clash.design.databinding.PreferenceCategoryBinding
+import com.github.kr328.clash.design.preference.ClickablePreference
+import com.github.kr328.clash.design.preference.OnChangedListener
+import com.github.kr328.clash.design.preference.Preference
+import com.github.kr328.clash.design.preference.PreferenceScreen
+import com.github.kr328.clash.design.preference.category
+import com.github.kr328.clash.design.preference.clickable
+import com.github.kr328.clash.design.preference.preferenceScreen
+import com.github.kr328.clash.design.preference.selectableList
+import com.github.kr328.clash.design.util.applyFrom
+import com.github.kr328.clash.design.util.bindAppBarElevation
+import com.github.kr328.clash.design.util.layoutInflater
+import com.github.kr328.clash.design.util.root
+import com.github.kr328.clash.service.AutoSwitchReceiver
+import com.github.kr328.clash.service.model.AutoSwitchStrategyType
+import com.github.kr328.clash.service.model.DailyAutoSwitchSchedule
+import com.github.kr328.clash.service.model.WeekDay
+import com.github.kr328.clash.service.model.WeeklyAutoSwitchSchedule
+import com.github.kr328.clash.service.store.ServiceStore
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import java.text.DateFormatSymbols
+import java.util.Calendar
+import java.util.EnumMap
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.resume
+
+class AutoSwitchSettingsDesign(
+ context: Context,
+ private val serviceStore: ServiceStore,
+) : Design(context) {
+ private val binding = DesignSettingsCommonBinding
+ .inflate(context.layoutInflater, context.root, false)
+
+ override val root: View
+ get() = binding.root
+
+ private var currentStrategy: AutoSwitchStrategyType = AutoSwitchStrategyType.None
+ private var weeklySchedule: WeeklyAutoSwitchSchedule = WeeklyAutoSwitchSchedule()
+
+ private val dayPreferences = EnumMap(WeekDay::class.java)
+ private lateinit var weeklyHeader: Preference
+
+ init {
+ binding.surface = surface
+ binding.activityBarLayout.applyFrom(context)
+ binding.scrollRoot.bindAppBarElevation(binding.activityBarLayout)
+
+ val screen = preferenceScreen(context) {
+ category(R.string.auto_switch_strategy_category)
+
+ selectableList(
+ value = serviceStore::autoSwitchStrategy,
+ values = AutoSwitchStrategyType.values(),
+ valuesText = arrayOf(
+ R.string.auto_switch_strategy_none,
+ R.string.auto_switch_strategy_weekly,
+ ),
+ title = R.string.auto_switch_strategy_title,
+ icon = R.drawable.ic_baseline_schedule,
+ ) {
+ listener = OnChangedListener {
+ refreshStrategy()
+ }
+ }
+
+ weeklyHeader = addCategory(R.string.auto_switch_weekly_category)
+
+ WeekDay.values().forEach { day ->
+ val preference = clickable(
+ title = R.string.auto_switch_day_placeholder,
+ ) {
+ launch(Dispatchers.Main) {
+ showDayOptions(day)
+ }
+ }
+
+ preference.title = day.displayName(context)
+ dayPreferences[day] = preference
+ }
+ }
+
+ binding.content.addView(screen.root)
+
+ launch(Dispatchers.Main) {
+ val (strategy, schedule) = withContext(Dispatchers.IO) {
+ serviceStore.autoSwitchStrategy to serviceStore.autoSwitchWeeklySchedule
+ }
+
+ currentStrategy = strategy
+ weeklySchedule = schedule
+
+ WeekDay.values().forEach(::updateDaySummary)
+ updateStrategyVisibility()
+ }
+ }
+
+ private fun refreshStrategy() {
+ launch(Dispatchers.Main) {
+ val strategy = withContext(Dispatchers.IO) {
+ serviceStore.autoSwitchStrategy.also {
+ AutoSwitchReceiver.requestReschedule(context)
+ }
+ }
+
+ currentStrategy = strategy
+ updateStrategyVisibility()
+ }
+ }
+
+ private fun updateStrategyVisibility() {
+ val visible = currentStrategy == AutoSwitchStrategyType.Weekly
+ val visibility = if (visible) View.VISIBLE else View.GONE
+
+ weeklyHeader.view.visibility = visibility
+ dayPreferences.values.forEach { preference ->
+ preference.view.visibility = visibility
+ }
+ }
+
+ private fun updateDaySummary(day: WeekDay) {
+ val preference = dayPreferences[day] ?: return
+ val schedule = weeklySchedule.get(day)
+ val startText = formatTime(schedule.startMinutes)
+ val stopText = formatTime(schedule.stopMinutes)
+
+ preference.summary = context.getString(
+ R.string.auto_switch_day_summary,
+ startText,
+ stopText,
+ )
+ }
+
+ private suspend fun requestTime(initial: Int?): Int? = suspendCancellableCoroutine { cont ->
+ val is24Hour = DateFormat.is24HourFormat(context)
+ val initialHour = initial?.div(60) ?: 0
+ val initialMinute = initial?.rem(60) ?: 0
+ var resumed = false
+
+ val dialog = TimePickerDialog(
+ context,
+ { _, hour, minute ->
+ if (!resumed && cont.isActive) {
+ resumed = true
+ cont.resume(hour * 60 + minute)
+ }
+ },
+ initialHour,
+ initialMinute,
+ is24Hour,
+ )
+
+ dialog.setOnCancelListener {
+ if (!resumed && cont.isActive) {
+ resumed = true
+ cont.resume(null)
+ }
+ }
+ dialog.setOnDismissListener {
+ if (!resumed && cont.isActive) {
+ resumed = true
+ cont.resume(null)
+ }
+ }
+ dialog.setButton(TimePickerDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel)) { _, _ ->
+ if (!resumed && cont.isActive) {
+ resumed = true
+ cont.resume(null)
+ }
+ }
+
+ dialog.show()
+
+ cont.invokeOnCancellation {
+ dialog.dismiss()
+ }
+ }
+
+ private fun showDayOptions(day: WeekDay) {
+ val schedule = weeklySchedule.get(day)
+
+ val items = arrayOf(
+ context.getString(R.string.auto_switch_set_start_time),
+ context.getString(R.string.auto_switch_clear_start_time),
+ context.getString(R.string.auto_switch_set_stop_time),
+ context.getString(R.string.auto_switch_clear_stop_time),
+ )
+
+ MaterialAlertDialogBuilder(context)
+ .setTitle(day.displayName(context))
+ .setItems(items) { _, which ->
+ when (which) {
+ 0 -> launch(Dispatchers.Main) {
+ val minutes = requestTime(schedule.startMinutes)
+ if (minutes != null) {
+ updateSchedule(day) { it.copy(startMinutes = minutes) }
+ }
+ }
+
+ 1 -> updateSchedule(day) { it.copy(startMinutes = null) }
+
+ 2 -> launch(Dispatchers.Main) {
+ val minutes = requestTime(schedule.stopMinutes)
+ if (minutes != null) {
+ updateSchedule(day) { it.copy(stopMinutes = minutes) }
+ }
+ }
+
+ 3 -> updateSchedule(day) { it.copy(stopMinutes = null) }
+ }
+ }
+ .show()
+ }
+
+ private fun updateSchedule(
+ day: WeekDay,
+ transform: (DailyAutoSwitchSchedule) -> DailyAutoSwitchSchedule,
+ ) {
+ launch(Dispatchers.Main) {
+ val updated = withContext(Dispatchers.IO) {
+ val schedule = serviceStore.autoSwitchWeeklySchedule
+ val newSchedule = schedule.update(day) { transform(it) }
+ serviceStore.autoSwitchWeeklySchedule = newSchedule
+ AutoSwitchReceiver.requestReschedule(context)
+ newSchedule
+ }
+
+ weeklySchedule = updated
+ updateDaySummary(day)
+ }
+ }
+
+ private fun formatTime(minutes: Int?): String {
+ if (minutes == null) {
+ return context.getString(R.string.auto_switch_time_not_set)
+ }
+
+ val calendar = Calendar.getInstance().apply {
+ set(Calendar.HOUR_OF_DAY, minutes / 60)
+ set(Calendar.MINUTE, minutes % 60)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }
+
+ return DateFormat.getTimeFormat(context).format(calendar.time)
+ }
+
+ private fun WeekDay.displayName(context: Context): String {
+ val names = DateFormatSymbols.getInstance().weekdays
+ return names.getOrNull(calendarValue) ?: name
+ }
+
+ private fun PreferenceScreen.addCategory(text: Int): Preference {
+ val binding = PreferenceCategoryBinding.inflate(context.layoutInflater, root, false)
+ binding.textView.text = context.getString(text)
+
+ val preference = object : Preference {
+ override val view: View
+ get() = binding.root
+ }
+
+ addElement(preference)
+ return preference
+ }
+}
diff --git a/design/src/main/res/drawable/ic_baseline_schedule.xml b/design/src/main/res/drawable/ic_baseline_schedule.xml
new file mode 100644
index 0000000000..db8574ae83
--- /dev/null
+++ b/design/src/main/res/drawable/ic_baseline_schedule.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/design/src/main/res/values-zh/strings.xml b/design/src/main/res/values-zh/strings.xml
index d1eb2b75b4..dc7f0055a3 100644
--- a/design/src/main/res/values-zh/strings.xml
+++ b/design/src/main/res/values-zh/strings.xml
@@ -65,6 +65,21 @@
设置
显示流量
在通知中自动刷新流量
+ 定时开关
+ 根据自定义计划自动启动或停止 Clash
+ 定时开关
+ 策略
+ 定时开关策略
+ 关闭
+ 按周计划
+ 按周计划
+ 日期
+ 开启:%1$s • 关闭:%2$s
+ 未设置
+ 设置开启时间
+ 清除开启时间
+ 设置关闭时间
+ 清除关闭时间
允许 Clash 自动重启
自动重启
已停止
diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml
index 9681f9f732..128b5bd640 100644
--- a/design/src/main/res/values/strings.xml
+++ b/design/src/main/res/values/strings.xml
@@ -235,6 +235,21 @@
Show Traffic
Auto refresh traffic in notification
+ Auto Switch
+ Automatically start or stop Clash on a custom schedule
+ Auto Switch
+ Strategy
+ Auto switch strategy
+ Disabled
+ Weekly schedule
+ Weekly schedule
+ Day
+ Start: %1$s • Stop: %2$s
+ Not set
+ Set start time
+ Clear start time
+ Set stop time
+ Clear stop time
Follow System (Android 10+)
Always Dark
diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml
index 167cddd595..6feb9e3ad7 100644
--- a/service/src/main/AndroidManifest.xml
+++ b/service/src/main/AndroidManifest.xml
@@ -85,5 +85,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/service/src/main/java/com/github/kr328/clash/service/AutoSwitchReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/AutoSwitchReceiver.kt
new file mode 100644
index 0000000000..e489abc0b0
--- /dev/null
+++ b/service/src/main/java/com/github/kr328/clash/service/AutoSwitchReceiver.kt
@@ -0,0 +1,138 @@
+package com.github.kr328.clash.service
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.net.VpnService
+import android.os.Build
+import com.github.kr328.clash.common.compat.pendingIntentFlags
+import com.github.kr328.clash.common.compat.startForegroundServiceCompat
+import com.github.kr328.clash.common.constants.Intents
+import com.github.kr328.clash.common.log.Log
+import com.github.kr328.clash.common.util.intent
+import com.github.kr328.clash.service.model.AutoSwitchAction
+import com.github.kr328.clash.service.model.AutoSwitchStrategyType
+import com.github.kr328.clash.service.model.ScheduledAutoSwitch
+import com.github.kr328.clash.service.store.ServiceStore
+import com.github.kr328.clash.service.util.sendBroadcastSelf
+import java.util.TimeZone
+
+class AutoSwitchReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intents.ACTION_AUTO_SWITCH_RESCHEDULE,
+ Intent.ACTION_BOOT_COMPLETED,
+ Intent.ACTION_MY_PACKAGE_REPLACED,
+ Intent.ACTION_TIMEZONE_CHANGED,
+ Intent.ACTION_TIME_CHANGED,
+ Intent.ACTION_TIME_SET -> schedule(context)
+
+ Intents.ACTION_AUTO_SWITCH_TRIGGER -> {
+ val actionName = intent.getStringExtra(EXTRA_ACTION) ?: return
+ val action = runCatching { AutoSwitchAction.valueOf(actionName) }.getOrNull() ?: return
+
+ when (action) {
+ AutoSwitchAction.Start -> startClash(context)
+ AutoSwitchAction.Stop -> stopClash(context)
+ }
+
+ schedule(context)
+ }
+ }
+ }
+
+ companion object {
+ private const val EXTRA_ACTION = "action"
+
+ fun requestReschedule(context: Context) {
+ val appContext = context.applicationContext
+ val intent = Intent(appContext, AutoSwitchReceiver::class.java).apply {
+ action = Intents.ACTION_AUTO_SWITCH_RESCHEDULE
+ }
+ appContext.sendBroadcastSelf(intent)
+ }
+
+ private fun schedule(context: Context) {
+ val appContext = context.applicationContext
+ val store = ServiceStore(appContext)
+ val strategy = store.autoSwitchStrategy
+
+ val alarmManager = appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val cancelIntent = pendingIntent(appContext, null)
+ alarmManager.cancel(cancelIntent)
+
+ if (strategy == AutoSwitchStrategyType.None) {
+ return
+ }
+
+ val next = when (strategy) {
+ AutoSwitchStrategyType.None -> null
+ AutoSwitchStrategyType.Weekly -> store.autoSwitchWeeklySchedule.next(
+ System.currentTimeMillis(),
+ TimeZone.getDefault(),
+ )
+ }
+
+ if (next == null) {
+ return
+ }
+
+ val triggerIntent = pendingIntent(appContext, next)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.RTC_WAKEUP,
+ next.triggerAtMillis,
+ triggerIntent,
+ )
+ } else {
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP,
+ next.triggerAtMillis,
+ triggerIntent,
+ )
+ }
+ }
+
+ private fun startClash(context: Context) {
+ val appContext = context.applicationContext
+ if (useVpnMode(appContext)) {
+ val vpnIntent = VpnService.prepare(appContext)
+ if (vpnIntent != null) {
+ Log.w("AutoSwitch", "VPN permission required, skip auto start")
+ return
+ }
+ appContext.startForegroundServiceCompat(TunService::class.intent)
+ } else {
+ appContext.startForegroundServiceCompat(ClashService::class.intent)
+ }
+ }
+
+ private fun stopClash(context: Context) {
+ context.applicationContext.sendBroadcastSelf(Intent(Intents.ACTION_CLASH_REQUEST_STOP))
+ }
+
+ private fun pendingIntent(context: Context, next: ScheduledAutoSwitch?): PendingIntent {
+ val intent = Intent(context, AutoSwitchReceiver::class.java).apply {
+ action = Intents.ACTION_AUTO_SWITCH_TRIGGER
+ if (next != null) {
+ putExtra(EXTRA_ACTION, next.action.name)
+ }
+ }
+
+ return PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT),
+ )
+ }
+
+ private fun useVpnMode(context: Context): Boolean {
+ val preferences = context.getSharedPreferences("ui", Context.MODE_PRIVATE)
+ return preferences.getBoolean("enable_vpn", true)
+ }
+ }
+}
diff --git a/service/src/main/java/com/github/kr328/clash/service/model/AutoSwitch.kt b/service/src/main/java/com/github/kr328/clash/service/model/AutoSwitch.kt
new file mode 100644
index 0000000000..0b92494ace
--- /dev/null
+++ b/service/src/main/java/com/github/kr328/clash/service/model/AutoSwitch.kt
@@ -0,0 +1,117 @@
+package com.github.kr328.clash.service.model
+
+import kotlinx.serialization.Serializable
+import java.util.Calendar
+import java.util.EnumMap
+import java.util.TimeZone
+
+@Serializable
+enum class AutoSwitchStrategyType {
+ None,
+ Weekly,
+}
+
+@Serializable
+enum class WeekDay(val calendarValue: Int) {
+ Sunday(Calendar.SUNDAY),
+ Monday(Calendar.MONDAY),
+ Tuesday(Calendar.TUESDAY),
+ Wednesday(Calendar.WEDNESDAY),
+ Thursday(Calendar.THURSDAY),
+ Friday(Calendar.FRIDAY),
+ Saturday(Calendar.SATURDAY);
+
+ companion object {
+ fun fromCalendar(value: Int): WeekDay {
+ return values().firstOrNull { it.calendarValue == value } ?: Sunday
+ }
+ }
+}
+
+@Serializable
+data class DailyAutoSwitchSchedule(
+ val startMinutes: Int? = null,
+ val stopMinutes: Int? = null,
+)
+
+@Serializable
+data class WeeklyAutoSwitchSchedule(
+ val entries: Map = defaultEntries(),
+) {
+ fun get(day: WeekDay): DailyAutoSwitchSchedule {
+ return entries[day] ?: DailyAutoSwitchSchedule()
+ }
+
+ fun update(
+ day: WeekDay,
+ transform: (DailyAutoSwitchSchedule) -> DailyAutoSwitchSchedule,
+ ): WeeklyAutoSwitchSchedule {
+ val map = EnumMap(WeekDay::class.java)
+ map.putAll(entries)
+ map[day] = transform(map[day] ?: DailyAutoSwitchSchedule())
+ return WeeklyAutoSwitchSchedule(map)
+ }
+
+ fun next(nowMillis: Long, zone: TimeZone): ScheduledAutoSwitch? {
+ var best: ScheduledAutoSwitch? = null
+
+ for (offset in 0..7) {
+ val dayCalendar = Calendar.getInstance(zone).apply {
+ timeInMillis = nowMillis
+ add(Calendar.DAY_OF_YEAR, offset)
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }
+
+ val day = WeekDay.fromCalendar(dayCalendar.get(Calendar.DAY_OF_WEEK))
+ val schedule = get(day)
+
+ schedule.startMinutes?.let { minutes ->
+ val triggerAt = triggerTime(dayCalendar, minutes)
+ if (triggerAt > nowMillis) {
+ val candidate = ScheduledAutoSwitch(triggerAt, AutoSwitchAction.Start)
+ best = best?.takeIf { it.triggerAtMillis <= candidate.triggerAtMillis } ?: candidate
+ }
+ }
+
+ schedule.stopMinutes?.let { minutes ->
+ val triggerAt = triggerTime(dayCalendar, minutes)
+ if (triggerAt > nowMillis) {
+ val candidate = ScheduledAutoSwitch(triggerAt, AutoSwitchAction.Stop)
+ best = best?.takeIf { it.triggerAtMillis <= candidate.triggerAtMillis } ?: candidate
+ }
+ }
+ }
+
+ return best
+ }
+
+ companion object {
+ private fun defaultEntries(): Map {
+ val map = EnumMap(WeekDay::class.java)
+ WeekDay.values().forEach { map[it] = DailyAutoSwitchSchedule() }
+ return map
+ }
+
+ private fun triggerTime(base: Calendar, minutes: Int): Long {
+ val calendar = base.clone() as Calendar
+ calendar.set(Calendar.HOUR_OF_DAY, minutes / 60)
+ calendar.set(Calendar.MINUTE, minutes % 60)
+ calendar.set(Calendar.SECOND, 0)
+ calendar.set(Calendar.MILLISECOND, 0)
+ return calendar.timeInMillis
+ }
+ }
+}
+
+data class ScheduledAutoSwitch(
+ val triggerAtMillis: Long,
+ val action: AutoSwitchAction,
+)
+
+enum class AutoSwitchAction {
+ Start,
+ Stop,
+}
diff --git a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt
index d361848ff5..7e8073833b 100644
--- a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt
+++ b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt
@@ -5,7 +5,12 @@ import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
import com.github.kr328.clash.service.PreferenceProvider
import com.github.kr328.clash.service.model.AccessControlMode
+import com.github.kr328.clash.service.model.AutoSwitchStrategyType
+import com.github.kr328.clash.service.model.WeeklyAutoSwitchSchedule
import java.util.*
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.decodeFromString
class ServiceStore(context: Context) {
private val store = Store(
@@ -65,4 +70,34 @@ class ServiceStore(context: Context) {
key = "dynamic_notification",
defaultValue = true
)
-}
\ No newline at end of file
+
+ var autoSwitchStrategy: AutoSwitchStrategyType by store.enum(
+ key = "auto_switch_strategy",
+ defaultValue = AutoSwitchStrategyType.None,
+ values = AutoSwitchStrategyType.values(),
+ )
+
+ private var autoSwitchWeeklyScheduleRaw: WeeklyAutoSwitchSchedule? by store.typedString(
+ key = "auto_switch_weekly_schedule",
+ from = {
+ if (it.isBlank()) {
+ null
+ } else {
+ runCatching { json.decodeFromString(it) }.getOrNull()
+ }
+ },
+ to = { schedule ->
+ schedule?.let { json.encodeToString(it) } ?: ""
+ },
+ )
+
+ var autoSwitchWeeklySchedule: WeeklyAutoSwitchSchedule
+ get() = autoSwitchWeeklyScheduleRaw ?: WeeklyAutoSwitchSchedule()
+ set(value) {
+ autoSwitchWeeklyScheduleRaw = value
+ }
+
+ companion object {
+ private val json = Json { ignoreUnknownKeys = true }
+ }
+}