@craft-native/android 0.0.225 → 0.0.227
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
|
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" -> {
|
|
@@ -643,6 +643,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
643
643
|
private var accessorySpec: JSONObject? = null
|
|
644
644
|
private val tabViews = mutableListOf<Pair<TextView, TextView>>()
|
|
645
645
|
private val tabIconText = mutableMapOf<String, String>()
|
|
646
|
+
private val tabTitles = mutableMapOf<String, String>()
|
|
647
|
+
private val tabAccessibilityLabels = mutableMapOf<String, String>()
|
|
648
|
+
private val tabIconTints = mutableMapOf<String, Int>()
|
|
649
|
+
private val tabIconPulses = mutableSetOf<String>()
|
|
646
650
|
private val tabImageURLs = mutableMapOf<String, String>()
|
|
647
651
|
private val tabImages = mutableMapOf<String, android.graphics.drawable.Drawable>()
|
|
648
652
|
private val navButtonSpecs = mutableMapOf<TextView, JSONObject>()
|
|
@@ -706,6 +710,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
706
710
|
cell.gravity = Gravity.CENTER
|
|
707
711
|
cell.background = rippleBackground()
|
|
708
712
|
cell.contentDescription = tab.title
|
|
713
|
+
cell.tag = "craft-tab-${tab.id}"
|
|
709
714
|
val icon = TextView(context)
|
|
710
715
|
icon.text = CraftNativeSymbols.glyph(tab.symbol)
|
|
711
716
|
icon.textSize = 22f
|
|
@@ -716,6 +721,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
716
721
|
val label = TextView(context)
|
|
717
722
|
label.text = tab.title
|
|
718
723
|
label.textSize = 12f
|
|
724
|
+
label.fontFeatureSettings = "tnum"
|
|
719
725
|
label.gravity = Gravity.CENTER
|
|
720
726
|
label.isSingleLine = true
|
|
721
727
|
cell.addView(icon, LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
|
@@ -844,12 +850,25 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
844
850
|
val id = item.optString("id")
|
|
845
851
|
val index = tabs.indexOfFirst { it.id == id }
|
|
846
852
|
if (index < 0) return
|
|
847
|
-
|
|
848
|
-
|
|
853
|
+
if (item.has("accessibilityLabel")) {
|
|
854
|
+
tabAccessibilityLabels.remove(id)
|
|
855
|
+
item.optString("accessibilityLabel").takeUnless { item.isNull("accessibilityLabel") || it.isBlank() }?.let { tabAccessibilityLabels[id] = it.take(160) }
|
|
856
|
+
}
|
|
849
857
|
if (item.has("iconText")) {
|
|
850
858
|
tabIconText.remove(id)
|
|
851
859
|
item.optString("iconText").takeUnless { item.isNull("iconText") || it.isBlank() }?.let { tabIconText[id] = it.take(8) }
|
|
852
860
|
}
|
|
861
|
+
if (item.has("title")) {
|
|
862
|
+
tabTitles.remove(id)
|
|
863
|
+
item.optString("title").takeUnless { item.isNull("title") || it.isBlank() }?.let { tabTitles[id] = it.take(40) }
|
|
864
|
+
}
|
|
865
|
+
if (item.has("iconTint")) {
|
|
866
|
+
tabIconTints.remove(id)
|
|
867
|
+
if (!item.isNull("iconTint")) runCatching { Color.parseColor(item.optString("iconTint")) }.getOrNull()?.let { tabIconTints[id] = it }
|
|
868
|
+
}
|
|
869
|
+
if (item.has("iconPulse")) {
|
|
870
|
+
if (item.optBoolean("iconPulse", false)) tabIconPulses.add(id) else tabIconPulses.remove(id)
|
|
871
|
+
}
|
|
853
872
|
if (item.has("image")) {
|
|
854
873
|
val address = item.optString("image").takeIf { !item.isNull("image") && it.startsWith("https://") }
|
|
855
874
|
if (address != tabImageURLs[id]) {
|
|
@@ -896,16 +915,41 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
896
915
|
tabViews.forEachIndexed { index, (icon, label) ->
|
|
897
916
|
tabBar.getChildAt(index).isSelected = index == selectedTab
|
|
898
917
|
val color = if (index == selectedTab) accent else muted
|
|
899
|
-
val
|
|
900
|
-
|
|
918
|
+
val id = tabs[index].id
|
|
919
|
+
val number = tabIconText[id]
|
|
920
|
+
val iconColor = tabIconTints[id] ?: if (number != null) Color.parseColor("#FF2D55") else color
|
|
921
|
+
icon.setTextColor(iconColor)
|
|
922
|
+
label.text = tabTitles[id] ?: tabs[index].title
|
|
923
|
+
tabBar.getChildAt(index).contentDescription = tabAccessibilityLabels[id] ?: label.text
|
|
901
924
|
label.setTextColor(color)
|
|
902
|
-
val vector = tabImages[
|
|
925
|
+
val vector = tabImages[id] ?: if (number == null) CraftNativeIcons.drawable(context, null, tabs[index].symbol, iconColor) else null
|
|
926
|
+
val pulse = tabIconPulses.contains(id) && number == null && tabImages[id] == null && hasWindowFocus() && android.animation.ValueAnimator.areAnimatorsEnabled()
|
|
927
|
+
if (pulse && icon.animation == null) {
|
|
928
|
+
icon.startAnimation(android.view.animation.AlphaAnimation(1f, 0.55f).apply {
|
|
929
|
+
duration = 700
|
|
930
|
+
repeatCount = android.view.animation.Animation.INFINITE
|
|
931
|
+
repeatMode = android.view.animation.Animation.REVERSE
|
|
932
|
+
})
|
|
933
|
+
} else if (!pulse) {
|
|
934
|
+
icon.clearAnimation()
|
|
935
|
+
icon.alpha = 1f
|
|
936
|
+
}
|
|
903
937
|
icon.foreground = vector
|
|
904
938
|
icon.text = if (vector != null) "" else number ?: CraftNativeSymbols.glyph(tabs[index].symbol)
|
|
905
939
|
}
|
|
906
940
|
navButtonSpecs.forEach { (view, spec) -> applyNavButtonTint(view, spec, dark) }
|
|
907
941
|
}
|
|
908
942
|
|
|
943
|
+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
944
|
+
super.onWindowFocusChanged(hasWindowFocus)
|
|
945
|
+
if (hasWindowFocus) refreshColors() else tabViews.forEach { (icon, _) -> icon.clearAnimation() }
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
override fun onDetachedFromWindow() {
|
|
949
|
+
tabViews.forEach { (icon, _) -> icon.clearAnimation() }
|
|
950
|
+
super.onDetachedFromWindow()
|
|
951
|
+
}
|
|
952
|
+
|
|
909
953
|
private fun navButton(button: JSONObject): View {
|
|
910
954
|
val id = button.optString("id")
|
|
911
955
|
val title = button.optString("title")
|
|
@@ -1021,7 +1065,8 @@ internal object CraftNativeSymbols {
|
|
|
1021
1065
|
* A native Video given an address with a copy here plays the copy.
|
|
1022
1066
|
*/
|
|
1023
1067
|
internal object CraftNativeMedia {
|
|
1024
|
-
private val
|
|
1068
|
+
private data class Flight(val id: String, val waiting: MutableList<(CraftNativeCapabilityResult) -> Unit>)
|
|
1069
|
+
private val inFlight = HashMap<String, Flight>()
|
|
1025
1070
|
private val executor = Executors.newFixedThreadPool(2)
|
|
1026
1071
|
private val main = Handler(Looper.getMainLooper())
|
|
1027
1072
|
|
|
@@ -1054,12 +1099,12 @@ internal object CraftNativeMedia {
|
|
|
1054
1099
|
if (!cellular && metered(context)) {
|
|
1055
1100
|
return done(CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("reason", "cellular")))
|
|
1056
1101
|
}
|
|
1057
|
-
synchronized(inFlight) {
|
|
1102
|
+
val flight = synchronized(inFlight) {
|
|
1058
1103
|
inFlight[address]?.let { waiting ->
|
|
1059
|
-
waiting.add(done)
|
|
1104
|
+
waiting.waiting.add(done)
|
|
1060
1105
|
return
|
|
1061
1106
|
}
|
|
1062
|
-
inFlight[address] =
|
|
1107
|
+
Flight(java.util.UUID.randomUUID().toString(), mutableListOf(done)).also { inFlight[address] = it }
|
|
1063
1108
|
}
|
|
1064
1109
|
executor.execute {
|
|
1065
1110
|
val result = runCatching {
|
|
@@ -1073,7 +1118,7 @@ internal object CraftNativeMedia {
|
|
|
1073
1118
|
val target = file(context, address)
|
|
1074
1119
|
target.parentFile?.mkdirs()
|
|
1075
1120
|
// Written aside and moved into place, so a half-written file is never played.
|
|
1076
|
-
val staging = File(target.parentFile, "${target.name}.part")
|
|
1121
|
+
val staging = File(target.parentFile, "${target.name}.${flight.id}.part")
|
|
1077
1122
|
try {
|
|
1078
1123
|
val size = connection.inputStream.use { input -> staging.outputStream().use { input.copyTo(it) } }
|
|
1079
1124
|
val expected = connection.contentLengthLong
|
|
@@ -1081,6 +1126,9 @@ internal object CraftNativeMedia {
|
|
|
1081
1126
|
return@runCatching CraftNativeCapabilityResult(code = "NETWORK_ERROR", message = "media.keep received an incomplete file")
|
|
1082
1127
|
}
|
|
1083
1128
|
synchronized(inFlight) {
|
|
1129
|
+
if (inFlight[address]?.id != flight.id) {
|
|
1130
|
+
return@runCatching CraftNativeCapabilityResult(code = "CANCELLED", message = "The video cache was cleared")
|
|
1131
|
+
}
|
|
1084
1132
|
if (!staging.renameTo(target)) {
|
|
1085
1133
|
return@runCatching CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not store the file")
|
|
1086
1134
|
}
|
|
@@ -1098,7 +1146,7 @@ internal object CraftNativeMedia {
|
|
|
1098
1146
|
}
|
|
1099
1147
|
CraftNativeCapabilityResult(code = if (full) "NO_SPACE" else if (error is java.net.SocketTimeoutException) "TIMEOUT" else "NETWORK_ERROR", message = error.message ?: "media.keep failed")
|
|
1100
1148
|
}
|
|
1101
|
-
val waiting = synchronized(inFlight) { inFlight.remove(address)
|
|
1149
|
+
val waiting = synchronized(inFlight) { if (inFlight[address]?.id == flight.id) inFlight.remove(address)?.waiting.orEmpty() else emptyList() }
|
|
1102
1150
|
main.post { waiting.forEach { it(result) } }
|
|
1103
1151
|
}
|
|
1104
1152
|
}
|
|
@@ -1115,12 +1163,31 @@ internal object CraftNativeMedia {
|
|
|
1115
1163
|
var removed = 0
|
|
1116
1164
|
synchronized(inFlight) {
|
|
1117
1165
|
// A concurrent keep owns its target and staging file until it finishes.
|
|
1118
|
-
val active = inFlight.
|
|
1166
|
+
val active = inFlight.flatMap { (address, flight) ->
|
|
1167
|
+
val name = file(context, address).name
|
|
1168
|
+
listOf(name, "$name.${flight.id}.part")
|
|
1169
|
+
}.toSet()
|
|
1170
|
+
root(context).listFiles()?.forEach { file ->
|
|
1171
|
+
if (file.name !in active && file.name !in wanted && file.delete()) removed++
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return removed
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** Invalidates old transfers before deleting files; a later keep starts afresh. */
|
|
1178
|
+
fun clear(context: Context): Int {
|
|
1179
|
+
var removed = 0
|
|
1180
|
+
var failure: java.io.IOException? = null
|
|
1181
|
+
val waiting = synchronized(inFlight) {
|
|
1182
|
+
val callbacks = inFlight.values.flatMap { it.waiting }
|
|
1183
|
+
inFlight.clear()
|
|
1119
1184
|
root(context).listFiles()?.forEach { file ->
|
|
1120
|
-
|
|
1121
|
-
if (target !in active && file.name !in wanted && file.delete()) removed++
|
|
1185
|
+
if (file.delete()) removed++ else failure = java.io.IOException("The video cache could not be cleared")
|
|
1122
1186
|
}
|
|
1187
|
+
callbacks
|
|
1123
1188
|
}
|
|
1189
|
+
main.post { waiting.forEach { it(CraftNativeCapabilityResult(code = "CANCELLED", message = "The video cache was cleared")) } }
|
|
1190
|
+
failure?.let { throw it }
|
|
1124
1191
|
return removed
|
|
1125
1192
|
}
|
|
1126
1193
|
|
|
@@ -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
|