@onekeyfe/react-native-text-input 3.0.158 → 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.
@@ -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
@@ -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:) && UIPasteboard.generalPasteboard.hasImages) {
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:) && UIPasteboard.generalPasteboard.hasImages) {
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-text-input",
3
- "version": "3.0.158",
3
+ "version": "3.0.159",
4
4
  "description": "React Native TextInput with native paste events",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",