/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai
*
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.ui.profile
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.component1
import androidx.activity.result.component2
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProfileManager
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers
import io.nekohasekai.sagernet.databinding.LayoutAddEntityBinding
import io.nekohasekai.sagernet.databinding.LayoutProfileBinding
import io.nekohasekai.sagernet.fmt.internal.BalancerBean
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.ui.ProfileSelectActivity
import io.nekohasekai.sagernet.utils.FormatFileSizeCompat
import io.nekohasekai.sagernet.widget.GroupPreference
class BalancerSettingsActivity : ProfileSettingsActivity(R.layout.layout_chain_settings) {
companion object {
const val KEY_PROXY_LIST = "proxyList"
}
override fun createEntity() = BalancerBean()
val proxyList = ArrayList()
var proxyListInSavedInstance = ""
override fun BalancerBean.init() {
DataStore.profileName = name
DataStore.balancerType = type
DataStore.balancerStrategy = strategy
DataStore.balancerGroup = groupId
DataStore.serverProtocol = proxies.joinToString(",")
DataStore.balancerProbeUrl = probeUrl
DataStore.balancerProbeInterval = probeInterval
DataStore.balancerNameFilter = nameFilter
DataStore.balancerNameFilter1 = nameFilter1
DataStore.balancerUseLandingProxy = useLandingProxy
DataStore.balancerUseFrontProxy = useFrontProxy
}
override fun BalancerBean.serialize() {
name = DataStore.profileName
type = DataStore.balancerType
strategy = DataStore.balancerStrategy
groupId = DataStore.balancerGroup
proxies = proxyList.map { it.id }
probeUrl = DataStore.balancerProbeUrl
probeInterval = DataStore.balancerProbeInterval
nameFilter = DataStore.balancerNameFilter
nameFilter1 = DataStore.balancerNameFilter1
useLandingProxy = DataStore.balancerUseLandingProxy
useFrontProxy = DataStore.balancerUseFrontProxy
}
lateinit var balancerType: ListPreference
lateinit var balancerGroup: GroupPreference
lateinit var balancerNameFilter: EditTextPreference
lateinit var balancerNameFilter1: EditTextPreference
lateinit var balancerUseLandingProxy: SwitchPreference
lateinit var balancerUseFrontProxy: SwitchPreference
lateinit var probeInterval: EditTextPreference
override fun PreferenceFragmentCompat.createPreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
addPreferencesFromResource(R.xml.balancer_preferences)
balancerType = findPreference(Key.BALANCER_TYPE)!!
balancerGroup = findPreference(Key.BALANCER_GROUP)!!
balancerNameFilter = findPreference(Key.BALANCER_NAME_FILTER)!!
balancerNameFilter1 = findPreference(Key.BALANCER_NAME_FILTER1)!!
balancerUseLandingProxy = findPreference(Key.BALANCER_USE_LANDING_PROXY)!!
balancerUseFrontProxy = findPreference(Key.BALANCER_USE_FRONT_PROXY)!!
probeInterval = findPreference(Key.PROBE_INTERVAL)!!
probeInterval.setOnBindEditTextListener(EditTextPreferenceModifiers.Number)
itemView = findViewById(R.id.list_cell)
balancerType.setOnPreferenceChangeListener { _, newValue ->
updateType(newValue.toString().toInt())
true
}
}
fun updateType(type: Int = DataStore.balancerType) {
when (type) {
BalancerBean.TYPE_LIST -> {
balancerGroup.isVisible = false
balancerNameFilter.isVisible = false
balancerNameFilter1.isVisible = false
balancerUseLandingProxy.isVisible = false
balancerUseFrontProxy.isVisible = false
configurationList.isVisible = true
itemView.isVisible = true
}
BalancerBean.TYPE_GROUP -> {
balancerGroup.isVisible = true
balancerNameFilter.isVisible = true
balancerNameFilter1.isVisible = true
balancerUseLandingProxy.isVisible = true
balancerUseFrontProxy.isVisible = true
configurationList.isVisible = false
itemView.isVisible = false
}
}
}
lateinit var itemView: LinearLayout
lateinit var configurationList: RecyclerView
lateinit var configurationAdapter: ProxiesAdapter
lateinit var layoutManager: LinearLayoutManager
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar!!.setTitle(R.string.balancer_settings)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.scrollView)) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding(
left = bars.left + dp2px(4),
right = bars.right + dp2px(4),
bottom = bars.bottom + dp2px(4),
)
insets
}
configurationList = findViewById(R.id.configuration_list)
layoutManager = FixedLinearLayoutManager(configurationList)
configurationList.layoutManager = layoutManager
configurationAdapter = ProxiesAdapter()
configurationList.adapter = configurationAdapter
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.START
) {
override fun getSwipeDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
) = if (viewHolder is ProfileHolder) {
super.getSwipeDirs(recyclerView, viewHolder)
} else 0
override fun getDragDirs(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
) = if (viewHolder is ProfileHolder) {
super.getDragDirs(recyclerView, viewHolder)
} else 0
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean {
return if (target !is ProfileHolder) false else {
configurationAdapter.move(
viewHolder.adapterPosition, target.adapterPosition
)
true
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
configurationAdapter.remove(viewHolder.adapterPosition)
}
}).attachToRecyclerView(configurationList)
savedInstanceState?.getString(KEY_PROXY_LIST)?.let {
proxyListInSavedInstance = it
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(ChainSettingsActivity.Companion.KEY_PROXY_LIST, proxyList.map { it.id }.joinToString(","))
}
override fun PreferenceFragmentCompat.viewCreated(view: View, savedInstanceState: Bundle?) {
// remove the padding by ProfileSettingsActivity
ViewCompat.setOnApplyWindowInsetsListener(listView) { v, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)
v.updatePadding()
insets
}
view.rootView.findViewById(R.id.recycler_view).apply {
(layoutParams ?: LinearLayout.LayoutParams(-1, -2)).apply {
height = -2
layoutParams = this
}
}
runOnDefaultDispatcher {
configurationAdapter.reload()
}
updateType()
}
inner class ProxiesAdapter : RecyclerView.Adapter() {
suspend fun reload() {
var idList = DataStore.serverProtocol.split(",")
.mapNotNull { it.takeIf { it.isNotBlank() }?.toLong() }
proxyListInSavedInstance.takeIf { it.isNotEmpty() }?.let {
idList = it.split(",")
.mapNotNull { it.takeIf { it.isNotBlank() }?.toLong() }
}
if (idList.isNotEmpty()) {
val profiles = ProfileManager.getProfiles(idList).map { it.id to it }.toMap()
for (id in idList) {
proxyList.add(profiles[id] ?: continue)
}
}
onMainDispatcher {
notifyDataSetChanged()
}
}
fun move(from: Int, to: Int) {
val toMove = proxyList[to - 1]
proxyList[to - 1] = proxyList[from - 1]
proxyList[from - 1] = toMove
notifyItemMoved(from, to)
dirty = true
onBackPressedCallback.isEnabled = true
}
fun remove(index: Int) {
proxyList.removeAt(index - 1)
notifyItemRemoved(index)
dirty = true
onBackPressedCallback.isEnabled = true
}
override fun getItemId(position: Int): Long {
return if (position == 0) 0 else proxyList[position - 1].id
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) 0 else 1
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 0) {
AddHolder(LayoutAddEntityBinding.inflate(layoutInflater, parent, false))
} else {
ProfileHolder(LayoutProfileBinding.inflate(layoutInflater, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is AddHolder) {
holder.bind()
} else if (holder is ProfileHolder) {
holder.bind(proxyList[position - 1])
}
}
override fun getItemCount(): Int {
return proxyList.size + 1
}
}
fun testProfileAllowed(profile: ProxyEntity): Boolean {
return profile.id != DataStore.editingId
}
/*fun testProfileAllowed(profile: ProxyEntity): Boolean {
if (profile.id == DataStore.editingId) return false
for (entity in proxyList) {
if (testProfileContains(entity, profile)) return false
}
return true
}
fun testProfileContains(profile: ProxyEntity, anotherProfile: ProxyEntity): Boolean {
if (profile.type != ProxyEntity.TYPE_BALANCER || anotherProfile.type != ProxyEntity.TYPE_BALANCER) return false
if (profile.id == anotherProfile.id) return true
val proxies = profile.balancerBean!!.proxies
if (proxies.contains(anotherProfile.id)) return true
if (proxies.isNotEmpty()) {
for (entity in ProfileManager.getProfiles(proxies)) {
if (testProfileContains(entity, anotherProfile)) {
return true
}
}
}
return false
}*/
var replacing = 0
val selectProfileForAdd = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { (resultCode, data) ->
if (resultCode == Activity.RESULT_OK) runOnDefaultDispatcher {
dirty = true
onBackPressedCallback.isEnabled = true
val profile = ProfileManager.getProfile(
data!!.getLongExtra(
ProfileSelectActivity.EXTRA_PROFILE_ID, 0
)
)!!
if (!testProfileAllowed(profile)) {
onMainDispatcher {
MaterialAlertDialogBuilder(this@BalancerSettingsActivity).setTitle(R.string.circular_reference)
.setMessage(R.string.circular_reference_sum)
.setPositiveButton(android.R.string.ok, null)
.show()
}
} else {
configurationList.post {
if (replacing != 0) {
proxyList[replacing - 1] = profile
configurationAdapter.notifyItemChanged(replacing)
} else {
proxyList.add(profile)
configurationAdapter.notifyItemInserted(proxyList.size)
}
}
}
}
}
inner class AddHolder(val binding: LayoutAddEntityBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind() {
binding.root.setOnClickListener {
replacing = 0
selectProfileForAdd.launch(
Intent(
this@BalancerSettingsActivity, ProfileSelectActivity::class.java
)
)
}
}
}
inner class ProfileHolder(binding: LayoutProfileBinding) : RecyclerView.ViewHolder(binding.root) {
val profileName = binding.profileName
val profileType = binding.profileType
val trafficText: TextView = binding.trafficText
val editButton = binding.edit
val deleteButton = binding.deleteIcon
val shareLayout = binding.share
fun bind(proxyEntity: ProxyEntity) {
profileName.text = proxyEntity.displayName()
profileType.text = proxyEntity.displayType()
var rx = proxyEntity.rx
var tx = proxyEntity.tx
val stats = proxyEntity.stats
if (stats != null) {
rx += stats.rxTotal
tx += stats.txTotal
}
val showTraffic = rx + tx != 0L
trafficText.isVisible = showTraffic
if (showTraffic) {
trafficText.text = itemView.context.getString(
R.string.traffic,
FormatFileSizeCompat.formatFileSize(itemView.context, tx, DataStore.useIECUnit),
FormatFileSizeCompat.formatFileSize(itemView.context, rx, DataStore.useIECUnit)
)
}
editButton.setOnClickListener {
replacing = adapterPosition
selectProfileForAdd.launch(Intent(
this@BalancerSettingsActivity, ProfileSelectActivity::class.java
).apply {
putExtra(ProfileSelectActivity.EXTRA_SELECTED, proxyEntity)
})
}
deleteButton.setOnClickListener {
MaterialAlertDialogBuilder(this@BalancerSettingsActivity)
.setTitle(getString(R.string.delete_confirm_prompt))
.setPositiveButton(android.R.string.ok) { _, _ ->
configurationAdapter.remove(adapterPosition)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
shareLayout.isVisible = false
}
}
}