package com.dmc.callcrm.data import android.content.Context import android.os.Environment import android.provider.CallLog import java.io.File /** * Harvests audio files written by the phone's BUILT-IN call recorder. * * Since Android 10 apps can't record the call stream themselves, the reliable * approach (used by Callyzer et al.) is: turn on auto call-recording in the * device dialer, then read the resulting files. This class finds new files and * matches each one to the client number by comparing the file's modified time * to the call log. */ class RecordingScanner(private val ctx: Context) { data class Rec( val file: File, val clientNumber: String, val callDateMs: Long, val durationSec: Int, val callType: String, // incoming | outgoing | unknown val contactName: String?, val deviceCallId: String? ) private val audioExt = setOf("m4a", "mp3", "amr", "wav", "ogg", "aac", "3gp") /** Common recorder directories across OEMs. */ private fun candidateDirs(): List { val root = Environment.getExternalStorageDirectory() // /storage/emulated/0 val paths = listOf( "Call", "Recordings/Call", "Recordings/Call Recordings", "Sounds", "MIUI/sound_recorder/call_rec", "PhoneRecord", "CallRecordings", "Music/Recordings", "Recordings", "Android/data/com.android.dialer/files" ) return paths.map { File(root, it) }.filter { it.isDirectory } } /** Return recordings whose file is newer than [sinceMs] and not already uploaded. */ fun scan(sinceMs: Long, alreadyUploaded: Set): List { val out = ArrayList() for (dir in candidateDirs()) { dir.walkTopDown().maxDepth(2).forEach { f -> if (!f.isFile) return@forEach if (f.extension.lowercase() !in audioExt) return@forEach if (f.lastModified() <= sinceMs) return@forEach if (f.absolutePath in alreadyUploaded) return@forEach if (f.length() < 2048) return@forEach // skip empty/partial val match = matchCall(f.lastModified()) out.add( Rec( file = f, clientNumber = match?.number ?: numberFromName(f.name) ?: "unknown", callDateMs = match?.date ?: f.lastModified(), durationSec = match?.duration ?: 0, callType = match?.type ?: "unknown", contactName = match?.name, deviceCallId = match?.id ) ) } } return out.sortedBy { it.file.lastModified() } } private data class CallMatch( val number: String, val date: Long, val duration: Int, val type: String, val name: String?, val id: String? ) /** * Find the call whose END time (date + duration) is closest to the file's * modified time, within a 2-minute tolerance. */ private fun matchCall(fileMs: Long): CallMatch? { val proj = arrayOf( CallLog.Calls._ID, CallLog.Calls.NUMBER, CallLog.Calls.CACHED_NAME, CallLog.Calls.TYPE, CallLog.Calls.DATE, CallLog.Calls.DURATION ) // look at calls in a window around the file time val from = fileMs - 30 * 60_000L val sel = "${CallLog.Calls.DATE} >= ?" val args = arrayOf(from.toString()) var best: CallMatch? = null var bestDelta = Long.MAX_VALUE try { ctx.contentResolver.query( CallLog.Calls.CONTENT_URI, proj, sel, args, "${CallLog.Calls.DATE} DESC" )?.use { c -> val iId = c.getColumnIndex(CallLog.Calls._ID) val iNum = c.getColumnIndex(CallLog.Calls.NUMBER) val iName = c.getColumnIndex(CallLog.Calls.CACHED_NAME) val iType = c.getColumnIndex(CallLog.Calls.TYPE) val iDate = c.getColumnIndex(CallLog.Calls.DATE) val iDur = c.getColumnIndex(CallLog.Calls.DURATION) while (c.moveToNext()) { val date = c.getLong(iDate) val dur = c.getInt(iDur) val endMs = date + dur * 1000L val delta = kotlin.math.abs(endMs - fileMs) if (delta < bestDelta) { bestDelta = delta best = CallMatch( number = c.getString(iNum) ?: "unknown", date = date, duration = dur, type = when (c.getInt(iType)) { 1 -> "incoming"; 2 -> "outgoing"; else -> "unknown" }, name = if (iName >= 0) c.getString(iName) else null, id = c.getString(iId) ) } } } } catch (_: SecurityException) { return null } return if (bestDelta <= 120_000L) best else null // 2-minute tolerance } /** Fallback: pull a phone-number-looking token out of the filename. */ private fun numberFromName(name: String): String? = Regex("(\\+?\\d[\\d\\-\\s]{6,15}\\d)").find(name)?.value?.replace(Regex("[\\s-]"), "") }