@craft-native/android 0.0.464 → 0.0.467
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -120,6 +120,7 @@ internal class CraftNativeServices(
|
|
|
120
120
|
// Videos kept on the phone for offline (CraftNativeMedia).
|
|
121
121
|
add("media")
|
|
122
122
|
add("media-cache-key")
|
|
123
|
+
add("media-remove")
|
|
123
124
|
if (config.optBoolean("enableShare")) add("share")
|
|
124
125
|
if (config.optBoolean("enablePushNotifications")) add("push")
|
|
125
126
|
if (health != null || healthServices != null) {
|
|
@@ -188,7 +189,22 @@ internal class CraftNativeServices(
|
|
|
188
189
|
done(success(CraftNativeMedia.prune(context, (0 until keep.length()).map { index -> keep.optJSONObject(index)?.let { CraftNativeMedia.identity(it.optString("url"), it) } ?: keep.optString(index) })))
|
|
189
190
|
}
|
|
190
191
|
"usage" -> done(success(CraftNativeMedia.usage(context)))
|
|
191
|
-
"clear" ->
|
|
192
|
+
"clear" -> {
|
|
193
|
+
try { done(success(CraftNativeMedia.clear(context))) }
|
|
194
|
+
catch (error: java.io.IOException) { done(failure("IO_ERROR", error.message ?: "The media cache could not be cleared")) }
|
|
195
|
+
}
|
|
196
|
+
"remove" -> {
|
|
197
|
+
val options = args.optJSONObject(1)
|
|
198
|
+
val key = options?.opt("cacheKey")
|
|
199
|
+
if (key != null && key != JSONObject.NULL && (key !is String || key.isEmpty() || key.toByteArray().size > 1024)) {
|
|
200
|
+
done(failure("INVALID_ARGUMENT", "media.remove cacheKey must be a nonempty string of at most 1024 bytes"))
|
|
201
|
+
} else {
|
|
202
|
+
val address = CraftNativeMedia.identity(args.optString(0), options)
|
|
203
|
+
if (address.isBlank()) done(failure("INVALID_ARGUMENT", "media.remove needs a source or cache key"))
|
|
204
|
+
else try { done(success(CraftNativeMedia.remove(context, address))) }
|
|
205
|
+
catch (error: java.io.IOException) { done(failure("IO_ERROR", error.message ?: "The media copy could not be removed")) }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
192
208
|
else -> done(failure("UNKNOWN_ACTION", "Unsupported Media method"))
|
|
193
209
|
}
|
|
194
210
|
"Background" -> {
|
|
@@ -1381,7 +1381,7 @@ internal object CraftNativeMedia {
|
|
|
1381
1381
|
private const val PREFS = "craft.media"
|
|
1382
1382
|
private val lock = Any()
|
|
1383
1383
|
/** Keeps waiting in this process for an address's transfer. */
|
|
1384
|
-
private data class Waiting(val generation: Int, val answer: (CraftNativeCapabilityResult) -> Unit)
|
|
1384
|
+
private data class Waiting(val generation: Int, val revision: String, val answer: (CraftNativeCapabilityResult) -> Unit)
|
|
1385
1385
|
private val waiting = HashMap<String, MutableList<Waiting>>()
|
|
1386
1386
|
/** One transfer writes an address's part file at a time. */
|
|
1387
1387
|
private val writers = ConcurrentHashMap<String, Any>()
|
|
@@ -1400,10 +1400,12 @@ internal object CraftNativeMedia {
|
|
|
1400
1400
|
}
|
|
1401
1401
|
|
|
1402
1402
|
/** Where an address's transfer writes until the file is whole; kept between attempts to resume. */
|
|
1403
|
-
fun part(context: Context, address: String
|
|
1403
|
+
fun part(context: Context, address: String, started: Int = generation(context), ownerRevision: String = revision(context, address)): File =
|
|
1404
|
+
File(root(context), "${file(context, address).name}.g$started.r${ownerRevision.ifEmpty { "legacy" }}.part")
|
|
1404
1405
|
|
|
1405
|
-
fun local(context: Context, address: String): File? =
|
|
1406
|
-
address.takeIf { it.isNotBlank() }?.let { file(context, it) }?.takeIf { it.isFile && it.length() > 0 }
|
|
1406
|
+
fun local(context: Context, address: String): File? = synchronized(lock) {
|
|
1407
|
+
address.takeIf { it.isNotBlank() && permitted(context, it) }?.let { file(context, it) }?.takeIf { it.isFile && it.length() > 0 }
|
|
1408
|
+
}
|
|
1407
1409
|
|
|
1408
1410
|
private fun describe(address: String, file: File) = JSONObject().put("ok", true).put("url", address).put("path", file.absolutePath).put("size", file.length())
|
|
1409
1411
|
|
|
@@ -1412,6 +1414,46 @@ internal object CraftNativeMedia {
|
|
|
1412
1414
|
/** How many times the cache was cleared; a transfer started before the last clear does not land. */
|
|
1413
1415
|
fun generation(context: Context): Int = prefs(context).getInt("generation", 0)
|
|
1414
1416
|
|
|
1417
|
+
private fun revision(context: Context, address: String): String = prefs(context).getString("revision:${digest(address)}", "").orEmpty()
|
|
1418
|
+
|
|
1419
|
+
private fun permitted(context: Context, address: String): Boolean {
|
|
1420
|
+
val store = prefs(context)
|
|
1421
|
+
val key = digest(address)
|
|
1422
|
+
return !store.getBoolean("revoked:$key", false) && (!store.getBoolean("cleared", false) || store.getBoolean("authorized:$key", false))
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
private fun current(context: Context, address: String, started: Int, ownerRevision: String): Boolean =
|
|
1426
|
+
generation(context) == started && revision(context, address) == ownerRevision && permitted(context, address)
|
|
1427
|
+
|
|
1428
|
+
private fun mediaFiles(context: Context): Array<File> {
|
|
1429
|
+
val directory = root(context)
|
|
1430
|
+
if (!directory.exists()) return emptyArray()
|
|
1431
|
+
return directory.listFiles() ?: throw java.io.IOException("The media cache could not be read")
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
private fun removeFiles(context: Context, address: String): Int {
|
|
1435
|
+
val prefix = digest(address) + "."
|
|
1436
|
+
var removed = 0
|
|
1437
|
+
var failure: java.io.IOException? = null
|
|
1438
|
+
mediaFiles(context).filter { it.name.startsWith(prefix) }.forEach { file ->
|
|
1439
|
+
if (file.delete()) removed++ else failure = java.io.IOException("The media copy could not be removed")
|
|
1440
|
+
}
|
|
1441
|
+
failure?.let { throw it }
|
|
1442
|
+
return removed
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/** A new explicit authorized keep may lift a durable tombstone, after removing retired bytes. */
|
|
1446
|
+
private fun activate(context: Context, address: String) {
|
|
1447
|
+
if (permitted(context, address)) return
|
|
1448
|
+
removeFiles(context, address)
|
|
1449
|
+
val key = digest(address)
|
|
1450
|
+
if (!prefs(context).edit().putBoolean("revoked:$key", false).putBoolean("authorized:$key", true).commit()) {
|
|
1451
|
+
throw java.io.IOException("Media authorization could not be persisted")
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private fun revisionTag(address: String, revision: String) = "$TAG:identity:${digest(address)}:${revision.ifEmpty { "legacy" }}"
|
|
1456
|
+
|
|
1415
1457
|
/** The unique job for an address and connection rule: `any` or `wifi`. */
|
|
1416
1458
|
private fun workName(address: String, wifiOnly: Boolean) = "$TAG:${digest(address)}:${if (wifiOnly) "wifi" else "any"}"
|
|
1417
1459
|
|
|
@@ -1423,7 +1465,10 @@ internal object CraftNativeMedia {
|
|
|
1423
1465
|
}
|
|
1424
1466
|
val app = context.applicationContext
|
|
1425
1467
|
var sourceChanged = false
|
|
1468
|
+
var ownerRevision = ""
|
|
1426
1469
|
val started = synchronized(lock) {
|
|
1470
|
+
try { activate(app, address) }
|
|
1471
|
+
catch (error: java.io.IOException) { return done(CraftNativeCapabilityResult(code = "IO_ERROR", message = error.message ?: "Media authorization could not be persisted")) }
|
|
1427
1472
|
local(app, address)?.let { return done(CraftNativeCapabilityResult(data = describe(address, it))) }
|
|
1428
1473
|
if (cacheKey != null) {
|
|
1429
1474
|
val sourceKey = "source:${digest(address)}"
|
|
@@ -1431,7 +1476,8 @@ internal object CraftNativeMedia {
|
|
|
1431
1476
|
sourceChanged = prefs(app).getString(sourceKey, null) != authorization
|
|
1432
1477
|
if (!prefs(app).edit().putString(sourceKey, authorization).commit()) return done(CraftNativeCapabilityResult(code = "IO_ERROR", message = "Could not persist media authorization"))
|
|
1433
1478
|
}
|
|
1434
|
-
|
|
1479
|
+
ownerRevision = revision(app, address)
|
|
1480
|
+
generation(app).also { waiting.getOrPut(address) { mutableListOf() }.add(Waiting(it, ownerRevision, done)) }
|
|
1435
1481
|
}
|
|
1436
1482
|
val wifiOnly = !cellular
|
|
1437
1483
|
val work = androidx.work.WorkManager.getInstance(app)
|
|
@@ -1439,20 +1485,22 @@ internal object CraftNativeMedia {
|
|
|
1439
1485
|
work.cancelUniqueWork(workName(address, !wifiOnly))
|
|
1440
1486
|
val request = androidx.work.OneTimeWorkRequestBuilder<CraftNativeMediaWorker>()
|
|
1441
1487
|
.setConstraints(androidx.work.Constraints.Builder().setRequiredNetworkType(if (wifiOnly) androidx.work.NetworkType.UNMETERED else androidx.work.NetworkType.CONNECTED).build())
|
|
1442
|
-
.setInputData(androidx.work.workDataOf("address" to address, "source" to source, "headers" to headers.toString(), "generation" to started))
|
|
1488
|
+
.setInputData(androidx.work.workDataOf("address" to address, "source" to source, "headers" to headers.toString(), "generation" to started, "revision" to ownerRevision))
|
|
1443
1489
|
.setBackoffCriteria(androidx.work.BackoffPolicy.EXPONENTIAL, 30, java.util.concurrent.TimeUnit.SECONDS)
|
|
1444
1490
|
.addTag(TAG)
|
|
1445
1491
|
.addTag("$TAG:generation:$started")
|
|
1446
1492
|
.addTag("$TAG:${digest(address)}")
|
|
1493
|
+
.addTag(revisionTag(address, ownerRevision))
|
|
1447
1494
|
.build()
|
|
1448
1495
|
// Join a running or freshly queued transfer. Only a failed attempt's
|
|
1449
1496
|
// backoff is replaced so an explicit retry can resume immediately.
|
|
1450
1497
|
val name = workName(address, wifiOnly)
|
|
1451
1498
|
background.execute {
|
|
1452
1499
|
try {
|
|
1453
|
-
if (
|
|
1500
|
+
if (current(app, address, started, ownerRevision)) {
|
|
1454
1501
|
val pending = work.getWorkInfosForUniqueWork(name).get().any {
|
|
1455
1502
|
"$TAG:generation:$started" in it.tags &&
|
|
1503
|
+
revisionTag(address, ownerRevision) in it.tags &&
|
|
1456
1504
|
!sourceChanged && !it.progress.getBoolean(ATTEMPT_SETTLED, false) &&
|
|
1457
1505
|
(it.state == androidx.work.WorkInfo.State.RUNNING ||
|
|
1458
1506
|
(it.state == androidx.work.WorkInfo.State.ENQUEUED && it.runAttemptCount == 0))
|
|
@@ -1462,8 +1510,8 @@ internal object CraftNativeMedia {
|
|
|
1462
1510
|
}
|
|
1463
1511
|
} catch (error: Exception) {
|
|
1464
1512
|
if (error is InterruptedException) Thread.currentThread().interrupt()
|
|
1465
|
-
if (
|
|
1466
|
-
deliver(context, address, started, CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not schedule the download"))
|
|
1513
|
+
if (current(app, address, started, ownerRevision)) {
|
|
1514
|
+
deliver(context, address, started, CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not schedule the download"), ownerRevision)
|
|
1467
1515
|
}
|
|
1468
1516
|
}
|
|
1469
1517
|
}
|
|
@@ -1472,25 +1520,25 @@ internal object CraftNativeMedia {
|
|
|
1472
1520
|
wifiOnly && metered(app) -> "cellular"
|
|
1473
1521
|
else -> null
|
|
1474
1522
|
}
|
|
1475
|
-
if (reason != null) deliver(context, address, started, queued(address, reason))
|
|
1523
|
+
if (reason != null) deliver(context, address, started, queued(address, reason), ownerRevision)
|
|
1476
1524
|
}
|
|
1477
1525
|
|
|
1478
1526
|
private fun queued(address: String, reason: String) =
|
|
1479
1527
|
CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("reason", reason).put("queued", true))
|
|
1480
1528
|
|
|
1481
1529
|
/** Answers only keeps belonging to this transfer's cache generation. */
|
|
1482
|
-
fun deliver(context: Context, address: String, started: Int, result: CraftNativeCapabilityResult) {
|
|
1530
|
+
fun deliver(context: Context, address: String, started: Int, result: CraftNativeCapabilityResult, ownerRevision: String = "") {
|
|
1483
1531
|
val answered = synchronized(lock) {
|
|
1484
1532
|
val pending = waiting[address].orEmpty()
|
|
1485
|
-
val matching = pending.filter { it.generation == started }
|
|
1486
|
-
val remaining = pending.filter { it.generation != started }.toMutableList()
|
|
1533
|
+
val matching = pending.filter { it.generation == started && it.revision == ownerRevision }
|
|
1534
|
+
val remaining = pending.filter { it.generation != started || it.revision != ownerRevision }.toMutableList()
|
|
1487
1535
|
if (remaining.isEmpty()) waiting.remove(address) else waiting[address] = remaining
|
|
1488
1536
|
matching
|
|
1489
1537
|
}
|
|
1490
1538
|
if (answered.isNotEmpty()) main.post {
|
|
1491
|
-
val
|
|
1539
|
+
val answer = if (current(context, address, started, ownerRevision)) result else
|
|
1492
1540
|
CraftNativeCapabilityResult(code = "CANCELLED", message = "The video cache was cleared")
|
|
1493
|
-
answered.forEach { it.answer(
|
|
1541
|
+
answered.forEach { it.answer(answer) }
|
|
1494
1542
|
}
|
|
1495
1543
|
}
|
|
1496
1544
|
|
|
@@ -1501,7 +1549,7 @@ internal object CraftNativeMedia {
|
|
|
1501
1549
|
* file where it stopped, moves it into place whole, and answers whoever
|
|
1502
1550
|
* waits. RETRY leaves the part file for the next attempt.
|
|
1503
1551
|
*/
|
|
1504
|
-
fun transfer(context: Context, address: String, headers: JSONObject, started: Int, attempt: Int, source: String = address, beforeAnswer: () -> Unit = {}, stopped: () -> Boolean): Outcome {
|
|
1552
|
+
fun transfer(context: Context, address: String, headers: JSONObject, started: Int, attempt: Int, source: String = address, beforeAnswer: () -> Unit = {}, ownerRevision: String = "", stopped: () -> Boolean): Outcome {
|
|
1505
1553
|
fun answer(result: CraftNativeCapabilityResult) {
|
|
1506
1554
|
// The callback can immediately call keep again. Publish completion
|
|
1507
1555
|
// before that callback, while WorkManager may still say RUNNING.
|
|
@@ -1509,12 +1557,15 @@ internal object CraftNativeMedia {
|
|
|
1509
1557
|
// A keep registered after this marker belongs to the next
|
|
1510
1558
|
// attempt; it must not enter the old callback batch.
|
|
1511
1559
|
beforeAnswer()
|
|
1512
|
-
deliver(context, address, started, result)
|
|
1560
|
+
deliver(context, address, started, result, ownerRevision)
|
|
1513
1561
|
}
|
|
1514
1562
|
}
|
|
1515
1563
|
// clear() already answered the old generation's waiters. Never answer
|
|
1516
1564
|
// a new athlete's waiters from an obsolete worker for the same URL.
|
|
1517
|
-
if (
|
|
1565
|
+
if (!current(context, address, started, ownerRevision)) {
|
|
1566
|
+
part(context, address, started, ownerRevision).delete()
|
|
1567
|
+
return Outcome.DONE
|
|
1568
|
+
}
|
|
1518
1569
|
local(context, address)?.let {
|
|
1519
1570
|
answer(CraftNativeCapabilityResult(data = describe(address, it)))
|
|
1520
1571
|
return Outcome.DONE
|
|
@@ -1523,21 +1574,25 @@ internal object CraftNativeMedia {
|
|
|
1523
1574
|
synchronized(writer) {
|
|
1524
1575
|
// Another worker can finish, or sign-out can clear the cache, while
|
|
1525
1576
|
// this worker waits for the address's writer.
|
|
1526
|
-
if (
|
|
1577
|
+
if (!current(context, address, started, ownerRevision)) {
|
|
1578
|
+
part(context, address, started, ownerRevision).delete()
|
|
1579
|
+
return Outcome.DONE
|
|
1580
|
+
}
|
|
1527
1581
|
if (stopped()) return Outcome.RETRY
|
|
1528
1582
|
local(context, address)?.let {
|
|
1529
1583
|
answer(CraftNativeCapabilityResult(data = describe(address, it)))
|
|
1530
1584
|
return Outcome.DONE
|
|
1531
1585
|
}
|
|
1532
1586
|
val target = file(context, address)
|
|
1533
|
-
val staging = part(context, address)
|
|
1587
|
+
val staging = part(context, address, started, ownerRevision)
|
|
1534
1588
|
target.parentFile?.mkdirs()
|
|
1535
1589
|
val download = CraftResumableDownload()
|
|
1536
1590
|
val asked = headers.keys().asSequence().associateWith { headers.optString(it) }
|
|
1537
1591
|
val fetched = try {
|
|
1538
1592
|
// A part that no longer lines up was dropped: the whole file, now, not after a backoff.
|
|
1539
|
-
|
|
1540
|
-
|
|
1593
|
+
val retired = { stopped() || !current(context, address, started, ownerRevision) }
|
|
1594
|
+
download.fetch(source, asked, staging, retired).let { first ->
|
|
1595
|
+
if (first is CraftResumableDownload.Fetched.Restart) download.fetch(source, asked, staging, retired) else first
|
|
1541
1596
|
}
|
|
1542
1597
|
} catch (error: Exception) {
|
|
1543
1598
|
val full = generateSequence<Throwable>(error) { it.cause }.take(16).any {
|
|
@@ -1548,13 +1603,17 @@ internal object CraftNativeMedia {
|
|
|
1548
1603
|
answer(CraftNativeCapabilityResult(code = "NO_SPACE", message = error.message ?: "The phone is out of space"))
|
|
1549
1604
|
return Outcome.DONE
|
|
1550
1605
|
}
|
|
1606
|
+
if (!current(context, address, started, ownerRevision)) { staging.delete(); return Outcome.DONE }
|
|
1551
1607
|
if (stopped()) return Outcome.RETRY
|
|
1552
1608
|
// Lost the connection part way: the part waits for the next attempt.
|
|
1553
1609
|
answer(if (!online(context)) queued(address, "offline") else CraftNativeCapabilityResult(code = if (error is java.net.SocketTimeoutException) "TIMEOUT" else "NETWORK_ERROR", message = error.message ?: "media.keep failed"))
|
|
1554
1610
|
return if (attempt < 8) Outcome.RETRY else Outcome.DONE.also { staging.delete() }
|
|
1555
1611
|
}
|
|
1556
1612
|
when (fetched) {
|
|
1557
|
-
is CraftResumableDownload.Fetched.Stopped, is CraftResumableDownload.Fetched.Restart ->
|
|
1613
|
+
is CraftResumableDownload.Fetched.Stopped, is CraftResumableDownload.Fetched.Restart -> {
|
|
1614
|
+
if (!current(context, address, started, ownerRevision)) { staging.delete(); return Outcome.DONE }
|
|
1615
|
+
return Outcome.RETRY
|
|
1616
|
+
}
|
|
1558
1617
|
is CraftResumableDownload.Fetched.Refused -> {
|
|
1559
1618
|
answer(CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("status", fetched.status)))
|
|
1560
1619
|
return Outcome.DONE
|
|
@@ -1566,7 +1625,7 @@ internal object CraftNativeMedia {
|
|
|
1566
1625
|
is CraftResumableDownload.Fetched.Complete -> Unit
|
|
1567
1626
|
}
|
|
1568
1627
|
synchronized(lock) {
|
|
1569
|
-
if (
|
|
1628
|
+
if (!current(context, address, started, ownerRevision)) {
|
|
1570
1629
|
staging.delete()
|
|
1571
1630
|
return Outcome.DONE
|
|
1572
1631
|
}
|
|
@@ -1603,8 +1662,8 @@ internal object CraftNativeMedia {
|
|
|
1603
1662
|
val active = waiting.keys.map { digest(it) }.toSet()
|
|
1604
1663
|
root(context).listFiles()?.forEach { file ->
|
|
1605
1664
|
val owner = file.name.substringBefore('.')
|
|
1606
|
-
|
|
1607
|
-
val stale = file.name.endsWith(".part") && file.name.
|
|
1665
|
+
val ownerRevision = prefs(context).getString("revision:$owner", "").orEmpty().ifEmpty { "legacy" }
|
|
1666
|
+
val stale = file.name.endsWith(".part") && !file.name.endsWith(".g${generation(context)}.r$ownerRevision.part")
|
|
1608
1667
|
if (owner !in active && (owner !in wanted || stale) && file.delete()) removed++
|
|
1609
1668
|
}
|
|
1610
1669
|
}
|
|
@@ -1628,20 +1687,60 @@ internal object CraftNativeMedia {
|
|
|
1628
1687
|
return removed
|
|
1629
1688
|
}
|
|
1630
1689
|
|
|
1690
|
+
/** Revoke one identity before cancelling its work or unlinking its bytes. */
|
|
1691
|
+
fun remove(context: Context, address: String): Int {
|
|
1692
|
+
require(address.isNotBlank()) { "media.remove needs a source or cache key" }
|
|
1693
|
+
var removed = 0
|
|
1694
|
+
var failure: java.io.IOException? = null
|
|
1695
|
+
val key = digest(address)
|
|
1696
|
+
val answered = synchronized(lock) {
|
|
1697
|
+
if (!prefs(context).edit().putString("revision:$key", java.util.UUID.randomUUID().toString()).putBoolean("revoked:$key", true).commit()) {
|
|
1698
|
+
failure = java.io.IOException("Media retirement could not be persisted")
|
|
1699
|
+
}
|
|
1700
|
+
val callbacks = waiting.remove(address).orEmpty()
|
|
1701
|
+
try { removed = removeFiles(context, address) }
|
|
1702
|
+
catch (error: java.io.IOException) { failure = error }
|
|
1703
|
+
callbacks
|
|
1704
|
+
}
|
|
1705
|
+
main.post { answered.forEach { it.answer(CraftNativeCapabilityResult(code = "CANCELLED", message = "Media ownership was retired")) } }
|
|
1706
|
+
val app = context.applicationContext
|
|
1707
|
+
background.execute {
|
|
1708
|
+
runCatching {
|
|
1709
|
+
val work = androidx.work.WorkManager.getInstance(app)
|
|
1710
|
+
work.getWorkInfosByTag("$TAG:$key").get().filter { !it.state.isFinished }.forEach { info ->
|
|
1711
|
+
synchronized(lock) {
|
|
1712
|
+
// A delayed enumeration must preserve a later explicit
|
|
1713
|
+
// re-grant, including one made after another remove.
|
|
1714
|
+
if (!permitted(app, address) || "$TAG:generation:${generation(app)}" !in info.tags || revisionTag(address, revision(app, address)) !in info.tags) {
|
|
1715
|
+
work.cancelWorkById(info.id)
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
failure?.let { throw it }
|
|
1722
|
+
return removed
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1631
1725
|
/** Stops every transfer and deletes every copy; none started before this lands after it. */
|
|
1632
1726
|
fun clear(context: Context): Int {
|
|
1633
1727
|
var removed = 0
|
|
1634
1728
|
var failure: java.io.IOException? = null
|
|
1635
1729
|
val answered = synchronized(lock) {
|
|
1636
1730
|
// Persisted before the files go: a transfer that outlives the process checks it too.
|
|
1637
|
-
|
|
1731
|
+
val previous = generation(context)
|
|
1732
|
+
if (!prefs(context).edit().clear().putInt("generation", previous + 1).putBoolean("cleared", true).commit()) {
|
|
1733
|
+
failure = java.io.IOException("Media retirement could not be persisted")
|
|
1734
|
+
}
|
|
1638
1735
|
val callbacks = waiting.values.flatten()
|
|
1639
1736
|
waiting.clear()
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1737
|
+
try {
|
|
1738
|
+
mediaFiles(context).forEach { file ->
|
|
1739
|
+
if (file.delete()) removed++ else failure = java.io.IOException("The video cache could not be cleared")
|
|
1740
|
+
}
|
|
1741
|
+
} catch (error: java.io.IOException) { failure = error }
|
|
1643
1742
|
// Schedule cancellation before a new generation can enqueue its work.
|
|
1644
|
-
androidx.work.WorkManager.getInstance(context.applicationContext).cancelAllWorkByTag(TAG)
|
|
1743
|
+
androidx.work.WorkManager.getInstance(context.applicationContext).cancelAllWorkByTag("$TAG:generation:$previous")
|
|
1645
1744
|
callbacks
|
|
1646
1745
|
}
|
|
1647
1746
|
main.post { answered.forEach { it.answer(CraftNativeCapabilityResult(code = "CANCELLED", message = "The video cache was cleared")) } }
|
|
@@ -1733,10 +1832,11 @@ internal class CraftNativeMediaWorker(context: Context, parameters: androidx.wor
|
|
|
1733
1832
|
val address = inputData.getString("address") ?: return Result.failure()
|
|
1734
1833
|
val headers = runCatching { JSONObject(inputData.getString("headers") ?: "{}") }.getOrDefault(JSONObject())
|
|
1735
1834
|
val started = inputData.getInt("generation", 0)
|
|
1835
|
+
val ownerRevision = inputData.getString("revision").orEmpty()
|
|
1736
1836
|
return try {
|
|
1737
1837
|
setProgressAsync(androidx.work.workDataOf(CraftNativeMedia.ATTEMPT_SETTLED to false)).get()
|
|
1738
1838
|
val outcome = CraftNativeMedia.transfer(applicationContext, address, headers, started, runAttemptCount, source = inputData.getString("source") ?: address,
|
|
1739
|
-
beforeAnswer = { setProgressAsync(androidx.work.workDataOf(CraftNativeMedia.ATTEMPT_SETTLED to true)).get() }
|
|
1839
|
+
beforeAnswer = { setProgressAsync(androidx.work.workDataOf(CraftNativeMedia.ATTEMPT_SETTLED to true)).get() }, ownerRevision = ownerRevision
|
|
1740
1840
|
) { isStopped }
|
|
1741
1841
|
if (outcome == CraftNativeMedia.Outcome.RETRY) Result.retry() else Result.success()
|
|
1742
1842
|
} catch (error: Exception) {
|
|
@@ -1744,7 +1844,7 @@ internal class CraftNativeMediaWorker(context: Context, parameters: androidx.wor
|
|
|
1744
1844
|
// A replacement owns the address's new waiters. A real progress
|
|
1745
1845
|
// storage failure still needs an answer instead of a lost callback.
|
|
1746
1846
|
if (!isStopped) CraftNativeMedia.deliver(applicationContext, address, started,
|
|
1747
|
-
CraftNativeCapabilityResult(code = "IO_ERROR", message = error.message ?: "media.keep could not store worker progress"))
|
|
1847
|
+
CraftNativeCapabilityResult(code = "IO_ERROR", message = error.message ?: "media.keep could not store worker progress"), ownerRevision)
|
|
1748
1848
|
if (isStopped) Result.retry() else Result.failure()
|
|
1749
1849
|
}
|
|
1750
1850
|
}
|