8 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
zarazaex69 d3a5a1af9c fix: preserve favorite status during subscription update 2026-04-21 23:07:51 +03:00
zarazaex69 33971a576a fix: #7 2026-04-21 23:00:36 +03:00
zarazaex69 a04c53a045 fix: real ping IPC stalls by batching and throttling UI updates 2026-04-21 04:27:37 +03:00
zarazaex69 278095015b feat: remove apk 2026-04-21 03:06:16 +03:00
19 changed files with 226 additions and 56 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 // 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 // An error is returned if the connection fails or returns an unexpected status
func (x *CoreController) MeasureDelay(url string) (int64, error) { 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() defer cancel()
return measureInstDelay(ctx, x.coreInstance, url) return measureInstDelay(ctx, x.coreInstance, url)
@@ -307,11 +307,11 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
client := &http.Client{ client := &http.Client{
Transport: tr, Transport: tr,
Timeout: 5 * time.Second, Timeout: 6 * time.Second,
} }
if url == "" { if url == "" {
url = "https://www.google.com/generate_204" url = "https://api.ipify.org"
} }
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 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_LOGLEVEL = "pref_hev_tunnel_loglevel"
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2" 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_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
const val PREF_SHOW_COPY_BUTTON = "pref_show_copy_button"
/** Cache keys. */ /** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" 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_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw=" const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
const val TG_CHANNEL_URL = "https://t.me/github_2dust" 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 DELAY_TEST_URL2 = "https://api64.ipify.org"
// const val IP_API_URL = "https://speed.cloudflare.com/meta" // const val IP_API_URL = "https://speed.cloudflare.com/meta"
const val IP_API_URL = "https://api.ip.sb/geoip" const val IP_API_URL = "https://api.ip.sb/geoip"
@@ -164,6 +165,7 @@ object AppConfig {
const val MSG_MEASURE_CONFIG_CANCEL = 72 const val MSG_MEASURE_CONFIG_CANCEL = 72
const val MSG_MEASURE_CONFIG_NOTIFY = 73 const val MSG_MEASURE_CONFIG_NOTIFY = 73
const val MSG_MEASURE_CONFIG_FINISH = 74 const val MSG_MEASURE_CONFIG_FINISH = 74
const val MSG_MEASURE_CONFIG_BATCH = 75
/** Notification channel IDs and names. */ /** Notification channel IDs and names. */
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID" const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
@@ -10,4 +10,6 @@ interface MainAdapterListener :BaseAdapterListener {
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
fun onCopyToClipboard(guid: String)
} }
@@ -0,0 +1,14 @@
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,4 +128,39 @@ data class ProfileItem(
&& this.pinnedCA256 == obj.pinnedCA256 && 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
}
} }
@@ -241,8 +241,8 @@ object AngConfigManager {
val subItem = MmkvManager.decodeSubscription(subid) val subItem = MmkvManager.decodeSubscription(subid)
val oldPingData = if (!append) { val oldServerData = if (!append) {
saveOldPingData(subid) saveOldServerData(subid)
} else { } else {
emptyMap() emptyMap()
} }
@@ -263,7 +263,7 @@ object AngConfigManager {
MmkvManager.removeServerViaSubid(subid) MmkvManager.removeServerViaSubid(subid)
} }
val keyToProfile = batchSaveConfigs(configs, subid, append) val keyToProfile = batchSaveConfigs(configs, subid, append)
restoreOldPingData(keyToProfile, oldPingData) restoreOldServerData(keyToProfile, oldServerData)
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected) val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
matchKey?.let { MmkvManager.setSelectServer(it) } matchKey?.let { MmkvManager.setSelectServer(it) }
} }
@@ -330,31 +330,39 @@ object AngConfigManager {
return keyToProfile return keyToProfile
} }
private fun saveOldPingData(subid: String): Map<ProfileItem, Long> { private fun saveOldServerData(subid: String): Map<ProfileItem, Pair<Long, Boolean>> {
val pingData = mutableMapOf<ProfileItem, Long>() val serverData = mutableMapOf<ProfileItem, Pair<Long, Boolean>>()
val serverList = MmkvManager.decodeServerList(subid) val serverList = MmkvManager.decodeServerList(subid)
serverList.forEach { guid -> serverList.forEach { guid ->
val profile = MmkvManager.decodeServerConfig(guid) val profile = MmkvManager.decodeServerConfig(guid)
val aff = MmkvManager.decodeServerAffiliationInfo(guid) if (profile != null) {
if (profile != null && aff != null && aff.testDelayMillis > 0) { val aff = MmkvManager.decodeServerAffiliationInfo(guid)
pingData[profile] = aff.testDelayMillis val delay = aff?.testDelayMillis ?: 0L
if (delay > 0 || profile.isFavorite) {
serverData[profile] = Pair(delay, profile.isFavorite)
}
} }
} }
return pingData return serverData
} }
private fun restoreOldPingData(keyToProfile: Map<String, ProfileItem>, oldPingData: Map<ProfileItem, Long>) { private fun restoreOldServerData(keyToProfile: Map<String, ProfileItem>, oldServerData: Map<ProfileItem, Pair<Long, Boolean>>) {
if (oldPingData.isEmpty()) return if (oldServerData.isEmpty()) return
keyToProfile.forEach { (key, newProfile) -> keyToProfile.forEach { (key, newProfile) ->
val oldPing = oldPingData.entries.firstOrNull { (oldProfile, _) -> val oldData = oldServerData[newProfile]
oldProfile == newProfile
}?.value
if (oldPing != null && oldPing > 0) { if (oldData != null) {
MmkvManager.encodeServerTestDelayMillis(key, oldPing) val (oldPing, isFavorite) = oldData
if (oldPing > 0) {
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
}
if (isFavorite) {
newProfile.isFavorite = true
MmkvManager.encodeServerConfig(key, newProfile)
}
} }
} }
} }
@@ -94,7 +94,7 @@ object SpeedtestManager {
var result: String var result: String
var elapsed = -1L 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, "") val conn = HttpUtil.createProxyConnection(testUrl, port, 15000, 15000) ?: return Pair(elapsed, "")
try { try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
@@ -6,8 +6,14 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.zarazaex.olc.AppConfig 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.SettingsManager
import xyz.zarazaex.olc.handler.V2RayNativeManager import xyz.zarazaex.olc.handler.V2RayNativeManager
import xyz.zarazaex.olc.handler.V2rayConfigManager import xyz.zarazaex.olc.handler.V2rayConfigManager
@@ -29,27 +35,42 @@ class RealPingWorkerService(
private val totalCount = AtomicInteger(guids.size) private val totalCount = AtomicInteger(guids.size)
private val finishedCount = AtomicInteger(0) private val finishedCount = AtomicInteger(0)
private val pendingResults = ArrayList<PingResultItem>()
private val pendingLock = Any()
private val delayTestUrl = SettingsManager.getDelayTestUrl() 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) data class PingItem(val guid: String, val config: String)
fun start() { fun start() {
scope.launch(Dispatchers.IO) {
while (isActive) {
delay(FLUSH_INTERVAL_MS)
flushPendingResults()
}
}
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
// Prepare configurations for batch test and shuffle for better async feel // Prepare configurations in parallel for faster startup
val items = val shuffledGuids = guids.shuffled()
guids.shuffled().mapNotNull { guid -> val deferredItems = shuffledGuids.map { guid ->
val configResult = async(Dispatchers.IO) {
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid) val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
if (configResult.status) { if (configResult.status) {
PingItem(guid, configResult.content) PingItem(guid, configResult.content)
} else { } else {
// Notify failure immediately for invalid configs reportResult(guid, -1L)
reportResult(guid, -1L) null
null
}
} }
}
}
val items = deferredItems.awaitAll().filterNotNull()
if (items.isNotEmpty()) { if (items.isNotEmpty()) {
val configsJson = JsonUtil.toJson(items) val configsJson = JsonUtil.toJson(items)
@@ -67,8 +88,10 @@ class RealPingWorkerService(
) )
} }
flushPendingResults()
onFinish("0") onFinish("0")
} catch (e: Exception) { } catch (e: Exception) {
flushPendingResults()
onFinish("-1") onFinish("-1")
} finally { } finally {
cancel() cancel()
@@ -78,14 +101,42 @@ class RealPingWorkerService(
private fun reportResult(guid: String, delay: Long) { private fun reportResult(guid: String, delay: Long) {
val finished = finishedCount.incrementAndGet() val finished = finishedCount.incrementAndGet()
val total = guids.size 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()
}
}
readyBatch?.let(::sendBatchUpdate)
}
// Notify UI about the individual result private fun flushPendingResults() {
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, delay)) val finished = finishedCount.get()
val update =
synchronized(pendingLock) {
if (pendingResults.isEmpty()) {
null
} else {
createProgressUpdateLocked(finished).also { pendingResults.clear() }
}
}
update?.let(::sendBatchUpdate)
}
// Notify UI about progress private fun createProgressUpdateLocked(finished: Int): PingProgressUpdate {
val left = total - finished return PingProgressUpdate(
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $total") 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() { fun cancel() {
@@ -12,6 +12,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R import xyz.zarazaex.olc.R
import xyz.zarazaex.olc.contracts.MainAdapterListener import xyz.zarazaex.olc.contracts.MainAdapterListener
@@ -137,9 +138,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
* @param guid The server unique identifier * @param guid The server unique identifier
*/ */
private fun share2Clipboard(guid: String) { private fun share2Clipboard(guid: String) {
if (AngConfigManager.share2Clipboard(ownerActivity, guid) == 0) { if (AngConfigManager.share2Clipboard(ownerActivity, guid) != 0) {
ownerActivity.toastSuccess(R.string.toast_success)
} else {
ownerActivity.toastError(R.string.toast_failure) ownerActivity.toastError(R.string.toast_failure)
} }
} }
@@ -264,6 +263,10 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
setSelectServer(guid) setSelectServer(guid)
} }
override fun onCopyToClipboard(guid: String) {
share2Clipboard(guid)
}
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) { override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP
@@ -17,6 +17,8 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@@ -84,6 +86,12 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
toggle.syncState() toggle.syncState()
binding.navView.setNavigationItemSelectedListener(this) 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 { findViewById<android.widget.TextView>(R.id.drawer_settings)?.setOnClickListener {
requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java)) requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java))
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START) binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
@@ -258,8 +266,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll() val result = mainViewModel.updateConfigViaSubAll()
delay(500L) withContext(Dispatchers.Main) {
launch(Dispatchers.Main) {
if (result.configCount > 0) { if (result.configCount > 0) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...") showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
@@ -268,7 +275,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
hideLoading() hideLoading()
delay(500L)
showStatus("Выполняется замер задержки. Ожидаем завершения...") showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing() mainViewModel.testAllRealPing()
} }
@@ -33,6 +33,8 @@ class MainRecyclerAdapter(
private val doubleColumnDisplay = private val doubleColumnDisplay =
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false) 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 data: MutableList<ServersCache> = mutableListOf()
private var recyclerView: RecyclerView? = null private var recyclerView: RecyclerView? = null
@@ -200,6 +202,11 @@ class MainRecyclerAdapter(
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
} }
holder.itemMainBinding.ivCopy.visibility = if (showCopyButton) View.VISIBLE else View.GONE
holder.itemMainBinding.ivCopy.setOnClickListener {
adapterListener?.onCopyToClipboard(guid)
}
holder.itemMainBinding.infoContainer.setOnClickListener { holder.itemMainBinding.infoContainer.setOnClickListener {
adapterListener?.onSelectServer(guid) adapterListener?.onSelectServer(guid)
} }
@@ -15,6 +15,7 @@ import xyz.zarazaex.olc.AngApplication
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R import xyz.zarazaex.olc.R
import xyz.zarazaex.olc.dto.GroupMapItem import xyz.zarazaex.olc.dto.GroupMapItem
import xyz.zarazaex.olc.dto.PingProgressUpdate
import xyz.zarazaex.olc.dto.ServersCache import xyz.zarazaex.olc.dto.ServersCache
import xyz.zarazaex.olc.dto.SubscriptionCache import xyz.zarazaex.olc.dto.SubscriptionCache
import xyz.zarazaex.olc.dto.SubscriptionUpdateResult import xyz.zarazaex.olc.dto.SubscriptionUpdateResult
@@ -628,6 +629,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
updateListAction.value = getPosition(resultPair.first) 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 -> { AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
val content = intent.getStringExtra("content") val content = intent.getStringExtra("content")
updateTestResultAction.value = updateTestResultAction.value =
@@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <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" /> 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> </vector>
@@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout" android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -150,6 +151,7 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/drawer_content_layout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
@@ -99,16 +99,35 @@
</LinearLayout> </LinearLayout>
<ImageView <LinearLayout
android:id="@+id/iv_favorite"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:orientation="vertical"
android:background="?attr/selectableItemBackgroundBorderless" android:gravity="end">
android:clickable="true"
android:focusable="true" <ImageView
android:padding="@dimen/padding_spacing_dp8" android:id="@+id/iv_favorite"
android:src="@drawable/ic_star_empty" /> 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> </LinearLayout>
@@ -236,6 +236,9 @@
<string name="title_pref_group_all_display">Общая вкладка групп</string> <string name="title_pref_group_all_display">Общая вкладка групп</string>
<string name="summary_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 --> <!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string> <string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</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="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="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_mux_settings">Mux Settings</string>
<string name="title_pref_mux_enabled">Enable Mux</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> <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:summary="@string/summary_pref_group_all_display"
android:title="@string/title_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 <ListPreference
android:defaultValue="auto" android:defaultValue="auto"
android:entries="@array/language_select" android:entries="@array/language_select"
BIN
View File
Binary file not shown.