Files

1587 lines
62 KiB
Kotlin

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<String> {
val lines = string.split("\n")
val result = ArrayList<String>()
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<String> {
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, String>): 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<String>): 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<String>, 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<String, String> {
val result = mutableMapOf<String, String>()
try {
val interfaces: Enumeration<NetworkInterface> = 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<InetAddress> = 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<String>) : 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<Intent>) {
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<String>, 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<String>()
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<out X509Certificate>?, authType: String?) {
// Delegate to the system manager by default.
systemTrustManager.checkClientTrusted(chain, authType)
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, 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<X509Certificate> {
// 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<File> {
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
}
}