@craft-native/android 0.0.451 → 0.0.464

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.
@@ -286,6 +286,26 @@ private data class CraftNativeLayoutStyle(
286
286
  }
287
287
  }
288
288
 
289
+ /** Component scope nodes preserve identity but lay out through their single non-modal root. */
290
+ internal fun craftNativeComponentLayoutStyle(node: JSONObject): JSONObject {
291
+ val own = node.optJSONObject("style") ?: JSONObject()
292
+ if (node.optJSONObject("props")?.has("data-stx-component-wrapper") != true) return own
293
+ val children = node.optJSONArray("children") ?: return own
294
+ if (children.length() != 1) return own
295
+ val root = children.optJSONObject(0) ?: return own
296
+ if (root.optString("type").equals("Modal", ignoreCase = true)) return own
297
+ if (root.optJSONObject("style")?.optString("display") == "none") return own
298
+ val inner = craftNativeComponentLayoutStyle(root)
299
+ val merged = JSONObject(own.toString())
300
+ for (key in listOf("width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", "aspectRatio",
301
+ "flex", "flexGrow", "flexShrink", "flexBasis", "alignSelf", "order", "position", "zIndex",
302
+ "left", "right", "top", "bottom", "margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
303
+ "marginHorizontal", "marginVertical")) {
304
+ if (!merged.has(key) && inner.has(key)) merged.put(key, inner.opt(key))
305
+ }
306
+ return merged
307
+ }
308
+
289
309
  private fun craftNativeGridColumnCount(value: Any?): Int {
290
310
  if (value is Number) return max(1, value.toInt())
291
311
  val text = value as? String ?: return 1
@@ -1196,6 +1216,13 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1196
1216
  private var applyingPadding = false
1197
1217
  private var safeAreaEdges = setOf("top", "right", "bottom", "left")
1198
1218
  private var overflowTouchTarget: View? = null
1219
+ var componentScope = false
1220
+
1221
+ private fun componentRoot(): View? {
1222
+ if (!componentScope || childCount != 1) return null
1223
+ return getChildAt(0).takeIf { it !is CraftNativeModalView && it.visibility != View.GONE }
1224
+ }
1225
+
1199
1226
 
1200
1227
  private fun childEvent(child: View, event: MotionEvent): MotionEvent = MotionEvent.obtain(event).also {
1201
1228
  it.offsetLocation((scrollX - child.left).toFloat(), (scrollY - child.top).toFloat())
@@ -1877,6 +1904,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1877
1904
 
1878
1905
  /** Flatten a flex or equal-track grid subtree for the shared ABI. */
1879
1906
  private fun sharedTreeChildren(width: Int, height: Int): List<Entry>? {
1907
+ if (componentRoot() != null) return null
1880
1908
  if (grid) {
1881
1909
  val layout = gridLayout(width, height)
1882
1910
  if (layout.absolute.isNotEmpty()) return null
@@ -2229,6 +2257,11 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
2229
2257
  }
2230
2258
 
2231
2259
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
2260
+ componentRoot()?.let { root ->
2261
+ root.measure(widthMeasureSpec, heightMeasureSpec)
2262
+ setMeasuredDimension(root.measuredWidth, root.measuredHeight)
2263
+ return
2264
+ }
2232
2265
  pageViewport?.let { (pageWidth, pageHeight) ->
2233
2266
  val pages = (0 until childCount).map { getChildAt(it) }.filter { it.visibility != View.GONE }
2234
2267
  pages.forEach { it.measure(MeasureSpec.makeMeasureSpec(pageWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(pageHeight, MeasureSpec.EXACTLY)) }
@@ -2271,6 +2304,11 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
2271
2304
 
2272
2305
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
2273
2306
  celebration?.layout()
2307
+ componentRoot()?.let { root ->
2308
+ place(root, 0, 0, right - left, bottom - top)
2309
+ onLayoutChanged?.invoke()
2310
+ return
2311
+ }
2274
2312
  pageViewport?.let { (pageWidth, pageHeight) ->
2275
2313
  val pages = (0 until childCount).map { getChildAt(it) }.filter { it.visibility != View.GONE }
2276
2314
  pages.forEachIndexed { index, child -> child.layout(index * pageWidth, 0, (index + 1) * pageWidth, pageHeight) }
@@ -3350,6 +3388,104 @@ private class CraftNativeTextField(context: Context) : EditText(context) {
3350
3388
  }
3351
3389
  }
3352
3390
 
3391
+ /** Hide only siblings behind the top blocking modal, restoring authored values on every update. */
3392
+ private fun craftNativeModalAccessibility(root: ViewGroup, saved: MutableMap<View, Int>, blocks: (View) -> Boolean) {
3393
+ saved.forEach { (view, previous) ->
3394
+ // Reconciliation may already have replaced the old value with a new role.
3395
+ if (view.importantForAccessibility == View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
3396
+ view.importantForAccessibility = previous
3397
+ }
3398
+ }
3399
+ saved.clear()
3400
+ val top = (0 until root.childCount).lastOrNull { blocks(root.getChildAt(it)) } ?: return
3401
+ for (index in 0 until top) {
3402
+ val view = root.getChildAt(index)
3403
+ saved[view] = view.importantForAccessibility
3404
+ view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
3405
+ }
3406
+ }
3407
+
3408
+ private fun craftNativeAccessibilityLanguage(value: Any?): Locale? =
3409
+ (value as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag)?.takeIf { it.language.isNotEmpty() }
3410
+
3411
+ private fun craftNativeSpokenText(text: CharSequence?, language: Locale?): CharSequence? {
3412
+ if (text == null || language == null || text.isEmpty()) return text
3413
+ return SpannableString(text).apply {
3414
+ setSpan(android.text.style.LocaleSpan(language), 0, length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
3415
+ }
3416
+ }
3417
+
3418
+ internal data class CraftNativeMenuItem(val id: String, val title: String, val symbol: String = "", val destructive: Boolean = false, val disabled: Boolean = false)
3419
+
3420
+ /** One anchored popup per retained view; a reconciled popup can never dispatch. */
3421
+ internal class CraftNativeContextMenu(private val anchor: View) {
3422
+ private var popup: android.widget.PopupMenu? = null
3423
+ private var revision = 0
3424
+ private var items: List<CraftNativeMenuItem> = emptyList()
3425
+ private var dispatch: ((String) -> Unit)? = null
3426
+ private var handlerIdentity: String? = null
3427
+
3428
+ fun update(next: List<CraftNativeMenuItem>, identity: String? = null, callback: (String) -> Unit) {
3429
+ val valid = next.filter { it.id.isNotBlank() && it.title.isNotBlank() }.distinctBy { it.id }
3430
+ if (identity != null && identity == handlerIdentity && valid == items) {
3431
+ dispatch = callback
3432
+ return
3433
+ }
3434
+ clear()
3435
+ handlerIdentity = identity
3436
+ items = valid
3437
+ dispatch = callback
3438
+ }
3439
+
3440
+ fun clear() {
3441
+ revision++
3442
+ popup?.dismiss()
3443
+ popup = null
3444
+ items = emptyList()
3445
+ dispatch = null
3446
+ }
3447
+
3448
+ fun show(decorate: (android.view.MenuItem, CraftNativeMenuItem) -> Unit): Boolean {
3449
+ if (items.isEmpty() || dispatch == null || !anchor.isEnabled) return false
3450
+ popup?.dismiss()
3451
+ val expected = revision
3452
+ val shown = android.widget.PopupMenu(anchor.context, anchor)
3453
+ popup = shown
3454
+ if (Build.VERSION.SDK_INT >= 29) shown.setForceShowIcon(true)
3455
+ items.forEachIndexed { index, item ->
3456
+ shown.menu.add(0, index, index, item.title).also {
3457
+ it.isEnabled = !item.disabled
3458
+ decorate(it, item)
3459
+ }
3460
+ }
3461
+ shown.setOnMenuItemClickListener { choice ->
3462
+ val item = items.getOrNull(choice.itemId)
3463
+ if (revision != expected || item == null || item.disabled || !anchor.isEnabled) false
3464
+ else { dispatch?.invoke(item.id); true }
3465
+ }
3466
+ shown.setOnDismissListener { if (popup === shown) popup = null }
3467
+ shown.show()
3468
+ return true
3469
+ }
3470
+ }
3471
+
3472
+ private fun craftNativeAnnounce(args: JSONArray, announce: (String) -> Unit): CraftNativeCapabilityResult {
3473
+ val text = args.opt(0) as? String
3474
+ if (text.isNullOrBlank()) return CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Accessibility.announce needs nonempty text")
3475
+ val options = args.opt(1)
3476
+ if (options != null && options != JSONObject.NULL && options !is JSONObject) {
3477
+ return CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Accessibility.announce options must be an object")
3478
+ }
3479
+ val polite = (options as? JSONObject)?.opt("polite")
3480
+ if (polite != null && polite !is Boolean) {
3481
+ return CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Accessibility.announce polite must be a Boolean")
3482
+ }
3483
+ // Android's announcement event has no queue-priority field. The service
3484
+ // owns speech scheduling; never move focus to force it to speak.
3485
+ announce(text)
3486
+ return CraftNativeCapabilityResult(data = true)
3487
+ }
3488
+
3353
3489
  private class CraftNativeModalView(context: Context) : FrameLayout(context) {
3354
3490
  override fun dispatchTouchEvent(event: MotionEvent): Boolean =
3355
3491
  CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
@@ -4563,7 +4699,9 @@ class MainActivity : AppCompatActivity() {
4563
4699
  val pressUnderlayShowHandlers = mutableMapOf<View, String>()
4564
4700
  val pressUnderlayHideHandlers = mutableMapOf<View, String>()
4565
4701
  val longPressHandlers = mutableMapOf<View, String>()
4702
+ val contextMenus = mutableMapOf<View, CraftNativeContextMenu>()
4566
4703
  val accessibilityActionHandlers = mutableMapOf<View, String>()
4704
+ val accessibilityActionRevisions = mutableMapOf<View, Any>()
4567
4705
  // TalkBack actions are added by id, so removing them on a reconcile
4568
4706
  // needs the ids back; without this a renamed action stacks up beside
4569
4707
  // the old one every render.
@@ -4588,6 +4726,7 @@ class MainActivity : AppCompatActivity() {
4588
4726
  val pressUnderlayColors = mutableMapOf<View, Int>()
4589
4727
  val pressBaseBackgrounds = mutableMapOf<View, Drawable?>()
4590
4728
  val modalVisibility = mutableMapOf<View, Boolean>()
4729
+ val modalAccessibility = mutableMapOf<View, Int>()
4591
4730
  val modalCloseHandlers = mutableMapOf<View, String>()
4592
4731
  val layoutHandlers = mutableMapOf<View, String>()
4593
4732
  val layoutListeners = mutableMapOf<View, View.OnLayoutChangeListener>()
@@ -4749,7 +4888,9 @@ class MainActivity : AppCompatActivity() {
4749
4888
  initialDeepLink = savedInstanceState?.getString("craft.initialDeepLink")?.let(Uri::parse) ?: intent?.data
4750
4889
  initialDeepLinkClaimed = savedInstanceState?.getBoolean("craft.initialDeepLinkClaimed", false) ?: false
4751
4890
  deliveredInitialDeepLink = savedInstanceState?.getBoolean("craft.deliveredInitialDeepLink", false) ?: false
4752
- if (savedInstanceState == null) pendingNotificationTap = notificationTap(intent)
4891
+ if (savedInstanceState == null && !CraftNativeNotifications.receiveAction(this, intent)) {
4892
+ pendingNotificationTap = notificationTap(intent)
4893
+ }
4753
4894
  setContentView(R.layout.activity_main)
4754
4895
  root = findViewById(R.id.native_root)
4755
4896
  root.foreground = navigationBarProtection
@@ -5428,12 +5569,12 @@ class MainActivity : AppCompatActivity() {
5428
5569
  "if (typeof globalThis.__craftInstallNativeDialog === 'function') globalThis.__craftInstallNativeDialog();"
5429
5570
  ).get()
5430
5571
  if (activeScreen.headless) {
5431
- CraftNativeBackground.foreground = { event, id, done ->
5572
+ CraftNativeBackground.foreground = { event, id, detail, done ->
5432
5573
  val app = appScreen
5433
5574
  if (app == null || !app.active) false
5434
5575
  else {
5435
5576
  backgroundWaiters[id] = done
5436
- send(app, "CRAFT_EVENT", JSONObject().put("event", event).put("detail", JSONObject().put("id", id)))
5577
+ send(app, "CRAFT_EVENT", JSONObject().put("event", event).put("detail", JSONObject(detail.toString()).put("id", id)))
5437
5578
  true
5438
5579
  }
5439
5580
  }
@@ -5589,6 +5730,7 @@ class MainActivity : AppCompatActivity() {
5589
5730
  put("viewNonModalPresentation")
5590
5731
  put("nativePlatformControls")
5591
5732
  put("nativePager")
5733
+ put("accessibility")
5592
5734
  }
5593
5735
 
5594
5736
  private fun appVersion(): Pair<String, String> = runCatching {
@@ -5897,7 +6039,7 @@ class MainActivity : AppCompatActivity() {
5897
6039
  makeView(screen, document, path, screen.controls, inheritedFontFamily = screen.inheritedFontFamilies[target])
5898
6040
  val updated = screen.nodeViews[target] ?: return false
5899
6041
  screen.lastLayoutBounds.remove(updated)
5900
- if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
6042
+ if (!updateParentLayoutStyle(updated, craftNativeComponentLayoutStyle(document))) return false
5901
6043
  }
5902
6044
  if (applied.isNotEmpty()) {
5903
6045
  // Updating a modal's content may reattach its nested popups to
@@ -5913,7 +6055,20 @@ class MainActivity : AppCompatActivity() {
5913
6055
  var current: String? = id
5914
6056
  while (current != null) {
5915
6057
  val target = screen.flatListOwners[current] ?: current
5916
- if (screen.nodeViews[target] != null) return target
6058
+ if (screen.nodeViews[target] != null) {
6059
+ // A root style mutation also changes its scope's layout in the
6060
+ // outer parent. Reconcile that owner, including nested scopes.
6061
+ var owner = target
6062
+ while (true) {
6063
+ val parentId = screen.mutations.parentId(owner) ?: break
6064
+ val parentNode = screen.mutations.node(parentId) ?: break
6065
+ if (parentNode.optJSONObject("props")?.has("data-stx-component-wrapper") != true) break
6066
+ val children = parentNode.optJSONArray("children") ?: break
6067
+ if (children.length() != 1 || parentNode.optJSONArray("children")?.optJSONObject(0)?.optString("type")?.equals("Modal", ignoreCase = true) == true) break
6068
+ owner = parentId
6069
+ }
6070
+ return owner
6071
+ }
5917
6072
  current = screen.mutations.parentId(current)
5918
6073
  }
5919
6074
  return null
@@ -5993,7 +6148,7 @@ class MainActivity : AppCompatActivity() {
5993
6148
  screen.controls.putAll(next)
5994
6149
  screen.handlers.keys.retainAll(retainedViews(screen))
5995
6150
  screen.view.setViewportChild(tree)
5996
- screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
6151
+ screen.view.setChildStyle(tree, craftNativeComponentLayoutStyle(document), resources.displayMetrics.density)
5997
6152
  promoteModalOverlays(screen)
5998
6153
  updateSafeAreaViews()
5999
6154
  // A full render can change a view's custom layout style without
@@ -6005,7 +6160,10 @@ class MainActivity : AppCompatActivity() {
6005
6160
 
6006
6161
  /** Keep Modal nodes in the screen viewport even when declared in a clipped subtree. */
6007
6162
  private fun promoteModalOverlays(screen: Screen) {
6008
- if (screen.modalVisibility.isEmpty()) return
6163
+ if (screen.modalVisibility.isEmpty()) {
6164
+ craftNativeModalAccessibility(screen.view, screen.modalAccessibility) { false }
6165
+ return
6166
+ }
6009
6167
  // Snapshot parent-before-child order before moving anything. The
6010
6168
  // controls map registers descendants first, which would place a
6011
6169
  // parent modal above its own popup when brought forward in that order.
@@ -6040,6 +6198,9 @@ class MainActivity : AppCompatActivity() {
6040
6198
  }
6041
6199
  modal.bringToFront()
6042
6200
  }
6201
+ craftNativeModalAccessibility(screen.view, screen.modalAccessibility) {
6202
+ it is CraftNativeModalView && it.blocksBackground && screen.modalVisibility[it] == true
6203
+ }
6043
6204
  }
6044
6205
 
6045
6206
  private fun updateStatusBarProps(screen: Screen, props: JSONObject) {
@@ -6066,6 +6227,7 @@ class MainActivity : AppCompatActivity() {
6066
6227
  }
6067
6228
 
6068
6229
  private fun clearRenderedTree(screen: Screen) {
6230
+ craftNativeModalAccessibility(screen.view, screen.modalAccessibility) { false }
6069
6231
  releaseViewState(screen, retainedViews(screen).toList())
6070
6232
  screen.view.removeAllViews()
6071
6233
  screen.controls.clear()
@@ -6095,7 +6257,7 @@ class MainActivity : AppCompatActivity() {
6095
6257
  ): View {
6096
6258
  val type = canonicalType(node.optString("type", "View"))
6097
6259
  val props = node.optJSONObject("props") ?: JSONObject()
6098
- val style = craftNativeInheritFontFamily(node.optJSONObject("style") ?: JSONObject(), inheritedFontFamily)
6260
+ val style = craftNativeInheritFontFamily(craftNativeComponentLayoutStyle(node), inheritedFontFamily)
6099
6261
  val events = node.optJSONObject("events") ?: JSONObject()
6100
6262
  val children = node.optJSONArray("children") ?: JSONArray()
6101
6263
  val protocolId = node.optString("id").takeIf { it.isNotBlank() }
@@ -6885,6 +7047,7 @@ class MainActivity : AppCompatActivity() {
6885
7047
  reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events, craftNativeInheritableFontFamily(style))
6886
7048
  }
6887
7049
  "View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
7050
+ componentScope = props.has("data-stx-component-wrapper")
6888
7051
  configureContainer(this, style)
6889
7052
  if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
6890
7053
  else setWindowSafeInsets(0, 0, 0, 0)
@@ -6997,6 +7160,33 @@ class MainActivity : AppCompatActivity() {
6997
7160
  control.setOnLongClickListener(null)
6998
7161
  control.isLongClickable = false
6999
7162
  }
7163
+ // STX emits @contextmenu as onLongPress and lowercases unknown bound props.
7164
+ val menuHandler = craftNativeEventName(events, "onContextMenuAction").ifBlank { longPressHandler }.takeUnless { longPressDisabled }.orEmpty()
7165
+ val authoredMenu = props.optJSONArray("contextMenu") ?: props.optJSONArray("contextmenu")
7166
+ val menuItems = (0 until (authoredMenu?.length() ?: 0)).mapNotNull { index ->
7167
+ val item = authoredMenu?.optJSONObject(index) ?: return@mapNotNull null
7168
+ val id = craftNativeOptionalString(item.opt("id"))?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
7169
+ val title = craftNativeOptionalString(item.opt("title"))?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
7170
+ CraftNativeMenuItem(id, title, craftNativeOptionalString(item.opt("symbol")).orEmpty(),
7171
+ craftNativeBoolean(item.opt("destructive")), craftNativeBoolean(item.opt("disabled")))
7172
+ }
7173
+ if (menuHandler.isNotBlank() && menuItems.isNotEmpty()) {
7174
+ val menu = screen.contextMenus.getOrPut(control) { CraftNativeContextMenu(control) }
7175
+ menu.update(menuItems, menuHandler) { id ->
7176
+ send(screen, "EVENT", JSONObject().put("handlerName", menuHandler).put("nativeEvent", JSONObject().put("id", id)))
7177
+ }
7178
+ // A configured menu owns the hold, rather than firing onLongPress too.
7179
+ control.setOnLongClickListener {
7180
+ menu.show { nativeItem, item ->
7181
+ nativeItem.icon = CraftNativeIcons.drawable(this, "", item.symbol, defaultTextColor)
7182
+ if (item.destructive) nativeItem.title = SpannableString(item.title).apply {
7183
+ setSpan(android.text.style.ForegroundColorSpan(Color.RED), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
7184
+ }
7185
+ }
7186
+ }
7187
+ } else {
7188
+ screen.contextMenus.remove(control)?.clear()
7189
+ }
7000
7190
  CraftNativePointers.configure(control, props, style, control is CraftNativeFlexLayout || control is CraftNativeImageBackgroundView ||
7001
7191
  control is CraftNativeKeyboardAvoidingView || control is CraftNativeScrollView || control is CraftNativeRefreshableFlatList || control is CraftNativeModalView)
7002
7192
  applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
@@ -7164,7 +7354,7 @@ class MainActivity : AppCompatActivity() {
7164
7354
  }
7165
7355
  if (childView != null) desired.add(childView)
7166
7356
  if (childView != null && child is JSONObject) {
7167
- val childStyle = child.optJSONObject("style") ?: JSONObject()
7357
+ val childStyle = craftNativeComponentLayoutStyle(child)
7168
7358
  layout.setChildStyle(childView, childStyle, resources.displayMetrics.density)
7169
7359
  desiredOrder[childView] = craftNativeFiniteNumber(childStyle, "order", 0.0).toInt()
7170
7360
  }
@@ -7916,6 +8106,12 @@ class MainActivity : AppCompatActivity() {
7916
8106
  * Runs after applyAccessibility, which installs its own delegate and
7917
8107
  * would drop actions added before it.
7918
8108
  */
8109
+ private fun clearAccessibilityActions(screen: Screen, control: View) {
8110
+ screen.accessibilityActionIds.remove(control)?.forEach { ViewCompat.removeAccessibilityAction(control, it) }
8111
+ screen.accessibilityActionHandlers.remove(control)
8112
+ screen.accessibilityActionRevisions.remove(control)
8113
+ }
8114
+
7919
8115
  private fun applyAccessibilityActions(
7920
8116
  screen: Screen,
7921
8117
  control: View,
@@ -7923,7 +8119,7 @@ class MainActivity : AppCompatActivity() {
7923
8119
  events: JSONObject,
7924
8120
  disabled: Boolean,
7925
8121
  ) {
7926
- screen.accessibilityActionIds.remove(control)?.forEach { ViewCompat.removeAccessibilityAction(control, it) }
8122
+ clearAccessibilityActions(screen, control)
7927
8123
  val handler = craftNativeEventName(events, "onAccessibilityAction").takeUnless { disabled } ?: ""
7928
8124
  val authored = props.optJSONArray("accessibilityActions")
7929
8125
  if (handler.isBlank() || authored == null || authored.length() == 0) {
@@ -7931,6 +8127,8 @@ class MainActivity : AppCompatActivity() {
7931
8127
  return
7932
8128
  }
7933
8129
  screen.accessibilityActionHandlers[control] = handler
8130
+ val revision = Any()
8131
+ screen.accessibilityActionRevisions[control] = revision
7934
8132
  val ids = mutableListOf<Int>()
7935
8133
  for (index in 0 until authored.length()) {
7936
8134
  val entry = authored.optJSONObject(index) ?: continue
@@ -7940,7 +8138,7 @@ class MainActivity : AppCompatActivity() {
7940
8138
  // Read the handler at firing time: a reconcile can rename it
7941
8139
  // while the action sits in TalkBack's menu.
7942
8140
  val current = screen.accessibilityActionHandlers[control]
7943
- if (current.isNullOrBlank()) {
8141
+ if (current.isNullOrBlank() || screen.accessibilityActionRevisions[control] !== revision) {
7944
8142
  false
7945
8143
  } else {
7946
8144
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("actionName", name)))
@@ -7952,6 +8150,10 @@ class MainActivity : AppCompatActivity() {
7952
8150
  }
7953
8151
 
7954
8152
  private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
8153
+ val language = craftNativeAccessibilityLanguage(props.opt("accessibilityLanguage"))
8154
+ if (view is TextView) {
8155
+ view.textLocales = if (language == null) view.context.resources.configuration.locales else android.os.LocaleList(language)
8156
+ }
7955
8157
  val testId = craftNativeOptionalString(props.opt("testID"))
7956
8158
  view.tag = testId?.takeIf { it.isNotBlank() } ?: fallbackLabel
7957
8159
  val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
@@ -8020,6 +8222,9 @@ class MainActivity : AppCompatActivity() {
8020
8222
  if (state?.has("checked") == true) {
8021
8223
  info.isChecked = craftNativeBoolean(state.opt("checked"))
8022
8224
  }
8225
+ info.text = craftNativeSpokenText(info.text, language)
8226
+ info.contentDescription = craftNativeSpokenText(info.contentDescription, language)
8227
+ info.hintText = craftNativeSpokenText(info.hintText, language)
8023
8228
  }
8024
8229
  })
8025
8230
  ViewCompat.setImportantForAccessibility(
@@ -8316,6 +8521,10 @@ class MainActivity : AppCompatActivity() {
8316
8521
 
8317
8522
  internal fun performImmediateCapability(module: String, method: String, args: JSONArray): CraftNativeCapabilityResult =
8318
8523
  when (module to method) {
8524
+ "Accessibility" to "announce" -> craftNativeAnnounce(args) { text ->
8525
+ @Suppress("DEPRECATION")
8526
+ window.decorView.announceForAccessibility(text)
8527
+ }
8319
8528
  "Device" to "getInfo" -> CraftNativeCapabilityResult(data = JSONObject()
8320
8529
  .put("platform", "android")
8321
8530
  .put("osVersion", Build.VERSION.RELEASE)
@@ -8445,7 +8654,8 @@ class MainActivity : AppCompatActivity() {
8445
8654
  override fun onNewIntent(intent: Intent) {
8446
8655
  super.onNewIntent(intent)
8447
8656
  setIntent(intent)
8448
- notificationTap(intent)?.let { tap ->
8657
+ val notificationAction = CraftNativeNotifications.receiveAction(this, intent)
8658
+ (if (notificationAction) null else notificationTap(intent))?.let { tap ->
8449
8659
  val app = appScreen
8450
8660
  if (app != null && app.active) send(app, "CRAFT_EVENT", JSONObject().put("event", "craftNotificationResponse").put("detail", tap))
8451
8661
  else pendingNotificationTap = tap
@@ -8469,6 +8679,9 @@ class MainActivity : AppCompatActivity() {
8469
8679
  private fun notificationTap(intent: Intent?): JSONObject? {
8470
8680
  val extras = intent?.extras ?: return null
8471
8681
  if (!extras.containsKey("google.message_id") && !extras.containsKey("craft.notification")) return null
8682
+ extras.getString("craft.notification.data")?.let { raw ->
8683
+ return runCatching { JSONObject(raw) }.getOrNull()
8684
+ }
8472
8685
  val detail = JSONObject()
8473
8686
  extras.keySet().forEach { key ->
8474
8687
  if (key.startsWith("google.") || key.startsWith("gcm.") || key == "from" || key == "collapse_key" || key == "craft.notification") return@forEach
@@ -8809,6 +9022,7 @@ class MainActivity : AppCompatActivity() {
8809
9022
  // released/recycled rows so a later identity cannot inherit a closure
8810
9023
  // that still captures the previous row's screen state.
8811
9024
  released.forEach {
9025
+ clearAccessibilityActions(screen, it)
8812
9026
  // Modals are promoted out of their logical layout into the
8813
9027
  // screen overlay. Removing the logical node therefore does not
8814
9028
  // remove the native view through ordinary child reconciliation;
@@ -8850,6 +9064,7 @@ class MainActivity : AppCompatActivity() {
8850
9064
  screen.pressUnderlayHideHandlers.keys.removeAll(released)
8851
9065
  screen.pressUnderlayColors.keys.removeAll(released)
8852
9066
  screen.pressBaseBackgrounds.keys.removeAll(released)
9067
+ released.forEach { screen.contextMenus.remove(it)?.clear() }
8853
9068
  screen.longPressHandlers.keys.removeAll(released)
8854
9069
  screen.layoutHandlers.keys.removeAll(released)
8855
9070
  screen.layoutListeners.forEach { (view, listener) ->
package/dist/testing.d.ts DELETED
@@ -1,2 +0,0 @@
1
- /** Uses the Android builder's Kotlin version, including its transitive compiler dependencies. */
2
- export declare function runKotlinFixture(source: string): Promise<void>;