package com.peachpasswords.shared import android.content.ClipData import android.content.ClipDescription import android.content.ClipboardManager import android.content.Context import android.os.Build import android.os.PersistableBundle import android.widget.Toast import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** * Centralised clipboard writes for sensitive values (MF-8). Every copy: * - flags the clip IS_SENSITIVE on API 33+ so the value is kept out of the * clipboard preview overlay and clipboard history, * - auto-clears [CLEAR_DELAY_MS] later, but only if our value is still on the * clipboard — it never stomps something the user copied afterwards, * - shows a brief "Copied — clears in 30s" hint, matching the browser. * * The wipe runs on a process-lifetime scope, NOT the caller's, so navigating away * from the screen that triggered the copy cannot cancel it. */ object PeachClipboard { const val CLEAR_DELAY_MS = 30_000L private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) private var pending: Job? = null fun copy(context: Context, label: String, value: String, hint: Boolean = true) { if (value.isEmpty()) return val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager cm.setPrimaryClip(sensitiveClip(label, value)) CopyFeedbackBus.pulse() if (hint) Toast.makeText(context, "Copied — clears in 30s", Toast.LENGTH_SHORT).show() pending?.cancel() pending = scheduleClear( scope = scope, expected = value, readCurrent = { cm.primaryClip?.getItemAt(0)?.text?.toString() }, clear = { cm.setPrimaryClip(ClipData.newPlainText("", "")) }, ) } private fun sensitiveClip(label: String, value: String): ClipData = ClipData.newPlainText(label, value).apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) } } } /** * Schedule a clipboard wipe after [delayMs] that fires only if [readCurrent] * still returns [expected]. Extracted as a pure seam so the timing can be * verified on a virtual scheduler without Android. */ internal fun scheduleClear( scope: CoroutineScope, expected: String, readCurrent: () -> String?, clear: () -> Unit, delayMs: Long = CLEAR_DELAY_MS, ): Job = scope.launch { delay(delayMs) if (readCurrent() == expected) clear() } }