package com.dmc.callcrm.sync import android.content.Context import android.util.Log import com.dmc.callcrm.data.Prefs import com.dmc.callcrm.network.ApiClient import com.dmc.callcrm.network.SyncCommand import com.dmc.callcrm.network.SyncReportRequest import kotlinx.coroutines.CancellationException import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Collections /** * Runs a "Sync" requested by an admin from the CRM (Employees page). * * Delivery: the server hands pending requests to the phone through * 1. the Quick Call poll in CallMonitorService (every ~15 s while the service is alive), and * 2. SyncWorker's periodic run (every 15 min) as a fallback. * The server marks a request "delivered" when it hands it out. While working, the phone * sends a heartbeat every 2 min; if the app is killed mid-sync the heartbeat stops and the * server hands the request out again after 10 silent minutes (given up after 3 hours). * * What it does: re-sends the last N days of call log (ignoring the cursor — the server * skips rows it already has) and, if asked, re-scans the last N days of recordings and * uploads any that never made it. Progress/results are reported back so the admin sees * "Running…" then "Done — 3 new calls, 1 recording". */ object AdminSync { private const val TAG = "AdminSync" /** While working, tell the server "still running" this often, so it doesn't treat * the request as abandoned and hand it out again (server: SYNC_STALE_MIN = 10 min). */ private const val HEARTBEAT_MS = 2L * 60 * 1000 private val inProgress: MutableSet = Collections.synchronizedSet(HashSet()) /** Fallback route: ask the server for pending requests and run them. */ suspend fun pollAndRun(ctx: Context) { val prefs = Prefs(ctx) if (!prefs.isRegistered) return val cmds = try { ApiClient.service.syncCommandPoll(prefs.bearer()).body()?.syncRequests.orEmpty() } catch (e: Exception) { Log.w(TAG, "sync command poll failed: ${e.message}") return } for (c in cmds) run(ctx, c) } suspend fun run(ctx: Context, cmd: SyncCommand) { if (!inProgress.add(cmd.id)) return val prefs = Prefs(ctx) val ver = appVersion(ctx) try { report(prefs, SyncReportRequest(cmd.id, "running", appVersion = ver)) val days = cmd.days.coerceIn(1, 120) val (calls, recs) = coroutineScope { // Heartbeat while the (possibly long) recording upload runs. val beat = launch { while (isActive) { delay(HEARTBEAT_MS) report(prefs, SyncReportRequest(cmd.id, "running", appVersion = ver)) } } try { val c = Syncer.resync(ctx, days) val r = if (cmd.includeRecordings != 0) RecordingUploader.rescan(ctx, days) else null c to r } finally { beat.cancel() } } val ok = calls.ok && (recs?.ok ?: true) val err = listOfNotNull(calls.error, recs?.error).joinToString("; ").ifBlank { null } finalReport( prefs, SyncReportRequest( id = cmd.id, status = if (ok) "done" else "failed", callsSent = calls.sent, callsInserted = calls.inserted, recordingsFound = recs?.found, recordingsUploaded = recs?.uploaded, error = err, appVersion = ver ) ) Log.d(TAG, "admin sync #${cmd.id} finished ok=$ok calls=${calls.sent}/${calls.inserted} recs=${recs?.found}/${recs?.uploaded}") } catch (e: CancellationException) { // Service is being stopped. Don't report "failed": the server hands the request // out again after 10 silent minutes and it will run to completion then. Log.w(TAG, "admin sync #${cmd.id} interrupted — server will re-deliver it") throw e } catch (e: Throwable) { Log.e(TAG, "admin sync #${cmd.id} crashed", e) finalReport(prefs, SyncReportRequest(cmd.id, "failed", error = e.javaClass.simpleName + ": " + (e.message ?: ""), appVersion = ver)) } finally { inProgress.remove(cmd.id) } } /** The result is what the admin is waiting for — send it even if the scope is cancelling. */ private suspend fun finalReport(prefs: Prefs, body: SyncReportRequest) = withContext(NonCancellable) { report(prefs, body) } private suspend fun report(prefs: Prefs, body: SyncReportRequest) { // A couple of attempts: the result is what the admin is waiting to see. repeat(3) { attempt -> try { val r = ApiClient.service.syncRequestReport(prefs.bearer(), body) if (r.isSuccessful) return } catch (e: Exception) { Log.w(TAG, "report attempt ${attempt + 1} failed: ${e.message}") } delay(3000L * (attempt + 1)) } } private fun appVersion(ctx: Context): String? = try { ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName } catch (_: Exception) { null } }