package com.tutpro.baresip import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.KeyguardManager import android.app.role.RoleManager import android.widget.Toast import android.content.ClipData import android.content.ClipboardManager import android.content.ContentResolver import android.content.Context import android.content.Context.ROLE_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.media.AudioAttributes import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.MediaPlayer import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import android.net.Uri import android.net.wifi.WifiManager import android.os.Build import android.os.Bundle import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore import android.provider.OpenableColumns import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import android.telecom.TelecomManager import android.telecom.PhoneAccountHandle import android.text.format.DateUtils import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatDelegate import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.core.content.ContextCompat import androidx.core.graphics.toColorInt import androidx.core.net.toUri import androidx.core.text.isDigitsOnly import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.navigation.NavController import com.tutpro.baresip.Call.Companion.inCall import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.RandomAccessFile import java.io.Serializable import java.lang.reflect.Method import java.net.InetAddress import java.net.NetworkInterface import java.net.SocketException import java.security.KeyStore import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.text.DateFormat import java.util.Calendar import java.util.Enumeration import java.util.GregorianCalendar import java.util.Locale import java.util.Random import java.util.concurrent.Executor import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream import javax.crypto.Cipher import javax.crypto.SecretKeyFactory import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager object Utils { fun getNameValue(string: String, name: String): ArrayList { val lines = string.split("\n") val result = ArrayList() for (line in lines) if (line.startsWith(name)) result.add((line.substring(name.length).trim()).split(" \t")[0]) return result } fun removeLinesStartingWithString(lines: String, string: String): String { var result = "" for (line in lines.split("\n")) if (!line.startsWith(string) && (line.isNotEmpty())) result += line + "\n" return result } fun uriHostPart(uri: String): String { return if (uri.contains("@")) { uri.substringAfter("@") .substringBefore(":") .substringBefore(";") .substringBefore("?") .substringBefore(">") } else { val parts = uri.split(":") when (parts.size) { 2 -> parts[1].substringBefore(";") .substringBefore("?") .substringBefore(">") 3 -> parts[1] else -> "" } } } fun uriUserPart(uri: String): String { return if (uri.contains("@")) uri.substringAfter(":").substringBefore("@") else if (uri.startsWith("tel:")) uri.substringAfter(":").substringBefore(";") else "" } fun uriMatch(firstUri: String, secondUri: String): Boolean { if (firstUri.startsWith("tel:")) return firstUri == secondUri || firstUri.substringAfter(":") == uriUserPart(secondUri) if (firstUri.startsWith("sip:")) return uriUserPart(firstUri) == uriUserPart(secondUri) && uriHostPart(firstUri) == uriHostPart(secondUri) return false } private fun uriParams(uri: String): List { val params = uri.split(";") return if (params.size == 1) listOf() else params.subList(1, params.size) } fun friendlyUri(ctx: Context, uri: String, account: Account, e164Check: Boolean = true, includeLabel: Boolean = true): String { return friendlyUri(uri, account, e164Check, includeLabel, ctx.getString(R.string.anonymous), ctx.getString(R.string.unknown)) } fun friendlyUri(uri: String, account: Account, e164Check: Boolean = true, includeLabel: Boolean = true, anonymous: String = "Anonymous", unknown: String = "Unknown"): String { var u = Contact.contactName(uri, includeLabel) if (u != uri) return u if (e164Check) { val e164Uri = e164Uri(uri, account.countryCode) u = Contact.contactName(e164Uri, includeLabel) if (u != e164Uri) return u } u = u.replace("%23", "#") if (u.contains("@")) { val user = uriUserPart(u) val host = uriHostPart(u) val params = uriParams(u).filter{it != "transport=udp"} return if (host == aorDomain(account.aor) || params.contains("user=phone")) user else if (host == "anonymous.invalid") anonymous else if (host == "unknown.invalid") unknown else if (params.isEmpty()) "$user@$host" else "$user@$host;" + params.joinToString(";") } if (u.startsWith("<") && u.endsWith(">")) u = u.substring(1).substringBeforeLast(">") if (u.startsWith("tel:", ignoreCase = true)) u = u.substring(4) u = u.substringBefore("?") u = u.replace(":5060", "") u = u.replace(";transport=udp", "", true) return u } fun e164Uri(uri: String, countryCode: String): String { val scheme = uri.take(4) val userPart = uriUserPart(uri) return if (userPart.isNotEmpty() && userPart.isDigitsOnly()) { when { userPart.startsWith("00") -> uri.replace("$scheme$userPart", scheme + "+" + userPart.substring(2)) countryCode == "" -> uri userPart.startsWith("0") -> uri.replace("${scheme}0", "$scheme$countryCode") else -> uri.replace(scheme, "$scheme$countryCode") } } else uri } fun uriComplete(uri: String, aor: String): String { val res = if (!uri.startsWith("sip:")) "sip:$uri" else uri return if (checkUriUser(uri)) "$res@${aorDomain(aor)}" else res } private fun String.replace(vararg pairs: Pair): String = pairs.fold(this) { acc, (old, new) -> acc.replace(old, new, ignoreCase = true) } fun uriUnescape(uri: String): String { return uri.replace("%2B" to "+", "%3A" to ":", "%3B" to ";", "%40" to "@", "%3D" to "=", "%23" to "#", "%2A" to "*") } fun aorDomain(aor: String): String { return uriHostPart(aor) } fun plainAor(aor: String): String { return uriUserPart(aor) + "@" + uriHostPart(aor) } fun checkAor(aor: String): Boolean { if (!checkSipUri(aor)) return false val params = uriParams(aor) return params.isEmpty() || ((params.size == 1) && params[0] in arrayOf("transport=udp", "transport=tcp", "transport=tls")) } private fun checkTransport(transport: String, transports: Set): Boolean { return transport.split("=")[0] == "transport" && transport.split("=")[1].lowercase() in transports } fun checkStunUri(uri: String): Boolean { if (uri.substringBefore(":").lowercase() !in setOf("stun", "stuns", "turn", "turns")) return false return checkHostPort(uri.substringAfter(":").substringBefore("?")) && (uri.indexOf("?") == -1 || checkTransport(uri.substringAfter("?"), setOf("udp", "tcp"))) } fun checkIpV4(ip: String): Boolean { return Regex("^(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))$").matches(ip) } private fun checkIpV6(ip: String): Boolean { return Regex("^(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4})$").matches(ip) } private fun checkIpv6InBrackets(bracketedIp: String): Boolean { return bracketedIp.startsWith("[") && bracketedIp.endsWith("]") && checkIpV6(bracketedIp.substring(1, bracketedIp.length - 2)) } fun checkUriUser(user: String): Boolean { val escaped = """%([\dABCDEFabcdef]){2}""".toRegex() escaped.replace(user, "").forEach { if (!(it.isLetterOrDigit() || "-_.!~*\'()&=+$,;?/".contains(it))) return false } return user.isNotEmpty() && !checkIpV4(user) && !checkIpV6(user) } fun checkDomain(domain: String): Boolean { val parts = domain.split(".") for (p in parts) { if (p.endsWith("-") || p.startsWith("-") || !Regex("^[-a-zA-Z0-9]+$").matches(p)) return false } return true } private fun checkPort(port: String): Boolean { val number = port.toIntOrNull() ?: return false return (number > 0) && (number < 65536) } fun checkIpPort(ipPort: String): Boolean { return if (ipPort.startsWith("[")) checkIpv6InBrackets(ipPort.substringBeforeLast(":")) && checkPort(ipPort.substringAfterLast(":")) else checkIpV4(ipPort.substringBeforeLast(":")) && checkPort(ipPort.substringAfterLast(":")) } private fun checkDomainPort(domainPort: String): Boolean { return checkDomain(domainPort.substringBeforeLast(":")) && checkPort(domainPort.substringAfterLast(":")) } private fun checkHostPort(hostPort: String): Boolean { return checkIpV4(hostPort) || checkIpv6InBrackets(hostPort) || checkDomain(hostPort) || checkIpPort(hostPort) || checkDomainPort(hostPort) } private fun checkParams(params: String): Boolean { for (param in params.split(";")) if (!checkParam(param)) return false return true } private fun checkParam(param: String): Boolean { val nameValue = param.split("=") if (nameValue.size == 1) return checkParamChars(nameValue[0]) if (nameValue.size == 2) { if (nameValue[0] == "transport") return setOf("udp", "tcp", "tls", "wss").contains(nameValue[1].lowercase()) return checkParamChars(nameValue[1]) } return false } private fun checkParamChars(s: String): Boolean { // Does not currently allow escaped characters val allowed = "[]/:&+$-_.!~*'()" for (c in s) if (!allowed.contains(c) && !c.isLetterOrDigit()) return false return true } fun paramValue(params: String, name: String): String { if (params == "") return "" for (param in params.split(";")) if (param.substringBefore("=") == name) return param.substringAfter("=") return "" } fun paramExists(params: String, name: String): Boolean { for (param in params.split(";")) if (param.substringBefore("=") == name) return true return false } fun checkHostPortParams(hpp: String) : Boolean { val restParams = hpp.split(";", limit = 2) return if (restParams.size == 1) checkHostPort(restParams[0]) else checkHostPort(restParams[0]) && checkParams(restParams[1]) } private fun checkSipUri(uri: String): Boolean { return if (uri.startsWith("sip:")) { val userRest = uri.substring(4).split("@") when (userRest.size) { 1 -> checkHostPortParams(userRest[0]) 2 -> checkUriUser(userRest[0]) && checkHostPortParams(userRest[1]) else -> false } } else { false } } fun isTelNumber(no: String): Boolean { return no.isNotEmpty() && Regex("^[0-9- (),*#+]{1,32}$").matches(no) } fun isTelUri(uri: String): Boolean { return uri.startsWith("tel:") && isTelNumber(uri.substring(4)) } fun isUssd(uri: String): Boolean { val u = uriUserPart(uri) // Must start with * or # and must end with #, but exclude "Secret Code" pattern (*#*#...#*#*) return (u.startsWith("*") || u.startsWith("#")) && u.endsWith("#") && !u.startsWith("*#*#") } fun checkUri(uri: String): Boolean { return checkSipUri(uri) || isTelUri(uri) } fun telToSip(telUri: String, account: Account): String { val hostPart = if (account.telProvider != "") account.telProvider else aorDomain(account.aor) return "sip:" + telUri.substring(4) .filterNot{setOf('-', ' ', '(', ')').contains(it)} .replace("#", "%23") + "@" + hostPart + ";user=phone" } fun sipToTel(sipUri: String): String { if (sipUri.contains(";user=phone")) { val user = uriUserPart(sipUri) if (user != "") return "tel:${user.replace("%23", "#")}" } return sipUri } fun checkName(name: String): Boolean { return name.isNotEmpty() && name == String(name.toByteArray(), Charsets.UTF_8) && name.lines().size == 1 && !name.contains('"') } fun checkCountryCode(cc: String): Boolean { return cc.startsWith("+") && cc.length > 1 && cc.length < 5 && cc.substring(1).isDigitsOnly() && cc[1] != '0' } fun checkServerVal(server: String): Boolean { val parts = server.replace(Regex("[(][^()\\\\]+[)]"), "") .trim().split("\\s+".toRegex()) for (part in parts) if (!checkProduct(part)) return false return true } private fun checkProduct(product: String): Boolean { val parts = product.split("/", limit = 2) return if (parts.count() == 2) checkToken(parts[0]) && checkToken(parts[1]) else checkToken(parts[0]) } private fun checkToken(token: String): Boolean { return Regex("^[-a-zA-Z0-9.!%*_+`'~]+$").matches(token) } @Suppress("unused") fun checkIfName(name: String): Boolean { if ((name.length < 2) || !name.first().isLetter()) return false for (c in name) if (!c.isLetterOrDigit()) return false return true } fun implode(list: List, sep: String): String { var res = "" for (s in list) { res = if (res == "") s else res + sep + s } return res } fun unaccent(input: String): String { val normalized = java.text.Normalizer.normalize(input, java.text.Normalizer.Form.NFD) return "\\p{InCombiningDiacriticalMarks}+".toRegex().replace(normalized, "") } fun buildAnnotatedStringWithHighlight(name: String, query: String): AnnotatedString { val normalizedName = unaccent(name) val normalizedQuery = unaccent(query) val startIndex = normalizedName.indexOf(normalizedQuery, ignoreCase = true) return if (startIndex == -1) { buildAnnotatedString { append(name) } } else { buildAnnotatedString { append(name.take(startIndex)) withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { append(name.drop(startIndex).take(normalizedQuery.length)) } append(name.drop(startIndex + normalizedQuery.length)) } } } fun isVisible(): Boolean { return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } fun isHotSpotOn(wm: WifiManager): Boolean { try { val method: Method = wm.javaClass.getDeclaredMethod("isWifiApEnabled") method.isAccessible = true return method.invoke(wm) as Boolean } catch (_: Throwable) { } return false } fun hotSpotAddresses(): Map { val result = mutableMapOf() try { val interfaces: Enumeration = NetworkInterface.getNetworkInterfaces() while (interfaces.hasMoreElements()) { val iface: NetworkInterface = interfaces.nextElement() val ifName = iface.name Log.d(TAG, "Found interface with name $ifName") if (ifName.startsWith("ap") || ifName.contains("wlan")) { val addresses: Enumeration = iface.inetAddresses while (addresses.hasMoreElements()) { val inetAddress: InetAddress = addresses.nextElement() if (inetAddress.isSiteLocalAddress) result[inetAddress.hostAddress!!] = ifName } if (result.isNotEmpty()) return result } } } catch (ex: SocketException) { Log.e(TAG, "hotSpotAddresses SocketException: $ex") } catch (ex: NullPointerException) { Log.e(TAG, "hotSpotAddresses NullPointerException: $ex") } return result } fun checkPermissions(ctx: Context, permissions: Array) : Boolean { for (p in permissions) { if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "Permission $p is denied") return false } else { Log.d(TAG, "Permission $p is granted") } } return true } @Suppress("unused") fun copyToClipboard(ctx: Context, text: String) { val clipboard = ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("text", text) clipboard.setPrimaryClip(clip) Toast.makeText(ctx, ctx.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show() } fun copyAssetToFile(context: Context, asset: String, path: String) { try { context.assets.open(asset).use { `is` -> FileOutputStream(path).use { os -> val buffer = ByteArray(512) var byteRead: Int = `is`.read(buffer) while (byteRead != -1) { os.write(buffer, 0, byteRead) byteRead = `is`.read(buffer) } } } } catch (e: IOException) { Log.e(TAG, "Failed to copy asset '$asset' to file: $e") } } fun deleteFile(file: File) { if (file.exists()) { try { file.delete() } catch (e: IOException) { Log.e(TAG, "Could not delete file ${file.absolutePath}: $e") } } } fun deleteFile(ctx: Context, uri: Uri): Boolean { val contentResolver: ContentResolver = ctx.contentResolver try { if (DocumentsContract.isDocumentUri(ctx, uri)) { if (DocumentsContract.deleteDocument(contentResolver, uri)) { Log.d(TAG, "File deleted successfully: $uri") return true } else { Log.d(TAG, "File not found or could not be deleted: $uri") return false } } else { Log.d(TAG, "Uri is not a document uri: $uri") return false } } catch (e: UnsupportedOperationException) { Log.w(TAG, "Error deleting file $uri: $e") return false } catch (e: Exception) { Log.e(TAG, "Error deleting file $uri: $e") return false } } fun getFileContents(filePath: String): ByteArray? { return try { File(filePath).readBytes() } catch (e: FileNotFoundException) { Log.e(TAG, "File '$filePath' not found", e) null } catch (e: Exception) { Log.e(TAG, "Failed to read file '$filePath'", e) null } } fun putFileContents(filePath: String, contents: ByteArray): Boolean { try { File(filePath).writeBytes(contents) } catch (e: IOException) { Log.e(TAG, "Failed to write file '$filePath': $e") return false } return true } fun File.copyInputStreamToFile(inputStream: InputStream): Boolean { try { this.outputStream().use { fileOut -> inputStream.copyTo(fileOut) } return true } catch (e: IOException) { Log.e(TAG, "Failed to write file '${this.absolutePath}': $e") } return false } @RequiresApi(29) fun selectInputFile(request: ActivityResultLauncher) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "*/*" putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } request.launch(intent) } @RequiresApi(29) @Suppress("unused") fun selectOutputFile(title: String) { Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/octet-stream" putExtra(Intent.EXTRA_TITLE, title) putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } } fun downloadsPath(fileName: String): String { return Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS).path + "/$fileName" } fun fileNameOfUri(ctx: Context, uri: Uri): String { val cursor = ctx.contentResolver.query(uri, null, null, null, null) var name = "" if (cursor != null) { val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) cursor.moveToFirst() name = cursor.getString(index) ?: "" cursor.close() } return if (name == "") "$uri".substringAfterLast("/") else name } class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable { companion object { @Suppress("unused") const val serialVersionUID: Long = -29238082928391L } } private fun encrypt(content: ByteArray, password: CharArray): ByteArray? { fun intToByteArray(int: Int): ByteArray { val bytes = ByteArray(2) bytes[0] = (int shr 0).toByte() bytes[1] = (int shr 8).toByte() return bytes } try { val sr = SecureRandom() val salt = ByteArray(128) sr.nextBytes(salt) val iterationCount = Random().nextInt(1024) + 512 val pbKeySpec = PBEKeySpec(password, salt, iterationCount, 128) val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded val keySpec = SecretKeySpec(keyBytes, "AES") val ivRandom = SecureRandom() val iv = ByteArray(16) ivRandom.nextBytes(iv) val ivSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) val cipherData = cipher.doFinal(content) val res = ByteArray(128 + 2 + 16 + cipherData.size) salt.copyInto(res, 0) intToByteArray(iterationCount).copyInto(res, salt.size) iv.copyInto(res, salt.size + 2) cipherData.copyInto(res, salt.size + 2 + iv.size) return res } catch (e: Exception) { Log.e(TAG, "Encrypt failed", e) } return null } private fun decrypt(content: ByteArray, password: CharArray): ByteArray? { fun byteArrayToInt(bytes: ByteArray) : Int { return (bytes[1].toInt() and 0xff shl 8) or (bytes[0].toInt() and 0xff) } try { val salt = content.copyOfRange(0, 128) val iterationCount = byteArrayToInt(content.copyOfRange(128, 130)) val iv = content.copyOfRange(130, 146) val data = content.copyOfRange(146, content.size) val pbKeySpec = PBEKeySpec(password, salt, iterationCount, 128) val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded val keySpec = SecretKeySpec(keyBytes, "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") val ivSpec = IvParameterSpec(iv) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) return cipher.doFinal(data) } catch (e: Exception) { Log.e(TAG, "Decrypt failed", e) } return null } private fun decryptOld(obj: Crypto, password: CharArray): ByteArray? { var plainData: ByteArray? = null try { val pbKeySpec = PBEKeySpec(password, obj.salt, obj.iter, 128) val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded val keySpec = SecretKeySpec(keyBytes, "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") val ivSpec = IvParameterSpec(obj.iv) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) plainData = cipher.doFinal(obj.data) } catch (e: Exception) { Log.e(TAG, "Decrypt failed", e) } return plainData } fun encryptToUri(ctx: Context, uri: Uri, content: ByteArray, password: String): Boolean { val obj = if (password == "") content else encrypt(content, password.toCharArray()) try { val stream = ctx.contentResolver.openOutputStream(uri) if (stream != null) ObjectOutputStream(stream).use { it.writeObject(obj) } else { Log.w(TAG, "encryptToUri: could not open output stream") return false } } catch (e: Exception) { Log.w(TAG, "encryptToUri failed: $e") return false } return true } fun decryptFromUri(ctx: Context, uri: Uri, password: String): ByteArray? { var plainData: ByteArray? = null var stream: InputStream try { stream = ctx.contentResolver.openInputStream(uri) ?: return null } catch(e: Exception) { Log.w(TAG, "decryptFromUri could not open stream: $e") return null } try { ObjectInputStream(stream).use { val content = it.readObject() as ByteArray plainData = if (password == "") content else decrypt(content, password.toCharArray()) } } catch (e: Exception) { Log.w(TAG, "decryptFromUri as ByteArray failed: $e") try { ctx.contentResolver.openInputStream(uri)?.use { newStream -> ObjectInputStream(newStream).use { val obj = it.readObject() as Crypto plainData = decryptOld(obj, password.toCharArray()) } } } catch (e2: Exception) { Log.w(TAG, "decryptFromUri as Crypto failed: $e2") } } return plainData } fun zip(fileNames: ArrayList, zipFileName: String): Boolean { val zipFilePath = BaresipService.filesPath + "/" + zipFileName try { ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath))).use { out -> val data = ByteArray(1024) for (fileName in fileNames) { val filePath = BaresipService.filesPath + "/" + fileName if (File(filePath).exists()) { FileInputStream(filePath).use { fi -> BufferedInputStream(fi).use { origin -> val entry = ZipEntry(fileName) out.putNextEntry(entry) while (true) { val readBytes = origin.read(data) if (readBytes == -1) break out.write(data, 0, readBytes) } } } } } } } catch (e: IOException) { Log.e(TAG, "Failed to zip file '$zipFilePath': $e") return false } return true } fun unZip(zipFilePath: String): Boolean { val allFiles = listOf("accounts", "call_history", "config", "contacts", "messages", "uuid", "gzrtp.zid", "cert.pem", "ca_cert", "ca_certs.crt") val zipFiles = mutableListOf() try { ZipFile(File(zipFilePath)).use { zip -> zip.entries().asSequence().forEach { entry -> val entryName = if (entry.name.startsWith("/")) entry.name.substringAfterLast("/") else entry.name zipFiles.add(entryName) zip.getInputStream(entry).use { input -> File(BaresipService.filesPath + "/" + entryName).outputStream().use { output -> input.copyTo(output) } } } } } catch (e: IOException) { Log.e(TAG, "Failed to unzip file '$zipFilePath': $e") return false } (allFiles - zipFiles.toSet()).iterator().forEach { deleteFile(File(BaresipService.filesPath, it)) } return true } @Suppress("unused") fun dumpIntent(intent: Intent) { val bundle: Bundle = intent.extras ?: return val keys = bundle.keySet() val it = keys.iterator() Log.d(TAG, "Dumping intent start") while (it.hasNext()) { val key = it.next() Log.d(TAG, "[" + key + "=" + bundle.getBundle(key) + "]") } Log.d(TAG, "Dumping intent finish") } fun randomColor(): Int { return Color.argb( 255, kotlin.random.Random.nextInt(256), kotlin.random.Random.nextInt(256), kotlin.random.Random.nextInt(256) ) } fun requestDismissKeyguard(activity: Activity) { val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager kgm.requestDismissKeyguard(activity, null) } fun isThemeDark(ctx: Context) : Boolean { return Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES || ctx.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES } fun isAirplaneModeOn(ctx: Context): Boolean { return android.provider.Settings.Global.getInt( ctx.contentResolver, android.provider.Settings.Global.AIRPLANE_MODE_ON, 0 ) != 0 } @Suppress("unused") fun isPSTNCallActive(ctx: Context): Boolean { val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager ?: return false val baresipHandle = BaresipService.getPhoneAccountHandle(ctx) try { val getCallsMethod = TelecomManager::class.java.getMethod("getCalls") val calls = getCallsMethod.invoke(tm) as? List<*> if (calls != null) { for (c in calls) { if (c == null) continue val state = c.javaClass.getMethod("getState").invoke(c) as Int if (state == 1 || state == 2 || state == 3 || state == 4) { // 1=DIALING, 2=RINGING, 3=HOLDING, 4=ACTIVE val details = c.javaClass.getMethod("getDetails").invoke(c) val accountHandle = details?.javaClass?.getMethod("getAccountHandle")?.invoke(details) as? PhoneAccountHandle if (accountHandle != baresipHandle) { Log.d(TAG, "External call detected from account: $accountHandle") return true } } } } } catch (e: Exception) { Log.e(TAG, "isPSTNCallActive reflection error: $e") } return false } fun isAudioMode(ctx: Context, mode: Int): Boolean { val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager return am.mode == mode } fun relativeTime(ctx: Context, time: GregorianCalendar): String { return if (DateUtils.isToday(time.timeInMillis)) { val fmt = DateFormat.getTimeInstance(DateFormat.SHORT) ctx.getString(R.string.today) + "\n" + fmt.format(time.time) } else { val month = time.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault())!! .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } val day = time.get(Calendar.DAY_OF_MONTH) val currentYear = Calendar.getInstance().get(Calendar.YEAR) if (time.get(Calendar.YEAR) == currentYear) { val fmt = DateFormat.getTimeInstance(DateFormat.SHORT) "$month $day" + "\n" + fmt.format(time.time) } else { "$month $day" + "\n" + time.get(Calendar.YEAR) } } } fun setSpeakerPhone(executor: Executor, am: AudioManager, enable: Boolean) { if (Build.VERSION.SDK_INT >= 31) { if (!enable) { Log.d(TAG, "Disabling speakerphone") clearCommunicationDevice(am) if (inCall() && am.mode == AudioManager.MODE_NORMAL) { Log.d(TAG, "Restoring MODE_IN_COMMUNICATION") am.mode = AudioManager.MODE_IN_COMMUNICATION } return } val current = am.communicationDevice?.type ?: AudioDeviceInfo.TYPE_UNKNOWN Log.d(TAG, "Current com dev/mode is $current/${am.mode}") var speakerDevice: AudioDeviceInfo? = null for (device in am.availableCommunicationDevices) if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { speakerDevice = device break } if (speakerDevice == null) { Log.w(TAG,"Could not find requested communication device") return } if (current != speakerDevice.type) { // Currently at API levels 31+, speakerphone needs normal mode if (am.mode == AudioManager.MODE_NORMAL) { Log.d(TAG, "Setting com device to ${speakerDevice.type} in MODE_NORMAL") if (!am.setCommunicationDevice(speakerDevice)) Log.e(TAG, "Could not set com device") } else { val normalListener = object : AudioManager.OnModeChangedListener { override fun onModeChanged(mode: Int) { if (mode == AudioManager.MODE_NORMAL) { am.removeOnModeChangedListener(this) Log.d( TAG, "Setting com device to ${speakerDevice.type}" + " in mode ${am.mode}" ) if (!am.setCommunicationDevice(speakerDevice)) Log.e(TAG, "Could not set com device") } } } am.addOnModeChangedListener(executor, normalListener) Log.d(TAG, "Setting mode to NORMAL") am.mode = AudioManager.MODE_NORMAL } Log.d(TAG, "New com device/mode is " + "${am.communicationDevice?.type ?: AudioDeviceInfo.TYPE_UNKNOWN}/${am.mode}") } } else { @Suppress("DEPRECATION") am.isSpeakerphoneOn = enable Log.d(TAG, "Speakerphone is $enable") } } @RequiresApi(Build.VERSION_CODES.S) fun setCommunicationDevice(am: AudioManager, type: Int) { val current = am.communicationDevice?.type ?: AudioDeviceInfo.TYPE_UNKNOWN Log.d(TAG, "Current com dev/mode $current/${am.mode}") for (device in am.availableCommunicationDevices) if (device.type == type) { am.setCommunicationDevice(device) break } Log.d(TAG, "New com dev/mode is " + "${am.communicationDevice?.type ?: AudioDeviceInfo.TYPE_UNKNOWN}/${am.mode}") } fun clearCommunicationDevice(am: AudioManager) { if (Build.VERSION.SDK_INT >= 31) am.clearCommunicationDevice() else { @Suppress("DEPRECATION") am.isSpeakerphoneOn = false } } @Suppress("unused") fun playFile(ctx: Context, path: String) { Log.d(TAG, "Playing file $path") MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) setOnPreparedListener { Log.d(TAG, "Starting MediaPlayer") it.start() Log.d(TAG, "MediaPlayer started") } setOnCompletionListener { Log.d(TAG, "Stopping MediaPlayer") it.stop() it.release() } try { Log.d(TAG, "Preparing $path") setDataSource(ctx, path.toUri()) prepareAsync() } catch (e: IllegalArgumentException) { Log.e(TAG, "MediaPlayer IllegalArgumentException: ${e.message}") } catch (e: IOException) { Log.e(TAG, "MediaPlayer IOException: ${e.message}") } catch (e: Exception) { Log.e(TAG, "MediaPlayer Exception: ${e.message}") } } } fun aecAgcNsCheck() { val sessionId = Api.AAudio_open_stream() if (sessionId <= 0) { Log.e(TAG, "Failed to open AAudio stream or invalid sessionId ($sessionId)") return } if (AcousticEchoCanceler.isAvailable()) { val aec = AcousticEchoCanceler.create(sessionId) if (aec != null) { BaresipService.aecAvailable = true aec.release() Log.i(TAG, "Creation of hardware AEC for $sessionId succeeded") } else { Log.w(TAG, "Creation of hardware AEC for $sessionId failed") } } else Log.i(TAG, "Hardware AEC is NOT available") if (AutomaticGainControl.isAvailable()) { val agc = AutomaticGainControl.create(sessionId) if (agc != null) { BaresipService.agcAvailable = true agc.release() Log.d(TAG, "Creation of hardware AGC for $sessionId succeeded") } else { Log.w(TAG, "Creation of hardware AGC for $sessionId failed") } } else Log.i(TAG, "Hardware AGC is NOT available") if (NoiseSuppressor.isAvailable()) { val ns = NoiseSuppressor.create(sessionId) if (ns != null) { BaresipService.nsAvailable = true ns.release() Log.d(TAG, "Creation of hardware NS for $sessionId succeeded") } else { Log.w(TAG, "Creation of hardware NS for $sessionId failed") } } else Log.i(TAG, "Hardware NS is NOT available") Api.AAudio_close_stream() } fun readUrlWithCustomCAs(urlConnection: HttpsURLConnection, caFile: File): String? { if (!caFile.exists()) { Log.d("Utils", "Custom CA file not found at ${caFile.path}") return null } try { // Create a TrustManager that trusts the CAs in the user-provided file val customTrustManager = fun(): X509TrustManager { val certificateFactory = CertificateFactory.getInstance("X.509") val certificateInputStream = caFile.inputStream() val certificates = certificateFactory.generateCertificates(certificateInputStream) certificateInputStream.close() val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) keyStore.load(null, null) certificates.forEachIndexed { index, certificate -> keyStore.setCertificateEntry("user_ca_$index", certificate) } val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) tmf.init(keyStore) return tmf.trustManagers.find { it is X509TrustManager } as X509TrustManager }() // Create a TrustManager that trusts the default system CAs val systemTrustManager = fun(): X509TrustManager { val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) factory.init(null as KeyStore?) // A null keystore loads the system's default CAs return factory.trustManagers.find { it is X509TrustManager } as X509TrustManager }() // Create a composite TrustManager that delegates to both system and custom CAs @SuppressLint("CustomX509TrustManager") val compositeTrustManager = object : X509TrustManager { override fun checkClientTrusted(chain: Array?, authType: String?) { // Delegate to the system manager by default. systemTrustManager.checkClientTrusted(chain, authType) } override fun checkServerTrusted(chain: Array?, authType: String?) { try { // Try to validate the chain with the system's default TrustManager. systemTrustManager.checkServerTrusted(chain, authType) } catch (_: CertificateException) { // If that fails, and only if that fails, try to validate with our custom TrustManager. // This will throw the final CertificateException if it also fails. customTrustManager.checkServerTrusted(chain, authType) } } override fun getAcceptedIssuers(): Array { // Return a combined list of issuers from both trust managers. return systemTrustManager.acceptedIssuers + customTrustManager.acceptedIssuers } } // Create an SSLContext that uses new composite TrustManager val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, arrayOf(compositeTrustManager), null) // Tell HttpsURLConnection to use custom SSLContext for this connection urlConnection.sslSocketFactory = sslContext.socketFactory // Proceed with the connection and return the result return urlConnection.inputStream.bufferedReader().use { it.readText() } } catch (e: Exception) { // Catch any exception from certificate loading or from the network connection Log.e("Utils", "readUrlWithCustomCa failed: ${e.message}") return null } } fun Bitmap.toCircle(): Bitmap { // Use full package names to avoid conflict with Compose classes val output = androidx.core.graphics.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) val canvas = android.graphics.Canvas(output) val paint = android.graphics.Paint() val rect = android.graphics.Rect(0, 0, this.width, this.height) paint.isAntiAlias = true canvas.drawARGB(0, 0, 0, 0) canvas.drawCircle(this.width / 2f, this.height / 2f, this.width / 2f, paint) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) canvas.drawBitmap(this, rect, rect, paint) return output } fun createTextAvatar(letter: String, colorHex: String): Bitmap { // Use decent resolution 128x128 and let Android scale it down. val size = 128 // Use KTX createBitmap to match toCircle style val bitmap = androidx.core.graphics.createBitmap(size, size, Bitmap.Config.ARGB_8888) // Use Fully Qualified Names to avoid Compose conflicts val canvas = android.graphics.Canvas(bitmap) // Draw the colored circle background val bgPaint = android.graphics.Paint() bgPaint.isAntiAlias = true try { bgPaint.color = colorHex.toColorInt() } catch (_: Exception) { bgPaint.color = Color.GRAY // Fallback color } canvas.drawCircle(size / 2f, size / 2f, size / 2f, bgPaint) // Draw the text (Initial) val textPaint = android.graphics.Paint() textPaint.isAntiAlias = true textPaint.color = Color.WHITE textPaint.textSize = size / 2f // Text size is half the circle size textPaint.textAlign = android.graphics.Paint.Align.CENTER // Use a bold font if possible textPaint.typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD) // Calculate vertical center to center the text properly val bounds = android.graphics.Rect() textPaint.getTextBounds(letter, 0, letter.length, bounds) val yOffset = (bounds.bottom - bounds.top) / 2f // Draw text at Center X, Center Y + half text height (to visually center) canvas.drawText(letter.uppercase(), size / 2f, (size / 2f) + (yOffset / 2) + (bounds.height()/4), textPaint) return bitmap } fun mergeWavFiles(file1: File, file2: File, mergedFile: File): Boolean { try { val fis1 = FileInputStream(file1) val fis2 = FileInputStream(file2) val fos = FileOutputStream(mergedFile) // Skip headers (assumed 44 bytes for standard WAV) // NOTE: A robust implementation parses the header to find the 'data' chunk. // For this quick fix, assuming 44 bytes is standard for Baresip output. val headerSize = 44 val header1 = ByteArray(headerSize) val header2 = ByteArray(headerSize) if (fis1.read(header1) != headerSize || fis2.read(header2) != headerSize) { Log.e(TAG, "MergeWav: Files too small") return false } // Construct new header for stereo // Copy header from file1 but update channels to 2 and block align val newHeader = header1.clone() // 1. Update File Size (Indices 4-7) - placeholder, fixed at end // 2. Update Channels (Index 22) to 2 (Stereo) newHeader[22] = 2 newHeader[23] = 0 // 3. Update Block Align (Index 32) - usually 2*Channels (16bit) -> 4 newHeader[32] = 4 newHeader[33] = 0 // 4. Update Byte Rate (Index 28) - usually SampleRate * BlockAlign // Assuming 8000Hz sample rate: 8000 * 4 = 32000 // You should calculate this dynamically based on the input header if possible. // For now, copying the rest is usually "okay" if players are lenient, // but setting channels to 2 is the critical part. fos.write(newHeader) // MERGE LOOP with Buffering val bufferSize = 4096 // 4KB buffer val buffer1 = ByteArray(bufferSize) val buffer2 = ByteArray(bufferSize) val stereoBuffer = ByteArray(bufferSize * 2) // Output is twice as large var bytesRead1: Int var bytesRead2: Int var totalBytesData = 0 while (true) { bytesRead1 = fis1.read(buffer1) bytesRead2 = fis2.read(buffer2) if (bytesRead1 == -1 && bytesRead2 == -1) break // Use the smaller read count to avoid out of bounds if files differ slightly val limit = maxOf(bytesRead1, bytesRead2) var outIndex = 0 // Interleave samples (Simple Left/Right merge) // Assuming 16-bit audio (2 bytes per sample) for (i in 0 until limit step 2) { // Left Channel (File 1) if (i + 1 < bytesRead1) { stereoBuffer[outIndex++] = buffer1[i] stereoBuffer[outIndex++] = buffer1[i+1] } else { // Padding if file1 ended stereoBuffer[outIndex++] = 0 stereoBuffer[outIndex++] = 0 } // Right Channel (File 2) if (i + 1 < bytesRead2) { stereoBuffer[outIndex++] = buffer2[i] stereoBuffer[outIndex++] = buffer2[i+1] } else { // Padding if file2 ended stereoBuffer[outIndex++] = 0 stereoBuffer[outIndex++] = 0 } } fos.write(stereoBuffer, 0, outIndex) totalBytesData += outIndex } fis1.close() fis2.close() // Fix Header Sizes // ChunkSize (4-7) = TotalFileSize - 8 val totalFileSize = totalBytesData + 44 - 8 val rFile = RandomAccessFile(mergedFile, "rw") rFile.seek(4) rFile.write(intToLittleEndian(totalFileSize), 0, 4) // Subchunk2Size (40-43) = DataSize rFile.seek(40) rFile.write(intToLittleEndian(totalBytesData), 0, 4) rFile.close() fos.close() return true } catch (e: Exception) { Log.e(TAG, "MergeWav error: $e") return false } } // Helper for header writing private fun intToLittleEndian(value: Int): ByteArray { return byteArrayOf( (value and 0xff).toByte(), (value shr 8 and 0xff).toByte(), (value shr 16 and 0xff).toByte(), (value shr 24 and 0xff).toByte() ) } fun createEmptyFile(path: String): File { val file = File(path) if (file.exists()) file.delete() file.createNewFile() return file } @SuppressLint("MissingPermission") fun cancelMissedCallsNotification(ctx: Context) { val telecom = ctx.getSystemService(TelecomManager::class.java) try { telecom.cancelMissedCallsNotification() Log.d(TAG, "cancelMissedCallsNotification() succeeded") } catch (e: SecurityException) { Log.w(TAG, "Cannot clear missed call notification: $e") } catch (e: Exception) { Log.e(TAG, "Unexpected failure clearing missed call notification", e) } } @RequiresApi(29) fun pstnAccountHandle(ctx: Context): PhoneAccountHandle? { val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager if (ctx.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED && roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) { val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as TelecomManager val preferredHandle: PhoneAccountHandle? = tm.userSelectedOutgoingPhoneAccount if (preferredHandle != null) return preferredHandle val baresipHandle = BaresipService.getPhoneAccountHandle(ctx) val phoneAccounts = tm.callCapablePhoneAccounts.filter { it != baresipHandle } return if (phoneAccounts.isNotEmpty()) phoneAccounts[0] else null } else { Log.d(TAG, "READ_PHONE_STATE permission not granted") return null } } fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { val (height: Int, width: Int) = options.outHeight to options.outWidth var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { val halfHeight = height / 2 val halfWidth = width / 2 while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) inSampleSize *= 2 } return inSampleSize } fun decodeSampledBitmapFromUri(ctx: Context, uri: Uri, reqWidth: Int, reqHeight: Int): Bitmap? { return try { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } ctx.contentResolver.openInputStream(uri).use { BitmapFactory.decodeStream(it, null, options) } options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) options.inJustDecodeBounds = false ctx.contentResolver.openInputStream(uri).use { BitmapFactory.decodeStream(it, null, options) } } catch (e: Exception) { Log.e(TAG, "Failed to decode sampled bitmap from URI: ${e.message}") null } } fun decodeSampledBitmapFromByteArray(data: ByteArray, reqWidth: Int, reqHeight: Int): Bitmap? { return try { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(data, 0, data.size, options) options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) options.inJustDecodeBounds = false BitmapFactory.decodeByteArray(data, 0, data.size, options) } catch (e: Exception) { Log.e(TAG, "Failed to decode sampled bitmap from ByteArray: ${e.message}") null } } fun decodeSampledBitmapFromFile(path: String, reqWidth: Int, reqHeight: Int): Bitmap? { return try { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeFile(path, options) options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) options.inJustDecodeBounds = false BitmapFactory.decodeFile(path, options) } catch (e: Exception) { Log.e(TAG, "Failed to decode sampled bitmap from file: ${e.message}") null } } @RequiresApi(29) @SuppressLint("HardwareIds") fun getLine1Number(ctx: Context, subscriptionId: Int = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID): String? { try { if (Build.VERSION.SDK_INT >= 33) { if (ctx.checkSelfPermission(Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED) { val sm = ctx.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager val number = sm.getPhoneNumber(subscriptionId) if (number != "") { Log.d(TAG, "Retrieved SIM number $number via SubscriptionManager for $subscriptionId") return number } else Log.d(TAG, "Did not get SIM number via SubscriptionManager for $subscriptionId") } else Log.d(TAG, "No READ_PHONE_NUMBERS permission") } else { if (checkPermissions(ctx, arrayOf(Manifest.permission.READ_PHONE_NUMBERS, Manifest.permission.READ_PHONE_STATE))) { val tm = ctx.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val targetTm = if (subscriptionId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) tm else tm.createForSubscriptionId(subscriptionId) @Suppress("DEPRECATION") val number = targetTm.line1Number if (!number.isNullOrEmpty()) { Log.d(TAG, "Retrieved SIM number $number via TelephonyManager for $subscriptionId") return number } Log.d(TAG, "Could not retrieve SIM number") } else Log.d(TAG, "No READ_PHONE_NUMBERS and/or READ_PHONE_STATE permissions") } } catch (e: Exception) { Log.w(TAG, "getLine1Number failed: ${e.message}") } return null } fun sendSms(ctx: Context, destination: String, message: String): Boolean { return try { val smsManager = if (Build.VERSION.SDK_INT >= 31) ctx.getSystemService(android.telephony.SmsManager::class.java) else @Suppress("DEPRECATION") android.telephony.SmsManager.getDefault() smsManager.sendTextMessage( destination, null, message, null, null ) Log.d(TAG, "Sent SMS message to $destination") true } catch (e: Exception) { Log.e(TAG, "Failed to send SMS: ${e.message}") false } } fun isDefaultSmsApp(ctx: Context): Boolean { return if (Build.VERSION.SDK_INT >= 29) { val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager roleManager.isRoleHeld(RoleManager.ROLE_SMS) } else { @Suppress("DEPRECATION") android.provider.Telephony.Sms.getDefaultSmsPackage(ctx) == ctx.packageName } } @Suppress("unused") fun listFilesInDirectory(directoryPath: String): List { val directory = File(directoryPath) if (!directory.exists()) { Log.w(TAG, "Directory does not exist: $directoryPath") return emptyList() } if (!directory.isDirectory) { Log.w(TAG, "Path is not a directory: $directoryPath") return emptyList() } val files = directory.listFiles() if (files == null) { Log.e( TAG, "Failed to list files in directory (listFiles returned null): $directoryPath" ) return emptyList() } return files.filter { it.isFile } } @SuppressLint("RestrictedApi") @Suppress("unused", "DEPRECATION") fun printBackStack(navController: NavController) { Log.e(TAG, "---- Current Navigation Back Stack ----") navController.currentBackStack.value.forEachIndexed { index, navBackStackEntry -> val route = navBackStackEntry.destination.route val arguments = navBackStackEntry.arguments?.let { bundle -> bundle.keySet().joinToString(", ") { key -> "$key=${bundle.get(key)}" } } ?: "null" Log.e(TAG, "$index: Route='${route}', Args=[$arguments], ID=${navBackStackEntry.id}") } Log.e(TAG, "--------------------------------------") } fun linkPropertiesEqual(p1: android.net.LinkProperties?, p2: android.net.LinkProperties?): Boolean { if (p1 === p2) return true if (p1 == null || p2 == null) return false if (p1.interfaceName != p2.interfaceName) return false if (p1.linkAddresses != p2.linkAddresses) return false if (p1.dnsServers != p2.dnsServers) return false if (p1.routes != p2.routes) return false return true } }