@onekeyfe/react-native-native-overlay 3.0.162-alpha.270 → 6.7.0-8
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/android/build.gradle +6 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolation.kt +37 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayHost.kt +27 -30
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayPageHostView.kt +21 -0
- package/android/src/main/java/com/onekey/nativeoverlay/NativeOverlayView.kt +7 -2
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolationTest.kt +112 -0
- package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayBackTest.kt +157 -0
- package/docs/SPEC.md +148 -26
- package/ios/NativeOverlayContainerView.swift +20 -7
- package/ios/NativeOverlaySheetController.swift +35 -1
- package/lib/module/OverlayPageHost.web.js +19 -6
- package/lib/module/OverlayPageHost.web.js.map +1 -1
- package/lib/module/OverlayStore.js +13 -1
- package/lib/module/OverlayStore.js.map +1 -1
- package/lib/module/OverlayView.web.js +107 -25
- package/lib/module/OverlayView.web.js.map +1 -1
- package/lib/module/useOverlayController.js +11 -3
- package/lib/module/useOverlayController.js.map +1 -1
- package/lib/module/web/animateTransition.js +20 -12
- package/lib/module/web/animateTransition.js.map +1 -1
- package/lib/module/web/overlayLayers.js +27 -9
- package/lib/module/web/overlayLayers.js.map +1 -1
- package/lib/module/web/useKeyboardShift.js +1 -1
- package/lib/module/web/useKeyboardShift.js.map +1 -1
- package/lib/module/web/useSheetDrag.js +22 -2
- package/lib/module/web/useSheetDrag.js.map +1 -1
- package/lib/typescript/src/OverlayPageHost.web.d.ts.map +1 -1
- package/lib/typescript/src/OverlayStore.d.ts +2 -0
- package/lib/typescript/src/OverlayStore.d.ts.map +1 -1
- package/lib/typescript/src/OverlayView.web.d.ts.map +1 -1
- package/lib/typescript/src/useOverlayController.d.ts.map +1 -1
- package/lib/typescript/src/web/animateTransition.d.ts +2 -2
- package/lib/typescript/src/web/animateTransition.d.ts.map +1 -1
- package/lib/typescript/src/web/overlayLayers.d.ts +3 -0
- package/lib/typescript/src/web/overlayLayers.d.ts.map +1 -1
- package/lib/typescript/src/web/useSheetDrag.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/OverlayPageHost.web.tsx +24 -9
- package/src/OverlayStore.ts +13 -1
- package/src/OverlayView.web.tsx +197 -56
- package/src/useOverlayController.ts +11 -4
- package/src/web/animateTransition.ts +30 -11
- package/src/web/overlayLayers.ts +48 -10
- package/src/web/useKeyboardShift.ts +1 -1
- package/src/web/useSheetDrag.ts +23 -2
package/android/build.gradle
CHANGED
|
@@ -60,6 +60,10 @@ android {
|
|
|
60
60
|
]
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
testOptions {
|
|
65
|
+
unitTests.includeAndroidResources = true
|
|
66
|
+
}
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
repositories {
|
|
@@ -73,6 +77,8 @@ dependencies {
|
|
|
73
77
|
implementation "com.facebook.react:react-android"
|
|
74
78
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
75
79
|
implementation "com.google.android.material:material:1.14.0-alpha05"
|
|
80
|
+
testImplementation "junit:junit:4.13.2"
|
|
81
|
+
testImplementation "org.robolectric:robolectric:4.17"
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
react {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package com.onekey.nativeoverlay
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
|
|
6
|
+
/** Restores the caller's flags when a blocking overlay no longer covers it. */
|
|
7
|
+
internal class NativeOverlayAccessibilityIsolation {
|
|
8
|
+
private val hidden = mutableMapOf<View, Int>()
|
|
9
|
+
|
|
10
|
+
fun update(
|
|
11
|
+
root: ViewGroup,
|
|
12
|
+
activeContainer: View?,
|
|
13
|
+
entriesBelow: List<View>,
|
|
14
|
+
globalHost: View,
|
|
15
|
+
) {
|
|
16
|
+
val below = mutableSetOf<View>()
|
|
17
|
+
if (activeContainer != null) {
|
|
18
|
+
var node: View = activeContainer
|
|
19
|
+
while (node !== root && node.parent is ViewGroup) {
|
|
20
|
+
val parent = node.parent as ViewGroup
|
|
21
|
+
for (index in 0 until parent.childCount) {
|
|
22
|
+
val sibling = parent.getChildAt(index)
|
|
23
|
+
if (sibling !== node && sibling !== globalHost) below.add(sibling)
|
|
24
|
+
}
|
|
25
|
+
node = parent
|
|
26
|
+
}
|
|
27
|
+
if (node === root) below.addAll(entriesBelow) else below.clear()
|
|
28
|
+
}
|
|
29
|
+
hidden.keys.filter { it !in below }.forEach { view ->
|
|
30
|
+
view.importantForAccessibility = hidden.remove(view) ?: View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
31
|
+
}
|
|
32
|
+
below.forEach { view ->
|
|
33
|
+
if (view !in hidden) hidden[view] = view.importantForAccessibility
|
|
34
|
+
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -2,13 +2,13 @@ package com.onekey.nativeoverlay
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.view.KeyEvent
|
|
5
|
-
import android.view.View
|
|
6
5
|
import android.view.ViewGroup
|
|
7
6
|
import android.view.Window
|
|
8
7
|
import android.widget.FrameLayout
|
|
9
8
|
import androidx.activity.ComponentActivity
|
|
10
9
|
import androidx.activity.OnBackPressedCallback
|
|
11
10
|
import java.util.WeakHashMap
|
|
11
|
+
import java.lang.ref.WeakReference
|
|
12
12
|
|
|
13
13
|
/** Mirrors `OVERLAY_LEVEL_ORDER`. */
|
|
14
14
|
internal enum class NativeOverlayLevel(val order: Int) {
|
|
@@ -49,12 +49,12 @@ internal class NativeOverlayHost private constructor(
|
|
|
49
49
|
)
|
|
50
50
|
private val backCallback = object : OnBackPressedCallback(false) {
|
|
51
51
|
override fun handleOnBackPressed() {
|
|
52
|
-
|
|
52
|
+
topBackEntry()?.let { entry ->
|
|
53
53
|
if (entry.dismissOnBackPress) entry.onRequestDismiss?.invoke("back")
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
private val
|
|
57
|
+
private val accessibilityIsolation = NativeOverlayAccessibilityIsolation()
|
|
58
58
|
|
|
59
59
|
init {
|
|
60
60
|
clipChildren = false
|
|
@@ -67,7 +67,7 @@ internal class NativeOverlayHost private constructor(
|
|
|
67
67
|
* predictive back, so the dispatcher alone never runs first.
|
|
68
68
|
*/
|
|
69
69
|
fun handleBackKey(event: KeyEvent): Boolean {
|
|
70
|
-
val entry =
|
|
70
|
+
val entry = topBackEntry() ?: return false
|
|
71
71
|
if (event.action == KeyEvent.ACTION_UP && !event.isCanceled && entry.dismissOnBackPress) {
|
|
72
72
|
entry.onRequestDismiss?.invoke("back")
|
|
73
73
|
}
|
|
@@ -109,14 +109,14 @@ internal class NativeOverlayHost private constructor(
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
fun onEntriesChanged() {
|
|
112
|
-
val
|
|
113
|
-
if (
|
|
112
|
+
val hasBackEntry = topBackEntry() != null
|
|
113
|
+
if (hasBackEntry && !backCallback.isEnabled) {
|
|
114
114
|
// The most recently added callback wins; re-add so ours runs before
|
|
115
115
|
// ReactActivity's and react-native-screens' callbacks.
|
|
116
116
|
backCallback.remove()
|
|
117
117
|
(activity as? ComponentActivity)?.onBackPressedDispatcher?.addCallback(backCallback)
|
|
118
118
|
}
|
|
119
|
-
backCallback.isEnabled =
|
|
119
|
+
backCallback.isEnabled = hasBackEntry
|
|
120
120
|
updateAccessibility()
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -134,41 +134,38 @@ internal class NativeOverlayHost private constructor(
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/** Global overlays win; otherwise the topmost visible page overlay. */
|
|
137
|
-
private fun
|
|
138
|
-
orderedEntries().lastOrNull { it.blocking && it.isShownForInput }
|
|
139
|
-
?:
|
|
140
|
-
.filter { it.isAttachedToWindow && it.isShown }
|
|
137
|
+
private fun topBackEntry(): NativeOverlayEntryRootView? =
|
|
138
|
+
orderedEntries().lastOrNull { (it.blocking || it.dismissOnBackPress) && it.isShownForInput }
|
|
139
|
+
?: shownPageHosts()
|
|
141
140
|
.flatMap { it.shownEntries() }
|
|
142
|
-
.lastOrNull { it.blocking }
|
|
141
|
+
.lastOrNull { it.blocking || it.dismissOnBackPress }
|
|
142
|
+
|
|
143
|
+
private fun shownPageHosts(): List<NativeOverlayPageHostView> =
|
|
144
|
+
NativeOverlayPageHostView.allHosts().filter {
|
|
145
|
+
it.isAttachedToWindow && it.isShown && it.rootView === activity.window.decorView
|
|
146
|
+
}
|
|
143
147
|
|
|
144
148
|
/** TalkBack only reads the topmost blocking overlay and what is above it. */
|
|
145
149
|
private fun updateAccessibility() {
|
|
146
150
|
val entries = orderedEntries()
|
|
147
151
|
val topIndex = entries.indexOfLast { it.blocking && it.isShownForInput }
|
|
148
|
-
val content =
|
|
149
|
-
val below = mutableListOf<View>()
|
|
152
|
+
val content = activity.findViewById<ViewGroup>(android.R.id.content) ?: return
|
|
150
153
|
if (topIndex >= 0) {
|
|
151
|
-
content
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
below.addAll(entries.subList(0, topIndex))
|
|
155
|
-
}
|
|
156
|
-
val restore = hiddenForAccessibility.keys.filter { it !in below }
|
|
157
|
-
restore.forEach { view ->
|
|
158
|
-
view.importantForAccessibility = hiddenForAccessibility.remove(view) ?: IMPORTANT_FOR_ACCESSIBILITY_AUTO
|
|
159
|
-
}
|
|
160
|
-
below.forEach { view ->
|
|
161
|
-
if (view !in hiddenForAccessibility) {
|
|
162
|
-
hiddenForAccessibility[view] = view.importantForAccessibility
|
|
163
|
-
view.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
164
|
-
}
|
|
154
|
+
accessibilityIsolation.update(content, this, entries.take(topIndex), this)
|
|
155
|
+
return
|
|
165
156
|
}
|
|
157
|
+
val pageEntry = shownPageHosts().flatMap { it.shownEntries() }.lastOrNull { it.blocking }
|
|
158
|
+
val pageHost = pageEntry?.parent as? NativeOverlayPageHostView
|
|
159
|
+
val below = pageHost?.shownEntries()?.takeWhile { it !== pageEntry }.orEmpty()
|
|
160
|
+
accessibilityIsolation.update(content, pageHost, below, this)
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
companion object {
|
|
169
|
-
private val hosts = WeakHashMap<Activity, NativeOverlayHost
|
|
164
|
+
private val hosts = WeakHashMap<Activity, WeakReference<NativeOverlayHost>>()
|
|
170
165
|
|
|
171
166
|
fun of(activity: Activity): NativeOverlayHost =
|
|
172
|
-
hosts
|
|
167
|
+
hosts[activity]?.get() ?: NativeOverlayHost(activity).also {
|
|
168
|
+
hosts[activity] = WeakReference(it)
|
|
169
|
+
}
|
|
173
170
|
}
|
|
174
171
|
}
|
|
@@ -3,6 +3,7 @@ package com.onekey.nativeoverlay
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.view.ViewGroup
|
|
6
|
+
import com.facebook.react.bridge.ReactContext
|
|
6
7
|
import com.facebook.react.uimanager.PointerEvents
|
|
7
8
|
import com.facebook.react.uimanager.ReactPointerEventsView
|
|
8
9
|
import java.lang.ref.WeakReference
|
|
@@ -33,6 +34,7 @@ class NativeOverlayPageHostView(context: Context) : ViewGroup(context), ReactPoi
|
|
|
33
34
|
if (owners == suspendedOwners) return
|
|
34
35
|
suspendedOwners = owners
|
|
35
36
|
entries().forEach(::applySuspension)
|
|
37
|
+
onEntriesChanged()
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
internal fun attach(entry: NativeOverlayEntryRootView) {
|
|
@@ -44,10 +46,12 @@ class NativeOverlayPageHostView(context: Context) : ViewGroup(context), ReactPoi
|
|
|
44
46
|
}
|
|
45
47
|
layoutEntries()
|
|
46
48
|
applySuspension(entry)
|
|
49
|
+
onEntriesChanged()
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
internal fun detach(entry: NativeOverlayEntryRootView) {
|
|
50
53
|
removeView(entry)
|
|
54
|
+
onEntriesChanged()
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/** Visible, input-ready entries in bottom-to-top order. */
|
|
@@ -56,6 +60,23 @@ class NativeOverlayPageHostView(context: Context) : ViewGroup(context), ReactPoi
|
|
|
56
60
|
|
|
57
61
|
fun invalidateHost() {
|
|
58
62
|
if (hostKey.isNotEmpty() && registry[hostKey]?.get() === this) registry.remove(hostKey)
|
|
63
|
+
onEntriesChanged()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onAttachedToWindow() {
|
|
67
|
+
super.onAttachedToWindow()
|
|
68
|
+
onEntriesChanged()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onDetachedFromWindow() {
|
|
72
|
+
super.onDetachedFromWindow()
|
|
73
|
+
onEntriesChanged()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun onEntriesChanged() {
|
|
77
|
+
(context as? ReactContext)?.currentActivity?.let {
|
|
78
|
+
NativeOverlayHost.of(it).onEntriesChanged()
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
private var layoutScheduled = false
|
|
@@ -135,6 +135,7 @@ class NativeOverlayView(
|
|
|
135
135
|
config = OverlayAnimationConfig.parse(animationConfig)
|
|
136
136
|
}
|
|
137
137
|
entry?.let(::applyEntryConfiguration)
|
|
138
|
+
notifyHostEntriesChanged()
|
|
138
139
|
if (visible) {
|
|
139
140
|
cycleOpen = true
|
|
140
141
|
if (phase == Phase.HIDDEN || phase == Phase.EXITING) present()
|
|
@@ -200,7 +201,7 @@ class NativeOverlayView(
|
|
|
200
201
|
cancelAnimators()
|
|
201
202
|
phase = Phase.ENTERING
|
|
202
203
|
target.isShownForInput = true
|
|
203
|
-
|
|
204
|
+
notifyHostEntriesChanged()
|
|
204
205
|
if (blocking) {
|
|
205
206
|
target.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
|
|
206
207
|
}
|
|
@@ -226,7 +227,7 @@ class NativeOverlayView(
|
|
|
226
227
|
cancelAnimators()
|
|
227
228
|
phase = Phase.EXITING
|
|
228
229
|
target.isShownForInput = false
|
|
229
|
-
|
|
230
|
+
notifyHostEntriesChanged()
|
|
230
231
|
val transition = config.exit.reduceMotionAdjusted(isReduceMotionEnabled())
|
|
231
232
|
if (transition.kind == "none") return finishDismiss("programmatic")
|
|
232
233
|
animate(target, hiddenState(target, transition), 0f, transition.motion) {
|
|
@@ -389,6 +390,10 @@ class NativeOverlayView(
|
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
|
|
393
|
+
private fun notifyHostEntriesChanged() {
|
|
394
|
+
reactContext.currentActivity?.let { NativeOverlayHost.of(it).onEntriesChanged() }
|
|
395
|
+
}
|
|
396
|
+
|
|
392
397
|
/**
|
|
393
398
|
* While hidden, keep the React child inside this invisible view. Fabric only
|
|
394
399
|
* reports onLayout for attached views, and fitted sheets size themselves
|
package/android/src/test/java/com/onekey/nativeoverlay/NativeOverlayAccessibilityIsolationTest.kt
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package com.onekey.nativeoverlay
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import android.widget.FrameLayout
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.runner.RunWith
|
|
8
|
+
import org.robolectric.RobolectricTestRunner
|
|
9
|
+
import org.robolectric.RuntimeEnvironment
|
|
10
|
+
import org.robolectric.annotation.Config
|
|
11
|
+
|
|
12
|
+
@RunWith(RobolectricTestRunner::class)
|
|
13
|
+
@Config(sdk = [34])
|
|
14
|
+
class NativeOverlayAccessibilityIsolationTest {
|
|
15
|
+
private val context get() = RuntimeEnvironment.getApplication()
|
|
16
|
+
|
|
17
|
+
private class Tree(context: android.content.Context) {
|
|
18
|
+
val root = FrameLayout(context)
|
|
19
|
+
val reactRoot = FrameLayout(context)
|
|
20
|
+
val card = FrameLayout(context)
|
|
21
|
+
val content = View(context).apply { importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES }
|
|
22
|
+
val chrome = View(context)
|
|
23
|
+
val pageHost = FrameLayout(context)
|
|
24
|
+
val lower = View(context)
|
|
25
|
+
val blocker = View(context)
|
|
26
|
+
val above = View(context)
|
|
27
|
+
val globalHost = FrameLayout(context)
|
|
28
|
+
|
|
29
|
+
init {
|
|
30
|
+
root.addView(reactRoot)
|
|
31
|
+
root.addView(globalHost)
|
|
32
|
+
reactRoot.addView(card)
|
|
33
|
+
reactRoot.addView(chrome)
|
|
34
|
+
card.addView(content)
|
|
35
|
+
card.addView(pageHost)
|
|
36
|
+
pageHost.addView(lower)
|
|
37
|
+
pageHost.addView(blocker)
|
|
38
|
+
pageHost.addView(above)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun pageBlockerHidesPageAndNavigationWithoutHidingTheHost() {
|
|
44
|
+
val tree = Tree(context)
|
|
45
|
+
val isolation = NativeOverlayAccessibilityIsolation()
|
|
46
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
47
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, tree.content.importantForAccessibility)
|
|
48
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, tree.chrome.importantForAccessibility)
|
|
49
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.pageHost.importantForAccessibility)
|
|
50
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.blocker.importantForAccessibility)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test
|
|
54
|
+
fun entriesAbovePageBlockerAndGlobalHostStayAccessible() {
|
|
55
|
+
val tree = Tree(context)
|
|
56
|
+
NativeOverlayAccessibilityIsolation().update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
57
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, tree.lower.importantForAccessibility)
|
|
58
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.above.importantForAccessibility)
|
|
59
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.globalHost.importantForAccessibility)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun dismissalOrSuspensionRestoresOriginalFlags() {
|
|
64
|
+
val tree = Tree(context)
|
|
65
|
+
tree.lower.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
66
|
+
val isolation = NativeOverlayAccessibilityIsolation()
|
|
67
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
68
|
+
isolation.update(tree.root, null, emptyList(), tree.globalHost)
|
|
69
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, tree.content.importantForAccessibility)
|
|
70
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO, tree.lower.importantForAccessibility)
|
|
71
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.chrome.importantForAccessibility)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
fun globalBlockerHidesReactRootThenPageIsolationReturns() {
|
|
76
|
+
val tree = Tree(context)
|
|
77
|
+
val globalLower = View(context)
|
|
78
|
+
tree.globalHost.addView(globalLower)
|
|
79
|
+
val isolation = NativeOverlayAccessibilityIsolation()
|
|
80
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
81
|
+
isolation.update(tree.root, tree.globalHost, listOf(globalLower), tree.globalHost)
|
|
82
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, tree.reactRoot.importantForAccessibility)
|
|
83
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, globalLower.importantForAccessibility)
|
|
84
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, tree.content.importantForAccessibility)
|
|
85
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
86
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.reactRoot.importantForAccessibility)
|
|
87
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, globalLower.importantForAccessibility)
|
|
88
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, tree.content.importantForAccessibility)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
fun detachedHostRestoresIsolationInsteadOfHidingAnotherTree() {
|
|
93
|
+
val tree = Tree(context)
|
|
94
|
+
val isolation = NativeOverlayAccessibilityIsolation()
|
|
95
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
96
|
+
tree.card.removeView(tree.pageHost)
|
|
97
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
98
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, tree.content.importantForAccessibility)
|
|
99
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.lower.importantForAccessibility)
|
|
100
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.chrome.importantForAccessibility)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Test
|
|
104
|
+
fun changingTheBlockingEntryRestoresEntriesNoLongerBelowIt() {
|
|
105
|
+
val tree = Tree(context)
|
|
106
|
+
val isolation = NativeOverlayAccessibilityIsolation()
|
|
107
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower, tree.blocker), tree.globalHost)
|
|
108
|
+
isolation.update(tree.root, tree.pageHost, listOf(tree.lower), tree.globalHost)
|
|
109
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, tree.blocker.importantForAccessibility)
|
|
110
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, tree.lower.importantForAccessibility)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
package com.onekey.nativeoverlay
|
|
2
|
+
|
|
3
|
+
import android.view.KeyEvent
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.widget.FrameLayout
|
|
6
|
+
import androidx.activity.ComponentActivity
|
|
7
|
+
import com.facebook.react.bridge.BridgeReactContext
|
|
8
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
|
+
import java.lang.ref.WeakReference
|
|
10
|
+
import java.util.WeakHashMap
|
|
11
|
+
import org.junit.Assert.assertEquals
|
|
12
|
+
import org.junit.Assert.assertFalse
|
|
13
|
+
import org.junit.Assert.assertNotSame
|
|
14
|
+
import org.junit.Assert.assertSame
|
|
15
|
+
import org.junit.Assert.assertTrue
|
|
16
|
+
import org.junit.Before
|
|
17
|
+
import org.junit.Test
|
|
18
|
+
import org.junit.runner.RunWith
|
|
19
|
+
import org.robolectric.Robolectric
|
|
20
|
+
import org.robolectric.RobolectricTestRunner
|
|
21
|
+
import org.robolectric.annotation.Config
|
|
22
|
+
|
|
23
|
+
@RunWith(RobolectricTestRunner::class)
|
|
24
|
+
@Config(sdk = [34])
|
|
25
|
+
class NativeOverlayBackTest {
|
|
26
|
+
private lateinit var activity: ComponentActivity
|
|
27
|
+
private lateinit var context: ThemedReactContext
|
|
28
|
+
private lateinit var host: NativeOverlayHost
|
|
29
|
+
private val dismissals = mutableListOf<String>()
|
|
30
|
+
|
|
31
|
+
@Before
|
|
32
|
+
fun setUp() {
|
|
33
|
+
activity = Robolectric.buildActivity(ComponentActivity::class.java).setup().visible().get()
|
|
34
|
+
val reactContext = BridgeReactContext(activity)
|
|
35
|
+
reactContext.onHostResume(activity)
|
|
36
|
+
context = ThemedReactContext(reactContext, activity, "OverlayBackTest", 1)
|
|
37
|
+
host = NativeOverlayHost.of(activity)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private fun entry(name: String, blocking: Boolean, dismissible: Boolean, order: Int = 0) =
|
|
41
|
+
NativeOverlayEntryRootView(context).apply {
|
|
42
|
+
this.blocking = blocking
|
|
43
|
+
dismissOnBackPress = dismissible
|
|
44
|
+
stackOrder = order
|
|
45
|
+
isShownForInput = true
|
|
46
|
+
onRequestDismiss = { dismissals.add("$name:$it") }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun back(action: Int = KeyEvent.ACTION_UP, flags: Int = 0) =
|
|
50
|
+
host.handleBackKey(KeyEvent(0L, 0L, action, KeyEvent.KEYCODE_BACK, 0, 0, 0, 0, flags))
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun dismissibleNonBlockingGlobalEntryWinsOverLowerBlocker() {
|
|
54
|
+
host.attach(entry("lower", true, true), NativeOverlayLevel.MODAL)
|
|
55
|
+
host.attach(entry("top", false, true), NativeOverlayLevel.TOAST)
|
|
56
|
+
assertTrue(back(KeyEvent.ACTION_DOWN))
|
|
57
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
58
|
+
assertTrue(back())
|
|
59
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun dispatcherHandlesNonBlockingEntryAndKeepsPageAccessible() {
|
|
64
|
+
val page = View(activity).apply { importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES }
|
|
65
|
+
activity.setContentView(page)
|
|
66
|
+
val top = entry("top", false, true)
|
|
67
|
+
host.attach(top, NativeOverlayLevel.MODAL)
|
|
68
|
+
activity.onBackPressedDispatcher.onBackPressed()
|
|
69
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
70
|
+
assertEquals(View.IMPORTANT_FOR_ACCESSIBILITY_YES, page.importantForAccessibility)
|
|
71
|
+
host.detach(top)
|
|
72
|
+
assertFalse(back())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun nonDismissibleBlockerConsumesBackWithoutClosingLowerEntry() {
|
|
77
|
+
host.attach(entry("lower", false, true), NativeOverlayLevel.MODAL)
|
|
78
|
+
host.attach(entry("lock", true, false), NativeOverlayLevel.LOCK)
|
|
79
|
+
assertTrue(back())
|
|
80
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
fun passiveOrNotInputReadyEntriesDoNotTakeBack() {
|
|
85
|
+
host.attach(entry("passive", false, false), NativeOverlayLevel.TOAST)
|
|
86
|
+
host.attach(entry("hidden", true, true).apply { isShownForInput = false }, NativeOverlayLevel.LOCK)
|
|
87
|
+
assertFalse(back())
|
|
88
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
fun canceledKeyUpDoesNotDismiss() {
|
|
93
|
+
host.attach(entry("top", false, true), NativeOverlayLevel.MODAL)
|
|
94
|
+
assertTrue(back(flags = KeyEvent.FLAG_CANCELED))
|
|
95
|
+
assertEquals(emptyList<String>(), dismissals)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun dismissiblePageEntryWinsOverLowerPageBlockerAndSuspensionSkipsIt() {
|
|
100
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-back-test" }
|
|
101
|
+
activity.setContentView(pageHost)
|
|
102
|
+
val lower = entry("lower", true, true).apply { ownerKey = "owner" }
|
|
103
|
+
val top = entry("top", false, true, 1).apply { ownerKey = "owner" }
|
|
104
|
+
pageHost.attach(lower)
|
|
105
|
+
pageHost.attach(top)
|
|
106
|
+
assertTrue(back())
|
|
107
|
+
assertEquals(listOf("top:back"), dismissals)
|
|
108
|
+
pageHost.setSuspendedOwners("owner")
|
|
109
|
+
assertFalse(back())
|
|
110
|
+
pageHost.invalidateHost()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun globalEntryTakesBackBeforePageEntryAndDismissalRestoresPageTarget() {
|
|
115
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-global-test" }
|
|
116
|
+
activity.setContentView(FrameLayout(activity).apply { addView(pageHost) })
|
|
117
|
+
pageHost.attach(entry("page", false, true))
|
|
118
|
+
val global = entry("global", false, true)
|
|
119
|
+
host.attach(global, NativeOverlayLevel.MODAL)
|
|
120
|
+
assertTrue(back())
|
|
121
|
+
host.detach(global)
|
|
122
|
+
assertTrue(back())
|
|
123
|
+
assertEquals(listOf("global:back", "page:back"), dismissals)
|
|
124
|
+
pageHost.invalidateHost()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun hostCacheReusesLiveHostAndSeparatesActivities() {
|
|
129
|
+
assertSame(host, NativeOverlayHost.of(activity))
|
|
130
|
+
val otherActivity = Robolectric.buildActivity(ComponentActivity::class.java).setup().get()
|
|
131
|
+
assertNotSame(host, NativeOverlayHost.of(otherActivity))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Test
|
|
135
|
+
fun hostCacheRebuildsAfterWeakValueIsCleared() {
|
|
136
|
+
val field = NativeOverlayHost::class.java.getDeclaredField("hosts").apply {
|
|
137
|
+
isAccessible = true
|
|
138
|
+
}
|
|
139
|
+
val cache = field.get(null) as WeakHashMap<*, *>
|
|
140
|
+
(cache[activity] as WeakReference<*>).clear()
|
|
141
|
+
val replacement = NativeOverlayHost.of(activity)
|
|
142
|
+
assertNotSame(host, replacement)
|
|
143
|
+
assertSame(replacement, NativeOverlayHost.of(activity))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
fun pageOnlyBackInterceptorRetainsItsHost() {
|
|
148
|
+
val pageHost = NativeOverlayPageHostView(context).apply { hostKey = "page-only" }
|
|
149
|
+
activity.setContentView(pageHost)
|
|
150
|
+
pageHost.attach(entry("page", false, true))
|
|
151
|
+
host.installBackInterceptor()
|
|
152
|
+
assertTrue(activity.window.callback.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)))
|
|
153
|
+
assertEquals(listOf("page:back"), dismissals)
|
|
154
|
+
assertSame(host, NativeOverlayHost.of(activity))
|
|
155
|
+
pageHost.invalidateHost()
|
|
156
|
+
}
|
|
157
|
+
}
|