This commit is contained in:
Kr328 committed 2020-02-22 00:35:25 +08:00
1 parent 14aa981ca4
commit f4b421c953
10 files changed
+263 -165

No files matched your search

@@ -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()
}
@@ -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<ProxyInfo>
)
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<String> = emptySet()
private var renderList = mutableListOf<RenderInfo>()
private var activeList: MutableMap<String, Int> = mutableMapOf()
@@ -98,7 +96,7 @@ class ProxyAdapter(
private var root = listOf<ProxyGroupInfo>()
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<ProxyGroupInfo>, testing: Set<String>) =
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)
}
@@ -0,0 +1,5 @@
package com.github.kr328.clash.pipeline
import com.github.kr328.clash.service.settings.BaseSettings
data class Pipeline<T>(val input: T, val settings: BaseSettings)
@@ -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<List<ProxyGroup>>.mergePrefix(): Pipeline<Map<ProxyEntry, ProxyMerged>> {
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<List<ProxyGroup>>.sort(): Pipeline<List<ProxyGroup>> {
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<List<ProxyGroup>>.toAdapterElement(prefixMerged: Map<ProxyEntry, ProxyMerged>, general: General):
List<ProxyAdapter.ProxyGroupInfo> {
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"}
}
}
}
}
@@ -11,16 +11,19 @@ object PrefixMerger {
suspend fun <T> merge(values: List<T>, transform: (T) -> String): List<Result<T>> =
withContext(Dispatchers.Default) {
val pairs = values.map {
transform(it).trim() to it
transform(it).trim().toCodePointList() to it
}
val groups = mutableListOf<List<Pair<String, T>>>()
var mergingGroup = mutableListOf<Pair<String, T>>()
val groups = mutableListOf<List<Pair<List<Int>, T>>>()
var mergingGroup = mutableListOf<Pair<List<Int>, T>>()
var currentCodePoint = 0
val result = mutableListOf<Result<T>>()
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))
}
}
@@ -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)
}
}
@@ -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)
}
@@ -0,0 +1,27 @@
package com.github.kr328.clash.utils
import java.lang.StringBuilder
fun String.toCodePointList(): List<Int> {
var offset = 0
val result = mutableListOf<Int>()
while ( offset < length ) {
val codePoint = codePointAt(offset)
result.add(codePoint)
offset += Character.charCount(codePoint)
}
return result.toList()
}
fun List<Int>.asCodePointString(): String {
val sb = StringBuilder()
forEach {
sb.appendCodePoint(it)
}
return sb.toString()
}
+2
View File
@@ -170,4 +170,6 @@
<string name="about_split_apks_url" translatable="false">https://developer.android.com/platform/technology/app-bundle</string>
<string name="google_play_url" translatable="false">https://play.google.com/store/apps/details?id=com.github.kr328.clash</string>
<string name="github_releases_url" translatable="false">https://github.com/Kr328/ClashForAndroid/releases</string>
<string name="format_proxy_group_title" translatable="false">%s - %s</string>
</resources>
@@ -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"
}
}