mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed4eaf6bd0 | |||
| 6ed13ed6fd | |||
| 4ddda4902f | |||
| ba53203dbc | |||
| 146f20e13b | |||
| 38554d4e55 | |||
| 23725a3210 | |||
| 50f435f250 | |||
| 80572e566c |
@@ -170,7 +170,9 @@ func (x *CoreController) MeasureDelay(url string) (int64, error) {
|
||||
|
||||
// MeasureOutboundDelay measures the outbound delay for a given configuration and URL
|
||||
func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) {
|
||||
return measureOutboundDelayInternal(ConfigureFileContent, url)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
return measureOutboundDelayInternal(ctx, ConfigureFileContent, url)
|
||||
}
|
||||
|
||||
// MeasureOutboundDelayBatch measures the outbound delay for multiple configurations in parallel
|
||||
@@ -190,28 +192,46 @@ func MeasureOutboundDelayBatch(itemsJson string, url string, callback PingCallba
|
||||
return
|
||||
}
|
||||
|
||||
// Semaphore to limit concurrency (max 128 concurrent tests)
|
||||
sem := make(chan struct{}, 128)
|
||||
var wg sync.WaitGroup
|
||||
// Use a worker pool to process items
|
||||
// Performance tuning: 24 concurrency
|
||||
concurrency := 24
|
||||
if len(items) < concurrency {
|
||||
concurrency = len(items)
|
||||
}
|
||||
|
||||
itemChan := make(chan PingItem, len(items))
|
||||
for _, item := range items {
|
||||
wg.Add(1)
|
||||
go func(it PingItem) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
itemChan <- item
|
||||
}
|
||||
close(itemChan)
|
||||
|
||||
delay, _ := measureOutboundDelayInternal(it.Config, url)
|
||||
if callback != nil {
|
||||
callback.OnResult(it.Guid, delay)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrency)
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for it := range itemChan {
|
||||
// Set a reasonable timeout for each individual test
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
|
||||
delay, _ := measureOutboundDelayInternal(ctx, it.Config, url)
|
||||
cancel() // cancel context as soon as we have a result
|
||||
|
||||
if callback != nil {
|
||||
callback.OnResult(it.Guid, delay)
|
||||
}
|
||||
|
||||
// Small sleep to prevent system congestion
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}(item)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func measureOutboundDelayInternal(ConfigureFileContent string, url string) (int64, error) {
|
||||
func measureOutboundDelayInternal(ctx context.Context, ConfigureFileContent string, url string) (int64, error) {
|
||||
config, err := coreserial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("config load error: %w", err)
|
||||
@@ -237,8 +257,28 @@ func measureOutboundDelayInternal(ConfigureFileContent string, url string) (int6
|
||||
if err := inst.Start(); err != nil {
|
||||
return -1, fmt.Errorf("startup failed: %w", err)
|
||||
}
|
||||
defer inst.Close()
|
||||
return measureInstDelay(context.Background(), inst, url)
|
||||
|
||||
// Measure delay
|
||||
delay, err := measureInstDelay(ctx, inst, url)
|
||||
|
||||
// Close instance with a short timeout to prevent hanging the worker
|
||||
closeCtx, closeCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer closeCancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
inst.Close()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Closed successfully
|
||||
case <-closeCtx.Done():
|
||||
// Close timed out, move on
|
||||
}
|
||||
|
||||
return delay, err
|
||||
}
|
||||
|
||||
// CheckVersionX returns the library and Xray versions
|
||||
@@ -295,7 +335,7 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSHandshakeTimeout: 6 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
DisableKeepAlives: true,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
|
||||
if err != nil {
|
||||
@@ -307,7 +347,7 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 5 * time.Second,
|
||||
Timeout: 6 * time.Second,
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
@@ -323,7 +363,7 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
success := false
|
||||
var lastErr error
|
||||
|
||||
// Use 2 attempts as requested by user
|
||||
// Use 2 attempts
|
||||
const attempts = 2
|
||||
for i := 0; i < attempts; i++ {
|
||||
select {
|
||||
@@ -344,8 +384,8 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
continue
|
||||
}
|
||||
|
||||
// Read body and close resp immediately
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
// Limit reading to 64KB to prevent OOM
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
@@ -358,11 +398,14 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
||||
continue
|
||||
}
|
||||
|
||||
// Strict IP check: the body must contain a valid IP address
|
||||
ipStr := strings.TrimSpace(string(body))
|
||||
if net.ParseIP(ipStr) == nil {
|
||||
lastErr = fmt.Errorf("response body is not a valid IP: %s", ipStr)
|
||||
continue
|
||||
// Relaxed IP check: only if the URL seems like an IP check service
|
||||
isIPCheckUrl := strings.Contains(strings.ToLower(url), "ip")
|
||||
if isIPCheckUrl {
|
||||
ipStr := strings.TrimSpace(string(body))
|
||||
if net.ParseIP(ipStr) == nil {
|
||||
lastErr = fmt.Errorf("response body is not a valid IP: %s", ipStr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(start).Milliseconds()
|
||||
|
||||
@@ -164,7 +164,6 @@ object AppConfig {
|
||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
||||
const val MSG_MEASURE_CONFIG_FINISH = 74
|
||||
const val MSG_MEASURE_CONFIG_BATCH = 75
|
||||
|
||||
/** Notification channel IDs and names. */
|
||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package xyz.zarazaex.olc.dto
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class PingResultItem(
|
||||
val guid: String,
|
||||
val delay: Long
|
||||
) : Serializable
|
||||
|
||||
data class PingProgressUpdate(
|
||||
val results: ArrayList<PingResultItem>,
|
||||
val finished: Int,
|
||||
val total: Int
|
||||
) : Serializable
|
||||
@@ -128,39 +128,4 @@ data class ProfileItem(
|
||||
&& this.pinnedCA256 == obj.pinnedCA256
|
||||
)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = server?.hashCode() ?: 0
|
||||
result = 31 * result + (serverPort?.hashCode() ?: 0)
|
||||
result = 31 * result + (password?.hashCode() ?: 0)
|
||||
result = 31 * result + (method?.hashCode() ?: 0)
|
||||
result = 31 * result + (flow?.hashCode() ?: 0)
|
||||
result = 31 * result + (username?.hashCode() ?: 0)
|
||||
result = 31 * result + (network?.hashCode() ?: 0)
|
||||
result = 31 * result + (headerType?.hashCode() ?: 0)
|
||||
result = 31 * result + (host?.hashCode() ?: 0)
|
||||
result = 31 * result + (path?.hashCode() ?: 0)
|
||||
result = 31 * result + (seed?.hashCode() ?: 0)
|
||||
result = 31 * result + (quicSecurity?.hashCode() ?: 0)
|
||||
result = 31 * result + (quicKey?.hashCode() ?: 0)
|
||||
result = 31 * result + (mode?.hashCode() ?: 0)
|
||||
result = 31 * result + (serviceName?.hashCode() ?: 0)
|
||||
result = 31 * result + (authority?.hashCode() ?: 0)
|
||||
result = 31 * result + (xhttpMode?.hashCode() ?: 0)
|
||||
result = 31 * result + (security?.hashCode() ?: 0)
|
||||
result = 31 * result + (sni?.hashCode() ?: 0)
|
||||
result = 31 * result + (alpn?.hashCode() ?: 0)
|
||||
result = 31 * result + (fingerPrint?.hashCode() ?: 0)
|
||||
result = 31 * result + (publicKey?.hashCode() ?: 0)
|
||||
result = 31 * result + (shortId?.hashCode() ?: 0)
|
||||
result = 31 * result + (secretKey?.hashCode() ?: 0)
|
||||
result = 31 * result + (localAddress?.hashCode() ?: 0)
|
||||
result = 31 * result + (reserved?.hashCode() ?: 0)
|
||||
result = 31 * result + (mtu ?: 0)
|
||||
result = 31 * result + (obfsPassword?.hashCode() ?: 0)
|
||||
result = 31 * result + (portHopping?.hashCode() ?: 0)
|
||||
result = 31 * result + (portHoppingInterval?.hashCode() ?: 0)
|
||||
result = 31 * result + (pinnedCA256?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,18 @@ open class FmtBase {
|
||||
* @return a map of query parameters
|
||||
*/
|
||||
fun getQueryParam(uri: URI): Map<String, String> {
|
||||
return uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.decodeURIComponent(v) } }
|
||||
return uri.rawQuery.orEmpty().split("&")
|
||||
.mapNotNull {
|
||||
val parts = it.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
parts[0] to Utils.decodeURIComponent(parts[1])
|
||||
} else if (parts.isNotEmpty() && parts[0].isNotEmpty()) {
|
||||
parts[0] to ""
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -241,8 +241,8 @@ object AngConfigManager {
|
||||
|
||||
val subItem = MmkvManager.decodeSubscription(subid)
|
||||
|
||||
val oldServerData = if (!append) {
|
||||
saveOldServerData(subid)
|
||||
val oldPingData = if (!append) {
|
||||
saveOldPingData(subid)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
@@ -263,7 +263,7 @@ object AngConfigManager {
|
||||
MmkvManager.removeServerViaSubid(subid)
|
||||
}
|
||||
val keyToProfile = batchSaveConfigs(configs, subid, append)
|
||||
restoreOldServerData(keyToProfile, oldServerData)
|
||||
restoreOldPingData(keyToProfile, oldPingData)
|
||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||
}
|
||||
@@ -330,39 +330,31 @@ object AngConfigManager {
|
||||
return keyToProfile
|
||||
}
|
||||
|
||||
private fun saveOldServerData(subid: String): Map<ProfileItem, Pair<Long, Boolean>> {
|
||||
val serverData = mutableMapOf<ProfileItem, Pair<Long, Boolean>>()
|
||||
private fun saveOldPingData(subid: String): Map<ProfileItem, Long> {
|
||||
val pingData = mutableMapOf<ProfileItem, Long>()
|
||||
val serverList = MmkvManager.decodeServerList(subid)
|
||||
|
||||
serverList.forEach { guid ->
|
||||
val profile = MmkvManager.decodeServerConfig(guid)
|
||||
if (profile != null) {
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
val delay = aff?.testDelayMillis ?: 0L
|
||||
if (delay > 0 || profile.isFavorite) {
|
||||
serverData[profile] = Pair(delay, profile.isFavorite)
|
||||
}
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
if (profile != null && aff != null && aff.testDelayMillis > 0) {
|
||||
pingData[profile] = aff.testDelayMillis
|
||||
}
|
||||
}
|
||||
|
||||
return serverData
|
||||
return pingData
|
||||
}
|
||||
|
||||
private fun restoreOldServerData(keyToProfile: Map<String, ProfileItem>, oldServerData: Map<ProfileItem, Pair<Long, Boolean>>) {
|
||||
if (oldServerData.isEmpty()) return
|
||||
private fun restoreOldPingData(keyToProfile: Map<String, ProfileItem>, oldPingData: Map<ProfileItem, Long>) {
|
||||
if (oldPingData.isEmpty()) return
|
||||
|
||||
keyToProfile.forEach { (key, newProfile) ->
|
||||
val oldData = oldServerData[newProfile]
|
||||
val oldPing = oldPingData.entries.firstOrNull { (oldProfile, _) ->
|
||||
oldProfile == newProfile
|
||||
}?.value
|
||||
|
||||
if (oldData != null) {
|
||||
val (oldPing, isFavorite) = oldData
|
||||
if (oldPing > 0) {
|
||||
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
|
||||
}
|
||||
if (isFavorite) {
|
||||
newProfile.isFavorite = true
|
||||
MmkvManager.encodeServerConfig(key, newProfile)
|
||||
}
|
||||
if (oldPing != null && oldPing > 0) {
|
||||
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,8 @@ import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||
import xyz.zarazaex.olc.dto.PingResultItem
|
||||
import xyz.zarazaex.olc.handler.SettingsManager
|
||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
||||
@@ -33,37 +29,29 @@ class RealPingWorkerService(
|
||||
|
||||
private val totalCount = AtomicInteger(guids.size)
|
||||
private val finishedCount = AtomicInteger(0)
|
||||
private val pendingResults = ArrayList<PingResultItem>()
|
||||
private val pendingLock = Any()
|
||||
|
||||
private val delayTestUrl = SettingsManager.getDelayTestUrl()
|
||||
|
||||
companion object {
|
||||
private const val RESULT_BATCH_SIZE = 32
|
||||
private const val FLUSH_INTERVAL_MS = 1000L
|
||||
}
|
||||
|
||||
data class PingItem(val guid: String, val config: String)
|
||||
|
||||
fun start() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
delay(FLUSH_INTERVAL_MS)
|
||||
flushPendingResults()
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Prepare configurations for batch test and shuffle for better async feel
|
||||
val items =
|
||||
guids.shuffled().mapNotNull { guid ->
|
||||
val configResult =
|
||||
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (configResult.status) {
|
||||
PingItem(guid, configResult.content)
|
||||
} else {
|
||||
// Notify failure immediately for invalid configs
|
||||
try {
|
||||
val configResult =
|
||||
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
if (configResult.status) {
|
||||
PingItem(guid, configResult.content)
|
||||
} else {
|
||||
// Notify failure immediately for invalid configs
|
||||
reportResult(guid, -1L)
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e(AppConfig.TAG, "Failed to prepare config for $guid", e)
|
||||
reportResult(guid, -1L)
|
||||
null
|
||||
}
|
||||
@@ -85,11 +73,13 @@ class RealPingWorkerService(
|
||||
)
|
||||
}
|
||||
|
||||
flushPendingResults()
|
||||
onFinish("0")
|
||||
if (job.isActive) {
|
||||
onFinish("0")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
flushPendingResults()
|
||||
onFinish("-1")
|
||||
if (job.isActive) {
|
||||
onFinish("-1")
|
||||
}
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
@@ -97,43 +87,22 @@ class RealPingWorkerService(
|
||||
}
|
||||
|
||||
private fun reportResult(guid: String, delay: Long) {
|
||||
val finished = finishedCount.incrementAndGet()
|
||||
var readyBatch: PingProgressUpdate? = null
|
||||
synchronized(pendingLock) {
|
||||
pendingResults.add(PingResultItem(guid, delay))
|
||||
if (pendingResults.size >= RESULT_BATCH_SIZE || finished >= totalCount.get()) {
|
||||
readyBatch = createProgressUpdateLocked(finished)
|
||||
pendingResults.clear()
|
||||
if (!job.isActive) return
|
||||
|
||||
// Launch in scope to unblock Go worker immediately
|
||||
scope.launch {
|
||||
val finished = finishedCount.incrementAndGet()
|
||||
val total = guids.size
|
||||
|
||||
// Notify UI about the individual result
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, delay))
|
||||
|
||||
// Throttle progress updates: every 10 items or the very last one
|
||||
if (finished % 10 == 0 || finished == total) {
|
||||
val left = total - finished
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $total")
|
||||
}
|
||||
}
|
||||
readyBatch?.let(::sendBatchUpdate)
|
||||
}
|
||||
|
||||
private fun flushPendingResults() {
|
||||
val finished = finishedCount.get()
|
||||
val update =
|
||||
synchronized(pendingLock) {
|
||||
if (pendingResults.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
createProgressUpdateLocked(finished).also { pendingResults.clear() }
|
||||
}
|
||||
}
|
||||
update?.let(::sendBatchUpdate)
|
||||
}
|
||||
|
||||
private fun createProgressUpdateLocked(finished: Int): PingProgressUpdate {
|
||||
return PingProgressUpdate(
|
||||
results = ArrayList(pendingResults),
|
||||
finished = finished,
|
||||
total = totalCount.get()
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendBatchUpdate(update: PingProgressUpdate) {
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_BATCH, update)
|
||||
val left = (update.total - update.finished).coerceAtLeast(0)
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / ${update.total}")
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
|
||||
@@ -17,8 +17,6 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
@@ -86,12 +84,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.drawerContentLayout) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(0, systemBars.top, 0, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
findViewById<android.widget.TextView>(R.id.drawer_settings)?.setOnClickListener {
|
||||
requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
|
||||
|
||||
@@ -15,6 +15,8 @@ import xyz.zarazaex.olc.databinding.ItemRecyclerFooterBinding
|
||||
import xyz.zarazaex.olc.databinding.ItemRecyclerMainBinding
|
||||
import xyz.zarazaex.olc.dto.ProfileItem
|
||||
import xyz.zarazaex.olc.dto.ServersCache
|
||||
import xyz.zarazaex.olc.extension.toastError
|
||||
import xyz.zarazaex.olc.extension.toastSuccess
|
||||
import xyz.zarazaex.olc.handler.AngConfigManager
|
||||
import xyz.zarazaex.olc.handler.MmkvManager
|
||||
import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter
|
||||
@@ -200,6 +202,14 @@ class MainRecyclerAdapter(
|
||||
mainViewModel.reloadServerList()
|
||||
}
|
||||
|
||||
holder.itemMainBinding.ivCopy.setOnClickListener {
|
||||
if (AngConfigManager.share2Clipboard(context, guid) == 0) {
|
||||
context.toastSuccess(R.string.toast_success)
|
||||
} else {
|
||||
context.toastError(R.string.toast_failure)
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||
adapterListener?.onSelectServer(guid)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import xyz.zarazaex.olc.AngApplication
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.R
|
||||
import xyz.zarazaex.olc.dto.GroupMapItem
|
||||
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||
import xyz.zarazaex.olc.dto.ServersCache
|
||||
import xyz.zarazaex.olc.dto.SubscriptionCache
|
||||
import xyz.zarazaex.olc.dto.SubscriptionUpdateResult
|
||||
@@ -629,14 +628,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
updateListAction.value = getPosition(resultPair.first)
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
|
||||
val update = intent.serializable<PingProgressUpdate>("content") ?: return
|
||||
update.results.forEach { result ->
|
||||
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
|
||||
}
|
||||
updateListAction.value = -1
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
||||
val content = intent.getStringExtra("content")
|
||||
updateTestResultAction.value =
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -151,7 +150,6 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/drawer_content_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
|
||||
@@ -110,6 +110,18 @@
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_star_empty" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_copy"
|
||||
app:tint="?attr/colorAccent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
Reference in New Issue
Block a user