From ce55eaef0eab79f07fc6ca875454c465db1ca06a Mon Sep 17 00:00:00 2001 From: xtclovver Date: Sun, 12 Apr 2026 22:55:09 +0300 Subject: [PATCH] lint fixes --- .idea/misc.xml | 2 +- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 7 ++- .../com/notcvnt/rknhardering/MainActivity.kt | 37 ++++++----- .../notcvnt/rknhardering/SettingsActivity.kt | 62 ++++++++++--------- .../rknhardering/TouchAwareScrollView.kt | 47 ++++++++++++++ .../rknhardering/checker/BypassChecker.kt | 8 ++- .../checker/IndirectSignsChecker.kt | 7 ++- .../checker/LocationSignalsChecker.kt | 42 +++++++++++-- app/src/main/res/layout/activity_main.xml | 30 +++++---- app/src/main/res/layout/activity_settings.xml | 16 +++-- app/src/main/res/values-fa/strings.xml | 24 +++---- app/src/main/res/values-night/colors.xml | 6 -- app/src/main/res/values-ru/strings.xml | 23 ++++--- app/src/main/res/values-zh-rCN/strings.xml | 23 ++++--- app/src/main/res/values/colors.xml | 5 -- app/src/main/res/values/strings.xml | 25 ++++---- gradle/libs.versions.toml | 4 ++ lint.xml | 6 ++ 19 files changed, 240 insertions(+), 138 deletions(-) create mode 100644 app/src/main/java/com/notcvnt/rknhardering/TouchAwareScrollView.kt create mode 100644 lint.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 7146cb1..92a4936 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 773e437..ec333f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a91afc5..48b2c32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -8,7 +9,6 @@ - @@ -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"> diff --git a/app/src/main/java/com/notcvnt/rknhardering/MainActivity.kt b/app/src/main/java/com/notcvnt/rknhardering/MainActivity.kt index 659070b..1372d2a 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/MainActivity.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/MainActivity.kt @@ -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) { if (content.isEmpty()) return - if (verdictDetailsContent.childCount > 0) { + if (verdictDetailsContent.isNotEmpty()) { verdictDetailsContent.addView( View(this).apply { layoutParams = LinearLayout.LayoutParams( diff --git a/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt b/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt index eb781cd..3155311 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt @@ -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(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 { diff --git a/app/src/main/java/com/notcvnt/rknhardering/TouchAwareScrollView.kt b/app/src/main/java/com/notcvnt/rknhardering/TouchAwareScrollView.kt new file mode 100644 index 0000000..ffdac56 --- /dev/null +++ b/app/src/main/java/com/notcvnt/rknhardering/TouchAwareScrollView.kt @@ -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) + } +} diff --git a/app/src/main/java/com/notcvnt/rknhardering/checker/BypassChecker.kt b/app/src/main/java/com/notcvnt/rknhardering/checker/BypassChecker.kt index 1d9f170..fad6181 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/checker/BypassChecker.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/checker/BypassChecker.kt @@ -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, diff --git a/app/src/main/java/com/notcvnt/rknhardering/checker/IndirectSignsChecker.kt b/app/src/main/java/com/notcvnt/rknhardering/checker/IndirectSignsChecker.kt index 6be2e4d..223f54f 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/checker/IndirectSignsChecker.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/checker/IndirectSignsChecker.kt @@ -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 { + return cm.allNetworks + } + private fun checkNotVpnCapability( context: Context, findings: MutableList, diff --git a/app/src/main/java/com/notcvnt/rknhardering/checker/LocationSignalsChecker.kt b/app/src/main/java/com/notcvnt/rknhardering/checker/LocationSignalsChecker.kt index 9412b6a..d6399bb 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/checker/LocationSignalsChecker.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/checker/LocationSignalsChecker.kt @@ -160,9 +160,11 @@ object LocationSignalsChecker { } private fun collectSimCards(context: Context, tm: TelephonyManager): List { - 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? { + 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() } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ef4f7b0..cab41f3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -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" /> - + android:contentDescription="@null" + android:importantForAccessibility="no" /> @@ -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" /> + android:contentDescription="@null" + android:importantForAccessibility="no" /> + android:contentDescription="@null" + android:importantForAccessibility="no" /> + android:contentDescription="@null" + android:importantForAccessibility="no" /> + android:contentDescription="@null" + android:importantForAccessibility="no" /> @@ -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" /> - + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 78226d3..9b5e7dc 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -2,10 +2,11 @@ + android:fitsSystemWindows="true" + tools:ignore="TooManyViews"> + android:text="@string/settings_port_range_start_default" /> @@ -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" /> @@ -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" /> @@ -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" /> diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index fd94fb2..bb0226a 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -1,6 +1,5 @@ RKNHardering - GitHub https://github.com/xtclovver/RKNHardering در زمان اجرای بررسی، برنامه به این دامنه‌ها درخواست شبکه ارسال می‌کند:\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برنامه لاگ این درخواست‌ها را نگه نمی‌دارد. دسترسی به شبکه را می‌توان از تنظیمات برنامه غیرفعال کرد. @@ -76,8 +75,6 @@ بررسی در حال انجام است. کارت‌ها هم‌زمان با رسیدن داده‌ها به‌روزرسانی می‌شوند. بررسی متوقف شد - در حال اسکن... - در حال آماده‌سازی... لغو شد متوقف شد @@ -208,7 +205,6 @@ IPv6 نادیده گرفته شد IPv4/IPv6 ناسازگار - ناقص یکسان IP: %1$s خطا: %1$s @@ -217,7 +213,6 @@ خطاهای IPv6 نادیده گرفته شدند، اما پاسخ معتبر IPv4 وجود ندارد سرویس‌ها آدرس‌هایی از خانواده‌های متفاوت برگرداندند: %1$s سرویس‌ها IPهای متفاوتی برگرداندند: %1$s - IP برابر %1$s است، اما %2$d از %3$d سرویس پاسخ ندادند همه سرویس‌های پاسخ‌داده در این گروه IP %1$s را برگرداندند ؛ خطاهای IPv6 نادیده گرفته شد: %1$d @@ -280,15 +275,15 @@ dumpsys activity services: %1$s اسکن پورت - در حال جست‌وجوی proxyهای باز روی localhost... + در حال جست‌وجوی proxyهای باز روی localhost… پورت‌های رایج اسکن کامل پورت %1$d (%2$d%%) - در حال جست‌وجوی gRPC API روی localhost... - در حال بررسی دسترسی به شبکه غیر VPN... + در حال جست‌وجوی gRPC API روی localhost… + در حال بررسی دسترسی به شبکه غیر VPN… بررسی IP - در حال به‌دست‌آوردن IP مستقیم و IP از طریق proxy... - در حال بررسی Telegram DC از طریق proxy... + در حال به‌دست‌آوردن IP مستقیم و IP از طریق proxy… + در حال بررسی Telegram DC از طریق proxy… در دسترس نیست IP مستقیم: %1$s IP از طریق proxy: %1$s @@ -301,16 +296,21 @@ (نیازمند تأیید دور زدن) Xray gRPC API: پیدا نشد Xray gRPC API: %1$s -   ...%1$d outbound دیگر + +   …%1$d outbound دیگر +   …%1$d outbound دیگر + Underlying network: VPN فعال نیست، این بررسی لازم نیست VPN network binding: برنامه هم به مسیر پیش‌فرض غیر VPN و هم به VPN Network دسترسی دارد (VPN IP: %1$s, IP پیش‌فرض: %2$s) - IP از طریق bind صریح به VPN Network: %1$s TUN active probe: درخواست از طریق VPN Network، IP %1$s را برگرداند (TUN یک اینترفیس زنده و قابل مسیریابی است) TUN active probe: درخواست از طریق VPN Network در دسترس نیست (اینترفیس مسدود یا filtered است) + بررسی فعال TUN: درخواست از طریق VPN Network با خطا شکست خورد (%1$s) IP پیش‌فرض غیر VPN: %1$s Underlying network: شبکه غیر VPN در دسترس نیست (full tunnel) + شبکه زیربنایی: شبکه غیر VPN در دسترس نیست (%1$s) VPN gateway leak: برنامه می‌تواند از تونل VPN عبور کند (IP VPN: %1$s، IP واقعی: %2$s) شبکه underlying در دسترس است، اما IP با VPN%1$s یکسان است: split tunnel تأیید نشد + مقایسه split tunnel ناقص است (IP VPN: %1$s، IP غیر VPN: %2$s): بررسی دستی لازم است برنامه‌های شناخته‌شده VPN/Proxy و ارائه‌دهندگان VpnService: موردی پیدا نشد برنامه نصب‌شده: %1$s (%2$s) diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index dc351c8..efbd8c8 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,8 +1,5 @@ - #FF000000 - #FFFFFFFF - #FFC9C5D0 #FF312E37 #FF47434D @@ -18,13 +15,10 @@ #FF89D998 #FF193823 - #FFB6F5C0 #FFFFB4AB #FF5A1D1B - #FFFFDAD6 #FFF1CC74 #FF4B3900 - #FFFFE7A8 @color/status_green @color/status_green_container diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d341d95..f935ec3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,5 @@ RKNHardering - GitHub https://github.com/xtclovver/RKNHardering Во время проверки приложение выполнит сетевые запросы к доменам:\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Приложение не ведет логи этих запросов. Доступ к сети можно будет отключить в настройках приложения. @@ -76,8 +75,6 @@ Проверка идет. Карточки обновляются прямо по мере получения данных. Проверка остановлена - Сканирование... - Подготовка... Отменено Остановлено @@ -219,7 +216,6 @@ IPv6 игнор IPv4/IPv6 Разнобой - Частично Совпадает IP: %1$s Ошибка: %1$s @@ -228,7 +224,6 @@ IPv6-ошибки проигнорированы, валидного IPv4-ответа нет Сервисы вернули адреса разных семейств: %1$s Сервисы вернули разные IP: %1$s - IP %1$s, но %2$d из %3$d сервисов не ответили Все ответившие сервисы группы вернули IP %1$s ; IPv6-ошибки проигнорированы: %1$d @@ -294,15 +289,15 @@ Сканирование портов - Поиск открытых прокси на localhost... + Поиск открытых прокси на localhost… Популярные порты Полное сканирование Порт %1$d (%2$d%%) - Поиск gRPC API на localhost... - Проверка доступа к non-VPN сети... + Поиск gRPC API на localhost… + Проверка доступа к non-VPN сети… Проверка IP - Получение прямого IP и IP через прокси... - Проверка Telegram DC через прокси... + Получение прямого IP и IP через прокси… + Проверка Telegram DC через прокси… не удалось получить Прямой IP: %1$s IP через прокси: %1$s @@ -315,10 +310,14 @@ (требует подтверждения обхода) Xray gRPC API: не обнаружен Xray gRPC API: %1$s -   ...ещё %1$d аутбаундов + +   …ещё %1$d аутбаунд +   …ещё %1$d аутбаунда +   …ещё %1$d аутбаундов +   …ещё %1$d аутбаунда + Underlying network: VPN не активен, проверка не требуется VPN network binding: приложение видит и VPN Network, и non-VPN default сеть (VPN IP: %1$s, direct IP: %2$s) - IP через явную привязку к VPN Network: %1$s TUN активный зонд: запрос через VPN Network вернул IP %1$s (TUN — живой маршрутизируемый интерфейс) TUN активный зонд: запрос через VPN Network недоступен (интерфейс заблокирован или filtered) TUN активный зонд: запрос через VPN Network завершился ошибкой (%1$s) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 242a664..e4491a1 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1,6 +1,5 @@ RKNHardering - GitHub https://github.com/xtclovver/RKNHardering 执行检查时,应用会向以下域名发起网络请求:\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应用不会记录这些请求的日志。可以在应用设置中关闭网络访问。 @@ -76,8 +75,6 @@ 检查正在进行中。卡片会随着数据到达实时更新。 检查已停止 - 正在扫描... - 正在准备... 已取消 已停止 @@ -207,7 +204,6 @@ 已忽略 IPv6 IPv4/IPv6 不一致 - 部分 一致 IP:%1$s 错误:%1$s @@ -216,7 +212,6 @@ 已忽略 IPv6 错误,但没有有效的 IPv4 响应 服务返回了不同地址族的地址:%1$s 服务返回了不同的 IP:%1$s - IP 为 %1$s,但 %2$d / %3$d 个服务未响应 该组所有有响应的服务都返回了 IP %1$s ;已忽略 IPv6 错误:%1$d @@ -279,15 +274,15 @@ dumpsys activity services:%1$s 端口扫描 - 正在查找 localhost 上的开放代理... + 正在查找 localhost 上的开放代理… 常用端口 完整扫描 端口 %1$d(%2$d%%) - 正在查找 localhost 上的 gRPC API... - 正在检查非 VPN 网络访问能力... + 正在查找 localhost 上的 gRPC API… + 正在检查非 VPN 网络访问能力… IP 检查 - 正在获取直接 IP 和经代理的 IP... - 正在通过代理检查 Telegram DC... + 正在获取直接 IP 和经代理的 IP… + 正在通过代理检查 Telegram DC… 不可用 直接 IP:%1$s 经代理的 IP:%1$s @@ -300,16 +295,20 @@ (需要确认是否存在绕过) Xray gRPC API:未发现 Xray gRPC API:%1$s -   ...还有 %1$d 个 outbound + +   …还有 %1$d 个 outbound + Underlying network:VPN 未激活,无需执行此检查 VPN network binding:应用同时访问到了默认非 VPN 路径和 VPN Network(VPN IP:%1$s,默认 IP:%2$s) - 通过显式绑定到 VPN Network 获得的 IP:%1$s TUN active probe:通过 VPN Network 发起的请求返回了 IP %1$s(TUN 是一个可路由的活动接口) TUN active probe:通过 VPN Network 发起的请求不可用(接口被阻止或被 filtered) + TUN 活跃探测:通过 VPN Network 发起的请求失败(%1$s) 默认非 VPN IP:%1$s Underlying network:非 VPN 网络不可用(full tunnel) + 底层网络:非 VPN 网络不可用(%1$s) VPN gateway leak:应用可以绕过 VPN 隧道(VPN IP:%1$s,真实 IP:%2$s) Underlying 网络可达,但 IP 与 VPN%1$s 相同:split tunnel 未确认 + Split tunnel 对比不完整(VPN IP:%1$s,非 VPN IP:%2$s),需要人工复核 已知 VPN/代理应用和 VpnService 提供方:未发现 已安装应用:%1$s(%2$s) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 0778ccc..7d52e7a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,7 +1,5 @@ - #FF000000 - #FFFFFFFF #FF5D5D6B @@ -21,13 +19,10 @@ #FF1A6B2A #FFD4EDDA - #FF0F4018 #FFB3261E #FFFFDAD6 - #FF680003 #FF7A5C00 #FFFFEFC8 - #FF4A3800 @color/status_green diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3049b16..20b2bbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ RKNHardering - GitHub https://github.com/xtclovver/RKNHardering 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. @@ -59,6 +58,7 @@ VPN/Proxy self-check Run check Stop + GeoIP IP comparison Direct signs Indirect signs @@ -76,13 +76,12 @@ Check in progress. Cards update live as data arrives. Check stopped - Scanning... - Preparing... Cancelled Stopped Scanning Checking + %1$s%2$s Checking country, ASN, and hosting indicators. Comparing the public IP through multiple independent services. @@ -112,6 +111,8 @@ IPv6 error ignored: Localhost proxy ports to scan: %1$s (%2$s) + 1024 + 65535 %1$s port %1$s ports @@ -217,7 +218,6 @@ IPv6 ignored IPv4/IPv6 Mismatch - Partial Match IP: %1$s Error: %1$s @@ -226,7 +226,6 @@ IPv6 errors were ignored, but there is no valid IPv4 response Services returned addresses from different families: %1$s Services returned different IPs: %1$s - IP %1$s, but %2$d of %3$d services did not respond All responding services in the group returned IP %1$s ; IPv6 errors ignored: %1$d @@ -292,15 +291,15 @@ Port scan - Looking for open proxies on localhost... + Looking for open proxies on localhost… Popular ports Full scan Port %1$d (%2$d%%) - Looking for a gRPC API on localhost... - Checking access to the non-VPN network... + Looking for a gRPC API on localhost… + Checking access to the non-VPN network… IP check - Obtaining the direct IP and the IP via proxy... - Checking Telegram DC via proxy... + Obtaining the direct IP and the IP via proxy… + Checking Telegram DC via proxy… unavailable Direct IP: %1$s IP via proxy: %1$s @@ -313,10 +312,12 @@ (requires bypass confirmation) Xray gRPC API: not found Xray gRPC API: %1$s -   ...%1$d more outbounds + +   …%1$d more outbound +   …%1$d more outbounds + Underlying network: VPN is not active, the check is not required VPN network binding: the app can reach both the default non-VPN path and VPN Network (VPN IP: %1$s, default IP: %2$s) - IP via explicit binding to VPN Network: %1$s TUN active probe: a request via VPN Network returned IP %1$s (TUN is a live routable interface) TUN active probe: a request via VPN Network is unavailable (the interface is blocked or filtered) TUN active probe: a request via VPN Network failed (%1$s) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 662be83..cac2939 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/lint.xml b/lint.xml new file mode 100644 index 0000000..60b83ae --- /dev/null +++ b/lint.xml @@ -0,0 +1,6 @@ + + + + + +