4 Commits

Author SHA1 Message Date
zarazaex69 60ce213f67 Replace delay test URL with api.ipify.org 2026-04-26 23:39:29 +03:00
zarazaex69 d0161826e2 Reduce MeasureDelay timeout from 12s to 6s and remove unnecessary delays 2026-04-26 23:14:39 +03:00
zarazaex69 641fcb943d feat: Parallelize config preparation for speed tests 2026-04-26 22:54:17 +03:00
zarazaex69 1fbb4f2bd3 feat: add copy server button to clipboard 2026-04-22 18:48:34 +03:00
13 changed files with 78 additions and 33 deletions
+4 -4
View File
@@ -159,10 +159,10 @@ func (x *CoreController) QueryStats(tag string, direct string) int64 {
}
// MeasureDelay measures network latency to a specified URL through the current core instance
// Uses a 12-second timeout context and returns the round-trip time in milliseconds
// Uses a 6-second timeout context and returns the round-trip time in milliseconds
// An error is returned if the connection fails or returns an unexpected status
func (x *CoreController) MeasureDelay(url string) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
return measureInstDelay(ctx, x.coreInstance, url)
@@ -307,11 +307,11 @@ 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 == "" {
url = "https://www.google.com/generate_204"
url = "https://api.ipify.org"
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@@ -73,6 +73,7 @@ object AppConfig {
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
const val PREF_SHOW_COPY_BUTTON = "pref_show_copy_button"
/** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
@@ -119,7 +120,7 @@ object AppConfig {
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
const val DELAY_TEST_URL = "https://icanhazip.com"
const val DELAY_TEST_URL = "https://api.ipify.org"
const val DELAY_TEST_URL2 = "https://api64.ipify.org"
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
const val IP_API_URL = "https://api.ip.sb/geoip"
@@ -10,4 +10,6 @@ interface MainAdapterListener :BaseAdapterListener {
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
fun onCopyToClipboard(guid: String)
}
@@ -94,7 +94,7 @@ object SpeedtestManager {
var result: String
var elapsed = -1L
val testUrl = "https://icanhazip.com"
val testUrl = "https://api.ipify.org"
val conn = HttpUtil.createProxyConnection(testUrl, port, 15000, 15000) ?: return Pair(elapsed, "")
try {
val start = SystemClock.elapsedRealtime()
@@ -6,6 +6,8 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -55,19 +57,20 @@ class RealPingWorkerService(
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
reportResult(guid, -1L)
null
}
// Prepare configurations in parallel for faster startup
val shuffledGuids = guids.shuffled()
val deferredItems = shuffledGuids.map { guid ->
async(Dispatchers.IO) {
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
if (configResult.status) {
PingItem(guid, configResult.content)
} else {
reportResult(guid, -1L)
null
}
}
}
val items = deferredItems.awaitAll().filterNotNull()
if (items.isNotEmpty()) {
val configsJson = JsonUtil.toJson(items)
@@ -12,6 +12,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R
import xyz.zarazaex.olc.contracts.MainAdapterListener
@@ -137,9 +138,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
* @param guid The server unique identifier
*/
private fun share2Clipboard(guid: String) {
if (AngConfigManager.share2Clipboard(ownerActivity, guid) == 0) {
ownerActivity.toastSuccess(R.string.toast_success)
} else {
if (AngConfigManager.share2Clipboard(ownerActivity, guid) != 0) {
ownerActivity.toastError(R.string.toast_failure)
}
}
@@ -264,6 +263,10 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
setSelectServer(guid)
}
override fun onCopyToClipboard(guid: String) {
share2Clipboard(guid)
}
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP
@@ -266,8 +266,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
withContext(Dispatchers.Main) {
if (result.configCount > 0) {
mainViewModel.reloadServerList()
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
@@ -276,7 +275,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
hideLoading()
delay(500L)
showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing()
}
@@ -33,6 +33,8 @@ class MainRecyclerAdapter(
private val doubleColumnDisplay =
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
private val showCopyButton =
MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_COPY_BUTTON, false)
private var data: MutableList<ServersCache> = mutableListOf()
private var recyclerView: RecyclerView? = null
@@ -200,6 +202,11 @@ class MainRecyclerAdapter(
mainViewModel.reloadServerList()
}
holder.itemMainBinding.ivCopy.visibility = if (showCopyButton) View.VISIBLE else View.GONE
holder.itemMainBinding.ivCopy.setOnClickListener {
adapterListener?.onCopyToClipboard(guid)
}
holder.itemMainBinding.infoContainer.setOnClickListener {
adapterListener?.onSelectServer(guid)
}
@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:fillColor="@android:color/white"
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
</vector>
@@ -99,16 +99,35 @@
</LinearLayout>
<ImageView
android:id="@+id/iv_favorite"
<LinearLayout
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_star_empty" />
android:orientation="vertical"
android:gravity="end">
<ImageView
android:id="@+id/iv_favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
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:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/padding_spacing_dp8"
android:visibility="gone"
app:srcCompat="@drawable/ic_copy"
app:tint="?attr/colorAccent" />
</LinearLayout>
</LinearLayout>
@@ -236,6 +236,9 @@
<string name="title_pref_group_all_display">Общая вкладка групп</string>
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
<string name="title_pref_show_copy_button">Кнопка копирования</string>
<string name="summary_pref_show_copy_button">Показывать кнопку копирования конфигурации сервера в буфер обмена</string>
<!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
@@ -160,6 +160,9 @@
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
<string name="title_pref_show_copy_button">Show copy button</string>
<string name="summary_pref_show_copy_button">Show button to copy server configuration to clipboard</string>
<string name="title_mux_settings">Mux Settings</string>
<string name="title_pref_mux_enabled">Enable Mux</string>
<string name="summary_pref_mux_enabled">Faster, but it may cause unstable connectivity\nCustomize how to handle TCP, UDP and QUIC below</string>
@@ -28,6 +28,12 @@
android:summary="@string/summary_pref_group_all_display"
android:title="@string/title_pref_group_all_display" />
<CheckBoxPreference
android:key="pref_show_copy_button"
android:defaultValue="false"
android:summary="@string/summary_pref_show_copy_button"
android:title="@string/title_pref_show_copy_button" />
<ListPreference
android:defaultValue="auto"
android:entries="@array/language_select"