@onekeyfe/react-native-text-input 3.0.157 → 3.0.159
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/OneKeyTextInput.podspec
CHANGED
|
@@ -12,7 +12,12 @@ Pod::Spec.new do |s|
|
|
|
12
12
|
s.platforms = { :ios => "15.5" }
|
|
13
13
|
s.source = { :git => "https://github.com/OneKeyHQ/app-modules.git", :tag => "#{s.version}" }
|
|
14
14
|
s.source_files = "ios/**/*.{h,m,mm}"
|
|
15
|
+
s.exclude_files = "ios/tests/**/*"
|
|
15
16
|
s.frameworks = "UIKit", "UniformTypeIdentifiers"
|
|
16
17
|
|
|
17
18
|
s.dependency "React-Core"
|
|
19
|
+
|
|
20
|
+
s.test_spec "Tests" do |test_spec|
|
|
21
|
+
test_spec.source_files = "ios/tests/**/*.{m,mm}"
|
|
22
|
+
end
|
|
18
23
|
end
|
package/android/build.gradle
CHANGED
|
@@ -100,6 +100,10 @@ android {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
testOptions {
|
|
105
|
+
unitTests.includeAndroidResources = true
|
|
106
|
+
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
repositories {
|
|
@@ -115,6 +119,8 @@ dependencies {
|
|
|
115
119
|
//noinspection GradleDynamicVersion
|
|
116
120
|
implementation "com.facebook.react:react-android"
|
|
117
121
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
122
|
+
testImplementation "junit:junit:4.13.2"
|
|
123
|
+
testImplementation "org.robolectric:robolectric:4.17"
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
if (isNewArchitectureEnabled()) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package com.textinput
|
|
2
|
+
|
|
3
|
+
import android.R
|
|
4
|
+
import android.content.ClipData
|
|
5
|
+
import android.content.ClipDescription
|
|
6
|
+
import android.content.ClipboardManager
|
|
7
|
+
import android.content.ContentProvider
|
|
8
|
+
import android.content.ContentValues
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.database.Cursor
|
|
11
|
+
import android.net.Uri
|
|
12
|
+
import com.facebook.react.bridge.BridgeReactContext
|
|
13
|
+
import com.facebook.react.bridge.NativeModule
|
|
14
|
+
import com.facebook.react.uimanager.DisplayMetricsHolder
|
|
15
|
+
import org.junit.Assert.assertEquals
|
|
16
|
+
import org.junit.Assert.assertTrue
|
|
17
|
+
import org.junit.Test
|
|
18
|
+
import org.junit.runner.RunWith
|
|
19
|
+
import org.robolectric.RobolectricTestRunner
|
|
20
|
+
import org.robolectric.RuntimeEnvironment
|
|
21
|
+
import org.robolectric.annotation.Config
|
|
22
|
+
import org.robolectric.shadows.ShadowContentResolver
|
|
23
|
+
import so.onekey.app.wallet.pasteinput.PasteWatcher
|
|
24
|
+
|
|
25
|
+
@RunWith(RobolectricTestRunner::class)
|
|
26
|
+
@Config(sdk = [34])
|
|
27
|
+
class TextInputViewPasteTest {
|
|
28
|
+
@Test
|
|
29
|
+
fun textPasteReportsAndRunsNativePasteForBothMenuActions() {
|
|
30
|
+
for (action in listOf(R.id.paste, R.id.pasteAsPlainText)) {
|
|
31
|
+
val context = context()
|
|
32
|
+
clipboard(context).setPrimaryClip(ClipData.newPlainText("text", "copied"))
|
|
33
|
+
val events = mutableListOf<Pair<String, String>>()
|
|
34
|
+
val view = view(context, events)
|
|
35
|
+
|
|
36
|
+
assertTrue(view.onTextContextMenuItem(action))
|
|
37
|
+
assertEquals(listOf(ClipDescription.MIMETYPE_TEXT_PLAIN to "copied"), events)
|
|
38
|
+
assertEquals("copied", view.text.toString())
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun uriPasteReportsResolvedMime() {
|
|
44
|
+
val context = context()
|
|
45
|
+
val uri = Uri.parse("content://text-input-image/item")
|
|
46
|
+
ShadowContentResolver.registerProviderInternal("text-input-image", MimeProvider("image/png"))
|
|
47
|
+
clipboard(context).setPrimaryClip(uriClip(uri))
|
|
48
|
+
val events = mutableListOf<Pair<String, String>>()
|
|
49
|
+
val view = view(context, events)
|
|
50
|
+
|
|
51
|
+
assertTrue(view.onTextContextMenuItem(R.id.paste))
|
|
52
|
+
assertEquals(listOf("image/png" to uri.toString()), events)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun uriWithoutResolvedMimeDoesNotReport() {
|
|
57
|
+
val context = context()
|
|
58
|
+
val uri = Uri.parse("content://text-input-no-mime/item")
|
|
59
|
+
ShadowContentResolver.registerProviderInternal("text-input-no-mime", MimeProvider(null))
|
|
60
|
+
clipboard(context).setPrimaryClip(uriClip(uri))
|
|
61
|
+
val events = mutableListOf<Pair<String, String>>()
|
|
62
|
+
val view = view(context, events)
|
|
63
|
+
|
|
64
|
+
assertTrue(view.onTextContextMenuItem(R.id.paste))
|
|
65
|
+
assertTrue(events.isEmpty())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun removedWatcherDoesNotReportButStillRunsNativePaste() {
|
|
70
|
+
val context = context()
|
|
71
|
+
clipboard(context).setPrimaryClip(ClipData.newPlainText("text", "copied"))
|
|
72
|
+
val events = mutableListOf<Pair<String, String>>()
|
|
73
|
+
val view = view(context, events)
|
|
74
|
+
view.setPasteWatcher(null)
|
|
75
|
+
|
|
76
|
+
assertTrue(view.onTextContextMenuItem(R.id.paste))
|
|
77
|
+
assertTrue(events.isEmpty())
|
|
78
|
+
assertEquals("copied", view.text.toString())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun context(): TestReactContext {
|
|
82
|
+
val application = RuntimeEnvironment.getApplication()
|
|
83
|
+
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(application)
|
|
84
|
+
return TestReactContext(application)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun clipboard(context: Context) =
|
|
88
|
+
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
89
|
+
|
|
90
|
+
private fun view(context: Context, events: MutableList<Pair<String, String>>) =
|
|
91
|
+
TextInputView(context).apply {
|
|
92
|
+
setPasteWatcher(object : PasteWatcher {
|
|
93
|
+
override fun onPaste(type: String, data: String) {
|
|
94
|
+
events += type to data
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun uriClip(uri: Uri) =
|
|
100
|
+
ClipData(ClipDescription("uri", arrayOf("text/uri-list")), ClipData.Item(uri))
|
|
101
|
+
|
|
102
|
+
private class TestReactContext(base: Context) : BridgeReactContext(base) {
|
|
103
|
+
override fun <T : NativeModule?> getNativeModule(nativeModuleInterface: Class<T>): T? = null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private class MimeProvider(private val mime: String?) : ContentProvider() {
|
|
107
|
+
override fun onCreate() = true
|
|
108
|
+
override fun getType(uri: Uri): String? = mime
|
|
109
|
+
override fun query(uri: Uri, projection: Array<out String>?, selection: String?,
|
|
110
|
+
selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
|
111
|
+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
|
112
|
+
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
|
113
|
+
override fun update(uri: Uri, values: ContentValues?, selection: String?,
|
|
114
|
+
selectionArgs: Array<out String>?): Int = 0
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -7,11 +7,80 @@
|
|
|
7
7
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
8
8
|
#import <objc/runtime.h>
|
|
9
9
|
|
|
10
|
+
#include <atomic>
|
|
11
|
+
|
|
10
12
|
static NSString *const OneKeyTextInputPasteEvent = @"OneKeyTextInputPaste";
|
|
11
13
|
static __weak OneKeyTextInputPasteObserver *OneKeyPasteObserver = nil;
|
|
12
14
|
static BOOL OneKeyPasteObserverHasListeners = NO;
|
|
13
15
|
static const void *OneKeyPasteInFlightKey = &OneKeyPasteInFlightKey;
|
|
14
16
|
|
|
17
|
+
// canPerformAction: runs on the main thread, and iOS 27 also calls it from
|
|
18
|
+
// system command validation without any user action. Reading UIPasteboard
|
|
19
|
+
// there is a synchronous XPC call that can block until the watchdog kills the
|
|
20
|
+
// app, so the image check reads this cache and refreshes it off the main thread.
|
|
21
|
+
enum class OneKeyPasteboardImageState { Unknown, NoImages, HasImages };
|
|
22
|
+
enum class OneKeyPasteboardRefreshState { Idle, Scheduled, Dirty };
|
|
23
|
+
static std::atomic<OneKeyPasteboardImageState> OneKeyPasteboardImageCache{OneKeyPasteboardImageState::Unknown};
|
|
24
|
+
static std::atomic<OneKeyPasteboardRefreshState> OneKeyPasteboardRefresh{OneKeyPasteboardRefreshState::Idle};
|
|
25
|
+
|
|
26
|
+
static void OneKeyRefreshPasteboardImageState(void)
|
|
27
|
+
{
|
|
28
|
+
for (;;) {
|
|
29
|
+
OneKeyPasteboardRefreshState state = OneKeyPasteboardRefresh.load();
|
|
30
|
+
if (state == OneKeyPasteboardRefreshState::Dirty) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (state == OneKeyPasteboardRefreshState::Scheduled) {
|
|
34
|
+
if (OneKeyPasteboardRefresh.compare_exchange_weak(state, OneKeyPasteboardRefreshState::Dirty)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
} else if (OneKeyPasteboardRefresh.compare_exchange_weak(state, OneKeyPasteboardRefreshState::Scheduled)) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
static dispatch_queue_t queue;
|
|
42
|
+
static dispatch_once_t onceToken;
|
|
43
|
+
dispatch_once(&onceToken, ^{
|
|
44
|
+
queue = dispatch_queue_create("so.onekey.textinput.pasteboard", DISPATCH_QUEUE_SERIAL);
|
|
45
|
+
});
|
|
46
|
+
dispatch_async(queue, ^{
|
|
47
|
+
for (;;) {
|
|
48
|
+
BOOL hasImages = UIPasteboard.generalPasteboard.hasImages;
|
|
49
|
+
OneKeyPasteboardImageCache.store(hasImages ? OneKeyPasteboardImageState::HasImages
|
|
50
|
+
: OneKeyPasteboardImageState::NoImages);
|
|
51
|
+
OneKeyPasteboardRefreshState expected = OneKeyPasteboardRefreshState::Scheduled;
|
|
52
|
+
if (OneKeyPasteboardRefresh.compare_exchange_strong(expected, OneKeyPasteboardRefreshState::Idle)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// A notification arrived during the read. Keep Paste available until
|
|
56
|
+
// another read observes the latest clipboard state.
|
|
57
|
+
OneKeyPasteboardRefresh.store(OneKeyPasteboardRefreshState::Scheduled);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static void OneKeyStartObservingPasteboard(void)
|
|
63
|
+
{
|
|
64
|
+
static dispatch_once_t onceToken;
|
|
65
|
+
dispatch_once(&onceToken, ^{
|
|
66
|
+
NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
|
|
67
|
+
for (NSNotificationName name in @[
|
|
68
|
+
UIPasteboardChangedNotification,
|
|
69
|
+
UIApplicationDidBecomeActiveNotification,
|
|
70
|
+
UITextFieldTextDidBeginEditingNotification,
|
|
71
|
+
UITextViewTextDidBeginEditingNotification,
|
|
72
|
+
]) {
|
|
73
|
+
[center addObserverForName:name
|
|
74
|
+
object:nil
|
|
75
|
+
queue:nil
|
|
76
|
+
usingBlock:^(__unused NSNotification *note) {
|
|
77
|
+
OneKeyRefreshPasteboardImageState();
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
OneKeyRefreshPasteboardImageState();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
15
84
|
@implementation OneKeyTextInputPasteObserver
|
|
16
85
|
|
|
17
86
|
RCT_EXPORT_MODULE(OneKeyTextInputPasteObserver)
|
|
@@ -145,6 +214,7 @@ static void OneKeySwizzle(Class cls, SEL originalSelector, SEL replacementSelect
|
|
|
145
214
|
dispatch_once(&onceToken, ^{
|
|
146
215
|
OneKeySwizzle(self, @selector(paste:), @selector(onekey_paste:));
|
|
147
216
|
OneKeySwizzle(self, @selector(canPerformAction:withSender:), @selector(onekey_canPerformAction:withSender:));
|
|
217
|
+
OneKeyStartObservingPasteboard();
|
|
148
218
|
});
|
|
149
219
|
}
|
|
150
220
|
|
|
@@ -162,7 +232,9 @@ static void OneKeySwizzle(Class cls, SEL originalSelector, SEL replacementSelect
|
|
|
162
232
|
|
|
163
233
|
- (BOOL)onekey_canPerformAction:(SEL)action withSender:(id)sender
|
|
164
234
|
{
|
|
165
|
-
if (action == @selector(paste:) &&
|
|
235
|
+
if (action == @selector(paste:) &&
|
|
236
|
+
(OneKeyPasteboardRefresh.load() != OneKeyPasteboardRefreshState::Idle ||
|
|
237
|
+
OneKeyPasteboardImageCache.load() != OneKeyPasteboardImageState::NoImages)) {
|
|
166
238
|
return YES;
|
|
167
239
|
}
|
|
168
240
|
return [self onekey_canPerformAction:action withSender:sender];
|
|
@@ -178,6 +250,7 @@ static void OneKeySwizzle(Class cls, SEL originalSelector, SEL replacementSelect
|
|
|
178
250
|
dispatch_once(&onceToken, ^{
|
|
179
251
|
OneKeySwizzle(self, @selector(paste:), @selector(onekey_paste:));
|
|
180
252
|
OneKeySwizzle(self, @selector(canPerformAction:withSender:), @selector(onekey_canPerformAction:withSender:));
|
|
253
|
+
OneKeyStartObservingPasteboard();
|
|
181
254
|
});
|
|
182
255
|
}
|
|
183
256
|
|
|
@@ -195,7 +268,9 @@ static void OneKeySwizzle(Class cls, SEL originalSelector, SEL replacementSelect
|
|
|
195
268
|
|
|
196
269
|
- (BOOL)onekey_canPerformAction:(SEL)action withSender:(id)sender
|
|
197
270
|
{
|
|
198
|
-
if (action == @selector(paste:) &&
|
|
271
|
+
if (action == @selector(paste:) &&
|
|
272
|
+
(OneKeyPasteboardRefresh.load() != OneKeyPasteboardRefreshState::Idle ||
|
|
273
|
+
OneKeyPasteboardImageCache.load() != OneKeyPasteboardImageState::NoImages)) {
|
|
199
274
|
return YES;
|
|
200
275
|
}
|
|
201
276
|
return [self onekey_canPerformAction:action withSender:sender];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#import <React/RCTUITextField.h>
|
|
2
|
+
#import <XCTest/XCTest.h>
|
|
3
|
+
#import <objc/runtime.h>
|
|
4
|
+
|
|
5
|
+
static dispatch_semaphore_t OneKeyTestReadStarted;
|
|
6
|
+
static dispatch_semaphore_t OneKeyTestReleaseRead;
|
|
7
|
+
|
|
8
|
+
@interface UIPasteboard (OneKeyPasteCacheTests)
|
|
9
|
+
- (BOOL)onekey_test_hasImages;
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation UIPasteboard (OneKeyPasteCacheTests)
|
|
13
|
+
|
|
14
|
+
- (BOOL)onekey_test_hasImages
|
|
15
|
+
{
|
|
16
|
+
dispatch_semaphore_t releaseRead;
|
|
17
|
+
dispatch_semaphore_t readStarted;
|
|
18
|
+
@synchronized (UIPasteboard.class) {
|
|
19
|
+
releaseRead = OneKeyTestReleaseRead;
|
|
20
|
+
readStarted = OneKeyTestReadStarted;
|
|
21
|
+
}
|
|
22
|
+
if (!NSThread.isMainThread && releaseRead != nil) {
|
|
23
|
+
dispatch_semaphore_signal(readStarted);
|
|
24
|
+
dispatch_semaphore_wait(releaseRead, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
|
25
|
+
}
|
|
26
|
+
return [self onekey_test_hasImages];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@end
|
|
30
|
+
|
|
31
|
+
@interface OneKeyTextInputPasteObserverTests : XCTestCase
|
|
32
|
+
@end
|
|
33
|
+
|
|
34
|
+
@implementation OneKeyTextInputPasteObserverTests
|
|
35
|
+
|
|
36
|
+
- (void)assertImagePasteAvailableWhileRefreshingAfterNotification:(NSNotificationName)name
|
|
37
|
+
{
|
|
38
|
+
UIPasteboard *pasteboard = UIPasteboard.generalPasteboard;
|
|
39
|
+
NSArray<NSDictionary<NSString *, id> *> *previousItems = pasteboard.items;
|
|
40
|
+
RCTUITextField *field = [[RCTUITextField alloc] initWithFrame:CGRectZero];
|
|
41
|
+
SEL paste = @selector(paste:);
|
|
42
|
+
|
|
43
|
+
@try {
|
|
44
|
+
pasteboard.items = @[];
|
|
45
|
+
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:5];
|
|
46
|
+
while ([field canPerformAction:paste withSender:nil] && [deadline timeIntervalSinceNow] > 0) {
|
|
47
|
+
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
|
|
48
|
+
}
|
|
49
|
+
XCTAssertFalse([field canPerformAction:paste withSender:nil]);
|
|
50
|
+
|
|
51
|
+
@synchronized (UIPasteboard.class) {
|
|
52
|
+
OneKeyTestReadStarted = dispatch_semaphore_create(0);
|
|
53
|
+
OneKeyTestReleaseRead = dispatch_semaphore_create(0);
|
|
54
|
+
}
|
|
55
|
+
static dispatch_once_t onceToken;
|
|
56
|
+
dispatch_once(&onceToken, ^{
|
|
57
|
+
Method original = class_getInstanceMethod(object_getClass(pasteboard), @selector(hasImages));
|
|
58
|
+
Method replacement = class_getInstanceMethod(UIPasteboard.class, @selector(onekey_test_hasImages));
|
|
59
|
+
method_exchangeImplementations(original, replacement);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(1, 1)];
|
|
63
|
+
pasteboard.image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
|
|
64
|
+
[UIColor.blackColor setFill];
|
|
65
|
+
UIRectFill(CGRectMake(0, 0, 1, 1));
|
|
66
|
+
}];
|
|
67
|
+
[NSNotificationCenter.defaultCenter postNotificationName:UIPasteboardChangedNotification object:pasteboard];
|
|
68
|
+
XCTAssertEqual(dispatch_semaphore_wait(OneKeyTestReadStarted, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)), 0L);
|
|
69
|
+
|
|
70
|
+
[NSNotificationCenter.defaultCenter postNotificationName:name object:field];
|
|
71
|
+
XCTAssertTrue([field canPerformAction:paste withSender:nil]);
|
|
72
|
+
} @finally {
|
|
73
|
+
dispatch_semaphore_t releaseRead;
|
|
74
|
+
@synchronized (UIPasteboard.class) {
|
|
75
|
+
releaseRead = OneKeyTestReleaseRead;
|
|
76
|
+
OneKeyTestReleaseRead = nil;
|
|
77
|
+
OneKeyTestReadStarted = nil;
|
|
78
|
+
}
|
|
79
|
+
if (releaseRead != nil) {
|
|
80
|
+
dispatch_semaphore_signal(releaseRead);
|
|
81
|
+
}
|
|
82
|
+
pasteboard.items = previousItems;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
- (void)testFirstFocusKeepsImagePasteAvailableDuringRefresh
|
|
87
|
+
{
|
|
88
|
+
[self assertImagePasteAvailableWhileRefreshingAfterNotification:UITextFieldTextDidBeginEditingNotification];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
- (void)testForegroundResumeKeepsImagePasteAvailableDuringRefresh
|
|
92
|
+
{
|
|
93
|
+
[self assertImagePasteAvailableWhileRefreshingAfterNotification:UIApplicationDidBecomeActiveNotification];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@end
|