owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/profile/ChainSettingsActivity.kt

github - dev - back to tree - upstream
/****************************************************************************** * * * 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 } } }