mirror of
https://github.com/xtclovver/RKNHardering.git
synced 2026-07-09 17:19:25 +00:00
parent
90e46df80d
commit
ce55eaef0e
19 changed files with 240 additions and 138 deletions
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ dependencies {
|
|||
implementation(libs.okhttp.dnsoverhttps)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.okhttp.mockwebserver)
|
||||
testImplementation("org.robolectric:robolectric:4.14.1")
|
||||
testImplementation("androidx.test:core:1.6.1")
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
|
@ -8,7 +9,6 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<queries>
|
||||
|
|
@ -51,7 +51,8 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RKNHardering"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
android:localeConfig="@xml/locales_config"
|
||||
tools:targetApi="tiramisu">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
|
|
|||
|
|
@ -11,21 +11,22 @@ import android.os.Bundle
|
|||
import android.provider.Settings
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.BidiFormatter
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isNotEmpty
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -77,7 +78,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private lateinit var btnRunCheck: MaterialButton
|
||||
private lateinit var btnStopCheck: MaterialButton
|
||||
private lateinit var cardRunCheckNotice: MaterialCardView
|
||||
private lateinit var resultsScrollView: ScrollView
|
||||
private lateinit var resultsScrollView: TouchAwareScrollView
|
||||
private lateinit var textCheckStatus: TextView
|
||||
private lateinit var viewModel: CheckViewModel
|
||||
private var hasDismissedRunCheckNotice = false
|
||||
|
|
@ -143,7 +144,7 @@ class MainActivity : AppCompatActivity() {
|
|||
ActivityResultContracts.RequestMultiplePermissions(),
|
||||
) { result ->
|
||||
markPermissionsRequested(result.keys)
|
||||
prefs.edit().putBoolean(PREF_RATIONALE_SHOWN, true).apply()
|
||||
prefs.edit { putBoolean(PREF_RATIONALE_SHOWN, true) }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -243,14 +244,8 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun setupResultsScrollTracking() {
|
||||
resultsScrollView.setOnTouchListener { _, event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> userTouchScrollInProgress = true
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
-> userTouchScrollInProgress = false
|
||||
}
|
||||
false
|
||||
resultsScrollView.onUserTouchChanged = { isTouching ->
|
||||
userTouchScrollInProgress = isTouching
|
||||
}
|
||||
resultsScrollView.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (userTouchScrollInProgress && !isAutoScrollInProgress) {
|
||||
|
|
@ -282,7 +277,7 @@ class MainActivity : AppCompatActivity() {
|
|||
launchPermissionRequest(permissions)
|
||||
}
|
||||
.setNegativeButton(getString(R.string.main_perm_skip)) { _, _ ->
|
||||
prefs.edit().putBoolean(PREF_RATIONALE_SHOWN, true).apply()
|
||||
prefs.edit { putBoolean(PREF_RATIONALE_SHOWN, true) }
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
|
|
@ -348,7 +343,7 @@ class MainActivity : AppCompatActivity() {
|
|||
?.toMutableSet()
|
||||
?: mutableSetOf()
|
||||
requested.addAll(permissions)
|
||||
prefs.edit().putStringSet(PREF_REQUESTED_PERMISSIONS, requested).apply()
|
||||
prefs.edit { putStringSet(PREF_REQUESTED_PERMISSIONS, requested) }
|
||||
}
|
||||
|
||||
private fun hasPermissionBeenRequested(permission: String): Boolean {
|
||||
|
|
@ -630,7 +625,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private fun showLoadingCardForStage(stage: RunningStage) {
|
||||
if (stage in completedStages) return
|
||||
if (stage in loadingStages && cardForStage(stage).visibility == View.VISIBLE) return
|
||||
if (stage in loadingStages && cardForStage(stage).isVisible) return
|
||||
|
||||
loadingStages += stage
|
||||
when (stage) {
|
||||
|
|
@ -855,7 +850,11 @@ class MainActivity : AppCompatActivity() {
|
|||
else -> "..."
|
||||
}
|
||||
loadingStages.forEach { stage ->
|
||||
statusViewForStage(stage).text = stageLoadingStatusBase(stage) + dots
|
||||
statusViewForStage(stage).text = getString(
|
||||
R.string.main_loading_status_progress,
|
||||
stageLoadingStatusBase(stage),
|
||||
dots,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -911,7 +910,7 @@ class MainActivity : AppCompatActivity() {
|
|||
animate: Boolean = true,
|
||||
shouldAutoScroll: Boolean = false,
|
||||
) {
|
||||
val wasVisible = card.visibility == View.VISIBLE
|
||||
val wasVisible = card.isVisible
|
||||
if (!wasVisible) {
|
||||
card.animate().cancel()
|
||||
card.visibility = View.VISIBLE
|
||||
|
|
@ -1498,7 +1497,7 @@ class MainActivity : AppCompatActivity() {
|
|||
content = narrative.reasonRows.map(::createVerdictBulletView),
|
||||
)
|
||||
|
||||
val hasDetails = verdictDetailsContent.childCount > 0
|
||||
val hasDetails = verdictDetailsContent.isNotEmpty()
|
||||
verdictDetailsDivider.visibility = if (hasDetails) View.VISIBLE else View.GONE
|
||||
btnVerdictDetails.visibility = if (hasDetails) View.VISIBLE else View.GONE
|
||||
verdictDetailsContent.visibility = if (hasDetails && isVerdictDetailsExpanded) View.VISIBLE else View.GONE
|
||||
|
|
@ -1508,7 +1507,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private fun addVerdictSection(title: String, content: List<View>) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
if (verdictDetailsContent.childCount > 0) {
|
||||
if (verdictDetailsContent.isNotEmpty()) {
|
||||
verdictDetailsContent.addView(
|
||||
View(this).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.notcvnt.rknhardering
|
|||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
|
|
@ -11,6 +10,8 @@ import androidx.activity.enableEdgeToEdge
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
|
|
@ -26,6 +27,7 @@ import com.notcvnt.rknhardering.network.DnsResolverPreset
|
|||
import com.notcvnt.rknhardering.network.DnsResolverPresets
|
||||
import com.notcvnt.rknhardering.probe.ProxyScanner
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
|
|
@ -115,8 +117,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
chipGroupPortRange.check(chipId)
|
||||
customPortRangeContainer.visibility = if (portRange == "custom") View.VISIBLE else View.GONE
|
||||
|
||||
editPortStart.setText(prefs.getInt(PREF_PORT_RANGE_START, 1024).toString())
|
||||
editPortEnd.setText(prefs.getInt(PREF_PORT_RANGE_END, 65535).toString())
|
||||
editPortStart.setText(formatPortInputValue(prefs.getInt(PREF_PORT_RANGE_START, 1024)))
|
||||
editPortEnd.setText(formatPortInputValue(prefs.getInt(PREF_PORT_RANGE_END, 65535)))
|
||||
updatePortRangePreview()
|
||||
|
||||
loadResolverSettings()
|
||||
|
|
@ -142,7 +144,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
|
||||
private fun setupListeners() {
|
||||
switchSplitTunnel.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean(PREF_SPLIT_TUNNEL_ENABLED, isChecked).apply()
|
||||
prefs.edit { putBoolean(PREF_SPLIT_TUNNEL_ENABLED, isChecked) }
|
||||
updatePortRangeEnabled(isChecked)
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +154,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
.setTitle(R.string.settings_network_disable_title)
|
||||
.setMessage(R.string.settings_network_disable_message)
|
||||
.setPositiveButton(R.string.settings_network_disable_confirm) { _, _ ->
|
||||
prefs.edit().putBoolean(PREF_NETWORK_REQUESTS_ENABLED, false).apply()
|
||||
prefs.edit { putBoolean(PREF_NETWORK_REQUESTS_ENABLED, false) }
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
switchNetworkRequests.isChecked = true
|
||||
|
|
@ -162,12 +164,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
.show()
|
||||
} else {
|
||||
prefs.edit().putBoolean(PREF_NETWORK_REQUESTS_ENABLED, true).apply()
|
||||
prefs.edit { putBoolean(PREF_NETWORK_REQUESTS_ENABLED, true) }
|
||||
}
|
||||
}
|
||||
|
||||
switchPrivacyMode.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.edit().putBoolean(PREF_PRIVACY_MODE, isChecked).apply()
|
||||
prefs.edit { putBoolean(PREF_PRIVACY_MODE, isChecked) }
|
||||
}
|
||||
|
||||
chipGroupPortRange.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
|
|
@ -179,7 +181,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
R.id.chipPortCustom -> "custom"
|
||||
else -> "full"
|
||||
}
|
||||
prefs.edit().putString(PREF_PORT_RANGE, value).apply()
|
||||
prefs.edit { putString(PREF_PORT_RANGE, value) }
|
||||
customPortRangeContainer.visibility = if (value == "custom") View.VISIBLE else View.GONE
|
||||
updatePortRangePreview()
|
||||
}
|
||||
|
|
@ -187,18 +189,18 @@ class SettingsActivity : AppCompatActivity() {
|
|||
chipGroupResolverMode.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
if (checkedIds.isEmpty()) return@setOnCheckedStateChangeListener
|
||||
saveCustomResolverFields()
|
||||
prefs.edit()
|
||||
.putString(PREF_DNS_RESOLVER_MODE, selectedResolverMode().prefValue)
|
||||
.apply()
|
||||
prefs.edit {
|
||||
putString(PREF_DNS_RESOLVER_MODE, selectedResolverMode().prefValue)
|
||||
}
|
||||
refreshResolverUi(restoreCustomValues = true)
|
||||
}
|
||||
|
||||
chipGroupResolverPreset.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
if (checkedIds.isEmpty()) return@setOnCheckedStateChangeListener
|
||||
saveCustomResolverFields()
|
||||
prefs.edit()
|
||||
.putString(PREF_DNS_RESOLVER_PRESET, selectedResolverPreset().prefValue)
|
||||
.apply()
|
||||
prefs.edit {
|
||||
putString(PREF_DNS_RESOLVER_PRESET, selectedResolverPreset().prefValue)
|
||||
}
|
||||
refreshResolverUi(restoreCustomValues = true)
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +233,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
R.id.chipThemeDark -> "dark"
|
||||
else -> "system"
|
||||
}
|
||||
prefs.edit().putString(PREF_THEME, value).apply()
|
||||
prefs.edit { putString(PREF_THEME, value) }
|
||||
applyTheme(value)
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +246,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
R.id.chipLangZh -> "zh-CN"
|
||||
else -> ""
|
||||
}
|
||||
prefs.edit().putString(PREF_LANGUAGE, value).commit()
|
||||
prefs.edit { putString(PREF_LANGUAGE, value) }
|
||||
AppUiSettings.applyLanguage(value)
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +255,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
findViewById<MaterialCardView>(R.id.cardGithub).setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.github_repo_url))))
|
||||
startActivity(Intent(Intent.ACTION_VIEW, getString(R.string.github_repo_url).toUri()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,12 +278,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||
val end = editPortEnd.text.toString().toIntOrNull()?.coerceIn(1024, 65535) ?: 65535
|
||||
val validStart = minOf(start, end)
|
||||
val validEnd = maxOf(start, end)
|
||||
prefs.edit()
|
||||
.putInt(PREF_PORT_RANGE_START, validStart)
|
||||
.putInt(PREF_PORT_RANGE_END, validEnd)
|
||||
.apply()
|
||||
editPortStart.setText(validStart.toString())
|
||||
editPortEnd.setText(validEnd.toString())
|
||||
prefs.edit {
|
||||
putInt(PREF_PORT_RANGE_START, validStart)
|
||||
putInt(PREF_PORT_RANGE_END, validEnd)
|
||||
}
|
||||
editPortStart.setText(formatPortInputValue(validStart))
|
||||
editPortEnd.setText(formatPortInputValue(validEnd))
|
||||
}
|
||||
|
||||
private fun updatePortRangePreview() {
|
||||
|
|
@ -390,11 +392,15 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private fun saveCustomResolverFields() {
|
||||
// Presets reuse the same text fields for display, so only persist while custom is active.
|
||||
if (persistedResolverPreset() != DnsResolverPreset.CUSTOM) return
|
||||
prefs.edit()
|
||||
.putString(PREF_DNS_RESOLVER_DIRECT_SERVERS, editResolverDirectServers.text?.toString().orEmpty().trim())
|
||||
.putString(PREF_DNS_RESOLVER_DOH_URL, editResolverDohUrl.text?.toString().orEmpty().trim())
|
||||
.putString(PREF_DNS_RESOLVER_DOH_BOOTSTRAP, editResolverBootstrap.text?.toString().orEmpty().trim())
|
||||
.apply()
|
||||
prefs.edit {
|
||||
putString(PREF_DNS_RESOLVER_DIRECT_SERVERS, editResolverDirectServers.text?.toString().orEmpty().trim())
|
||||
putString(PREF_DNS_RESOLVER_DOH_URL, editResolverDohUrl.text?.toString().orEmpty().trim())
|
||||
putString(PREF_DNS_RESOLVER_DOH_BOOTSTRAP, editResolverBootstrap.text?.toString().orEmpty().trim())
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPortInputValue(value: Int): String {
|
||||
return String.format(Locale.US, "%d", value)
|
||||
}
|
||||
|
||||
private fun persistedResolverPreset(): DnsResolverPreset {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.notcvnt.rknhardering
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.widget.ScrollView
|
||||
|
||||
class TouchAwareScrollView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : ScrollView(context, attrs, defStyleAttr) {
|
||||
|
||||
var onUserTouchChanged: ((Boolean) -> Unit)? = null
|
||||
|
||||
private var isUserTouchActive = false
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
||||
updateTouchState(ev)
|
||||
return super.onInterceptTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
updateTouchState(ev)
|
||||
if (ev.actionMasked == MotionEvent.ACTION_UP) {
|
||||
performClick()
|
||||
}
|
||||
return super.onTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
return super.performClick()
|
||||
}
|
||||
|
||||
private fun updateTouchState(event: MotionEvent) {
|
||||
val nextState = when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> true
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
-> false
|
||||
else -> isUserTouchActive
|
||||
}
|
||||
if (nextState == isUserTouchActive) return
|
||||
isUserTouchActive = nextState
|
||||
onUserTouchChanged?.invoke(nextState)
|
||||
}
|
||||
}
|
||||
|
|
@ -399,11 +399,13 @@ object BypassChecker {
|
|||
)
|
||||
}
|
||||
if (xrayApiScanResult.outbounds.size > 10) {
|
||||
val extraOutboundsCount = xrayApiScanResult.outbounds.size - 10
|
||||
findings.add(
|
||||
Finding(
|
||||
description = context.getString(
|
||||
R.string.checker_bypass_extra_outbounds,
|
||||
xrayApiScanResult.outbounds.size - 10,
|
||||
description = context.resources.getQuantityString(
|
||||
R.plurals.checker_bypass_extra_outbounds,
|
||||
extraOutboundsCount,
|
||||
extraOutboundsCount,
|
||||
),
|
||||
detected = true,
|
||||
source = EvidenceSource.XRAY_API,
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ object IndirectSignsChecker {
|
|||
return try {
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val activeNetwork = cm.activeNetwork
|
||||
cm.allNetworks.mapNotNull { network ->
|
||||
allNetworksSnapshot(cm).mapNotNull { network ->
|
||||
val caps = cm.getNetworkCapabilities(network) ?: return@mapNotNull null
|
||||
val linkProperties = cm.getLinkProperties(network) ?: return@mapNotNull null
|
||||
if (!caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
|
|
@ -217,6 +217,11 @@ object IndirectSignsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun allNetworksSnapshot(cm: ConnectivityManager): Array<android.net.Network> {
|
||||
return cm.allNetworks
|
||||
}
|
||||
|
||||
private fun checkNotVpnCapability(
|
||||
context: Context,
|
||||
findings: MutableList<Finding>,
|
||||
|
|
|
|||
|
|
@ -160,9 +160,11 @@ object LocationSignalsChecker {
|
|||
}
|
||||
|
||||
private fun collectSimCards(context: Context, tm: TelephonyManager): List<SimCardInfo> {
|
||||
val sm = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)
|
||||
as? android.telephony.SubscriptionManager
|
||||
val subscriptions = runCatching { sm?.activeSubscriptionInfoList }.getOrNull()
|
||||
val subscriptions = if (hasPermission(context, Manifest.permission.READ_PHONE_STATE)) {
|
||||
getActiveSubscriptions(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (!subscriptions.isNullOrEmpty()) {
|
||||
return subscriptions.mapNotNull { info ->
|
||||
|
|
@ -203,6 +205,13 @@ object LocationSignalsChecker {
|
|||
}.getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
@Suppress("MissingPermission")
|
||||
private fun getActiveSubscriptions(context: Context): List<android.telephony.SubscriptionInfo>? {
|
||||
val subscriptionManager = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)
|
||||
as? android.telephony.SubscriptionManager
|
||||
return runCatching { subscriptionManager?.activeSubscriptionInfoList }.getOrNull()
|
||||
}
|
||||
|
||||
private suspend fun collectCellCandidates(
|
||||
context: Context,
|
||||
tm: TelephonyManager,
|
||||
|
|
@ -387,7 +396,7 @@ object LocationSignalsChecker {
|
|||
|
||||
private fun toWifiLookupCandidate(scanResult: ScanResult): WifiLookupCandidate? {
|
||||
val macAddress = normalizeMacAddress(scanResult.BSSID) ?: return null
|
||||
val ssid = normalizeSsid(scanResult.SSID) ?: return null
|
||||
val ssid = normalizeSsid(scanResultSsid(scanResult)) ?: return null
|
||||
if (ssid.endsWith("_nomap", ignoreCase = true)) return null
|
||||
|
||||
return WifiLookupCandidate(
|
||||
|
|
@ -408,6 +417,16 @@ object LocationSignalsChecker {
|
|||
?.let(::normalizeOperatorCode)
|
||||
}
|
||||
|
||||
private fun scanResultSsid(scanResult: ScanResult): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Api33Impl.scanResultSsid(scanResult)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
scanResult.SSID
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun gsmMcc(identity: android.telephony.CellIdentityGsm): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Api28Impl.gsmMcc(identity)
|
||||
|
|
@ -416,6 +435,7 @@ object LocationSignalsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun gsmMnc(identity: android.telephony.CellIdentityGsm): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Api28Impl.gsmMnc(identity)
|
||||
|
|
@ -424,6 +444,7 @@ object LocationSignalsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun lteMcc(identity: android.telephony.CellIdentityLte): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Api28Impl.lteMcc(identity)
|
||||
|
|
@ -432,6 +453,7 @@ object LocationSignalsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun lteMnc(identity: android.telephony.CellIdentityLte): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Api28Impl.lteMnc(identity)
|
||||
|
|
@ -440,6 +462,7 @@ object LocationSignalsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun wcdmaMcc(identity: android.telephony.CellIdentityWcdma): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Api28Impl.wcdmaMcc(identity)
|
||||
|
|
@ -448,6 +471,7 @@ object LocationSignalsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun wcdmaMnc(identity: android.telephony.CellIdentityWcdma): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Api28Impl.wcdmaMnc(identity)
|
||||
|
|
@ -490,6 +514,16 @@ object LocationSignalsChecker {
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private object Api33Impl {
|
||||
@DoNotInline
|
||||
fun scanResultSsid(scanResult: ScanResult): String? {
|
||||
return scanResult.wifiSsid
|
||||
?.toString()
|
||||
?.trim('"')
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeCellValue(value: Int): Long? {
|
||||
return value.toLong().takeIf { it in 0 until Int.MAX_VALUE.toLong() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
app:menu="@menu/menu_main" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<ScrollView
|
||||
<com.notcvnt.rknhardering.TouchAwareScrollView
|
||||
android:id="@+id/resultsScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
@ -136,13 +135,14 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="status" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="GeoIP"
|
||||
android:text="@string/main_card_geo_ip"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
|
@ -205,7 +205,8 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="status" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -266,7 +267,8 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="status" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -335,7 +337,8 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="status" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -388,7 +391,8 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="status" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -457,13 +461,14 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="status" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Split tunnel bypass"
|
||||
android:text="@string/settings_split_tunnel"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
|
@ -521,7 +526,8 @@
|
|||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:contentDescription="verdict" />
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textVerdict"
|
||||
|
|
@ -577,5 +583,5 @@
|
|||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</com.notcvnt.rknhardering.TouchAwareScrollView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
tools:ignore="TooManyViews">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -180,7 +181,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="1024" />
|
||||
android:text="@string/settings_port_range_start_default" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
|
|
@ -188,6 +189,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingHorizontal="8dp"
|
||||
tools:ignore="HardcodedText"
|
||||
android:text="–"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
|
|
@ -203,7 +205,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="65535" />
|
||||
android:text="@string/settings_port_range_end_default" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
|
@ -667,7 +669,8 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_warning"
|
||||
android:contentDescription="navigate"
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no"
|
||||
app:tint="?android:attr/textColorSecondary" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
@ -718,7 +721,8 @@
|
|||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_check_circle"
|
||||
android:contentDescription="external link"
|
||||
android:contentDescription="@null"
|
||||
android:importantForAccessibility="no"
|
||||
app:tint="?android:attr/textColorSecondary" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">RKNHardering</string>
|
||||
<string name="github_repo_label">GitHub</string>
|
||||
<string name="github_repo_url">https://github.com/xtclovver/RKNHardering</string>
|
||||
<string name="run_check_notice">در زمان اجرای بررسی، برنامه به این دامنهها درخواست شبکه ارسال میکند:\n• api.beacondb.net\n• api.ipapi.is\n• www.iplocate.io\n• ipv4-internet.yandex.net\n• ipv6-internet.yandex.net\n• 2ip.ru\n• ifconfig.me\n• ipv4.ifconfig.me\n• ipv6.ifconfig.me\n• checkip.amazonaws.com\n• api.ipify.org\n• api-ipv4.ip.sb\n• api-ipv6.ip.sb\n\nاگر در تنظیمات یک DNS resolver سفارشی انتخاب شده باشد، برنامه همچنین به آدرسهای IP آن یا دامنه DoH متصل میشود.\n\nبا لمس دکمه «اجرای بررسی»، موافقت میکنید که این درخواستهای شبکه از دستگاه شما ارسال شوند.\n\nبرنامه لاگ این درخواستها را نگه نمیدارد. دسترسی به شبکه را میتوان از تنظیمات برنامه غیرفعال کرد.</string>
|
||||
|
||||
|
|
@ -76,8 +75,6 @@
|
|||
|
||||
<string name="main_check_running">بررسی در حال انجام است. کارتها همزمان با رسیدن دادهها بهروزرسانی میشوند.</string>
|
||||
<string name="main_check_stopped">بررسی متوقف شد</string>
|
||||
<string name="main_status_scanning">در حال اسکن...</string>
|
||||
<string name="main_status_preparing">در حال آمادهسازی...</string>
|
||||
<string name="main_status_cancelled">لغو شد</string>
|
||||
<string name="main_status_stopped">متوقف شد</string>
|
||||
|
||||
|
|
@ -208,7 +205,6 @@
|
|||
<string name="checker_ip_comp_status_ipv6_ignored">IPv6 نادیده گرفته شد</string>
|
||||
<string name="checker_ip_comp_status_ipv4_ipv6">IPv4/IPv6</string>
|
||||
<string name="checker_ip_comp_status_mismatch">ناسازگار</string>
|
||||
<string name="checker_ip_comp_status_partial">ناقص</string>
|
||||
<string name="checker_ip_comp_status_match">یکسان</string>
|
||||
<string name="checker_ip_comp_single_ip">IP: %1$s</string>
|
||||
<string name="checker_ip_comp_single_error">خطا: %1$s</string>
|
||||
|
|
@ -217,7 +213,6 @@
|
|||
<string name="checker_ip_comp_ipv6_ignored_no_ipv4">خطاهای IPv6 نادیده گرفته شدند، اما پاسخ معتبر IPv4 وجود ندارد</string>
|
||||
<string name="checker_ip_comp_mixed_families">سرویسها آدرسهایی از خانوادههای متفاوت برگرداندند: %1$s</string>
|
||||
<string name="checker_ip_comp_different_ips">سرویسها IPهای متفاوتی برگرداندند: %1$s</string>
|
||||
<string name="checker_ip_comp_partial">IP برابر %1$s است، اما %2$d از %3$d سرویس پاسخ ندادند</string>
|
||||
<string name="checker_ip_comp_all_same">همه سرویسهای پاسخداده در این گروه IP %1$s را برگرداندند</string>
|
||||
<string name="checker_ip_comp_ipv6_ignored_suffix">؛ خطاهای IPv6 نادیده گرفته شد: %1$d</string>
|
||||
|
||||
|
|
@ -280,15 +275,15 @@
|
|||
<string name="checker_indirect_dumpsys_service_error">dumpsys activity services: %1$s</string>
|
||||
|
||||
<string name="checker_bypass_progress_port_scan_phase">اسکن پورت</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">در حال جستوجوی proxyهای باز روی localhost...</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">در حال جستوجوی proxyهای باز روی localhost…</string>
|
||||
<string name="checker_bypass_progress_popular_ports">پورتهای رایج</string>
|
||||
<string name="checker_bypass_progress_full_scan">اسکن کامل</string>
|
||||
<string name="checker_bypass_progress_port_detail">پورت %1$d (%2$d%%)</string>
|
||||
<string name="checker_bypass_progress_xray_detail">در حال جستوجوی gRPC API روی localhost...</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">در حال بررسی دسترسی به شبکه غیر VPN...</string>
|
||||
<string name="checker_bypass_progress_xray_detail">در حال جستوجوی gRPC API روی localhost…</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">در حال بررسی دسترسی به شبکه غیر VPN…</string>
|
||||
<string name="checker_bypass_progress_ip_phase">بررسی IP</string>
|
||||
<string name="checker_bypass_progress_ip_detail">در حال بهدستآوردن IP مستقیم و IP از طریق proxy...</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">در حال بررسی Telegram DC از طریق proxy...</string>
|
||||
<string name="checker_bypass_progress_ip_detail">در حال بهدستآوردن IP مستقیم و IP از طریق proxy…</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">در حال بررسی Telegram DC از طریق proxy…</string>
|
||||
<string name="checker_bypass_ip_unavailable">در دسترس نیست</string>
|
||||
<string name="checker_bypass_direct_ip">IP مستقیم: %1$s</string>
|
||||
<string name="checker_bypass_proxy_ip">IP از طریق proxy: %1$s</string>
|
||||
|
|
@ -301,16 +296,21 @@
|
|||
<string name="checker_bypass_open_proxy_review_suffix"> (نیازمند تأیید دور زدن)</string>
|
||||
<string name="checker_bypass_no_xray">Xray gRPC API: پیدا نشد</string>
|
||||
<string name="checker_bypass_xray_api">Xray gRPC API: %1$s</string>
|
||||
<string name="checker_bypass_extra_outbounds">  ...%1$d outbound دیگر</string>
|
||||
<plurals name="checker_bypass_extra_outbounds">
|
||||
<item quantity="one">  …%1$d outbound دیگر</item>
|
||||
<item quantity="other">  …%1$d outbound دیگر</item>
|
||||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network: VPN فعال نیست، این بررسی لازم نیست</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding: برنامه هم به مسیر پیشفرض غیر VPN و هم به VPN Network دسترسی دارد (VPN IP: %1$s, IP پیشفرض: %2$s)</string>
|
||||
<string name="checker_bypass_vpn_bound_ip">IP از طریق bind صریح به VPN Network: %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe: درخواست از طریق VPN Network، IP %1$s را برگرداند (TUN یک اینترفیس زنده و قابل مسیریابی است)</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN active probe: درخواست از طریق VPN Network در دسترس نیست (اینترفیس مسدود یا filtered است)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">بررسی فعال TUN: درخواست از طریق VPN Network با خطا شکست خورد (%1$s)</string>
|
||||
<string name="checker_bypass_default_non_vpn_ip">IP پیشفرض غیر VPN: %1$s</string>
|
||||
<string name="checker_bypass_underlying_unreachable">Underlying network: شبکه غیر VPN در دسترس نیست (full tunnel)</string>
|
||||
<string name="checker_bypass_underlying_unreachable_reason">شبکه زیربنایی: شبکه غیر VPN در دسترس نیست (%1$s)</string>
|
||||
<string name="checker_bypass_gateway_leak">VPN gateway leak: برنامه میتواند از تونل VPN عبور کند (IP VPN: %1$s، IP واقعی: %2$s)</string>
|
||||
<string name="checker_bypass_underlying_same_ip">شبکه underlying در دسترس است، اما IP با VPN%1$s یکسان است: split tunnel تأیید نشد</string>
|
||||
<string name="checker_bypass_compare_incomplete">مقایسه split tunnel ناقص است (IP VPN: %1$s، IP غیر VPN: %2$s): بررسی دستی لازم است</string>
|
||||
|
||||
<string name="checker_vpn_known_apps_none">برنامههای شناختهشده VPN/Proxy و ارائهدهندگان VpnService: موردی پیدا نشد</string>
|
||||
<string name="checker_vpn_installed_app">برنامه نصبشده: %1$s (%2$s)</string>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="md_primary">#FFC9C5D0</color>
|
||||
<color name="md_on_primary">#FF312E37</color>
|
||||
<color name="md_primary_container">#FF47434D</color>
|
||||
|
|
@ -18,13 +15,10 @@
|
|||
|
||||
<color name="status_green">#FF89D998</color>
|
||||
<color name="status_green_container">#FF193823</color>
|
||||
<color name="status_green_on_container">#FFB6F5C0</color>
|
||||
<color name="status_red">#FFFFB4AB</color>
|
||||
<color name="status_red_container">#FF5A1D1B</color>
|
||||
<color name="status_red_on_container">#FFFFDAD6</color>
|
||||
<color name="status_amber">#FFF1CC74</color>
|
||||
<color name="status_amber_container">#FF4B3900</color>
|
||||
<color name="status_amber_on_container">#FFFFE7A8</color>
|
||||
|
||||
<color name="verdict_green">@color/status_green</color>
|
||||
<color name="verdict_green_bg">@color/status_green_container</color>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">RKNHardering</string>
|
||||
<string name="github_repo_label">GitHub</string>
|
||||
<string name="github_repo_url">https://github.com/xtclovver/RKNHardering</string>
|
||||
<string name="run_check_notice">Во время проверки приложение выполнит сетевые запросы к доменам:\n• api.beacondb.net\n• api.ipapi.is\n• www.iplocate.io\n• ipv4-internet.yandex.net\n• ipv6-internet.yandex.net\n• 2ip.ru\n• ifconfig.me\n• ipv4.ifconfig.me\n• ipv6.ifconfig.me\n• checkip.amazonaws.com\n• api.ipify.org\n• api-ipv4.ip.sb\n• api-ipv6.ip.sb\n\nЕсли в настройках выбран кастомный DNS-резолвер, приложение также будет обращаться к его IP-адресам или DoH-домену.\n\nНажимая кнопку «Запустить проверку», вы соглашаетесь, что с вашего устройства будут выполнены эти сетевые запросы.\n\nПриложение не ведет логи этих запросов. Доступ к сети можно будет отключить в настройках приложения.</string>
|
||||
|
||||
|
|
@ -76,8 +75,6 @@
|
|||
|
||||
<string name="main_check_running">Проверка идет. Карточки обновляются прямо по мере получения данных.</string>
|
||||
<string name="main_check_stopped">Проверка остановлена</string>
|
||||
<string name="main_status_scanning">Сканирование...</string>
|
||||
<string name="main_status_preparing">Подготовка...</string>
|
||||
<string name="main_status_cancelled">Отменено</string>
|
||||
<string name="main_status_stopped">Остановлено</string>
|
||||
|
||||
|
|
@ -219,7 +216,6 @@
|
|||
<string name="checker_ip_comp_status_ipv6_ignored">IPv6 игнор</string>
|
||||
<string name="checker_ip_comp_status_ipv4_ipv6">IPv4/IPv6</string>
|
||||
<string name="checker_ip_comp_status_mismatch">Разнобой</string>
|
||||
<string name="checker_ip_comp_status_partial">Частично</string>
|
||||
<string name="checker_ip_comp_status_match">Совпадает</string>
|
||||
<string name="checker_ip_comp_single_ip">IP: %1$s</string>
|
||||
<string name="checker_ip_comp_single_error">Ошибка: %1$s</string>
|
||||
|
|
@ -228,7 +224,6 @@
|
|||
<string name="checker_ip_comp_ipv6_ignored_no_ipv4">IPv6-ошибки проигнорированы, валидного IPv4-ответа нет</string>
|
||||
<string name="checker_ip_comp_mixed_families">Сервисы вернули адреса разных семейств: %1$s</string>
|
||||
<string name="checker_ip_comp_different_ips">Сервисы вернули разные IP: %1$s</string>
|
||||
<string name="checker_ip_comp_partial">IP %1$s, но %2$d из %3$d сервисов не ответили</string>
|
||||
<string name="checker_ip_comp_all_same">Все ответившие сервисы группы вернули IP %1$s</string>
|
||||
<string name="checker_ip_comp_ipv6_ignored_suffix">; IPv6-ошибки проигнорированы: %1$d</string>
|
||||
|
||||
|
|
@ -294,15 +289,15 @@
|
|||
|
||||
<!-- BypassChecker -->
|
||||
<string name="checker_bypass_progress_port_scan_phase">Сканирование портов</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">Поиск открытых прокси на localhost...</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">Поиск открытых прокси на localhost…</string>
|
||||
<string name="checker_bypass_progress_popular_ports">Популярные порты</string>
|
||||
<string name="checker_bypass_progress_full_scan">Полное сканирование</string>
|
||||
<string name="checker_bypass_progress_port_detail">Порт %1$d (%2$d%%)</string>
|
||||
<string name="checker_bypass_progress_xray_detail">Поиск gRPC API на localhost...</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">Проверка доступа к non-VPN сети...</string>
|
||||
<string name="checker_bypass_progress_xray_detail">Поиск gRPC API на localhost…</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">Проверка доступа к non-VPN сети…</string>
|
||||
<string name="checker_bypass_progress_ip_phase">Проверка IP</string>
|
||||
<string name="checker_bypass_progress_ip_detail">Получение прямого IP и IP через прокси...</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">Проверка Telegram DC через прокси...</string>
|
||||
<string name="checker_bypass_progress_ip_detail">Получение прямого IP и IP через прокси…</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">Проверка Telegram DC через прокси…</string>
|
||||
<string name="checker_bypass_ip_unavailable">не удалось получить</string>
|
||||
<string name="checker_bypass_direct_ip">Прямой IP: %1$s</string>
|
||||
<string name="checker_bypass_proxy_ip">IP через прокси: %1$s</string>
|
||||
|
|
@ -315,10 +310,14 @@
|
|||
<string name="checker_bypass_open_proxy_review_suffix"> (требует подтверждения обхода)</string>
|
||||
<string name="checker_bypass_no_xray">Xray gRPC API: не обнаружен</string>
|
||||
<string name="checker_bypass_xray_api">Xray gRPC API: %1$s</string>
|
||||
<string name="checker_bypass_extra_outbounds">  ...ещё %1$d аутбаундов</string>
|
||||
<plurals name="checker_bypass_extra_outbounds">
|
||||
<item quantity="one">  …ещё %1$d аутбаунд</item>
|
||||
<item quantity="few">  …ещё %1$d аутбаунда</item>
|
||||
<item quantity="many">  …ещё %1$d аутбаундов</item>
|
||||
<item quantity="other">  …ещё %1$d аутбаунда</item>
|
||||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network: VPN не активен, проверка не требуется</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding: приложение видит и VPN Network, и non-VPN default сеть (VPN IP: %1$s, direct IP: %2$s)</string>
|
||||
<string name="checker_bypass_vpn_bound_ip">IP через явную привязку к VPN Network: %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN активный зонд: запрос через VPN Network вернул IP %1$s (TUN — живой маршрутизируемый интерфейс)</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN активный зонд: запрос через VPN Network недоступен (интерфейс заблокирован или filtered)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">TUN активный зонд: запрос через VPN Network завершился ошибкой (%1$s)</string>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">RKNHardering</string>
|
||||
<string name="github_repo_label">GitHub</string>
|
||||
<string name="github_repo_url">https://github.com/xtclovver/RKNHardering</string>
|
||||
<string name="run_check_notice">执行检查时,应用会向以下域名发起网络请求:\n• api.beacondb.net\n• api.ipapi.is\n• www.iplocate.io\n• ipv4-internet.yandex.net\n• ipv6-internet.yandex.net\n• 2ip.ru\n• ifconfig.me\n• ipv4.ifconfig.me\n• ipv6.ifconfig.me\n• checkip.amazonaws.com\n• api.ipify.org\n• api-ipv4.ip.sb\n• api-ipv6.ip.sb\n\n如果在设置中选择了自定义 DNS resolver,应用还会连接到其 IP 地址或 DoH 域名。\n\n点击“运行检查”即表示你同意这些网络请求将从你的设备发出。\n\n应用不会记录这些请求的日志。可以在应用设置中关闭网络访问。</string>
|
||||
|
||||
|
|
@ -76,8 +75,6 @@
|
|||
|
||||
<string name="main_check_running">检查正在进行中。卡片会随着数据到达实时更新。</string>
|
||||
<string name="main_check_stopped">检查已停止</string>
|
||||
<string name="main_status_scanning">正在扫描...</string>
|
||||
<string name="main_status_preparing">正在准备...</string>
|
||||
<string name="main_status_cancelled">已取消</string>
|
||||
<string name="main_status_stopped">已停止</string>
|
||||
|
||||
|
|
@ -207,7 +204,6 @@
|
|||
<string name="checker_ip_comp_status_ipv6_ignored">已忽略 IPv6</string>
|
||||
<string name="checker_ip_comp_status_ipv4_ipv6">IPv4/IPv6</string>
|
||||
<string name="checker_ip_comp_status_mismatch">不一致</string>
|
||||
<string name="checker_ip_comp_status_partial">部分</string>
|
||||
<string name="checker_ip_comp_status_match">一致</string>
|
||||
<string name="checker_ip_comp_single_ip">IP:%1$s</string>
|
||||
<string name="checker_ip_comp_single_error">错误:%1$s</string>
|
||||
|
|
@ -216,7 +212,6 @@
|
|||
<string name="checker_ip_comp_ipv6_ignored_no_ipv4">已忽略 IPv6 错误,但没有有效的 IPv4 响应</string>
|
||||
<string name="checker_ip_comp_mixed_families">服务返回了不同地址族的地址:%1$s</string>
|
||||
<string name="checker_ip_comp_different_ips">服务返回了不同的 IP:%1$s</string>
|
||||
<string name="checker_ip_comp_partial">IP 为 %1$s,但 %2$d / %3$d 个服务未响应</string>
|
||||
<string name="checker_ip_comp_all_same">该组所有有响应的服务都返回了 IP %1$s</string>
|
||||
<string name="checker_ip_comp_ipv6_ignored_suffix">;已忽略 IPv6 错误:%1$d</string>
|
||||
|
||||
|
|
@ -279,15 +274,15 @@
|
|||
<string name="checker_indirect_dumpsys_service_error">dumpsys activity services:%1$s</string>
|
||||
|
||||
<string name="checker_bypass_progress_port_scan_phase">端口扫描</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">正在查找 localhost 上的开放代理...</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">正在查找 localhost 上的开放代理…</string>
|
||||
<string name="checker_bypass_progress_popular_ports">常用端口</string>
|
||||
<string name="checker_bypass_progress_full_scan">完整扫描</string>
|
||||
<string name="checker_bypass_progress_port_detail">端口 %1$d(%2$d%%)</string>
|
||||
<string name="checker_bypass_progress_xray_detail">正在查找 localhost 上的 gRPC API...</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">正在检查非 VPN 网络访问能力...</string>
|
||||
<string name="checker_bypass_progress_xray_detail">正在查找 localhost 上的 gRPC API…</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">正在检查非 VPN 网络访问能力…</string>
|
||||
<string name="checker_bypass_progress_ip_phase">IP 检查</string>
|
||||
<string name="checker_bypass_progress_ip_detail">正在获取直接 IP 和经代理的 IP...</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">正在通过代理检查 Telegram DC...</string>
|
||||
<string name="checker_bypass_progress_ip_detail">正在获取直接 IP 和经代理的 IP…</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">正在通过代理检查 Telegram DC…</string>
|
||||
<string name="checker_bypass_ip_unavailable">不可用</string>
|
||||
<string name="checker_bypass_direct_ip">直接 IP:%1$s</string>
|
||||
<string name="checker_bypass_proxy_ip">经代理的 IP:%1$s</string>
|
||||
|
|
@ -300,16 +295,20 @@
|
|||
<string name="checker_bypass_open_proxy_review_suffix">(需要确认是否存在绕过)</string>
|
||||
<string name="checker_bypass_no_xray">Xray gRPC API:未发现</string>
|
||||
<string name="checker_bypass_xray_api">Xray gRPC API:%1$s</string>
|
||||
<string name="checker_bypass_extra_outbounds">  ...还有 %1$d 个 outbound</string>
|
||||
<plurals name="checker_bypass_extra_outbounds">
|
||||
<item quantity="other">  …还有 %1$d 个 outbound</item>
|
||||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network:VPN 未激活,无需执行此检查</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding:应用同时访问到了默认非 VPN 路径和 VPN Network(VPN IP:%1$s,默认 IP:%2$s)</string>
|
||||
<string name="checker_bypass_vpn_bound_ip">通过显式绑定到 VPN Network 获得的 IP:%1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe:通过 VPN Network 发起的请求返回了 IP %1$s(TUN 是一个可路由的活动接口)</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN active probe:通过 VPN Network 发起的请求不可用(接口被阻止或被 filtered)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">TUN 活跃探测:通过 VPN Network 发起的请求失败(%1$s)</string>
|
||||
<string name="checker_bypass_default_non_vpn_ip">默认非 VPN IP:%1$s</string>
|
||||
<string name="checker_bypass_underlying_unreachable">Underlying network:非 VPN 网络不可用(full tunnel)</string>
|
||||
<string name="checker_bypass_underlying_unreachable_reason">底层网络:非 VPN 网络不可用(%1$s)</string>
|
||||
<string name="checker_bypass_gateway_leak">VPN gateway leak:应用可以绕过 VPN 隧道(VPN IP:%1$s,真实 IP:%2$s)</string>
|
||||
<string name="checker_bypass_underlying_same_ip">Underlying 网络可达,但 IP 与 VPN%1$s 相同:split tunnel 未确认</string>
|
||||
<string name="checker_bypass_compare_incomplete">Split tunnel 对比不完整(VPN IP:%1$s,非 VPN IP:%2$s),需要人工复核</string>
|
||||
|
||||
<string name="checker_vpn_known_apps_none">已知 VPN/代理应用和 VpnService 提供方:未发现</string>
|
||||
<string name="checker_vpn_installed_app">已安装应用:%1$s(%2$s)</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- M3 Grey Primary palette -->
|
||||
<color name="md_primary">#FF5D5D6B</color>
|
||||
|
|
@ -21,13 +19,10 @@
|
|||
<!-- Status colours — used in findings and verdicts -->
|
||||
<color name="status_green">#FF1A6B2A</color>
|
||||
<color name="status_green_container">#FFD4EDDA</color>
|
||||
<color name="status_green_on_container">#FF0F4018</color>
|
||||
<color name="status_red">#FFB3261E</color>
|
||||
<color name="status_red_container">#FFFFDAD6</color>
|
||||
<color name="status_red_on_container">#FF680003</color>
|
||||
<color name="status_amber">#FF7A5C00</color>
|
||||
<color name="status_amber_container">#FFFFEFC8</color>
|
||||
<color name="status_amber_on_container">#FF4A3800</color>
|
||||
|
||||
<!-- Legacy aliases — keep for existing code that references these -->
|
||||
<color name="verdict_green">@color/status_green</color>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">RKNHardering</string>
|
||||
<string name="github_repo_label">GitHub</string>
|
||||
<string name="github_repo_url">https://github.com/xtclovver/RKNHardering</string>
|
||||
<string name="run_check_notice">During a check, the app will make network requests to these domains:\n• api.beacondb.net\n• api.ipapi.is\n• www.iplocate.io\n• ipv4-internet.yandex.net\n• ipv6-internet.yandex.net\n• 2ip.ru\n• ifconfig.me\n• ipv4.ifconfig.me\n• ipv6.ifconfig.me\n• checkip.amazonaws.com\n• api.ipify.org\n• api-ipv4.ip.sb\n• api-ipv6.ip.sb\n\nIf a custom DNS resolver is selected in settings, the app will also connect to its IP addresses or DoH domain.\n\nBy tapping "Run check", you agree that these network requests will be made from your device.\n\nThe app does not keep logs of these requests. Network access can be disabled in the app settings.</string>
|
||||
|
||||
|
|
@ -59,6 +58,7 @@
|
|||
<string name="main_subtitle">VPN/Proxy self-check</string>
|
||||
<string name="main_run_check">Run check</string>
|
||||
<string name="main_stop_check">Stop</string>
|
||||
<string name="main_card_geo_ip" translatable="false">GeoIP</string>
|
||||
<string name="main_card_ip_comparison">IP comparison</string>
|
||||
<string name="main_card_direct_signs">Direct signs</string>
|
||||
<string name="main_card_indirect_signs">Indirect signs</string>
|
||||
|
|
@ -76,13 +76,12 @@
|
|||
|
||||
<string name="main_check_running">Check in progress. Cards update live as data arrives.</string>
|
||||
<string name="main_check_stopped">Check stopped</string>
|
||||
<string name="main_status_scanning">Scanning...</string>
|
||||
<string name="main_status_preparing">Preparing...</string>
|
||||
<string name="main_status_cancelled">Cancelled</string>
|
||||
<string name="main_status_stopped">Stopped</string>
|
||||
|
||||
<string name="main_loading_status_scanning">Scanning</string>
|
||||
<string name="main_loading_status_checking">Checking</string>
|
||||
<string name="main_loading_status_progress" translatable="false">%1$s%2$s</string>
|
||||
|
||||
<string name="main_loading_geo_ip">Checking country, ASN, and hosting indicators.</string>
|
||||
<string name="main_loading_ip_comparison">Comparing the public IP through multiple independent services.</string>
|
||||
|
|
@ -112,6 +111,8 @@
|
|||
<string name="main_ipv6_error_ignored">IPv6 error ignored: </string>
|
||||
|
||||
<string name="settings_port_range_preview">Localhost proxy ports to scan: %1$s (%2$s)</string>
|
||||
<string name="settings_port_range_start_default" translatable="false">1024</string>
|
||||
<string name="settings_port_range_end_default" translatable="false">65535</string>
|
||||
<plurals name="settings_port_word">
|
||||
<item quantity="one">%1$s port</item>
|
||||
<item quantity="other">%1$s ports</item>
|
||||
|
|
@ -217,7 +218,6 @@
|
|||
<string name="checker_ip_comp_status_ipv6_ignored">IPv6 ignored</string>
|
||||
<string name="checker_ip_comp_status_ipv4_ipv6">IPv4/IPv6</string>
|
||||
<string name="checker_ip_comp_status_mismatch">Mismatch</string>
|
||||
<string name="checker_ip_comp_status_partial">Partial</string>
|
||||
<string name="checker_ip_comp_status_match">Match</string>
|
||||
<string name="checker_ip_comp_single_ip">IP: %1$s</string>
|
||||
<string name="checker_ip_comp_single_error">Error: %1$s</string>
|
||||
|
|
@ -226,7 +226,6 @@
|
|||
<string name="checker_ip_comp_ipv6_ignored_no_ipv4">IPv6 errors were ignored, but there is no valid IPv4 response</string>
|
||||
<string name="checker_ip_comp_mixed_families">Services returned addresses from different families: %1$s</string>
|
||||
<string name="checker_ip_comp_different_ips">Services returned different IPs: %1$s</string>
|
||||
<string name="checker_ip_comp_partial">IP %1$s, but %2$d of %3$d services did not respond</string>
|
||||
<string name="checker_ip_comp_all_same">All responding services in the group returned IP %1$s</string>
|
||||
<string name="checker_ip_comp_ipv6_ignored_suffix">; IPv6 errors ignored: %1$d</string>
|
||||
|
||||
|
|
@ -292,15 +291,15 @@
|
|||
|
||||
<!-- BypassChecker -->
|
||||
<string name="checker_bypass_progress_port_scan_phase">Port scan</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">Looking for open proxies on localhost...</string>
|
||||
<string name="checker_bypass_progress_port_scan_detail">Looking for open proxies on localhost…</string>
|
||||
<string name="checker_bypass_progress_popular_ports">Popular ports</string>
|
||||
<string name="checker_bypass_progress_full_scan">Full scan</string>
|
||||
<string name="checker_bypass_progress_port_detail">Port %1$d (%2$d%%)</string>
|
||||
<string name="checker_bypass_progress_xray_detail">Looking for a gRPC API on localhost...</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">Checking access to the non-VPN network...</string>
|
||||
<string name="checker_bypass_progress_xray_detail">Looking for a gRPC API on localhost…</string>
|
||||
<string name="checker_bypass_progress_underlying_detail">Checking access to the non-VPN network…</string>
|
||||
<string name="checker_bypass_progress_ip_phase">IP check</string>
|
||||
<string name="checker_bypass_progress_ip_detail">Obtaining the direct IP and the IP via proxy...</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">Checking Telegram DC via proxy...</string>
|
||||
<string name="checker_bypass_progress_ip_detail">Obtaining the direct IP and the IP via proxy…</string>
|
||||
<string name="checker_bypass_progress_mtproto_detail">Checking Telegram DC via proxy…</string>
|
||||
<string name="checker_bypass_ip_unavailable">unavailable</string>
|
||||
<string name="checker_bypass_direct_ip">Direct IP: %1$s</string>
|
||||
<string name="checker_bypass_proxy_ip">IP via proxy: %1$s</string>
|
||||
|
|
@ -313,10 +312,12 @@
|
|||
<string name="checker_bypass_open_proxy_review_suffix"> (requires bypass confirmation)</string>
|
||||
<string name="checker_bypass_no_xray">Xray gRPC API: not found</string>
|
||||
<string name="checker_bypass_xray_api">Xray gRPC API: %1$s</string>
|
||||
<string name="checker_bypass_extra_outbounds">  ...%1$d more outbounds</string>
|
||||
<plurals name="checker_bypass_extra_outbounds">
|
||||
<item quantity="one">  …%1$d more outbound</item>
|
||||
<item quantity="other">  …%1$d more outbounds</item>
|
||||
</plurals>
|
||||
<string name="checker_bypass_vpn_not_active">Underlying network: VPN is not active, the check is not required</string>
|
||||
<string name="checker_bypass_vpn_network_binding">VPN network binding: the app can reach both the default non-VPN path and VPN Network (VPN IP: %1$s, default IP: %2$s)</string>
|
||||
<string name="checker_bypass_vpn_bound_ip">IP via explicit binding to VPN Network: %1$s</string>
|
||||
<string name="checker_bypass_tun_probe_success">TUN active probe: a request via VPN Network returned IP %1$s (TUN is a live routable interface)</string>
|
||||
<string name="checker_bypass_tun_probe_failure">TUN active probe: a request via VPN Network is unavailable (the interface is blocked or filtered)</string>
|
||||
<string name="checker_bypass_tun_probe_failure_reason">TUN active probe: a request via VPN Network failed (%1$s)</string>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ grpc = "1.64.0"
|
|||
protobuf = "3.25.5"
|
||||
protobufPlugin = "0.9.6"
|
||||
okhttp = "4.12.0"
|
||||
robolectric = "4.14.1"
|
||||
androidxTestCore = "1.6.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
|
@ -27,6 +29,7 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
|
|||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }
|
||||
grpc-okhttp = { group = "io.grpc", name = "grpc-okhttp", version.ref = "grpc" }
|
||||
grpc-protobuf-lite = { group = "io.grpc", name = "grpc-protobuf-lite", version.ref = "grpc" }
|
||||
grpc-stub = { group = "io.grpc", name = "grpc-stub", version.ref = "grpc" }
|
||||
|
|
@ -35,6 +38,7 @@ javax-annotation-api = { group = "javax.annotation", name = "javax.annotation-ap
|
|||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttp-dnsoverhttps = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||
okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" }
|
||||
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
|
|||
6
lint.xml
Normal file
6
lint.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<lint>
|
||||
<!-- These checks are time-dependent ecosystem advisories and create noisy CI drift. -->
|
||||
<issue id="GradleDependency" severity="ignore" />
|
||||
<issue id="NewerVersionAvailable" severity="ignore" />
|
||||
</lint>
|
||||
Loading…
Add table
Add a link
Reference in a new issue