1package com.peachpasswords.shared23import android.content.ClipData4import android.content.ClipDescription5import android.content.ClipboardManager6import android.content.Context7import android.os.Build8import android.os.PersistableBundle9import android.widget.Toast10import kotlinx.coroutines.CoroutineScope11import kotlinx.coroutines.Dispatchers12import kotlinx.coroutines.Job13import kotlinx.coroutines.SupervisorJob14import kotlinx.coroutines.delay15import kotlinx.coroutines.launch16171819202122232425262728object PeachClipboard {29 const val CLEAR_DELAY_MS = 30_000L3031 private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())32 private var pending: Job? = null3334 35 private var lastWritten: String? = null36 private var writeSeq = 0L3738 fun copy(context: Context, label: String, value: String, hint: Boolean = true) {39 if (value.isEmpty()) return40 val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager41 cm.setPrimaryClip(sensitiveClip(label, value))42 CopyFeedbackBus.pulse()43 if (hint) Toast.makeText(context, "Copied — clears in 30s", Toast.LENGTH_SHORT).show()44 val seq = ++writeSeq45 lastWritten = value46 pending?.cancel()47 pending = scheduleClear(48 scope = scope,49 expected = value,50 readCurrent = { readClip(cm) },51 clear = {52 cm.setPrimaryClip(ClipData.newPlainText("", ""))53 if (writeSeq == seq) lastWritten = null54 },55 superseded = { writeSeq != seq },56 )57 }5859 6061626364 fun clearNow(context: Context) {65 pending?.cancel()66 pending = null67 val expected = lastWritten ?: return68 lastWritten = null69 val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager70 val current = readClip(cm)71 if (current == null || current == expected) cm.setPrimaryClip(ClipData.newPlainText("", ""))72 }7374 7576777879 private fun readClip(cm: ClipboardManager): String? =80 runCatching { cm.primaryClip?.getItemAt(0)?.text?.toString() }.getOrNull()8182 private fun sensitiveClip(label: String, value: String): ClipData =83 ClipData.newPlainText(label, value).apply {84 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {85 description.extras = PersistableBundle().apply {86 putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)87 }88 }89 }9091 9293949596979899100101102103 internal fun scheduleClear(104 scope: CoroutineScope,105 expected: String,106 readCurrent: () -> String?,107 clear: () -> Unit,108 superseded: () -> Boolean = { false },109 delayMs: Long = CLEAR_DELAY_MS,110 ): Job = scope.launch {111 delay(delayMs)112 if (superseded()) return@launch113 val current = readCurrent()114 if (current == null || current == expected) clear()115 }116}