Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@
android:configChanges="uiMode"
android:exported="false"
android:label="@string/app" />
<activity
android:name=".AutoSwitchSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/auto_switch_settings" />
<activity
android:name=".OverrideSettingsActivity"
android:configChanges="uiMode"
Expand Down
11 changes: 9 additions & 2 deletions app/src/main/java/com/github/kr328/clash/AppSettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.kr328.clash
import android.content.ComponentName
import android.content.pm.PackageManager
import com.github.kr328.clash.common.util.componentName
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.AppSettingsDesign
import com.github.kr328.clash.design.model.Behavior
import com.github.kr328.clash.service.store.ServiceStore
Expand Down Expand Up @@ -33,8 +34,14 @@ class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), 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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AutoSwitchSettingsDesign>() {
override suspend fun main() {
val design = AutoSwitchSettingsDesign(
this,
ServiceStore(this),
)

setContentDesign(design)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class AppSettingsDesign(
onHideIconChange: (hide: Boolean) -> Unit,
) : Design<AppSettingsDesign.Request>(context) {
enum class Request {
ReCreateAllActivities
ReCreateAllActivities,
OpenAutoSwitchSettings,
}

private val binding = DesignSettingsCommonBinding
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit>(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, ClickablePreference>(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
}
}
10 changes: 10 additions & 0 deletions design/src/main/res/drawable/ic_baseline_schedule.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2a10,10 0,1 0,10 10A10,10 0,0 0,12 2Zm0,18a8,8 0,1 1,8 -8A8,8 0,0 1,12 20Zm.5,-13h-1v6l5.25,3.15.75,-1.23 -5,-2.92Z" />
</vector>
15 changes: 15 additions & 0 deletions design/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@
<string name="settings">设置</string>
<string name="show_traffic">显示流量</string>
<string name="show_traffic_summary">在通知中自动刷新流量</string>
<string name="auto_switch">定时开关</string>
<string name="auto_switch_summary">根据自定义计划自动启动或停止 Clash</string>
<string name="auto_switch_settings">定时开关</string>
<string name="auto_switch_strategy_category">策略</string>
<string name="auto_switch_strategy_title">定时开关策略</string>
<string name="auto_switch_strategy_none">关闭</string>
<string name="auto_switch_strategy_weekly">按周计划</string>
<string name="auto_switch_weekly_category">按周计划</string>
<string name="auto_switch_day_placeholder">日期</string>
<string name="auto_switch_day_summary">开启:%1$s • 关闭:%2$s</string>
<string name="auto_switch_time_not_set">未设置</string>
<string name="auto_switch_set_start_time">设置开启时间</string>
<string name="auto_switch_clear_start_time">清除开启时间</string>
<string name="auto_switch_set_stop_time">设置关闭时间</string>
<string name="auto_switch_clear_stop_time">清除关闭时间</string>
<string name="allow_clash_auto_restart">允许 Clash 自动重启</string>
<string name="auto_restart">自动重启</string>
<string name="stopped">已停止</string>
Expand Down
Loading