@craft-native/android 0.0.225 → 0.0.226

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/android",
3
- "version": "0.0.225",
3
+ "version": "0.0.226",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/android#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -100,6 +100,7 @@ internal class CraftNativeImageCache(
100
100
  true
101
101
  }
102
102
  catch (error: Exception) { false }
103
+ catch (error: OutOfMemoryError) { false }
103
104
  finally { staging?.delete() }
104
105
  }
105
106
 
@@ -120,6 +120,7 @@ internal class CraftNativeServices(
120
120
  done(success(CraftNativeMedia.prune(context, (0 until keep.length()).map { keep.optString(it) })))
121
121
  }
122
122
  "usage" -> done(success(CraftNativeMedia.usage(context)))
123
+ "clear" -> done(success(CraftNativeMedia.clear(context)))
123
124
  else -> done(failure("UNKNOWN_ACTION", "Unsupported Media method"))
124
125
  }
125
126
  "Background" -> {
@@ -1021,7 +1021,8 @@ internal object CraftNativeSymbols {
1021
1021
  * A native Video given an address with a copy here plays the copy.
1022
1022
  */
1023
1023
  internal object CraftNativeMedia {
1024
- private val inFlight = HashMap<String, MutableList<(CraftNativeCapabilityResult) -> Unit>>()
1024
+ private data class Flight(val id: String, val waiting: MutableList<(CraftNativeCapabilityResult) -> Unit>)
1025
+ private val inFlight = HashMap<String, Flight>()
1025
1026
  private val executor = Executors.newFixedThreadPool(2)
1026
1027
  private val main = Handler(Looper.getMainLooper())
1027
1028
 
@@ -1054,12 +1055,12 @@ internal object CraftNativeMedia {
1054
1055
  if (!cellular && metered(context)) {
1055
1056
  return done(CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("reason", "cellular")))
1056
1057
  }
1057
- synchronized(inFlight) {
1058
+ val flight = synchronized(inFlight) {
1058
1059
  inFlight[address]?.let { waiting ->
1059
- waiting.add(done)
1060
+ waiting.waiting.add(done)
1060
1061
  return
1061
1062
  }
1062
- inFlight[address] = mutableListOf(done)
1063
+ Flight(java.util.UUID.randomUUID().toString(), mutableListOf(done)).also { inFlight[address] = it }
1063
1064
  }
1064
1065
  executor.execute {
1065
1066
  val result = runCatching {
@@ -1073,7 +1074,7 @@ internal object CraftNativeMedia {
1073
1074
  val target = file(context, address)
1074
1075
  target.parentFile?.mkdirs()
1075
1076
  // Written aside and moved into place, so a half-written file is never played.
1076
- val staging = File(target.parentFile, "${target.name}.part")
1077
+ val staging = File(target.parentFile, "${target.name}.${flight.id}.part")
1077
1078
  try {
1078
1079
  val size = connection.inputStream.use { input -> staging.outputStream().use { input.copyTo(it) } }
1079
1080
  val expected = connection.contentLengthLong
@@ -1081,6 +1082,9 @@ internal object CraftNativeMedia {
1081
1082
  return@runCatching CraftNativeCapabilityResult(code = "NETWORK_ERROR", message = "media.keep received an incomplete file")
1082
1083
  }
1083
1084
  synchronized(inFlight) {
1085
+ if (inFlight[address]?.id != flight.id) {
1086
+ return@runCatching CraftNativeCapabilityResult(code = "CANCELLED", message = "The video cache was cleared")
1087
+ }
1084
1088
  if (!staging.renameTo(target)) {
1085
1089
  return@runCatching CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not store the file")
1086
1090
  }
@@ -1098,7 +1102,7 @@ internal object CraftNativeMedia {
1098
1102
  }
1099
1103
  CraftNativeCapabilityResult(code = if (full) "NO_SPACE" else if (error is java.net.SocketTimeoutException) "TIMEOUT" else "NETWORK_ERROR", message = error.message ?: "media.keep failed")
1100
1104
  }
1101
- val waiting = synchronized(inFlight) { inFlight.remove(address) ?: mutableListOf() }
1105
+ val waiting = synchronized(inFlight) { if (inFlight[address]?.id == flight.id) inFlight.remove(address)?.waiting.orEmpty() else emptyList() }
1102
1106
  main.post { waiting.forEach { it(result) } }
1103
1107
  }
1104
1108
  }
@@ -1115,12 +1119,31 @@ internal object CraftNativeMedia {
1115
1119
  var removed = 0
1116
1120
  synchronized(inFlight) {
1117
1121
  // A concurrent keep owns its target and staging file until it finishes.
1118
- val active = inFlight.keys.map { file(context, it).name }.toSet()
1122
+ val active = inFlight.flatMap { (address, flight) ->
1123
+ val name = file(context, address).name
1124
+ listOf(name, "$name.${flight.id}.part")
1125
+ }.toSet()
1126
+ root(context).listFiles()?.forEach { file ->
1127
+ if (file.name !in active && file.name !in wanted && file.delete()) removed++
1128
+ }
1129
+ }
1130
+ return removed
1131
+ }
1132
+
1133
+ /** Invalidates old transfers before deleting files; a later keep starts afresh. */
1134
+ fun clear(context: Context): Int {
1135
+ var removed = 0
1136
+ var failure: java.io.IOException? = null
1137
+ val waiting = synchronized(inFlight) {
1138
+ val callbacks = inFlight.values.flatMap { it.waiting }
1139
+ inFlight.clear()
1119
1140
  root(context).listFiles()?.forEach { file ->
1120
- val target = file.name.removeSuffix(".part")
1121
- if (target !in active && file.name !in wanted && file.delete()) removed++
1141
+ if (file.delete()) removed++ else failure = java.io.IOException("The video cache could not be cleared")
1122
1142
  }
1143
+ callbacks
1123
1144
  }
1145
+ main.post { waiting.forEach { it(CraftNativeCapabilityResult(code = "CANCELLED", message = "The video cache was cleared")) } }
1146
+ failure?.let { throw it }
1124
1147
  return removed
1125
1148
  }
1126
1149
 
@@ -4091,6 +4091,7 @@ class MainActivity : AppCompatActivity() {
4091
4091
  show(target)
4092
4092
  } else {
4093
4093
  host.removeView(target.view)
4094
+ target.view.translationX = 0f
4094
4095
  current.view.translationX = 0f
4095
4096
  chrome?.selectTab(currentTab)
4096
4097
  tabSwipeTarget = null
@@ -4504,6 +4505,11 @@ class MainActivity : AppCompatActivity() {
4504
4505
  * screen shown before hears true (its `navigation.onAppear`).
4505
4506
  */
4506
4507
  private fun show(screen: Screen) {
4508
+ // A retained tab may have been the offscreen preview of a cancelled
4509
+ // swipe, or the outgoing view of a completed swipe. Its animator can
4510
+ // finish after the peer animator, so clear that ownership on display.
4511
+ screen.view.animate().withEndAction(null).setListener(null).cancel()
4512
+ screen.view.translationX = 0f
4507
4513
  // A navigation breadcrumb: the trail a crash report carries.
4508
4514
  if (visibleScreen !== screen) CraftCrashReporter.screenAppeared(screen.route ?: "home")
4509
4515
  val bars = chrome