diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt index 655ed2c2..25cd1594 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/Utils.kt @@ -28,6 +28,10 @@ import java.util.UUID object Utils { + private val IPV4_REGEX = + Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") + private val IPV6_REGEX = Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") + /** * Convert string to editable for Kotlin. * @@ -46,22 +50,7 @@ object Utils { * @return The index of the value in the array, or -1 if not found. */ fun arrayFind(array: Array, value: String): Int { - for (i in array.indices) { - if (array[i] == value) { - return i - } - } - return -1 - } - - /** - * Parse a string to an integer. - * - * @param str The string to parse. - * @return The parsed integer, or 0 if parsing fails. - */ - fun parseInt(str: String): Int { - return parseInt(str, 0) + return array.indexOf(value) } /** @@ -71,7 +60,7 @@ object Utils { * @param default The default value if parsing fails. * @return The parsed integer, or the default value if parsing fails. */ - fun parseInt(str: String?, default: Int): Int { + fun parseInt(str: String?, default: Int = 0): Int { return str?.toIntOrNull() ?: default } @@ -86,7 +75,7 @@ object Utils { val cmb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cmb.primaryClip?.getItemAt(0)?.text.toString() } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to get clipboard content", e) "" } } @@ -103,7 +92,7 @@ object Utils { val clipData = ClipData.newPlainText(null, content) cmb.setPrimaryClip(clipData) } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to set clipboard content", e) } } @@ -124,15 +113,17 @@ object Utils { * @return The decoded string, or null if decoding fails. */ fun tryDecodeBase64(text: String?): String? { + if (text.isNullOrEmpty()) return null + try { return Base64.decode(text, Base64.NO_WRAP).toString(Charsets.UTF_8) } catch (e: Exception) { - Log.i(ANG_PACKAGE, "Parse base64 standard failed $e") + Log.w(AppConfig.TAG, "Failed to decode standard base64", e) } try { return Base64.decode(text, Base64.NO_WRAP.or(Base64.URL_SAFE)).toString(Charsets.UTF_8) } catch (e: Exception) { - Log.i(ANG_PACKAGE, "Parse base64 url safe failed $e") + Log.w(AppConfig.TAG, "Failed to decode URL-safe base64", e) } return null } @@ -147,7 +138,7 @@ object Utils { return try { Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to encode text to base64", e) "" } } @@ -159,43 +150,38 @@ object Utils { * @return True if the string is a valid IP address, false otherwise. */ fun isIpAddress(value: String?): Boolean { + if (value.isNullOrEmpty()) return false + try { - if (value.isNullOrEmpty()) { - return false - } - var addr = value - if (addr.isEmpty() || addr.isBlank()) { - return false - } + var addr = value.trim() + if (addr.isEmpty()) return false + //CIDR - if (addr.indexOf("/") > 0) { + if (addr.contains("/")) { val arr = addr.split("/") - if (arr.count() == 2 && Integer.parseInt(arr[1]) > -1) { + if (arr.size == 2 && arr[1].toIntOrNull() != null && arr[1].toInt() > -1) { addr = arr[0] } } - // "::ffff:192.168.173.22" - // "[::ffff:192.168.173.22]:80" + // Handle IPv4-mapped IPv6 addresses if (addr.startsWith("::ffff:") && '.' in addr) { addr = addr.drop(7) } else if (addr.startsWith("[::ffff:") && '.' in addr) { addr = addr.drop(8).replace("]", "") } - // addr = addr.toLowerCase() - val octets = addr.split('.').toTypedArray() + val octets = addr.split('.') if (octets.size == 4) { - if (octets[3].indexOf(":") > 0) { + if (octets[3].contains(":")) { addr = addr.substring(0, addr.indexOf(":")) } return isIpv4Address(addr) } - // Ipv6addr [2001:abc::123]:8080 return isIpv6Address(addr) } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to validate IP address", e) return false } } @@ -217,9 +203,7 @@ object Utils { * @return True if the string is a valid IPv4 address, false otherwise. */ private fun isIpv4Address(value: String): Boolean { - val regV4 = - Regex("^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$") - return regV4.matches(value) + return IPV4_REGEX.matches(value) } /** @@ -230,13 +214,10 @@ object Utils { */ private fun isIpv6Address(value: String): Boolean { var addr = value - if (addr.indexOf("[") == 0 && addr.lastIndexOf("]") > 0) { - addr = addr.drop(1) - addr = addr.dropLast(addr.count() - addr.lastIndexOf("]")) + if (addr.startsWith("[") && addr.endsWith("]")) { + addr = addr.drop(1).dropLast(1) } - val regV6 = - Regex("^((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))?((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7}$") - return regV6.matches(addr) + return IPV6_REGEX.matches(addr) } /** @@ -246,10 +227,10 @@ object Utils { * @return True if the string is a CoreDNS address, false otherwise. */ fun isCoreDNSAddress(s: String): Boolean { - return s.startsWith("https") - || s.startsWith("tcp") - || s.startsWith("quic") - || s == "localhost" + return s.startsWith("https") || + s.startsWith("tcp") || + s.startsWith("quic") || + s == "localhost" } /** @@ -259,21 +240,16 @@ object Utils { * @return True if the string is a valid URL, false otherwise. */ fun isValidUrl(value: String?): Boolean { - try { - if (value.isNullOrEmpty()) { - return false - } - if (Patterns.WEB_URL.matcher(value).matches() - || Patterns.DOMAIN_NAME.matcher(value).matches() - || URLUtil.isValidUrl(value) - ) { - return true - } + if (value.isNullOrEmpty()) return false + + return try { + Patterns.WEB_URL.matcher(value).matches() || + Patterns.DOMAIN_NAME.matcher(value).matches() || + URLUtil.isValidUrl(value) } catch (e: Exception) { - e.printStackTrace() - return false + Log.e(AppConfig.TAG, "Failed to validate URL", e) + false } - return false } /** @@ -283,8 +259,12 @@ object Utils { * @param uriString The URI string to open. */ fun openUri(context: Context, uriString: String) { - val uri = uriString.toUri() - context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + try { + val uri = uriString.toUri() + context.startActivity(Intent(Intent.ACTION_VIEW, uri)) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to open URI", e) + } } /** @@ -296,7 +276,7 @@ object Utils { return try { UUID.randomUUID().toString().replace("-", "") } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to generate UUID", e) "" } } @@ -311,7 +291,7 @@ object Utils { return try { URLDecoder.decode(url, Charsets.UTF_8.toString()) } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to decode URL", e) url } } @@ -326,7 +306,7 @@ object Utils { return try { URLEncoder.encode(url, Charsets.UTF_8.toString()).replace("+", "%20") } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to encode URL", e) url } } @@ -339,13 +319,18 @@ object Utils { * @return The content of the asset file as a string. */ fun readTextFromAssets(context: Context?, fileName: String): String { - if (context == null) { - return "" + if (context == null) return "" + + return try { + context.assets.open(fileName).use { inputStream -> + inputStream.bufferedReader().use { reader -> + reader.readText() + } + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to read asset file: $fileName", e) + "" } - val content = context.assets.open(fileName).bufferedReader().use { - it.readText() - } - return content } /** @@ -355,11 +340,15 @@ object Utils { * @return The path to the user asset directory. */ fun userAssetPath(context: Context?): String { - if (context == null) - return "" - val extDir = context.getExternalFilesDir(AppConfig.DIR_ASSETS) - ?: return context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath - return extDir.absolutePath + if (context == null) return "" + + return try { + context.getExternalFilesDir(AppConfig.DIR_ASSETS)?.absolutePath + ?: context.getDir(AppConfig.DIR_ASSETS, 0).absolutePath + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get user asset path", e) + "" + } } /** @@ -369,11 +358,15 @@ object Utils { * @return The path to the backup directory. */ fun backupPath(context: Context?): String { - if (context == null) - return "" - val extDir = context.getExternalFilesDir(AppConfig.DIR_BACKUPS) - ?: return context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath - return extDir.absolutePath + if (context == null) return "" + + return try { + context.getExternalFilesDir(AppConfig.DIR_BACKUPS)?.absolutePath + ?: context.getDir(AppConfig.DIR_BACKUPS, 0).absolutePath + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to get backup path", e) + "" + } } /** @@ -382,8 +375,13 @@ object Utils { * @return The device ID for XUDP base key. */ fun getDeviceIdForXUDPBaseKey(): String { - val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8) - return Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) + return try { + val androidId = Settings.Secure.ANDROID_ID.toByteArray(Charsets.UTF_8) + Base64.encodeToString(androidId.copyOf(32), Base64.NO_PADDING.or(Base64.URL_SAFE)) + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to generate device ID", e) + "" + } } /** @@ -403,11 +401,10 @@ object Utils { * @return The formatted IPv6 address, or the original address if not valid. */ fun getIpv6Address(address: String?): String { - if (address == null) { - return "" - } + if (address.isNullOrEmpty()) return "" + return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) { - String.format("[%s]", address) + "[$address]" } else { address } @@ -431,8 +428,7 @@ object Utils { * @return The URL string with illegal characters replaced. */ fun fixIllegalUrl(str: String): String { - return str - .replace(" ", "%20") + return str.replace(" ", "%20") .replace("|", "%7C") } @@ -463,14 +459,15 @@ object Utils { * @return True if the string is a valid subscription URL, false otherwise. */ fun isValidSubUrl(value: String?): Boolean { - try { - if (value.isNullOrEmpty()) return false - if (URLUtil.isHttpsUrl(value)) return true - if (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) return true + if (value.isNullOrEmpty()) return false + + return try { + URLUtil.isHttpsUrl(value) || + (URLUtil.isHttpUrl(value) && value.contains(LOOPBACK)) } catch (e: Exception) { - e.printStackTrace() + Log.e(AppConfig.TAG, "Failed to validate subscription URL", e) + false } - return false } /** @@ -489,7 +486,7 @@ object Utils { * * @return True if the package is Xray, false otherwise. */ - fun isXray(): Boolean = (ANG_PACKAGE.startsWith("com.v2ray.ang")) + fun isXray(): Boolean = ANG_PACKAGE.startsWith("com.v2ray.ang") }