/******************************************************************************
* *
* 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.PreferenceFragmentCompat
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.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProfileManager
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.databinding.LayoutAddEntityBinding
import io.nekohasekai.sagernet.databinding.LayoutProfileBinding
import io.nekohasekai.sagernet.fmt.internal.ChainBean
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.ui.ProfileSelectActivity
import io.nekohasekai.sagernet.utils.FormatFileSizeCompat
class ChainSettingsActivity : ProfileSettingsActivity(R.layout.layout_chain_settings) {
companion object {
const val KEY_PROXY_LIST = "proxyList"
}
override fun createEntity() = ChainBean()
val proxyList = ArrayList()
var proxyListInSavedInstanceState = ""
override fun ChainBean.init() {
DataStore.profileName = name
DataStore.serverProtocol = proxies.joinToString(",")
}
override fun ChainBean.serialize() {
name = DataStore.profileName
proxies = proxyList.map { it.id }
}
override fun PreferenceFragmentCompat.createPreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
addPreferencesFromResource(R.xml.name_preferences)
}
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.chain_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 {
proxyListInSavedInstanceState = it
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(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()
}
}
inner class ProxiesAdapter : RecyclerView.Adapter() {
suspend fun reload() {
val idList = (proxyListInSavedInstanceState.takeIf { it.isNotEmpty() } ?: DataStore.serverProtocol)
.split(",").mapNotNull { it.takeIf { it.isNotBlank() }?.toLong() }
if (idList.isNotEmpty()) {
val profiles = ProfileManager.getProfiles(idList).associateBy { it.id }
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_CHAIN || anotherProfile.type != ProxyEntity.TYPE_CHAIN) return false
if (profile.id == anotherProfile.id) return true
val proxies = profile.chainBean!!.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@ChainSettingsActivity).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@ChainSettingsActivity, 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@ChainSettingsActivity, ProfileSelectActivity::class.java
).apply {
putExtra(ProfileSelectActivity.EXTRA_SELECTED, proxyEntity)
})
}
deleteButton.setOnClickListener {
MaterialAlertDialogBuilder(this@ChainSettingsActivity)
.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
}
}
}