diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index 5ffd367..f2cfc58 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -9,30 +9,21 @@ import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.adapter.ProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.core.model.General -import com.github.kr328.clash.core.model.Proxy -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.pipeline.Pipeline +import com.github.kr328.clash.pipeline.mergePrefix +import com.github.kr328.clash.pipeline.sort +import com.github.kr328.clash.pipeline.toAdapterElement import com.github.kr328.clash.preference.UiSettings import com.github.kr328.clash.remote.withClash -import com.github.kr328.clash.utils.PrefixMerger -import com.github.kr328.clash.utils.ProxySorter import com.github.kr328.clash.utils.ScrollBinding import kotlinx.android.synthetic.main.activity_proxies.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withContext class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { private val refreshMutex = Mutex() private val scrollBinding = ScrollBinding(this, this) - private val doScrollToLastProxy by lazy { - val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP) - - launch { - scrollBinding.scrollMaster(selected) - } - } + private var scrollToLast = true private val mainListAdapter: ProxyAdapter get() = mainList.adapter as ProxyAdapter @@ -45,35 +36,10 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { setContentView(R.layout.activity_proxies) setSupportActionBar(toolbar) - mainList.adapter = ProxyAdapter(this, { group, proxy -> - launch { - withClash { - setSelectProxy(group, proxy) - } - } - }, { - launch { - urlTesting.add(it) - - withClash { - urlTesting.add(it) - - startHealthCheck(it) - - urlTesting.remove(it) - - refreshList() - } - } - }) - + mainList.adapter = ProxyAdapter(this, this::setGroupSelected, this::startUrlTesting) mainList.layoutManager = mainListAdapter.layoutManager - chipList.adapter = ProxyChipAdapter(this) { - launch { - scrollBinding.scrollMaster(it) - } - } + chipList.adapter = ProxyChipAdapter(this, this::chipClicked) chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) chipList.itemAnimator?.changeDuration = 0 @@ -195,7 +161,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { override val activityLabel: CharSequence? get() = getText(R.string.proxy) - override suspend fun onClashStarted() { + override suspend fun onClashStopped(reason: String?) { finish() } @@ -237,6 +203,34 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { } } + private fun setGroupSelected(group: String, select: String) { + launch { + withClash { + setSelectProxy(group, select) + } + } + } + + private fun startUrlTesting(group: String) { + launch { + urlTesting.add(group) + + withClash { + startHealthCheck(group) + } + + urlTesting.remove(group) + + refreshList() + } + } + + private fun chipClicked(name: String) { + launch { + scrollBinding.scrollMaster(name) + } + } + private fun refreshList(scrollTop: Boolean = false) { launch { if ( !refreshMutex.tryLock() ) @@ -249,93 +243,27 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { queryAllProxyGroups() } - val prefixDeferred = async { - if (uiSettings.get(UiSettings.PROXY_MERGE_PREFIX)) { - proxies.map { - async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } } - }.flatMap { - it.await() - }.map { - it.value to it - }.toMap() - } else emptyMap() - } + val merged = Pipeline(proxies, uiSettings).mergePrefix() + val sorted = Pipeline(proxies, uiSettings).sort() - val sortDeferred = async { - val groupSort = when (uiSettings.get(UiSettings.PROXY_GROUP_SORT)) { - UiSettings.PROXY_SORT_DEFAULT -> - ProxySorter.Order.DEFAULT - UiSettings.PROXY_SORT_NAME -> - ProxySorter.Order.NAME_INCREASE - UiSettings.PROXY_SORT_DELAY -> - ProxySorter.Order.DELAY_INCREASE - else -> throw IllegalArgumentException() - } - - val proxySort = when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) { - UiSettings.PROXY_SORT_DEFAULT -> - ProxySorter.Order.DEFAULT - UiSettings.PROXY_SORT_NAME -> - ProxySorter.Order.NAME_INCREASE - UiSettings.PROXY_SORT_DELAY -> - ProxySorter.Order.DELAY_INCREASE - else -> throw IllegalArgumentException() - } - - val sorter = ProxySorter(groupSort, proxySort) - - sorter.sort(proxies).run { - when (general.mode) { - General.Mode.GLOBAL -> this - General.Mode.DIRECT -> emptyList() - General.Mode.RULE -> this.filter { it.name != "GLOBAL" } - } - } - } - - val prefix = prefixDeferred.await() - val sorted = sortDeferred.await() - - val newList = withContext(Dispatchers.Default) { - sorted.map { - ProxyAdapter.ProxyGroupInfo(it.name, - it.proxies.map { p -> - val r = prefix.getOrElse(it.name to p.name) { - PrefixMerger.Result(p.name, "", p) - } - - val data = if ( r.content.isEmpty() ) { - r.prefix to p.type.toString() - } - else { - r.content to r.prefix - } - - ProxyAdapter.ProxyInfo( - p.name, - it.name, - data.first, - data.second, - p.delay.toShort(), - it.type == Proxy.Type.SELECT, - p.name == it.current - ) - } - ) - } - } + val newList = sorted.toAdapterElement(merged.input, general) mainListAdapter.applyChange(newList, urlTesting) (chipList.adapter!! as ProxyChipAdapter).apply { - chips = sorted.map { it.name } + chips = newList.map { it.name } notifyDataSetChanged() } - doScrollToLastProxy - if ( scrollTop ) mainList.smoothScrollToPosition(0) + else if ( scrollToLast ) { + scrollToLast = false + + val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP) + + scrollBinding.scrollMaster(selected) + } refreshMutex.unlock() } diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index d735031..dedc89a 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -14,7 +14,6 @@ import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.R import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext @@ -29,14 +28,15 @@ class ProxyAdapter( data class ProxyGroupInfo( val name: String, + val current: String, val proxies: List ) data class ProxyInfo( val name: String, val group: String, - val prefix: String, - val content: String, + val title: String, + val summary: String, val delay: Short, val selectable: Boolean, val active: Boolean @@ -62,8 +62,6 @@ class ProxyAdapter( get() = info.group } - - private var rootMutex = Mutex() private var urlTesting: Set = emptySet() private var renderList = mutableListOf() private var activeList: MutableMap = mutableMapOf() @@ -98,7 +96,7 @@ class ProxyAdapter( private var root = listOf() private class ProxyGroupHeader(view: View) : RecyclerView.ViewHolder(view) { - val name: TextView = view.findViewById(R.id.name) + val title: TextView = view.findViewById(R.id.name) val urlTest: View = view.findViewById(R.id.urlTest) val urlTestProgress: View = view.findViewById(R.id.urlTestProgress) } @@ -112,8 +110,6 @@ class ProxyAdapter( suspend fun applyChange(newList: List, testing: Set) = withContext(Dispatchers.Default) { - rootMutex.lock() - val newRenderList = newList .flatMap { listOf(ProxyGroupRenderInfo(it)) + it.proxies.map { p -> ProxyRenderInfo(p) } @@ -155,8 +151,6 @@ class ProxyAdapter( activeList = activeCache result.dispatchUpdatesTo(this@ProxyAdapter) } - - rootMutex.unlock() } fun getGroupPosition(name: String): Int { @@ -195,7 +189,8 @@ class ProxyAdapter( groupPosition[current.name] = position - holder.name.text = current.info.name + holder.title.text = context.getString(R.string.format_proxy_group_title, + current.info.name, current.info.current) holder.urlTest.setOnClickListener { holder.urlTest.visibility = View.GONE holder.urlTestProgress.visibility = View.VISIBLE @@ -214,8 +209,8 @@ class ProxyAdapter( is ProxyItem -> { val current = renderList[position] as ProxyRenderInfo - holder.prefix.text = current.info.prefix - holder.content.text = current.info.content + holder.prefix.text = current.info.title + holder.content.text = current.info.summary if (current.info.delay > 0) holder.delay.text = current.info.delay.toString() @@ -239,14 +234,18 @@ class ProxyAdapter( if (current.info.selectable) { holder.root.setOnClickListener { val oldPosition = activeList[current.group] ?: return@setOnClickListener + val groupPosition = groupPosition[current.group] ?: return@setOnClickListener val old = renderList[oldPosition] as ProxyRenderInfo val new = renderList[position] as ProxyRenderInfo + val group = renderList[groupPosition] as ProxyGroupRenderInfo renderList[oldPosition] = old.copy(info = old.info.copy(active = false)) renderList[position] = new.copy(info = new.info.copy(active = true)) + renderList[groupPosition] = group.copy(info = group.info.copy(current = current.name)) notifyItemChanged(oldPosition) notifyItemChanged(position) + notifyItemChanged(groupPosition) onSelect(current.group, current.name) } diff --git a/app/src/main/java/com/github/kr328/clash/pipeline/Pipeline.kt b/app/src/main/java/com/github/kr328/clash/pipeline/Pipeline.kt new file mode 100644 index 0000000..d7da00a --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/pipeline/Pipeline.kt @@ -0,0 +1,5 @@ +package com.github.kr328.clash.pipeline + +import com.github.kr328.clash.service.settings.BaseSettings + +data class Pipeline(val input: T, val settings: BaseSettings) \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/pipeline/ProxiesPipeline.kt b/app/src/main/java/com/github/kr328/clash/pipeline/ProxiesPipeline.kt new file mode 100644 index 0000000..c77665f --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/pipeline/ProxiesPipeline.kt @@ -0,0 +1,93 @@ +package com.github.kr328.clash.pipeline + +import com.github.kr328.clash.adapter.ProxyAdapter +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.Proxy +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.preference.UiSettings +import com.github.kr328.clash.utils.PrefixMerger +import com.github.kr328.clash.utils.ProxySorter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext + +data class ProxyEntry(val group: String, val name: String) +data class ProxyMerged(val prefix: String, val content: String) + +suspend fun Pipeline>.mergePrefix(): Pipeline> { + if ( !settings.get(UiSettings.PROXY_MERGE_PREFIX) ) + return Pipeline(emptyMap(), settings) + + val result = coroutineScope { + input + .map { + async { + it.name to PrefixMerger.merge(it.proxies, Proxy::name) + } + } + .map { + it.await() + } + .flatMap { + it.second.map { merged -> + ProxyEntry(it.first, merged.value.name) to ProxyMerged(merged.prefix, merged.content) + } + } + .toMap() + } + + return Pipeline(result, settings) +} + +suspend fun Pipeline>.sort(): Pipeline> { + val groupSort = when (settings.get(UiSettings.PROXY_GROUP_SORT)) { + UiSettings.PROXY_SORT_DEFAULT -> + ProxySorter.Order.DEFAULT + UiSettings.PROXY_SORT_NAME -> + ProxySorter.Order.NAME_INCREASE + UiSettings.PROXY_SORT_DELAY -> + ProxySorter.Order.DELAY_INCREASE + else -> throw IllegalArgumentException() + } + + val proxySort = when (settings.get(UiSettings.PROXY_PROXY_SORT)) { + UiSettings.PROXY_SORT_DEFAULT -> + ProxySorter.Order.DEFAULT + UiSettings.PROXY_SORT_NAME -> + ProxySorter.Order.NAME_INCREASE + UiSettings.PROXY_SORT_DELAY -> + ProxySorter.Order.DELAY_INCREASE + else -> throw IllegalArgumentException() + } + + val sorter = ProxySorter(groupSort, proxySort) + + return copy(input = sorter.sort(input)) +} + +suspend fun Pipeline>.toAdapterElement(prefixMerged: Map, general: General): + List { + return input.map { group -> + val proxies = group.proxies.map { proxy -> + val merged = prefixMerged[ProxyEntry(group.name, proxy.name)]?.takeIf { + it.prefix.isNotBlank() && it.content.isNotBlank() + } ?: ProxyMerged(proxy.type.toString(), proxy.name) + + ProxyAdapter.ProxyInfo(proxy.name, group.name, merged.content, merged.prefix, + proxy.delay.toShort(), group.type == Proxy.Type.SELECT, + group.current == proxy.name) + } + + + ProxyAdapter.ProxyGroupInfo(group.name, group.current, proxies) + }.let { + withContext(Dispatchers.Default) { + when ( general.mode ) { + General.Mode.DIRECT -> emptyList() + General.Mode.GLOBAL -> it + General.Mode.RULE -> it.filter { it.name != "GLOBAL"} + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt b/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt index 89a1e25..f8add81 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt @@ -11,16 +11,19 @@ object PrefixMerger { suspend fun merge(values: List, transform: (T) -> String): List> = withContext(Dispatchers.Default) { val pairs = values.map { - transform(it).trim() to it + transform(it).trim().toCodePointList() to it } - val groups = mutableListOf>>() - var mergingGroup = mutableListOf>() + val groups = mutableListOf, T>>>() + var mergingGroup = mutableListOf, T>>() var currentCodePoint = 0 val result = mutableListOf>() for (pair in pairs) { - if (pair.first.codePointAt(0) == currentCodePoint) { + if ( pair.first.isEmpty() ) + continue + + if (pair.first[0] == currentCodePoint) { mergingGroup.add(pair) } else { if (mergingGroup.isNotEmpty()) { @@ -28,7 +31,7 @@ object PrefixMerger { mergingGroup = mutableListOf() } - currentCodePoint = pair.first.codePointAt(0) + currentCodePoint = pair.first[0] mergingGroup.add(pair) } } @@ -38,7 +41,7 @@ object PrefixMerger { for (group in groups) { var diffIndex = 0 - val size = group.map { it.first.length }.min() ?: 0 + val size = group.map { it.first.size }.min() ?: 0 diff@ for (charIndex in 0 until size) { for (stringIndex in 0 until (group.size - 1)) { @@ -50,11 +53,11 @@ object PrefixMerger { } group.forEach { - val prefix = it.first.substring(0, diffIndex) - .replace(REGEX_PREFIX_TRIM, "") - val content = it.first.substring(diffIndex) + val prefix = it.first.subList(0, diffIndex) + val content = it.first.subList(diffIndex, it.first.size) - result.add(Result(prefix, content, it.second)) + result.add(Result(prefix.asCodePointString().replace(REGEX_PREFIX_TRIM, ""), + content.asCodePointString(), it.second)) } } diff --git a/app/src/main/java/com/github/kr328/clash/utils/QuickSmoothScroller.kt b/app/src/main/java/com/github/kr328/clash/utils/QuickSmoothScroller.kt new file mode 100644 index 0000000..af4b6cb --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/QuickSmoothScroller.kt @@ -0,0 +1,61 @@ +package com.github.kr328.clash.utils + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView + +class QuickSmoothScroller(context: Context, target: Int): + LinearSmoothScroller(context) { + companion object { + const val MAX_OFFSET = 2 + } + + var started = {} + var stopped = {} + + init { + targetPosition = target + } + + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun onStop() { + super.onStop() + + stopped() + } + + override fun onStart() { + super.onStart() + + started() + } + + override fun onSeekTargetStep(dx: Int, dy: Int, state: RecyclerView.State, action: Action) { + when (val lm = layoutManager) { + is LinearLayoutManager -> { + val current = lm.findFirstCompletelyVisibleItemPosition() + + if (targetPosition > current && targetPosition - current > MAX_OFFSET) + action.jumpTo(targetPosition - MAX_OFFSET) + else if (current > targetPosition && current - targetPosition > MAX_OFFSET) + action.jumpTo(targetPosition + MAX_OFFSET) + } + is GridLayoutManager -> { + val current = lm.findFirstCompletelyVisibleItemPosition() + + if (targetPosition > current && targetPosition - current > MAX_OFFSET) + action.jumpTo(targetPosition - MAX_OFFSET) + else if (current > targetPosition && current - targetPosition > MAX_OFFSET) + action.jumpTo(targetPosition + MAX_OFFSET) + } + } + + super.onSeekTargetStep(dx, dy, state, action) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt index b79dc7d..8519ec4 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt @@ -30,27 +30,7 @@ class ScrollBinding( if (position < 0) return - val scroller = (object : LinearSmoothScroller(context) { - override fun getVerticalSnapPreference(): Int { - return SNAP_TO_START - } - - override fun onStop() { - super.onStop() - - preventSlaveScroll = false - } - - override fun onStart() { - super.onStart() - - preventSlaveScroll = true - } - - init { - targetPosition = position - } - }) + val scroller = QuickSmoothScroller(context, position) callback.doMasterScroll(scroller, position) } diff --git a/app/src/main/java/com/github/kr328/clash/utils/StringUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/StringUtils.kt new file mode 100644 index 0000000..81f3061 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/StringUtils.kt @@ -0,0 +1,27 @@ +package com.github.kr328.clash.utils + +import java.lang.StringBuilder + +fun String.toCodePointList(): List { + var offset = 0 + val result = mutableListOf() + + while ( offset < length ) { + val codePoint = codePointAt(offset) + result.add(codePoint) + + offset += Character.charCount(codePoint) + } + + return result.toList() +} + +fun List.asCodePointString(): String { + val sb = StringBuilder() + + forEach { + sb.appendCodePoint(it) + } + + return sb.toString() +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b4600c..8e836b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -170,4 +170,6 @@ https://developer.android.com/platform/technology/app-bundle https://play.google.com/store/apps/details?id=com.github.kr328.clash https://github.com/Kr328/ClashForAndroid/releases + + %s - %s diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 61b5a5e..5c7a43a 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -89,9 +89,9 @@ class TunService : VpnService(), CoroutineScope by MainScope() { val dnsStrings = dnsServers.map { when ( it ) { is Inet6Address -> - "[${it.hostName}]:53" + "[${it.hostAddress}]:53" else -> - "${it.hostName}:53" + "${it.hostAddress}:53" } }