react-native-s-pen 0.1.0
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/README.md +172 -0
- package/android/build.gradle +33 -0
- package/android/consumer-rules.pro +2 -0
- package/android/gradle.properties +3 -0
- package/android/libs/README.md +10 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/samsungspen/NativeSpenModule.java +910 -0
- package/android/src/main/java/com/samsungspen/SpenCanvasManager.java +67 -0
- package/android/src/main/java/com/samsungspen/SpenCanvasView.java +186 -0
- package/android/src/main/java/com/samsungspen/SpenPackage.java +57 -0
- package/package.json +46 -0
- package/react-native.config.js +9 -0
- package/src/SPen.ts +179 -0
- package/src/SpenCanvas.tsx +47 -0
- package/src/index.ts +15 -0
- package/src/native.ts +29 -0
- package/src/specs/NativeSpenModule.ts +50 -0
- package/src/specs/SpenCanvasNativeComponent.ts +23 -0
- package/src/types.ts +79 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
package com.samsungspen;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.content.BroadcastReceiver;
|
|
5
|
+
import android.content.Context;
|
|
6
|
+
import android.content.Intent;
|
|
7
|
+
import android.content.IntentFilter;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.util.Log;
|
|
10
|
+
import android.view.MotionEvent;
|
|
11
|
+
import android.view.View;
|
|
12
|
+
import android.view.Window;
|
|
13
|
+
|
|
14
|
+
import androidx.annotation.NonNull;
|
|
15
|
+
import androidx.annotation.Nullable;
|
|
16
|
+
|
|
17
|
+
import com.facebook.react.bridge.Arguments;
|
|
18
|
+
import com.facebook.react.bridge.Promise;
|
|
19
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
20
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
21
|
+
import com.facebook.react.bridge.WritableArray;
|
|
22
|
+
import com.facebook.react.bridge.WritableMap;
|
|
23
|
+
import com.facebook.react.bridge.LifecycleEventListener;
|
|
24
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
25
|
+
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
|
|
26
|
+
import com.samsung.android.sdk.penremote.SpenEvent;
|
|
27
|
+
import com.samsung.android.sdk.penremote.SpenEventListener;
|
|
28
|
+
import com.samsung.android.sdk.penremote.SpenUnit;
|
|
29
|
+
import com.samsung.android.sdk.penremote.SpenUnitManager;
|
|
30
|
+
|
|
31
|
+
import java.lang.reflect.Constructor;
|
|
32
|
+
import java.lang.reflect.InvocationHandler;
|
|
33
|
+
import java.lang.reflect.Method;
|
|
34
|
+
import java.lang.reflect.Proxy;
|
|
35
|
+
import java.util.Locale;
|
|
36
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
37
|
+
|
|
38
|
+
public class NativeSpenModule extends com.facebook.react.bridge.ReactContextBaseJavaModule implements TurboModule, LifecycleEventListener {
|
|
39
|
+
private static final String TAG = "SpenModule";
|
|
40
|
+
private static final String EVENT_NAME = "SpenEvent";
|
|
41
|
+
private static final String SPEN_INSERT_ACTION = "com.samsung.pen.INSERT";
|
|
42
|
+
|
|
43
|
+
private final ReactApplicationContext reactContext;
|
|
44
|
+
private final AtomicBoolean listening = new AtomicBoolean(false);
|
|
45
|
+
|
|
46
|
+
@Nullable private View attachedRootView;
|
|
47
|
+
@Nullable private Object spenRemote;
|
|
48
|
+
@Nullable private Object spenUnitManager;
|
|
49
|
+
@Nullable private Object spenConnectionCallback;
|
|
50
|
+
@Nullable private Object spenConnectionStateListener;
|
|
51
|
+
@Nullable private Object buttonEventListener;
|
|
52
|
+
@Nullable private Object airMotionEventListener;
|
|
53
|
+
@Nullable private Object buttonUnit;
|
|
54
|
+
@Nullable private Object airMotionUnit;
|
|
55
|
+
@Nullable private String lastConnectionState;
|
|
56
|
+
@Nullable private String lastPenInsertionState;
|
|
57
|
+
private boolean insertionReceiverRegistered = false;
|
|
58
|
+
private final AtomicBoolean connecting = new AtomicBoolean(false);
|
|
59
|
+
private final BroadcastReceiver insertionReceiver =
|
|
60
|
+
new BroadcastReceiver() {
|
|
61
|
+
@Override
|
|
62
|
+
public void onReceive(Context context, Intent intent) {
|
|
63
|
+
updatePenInsertionState(intent, true);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
public NativeSpenModule(ReactApplicationContext reactContext) {
|
|
68
|
+
super(reactContext);
|
|
69
|
+
this.reactContext = reactContext;
|
|
70
|
+
reactContext.addLifecycleEventListener(this);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@NonNull
|
|
74
|
+
@Override
|
|
75
|
+
public String getName() {
|
|
76
|
+
return "SpenModule";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@ReactMethod
|
|
80
|
+
public void isSupported(Promise promise) {
|
|
81
|
+
promise.resolve(Boolean.valueOf(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@ReactMethod
|
|
85
|
+
public void getDeviceInfo(Promise promise) {
|
|
86
|
+
WritableMap info = Arguments.createMap();
|
|
87
|
+
info.putString("platform", "android");
|
|
88
|
+
info.putString("manufacturer", Build.MANUFACTURER == null ? "" : Build.MANUFACTURER);
|
|
89
|
+
info.putString("brand", Build.BRAND == null ? "" : Build.BRAND);
|
|
90
|
+
info.putString("model", Build.MODEL == null ? "" : Build.MODEL);
|
|
91
|
+
info.putInt("sdkInt", Build.VERSION.SDK_INT);
|
|
92
|
+
info.putBoolean("isSamsungDevice", isSamsungDevice());
|
|
93
|
+
info.putArray("capabilities", buildCapabilities());
|
|
94
|
+
info.putMap("samsungSdk", buildSamsungSdkInfo());
|
|
95
|
+
promise.resolve(info);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@ReactMethod
|
|
99
|
+
public void getVersionCode(Promise promise) {
|
|
100
|
+
promise.resolve(Double.valueOf(getSamsungRemoteInteger("getVersionCode", 0)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@ReactMethod
|
|
104
|
+
public void getVersionName(Promise promise) {
|
|
105
|
+
promise.resolve(getSamsungRemoteString("getVersionName", ""));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@ReactMethod
|
|
109
|
+
public void getPenInsertionState(Promise promise) {
|
|
110
|
+
promise.resolve(readPenInsertionState());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@ReactMethod
|
|
114
|
+
public void isConnected(Promise promise) {
|
|
115
|
+
promise.resolve(Boolean.valueOf(isSamsungRemoteConnected()));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@ReactMethod
|
|
119
|
+
public void isFeatureEnabled(String feature, Promise promise) {
|
|
120
|
+
int featureType = mapFeatureNameToType(feature);
|
|
121
|
+
promise.resolve(Boolean.valueOf(getSamsungRemoteBoolean("isFeatureEnabled", false, new Class<?>[] {int.class}, new Object[] {featureType})));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@ReactMethod
|
|
125
|
+
public void startListening(Promise promise) {
|
|
126
|
+
listening.set(true);
|
|
127
|
+
attachRootViewListeners();
|
|
128
|
+
registerPenInsertionReceiver();
|
|
129
|
+
connectSamsungRemoteIfAvailable();
|
|
130
|
+
promise.resolve(null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@ReactMethod
|
|
134
|
+
public void stopListening(Promise promise) {
|
|
135
|
+
listening.set(false);
|
|
136
|
+
disconnectSamsungRemote();
|
|
137
|
+
unregisterPenInsertionReceiver();
|
|
138
|
+
detachRootViewListeners();
|
|
139
|
+
promise.resolve(null);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@ReactMethod
|
|
143
|
+
public void addListener(String eventName) {
|
|
144
|
+
// Required by React Native's event emitter contract.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@ReactMethod
|
|
148
|
+
public void removeListeners(double count) {
|
|
149
|
+
// Required by React Native's event emitter contract.
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@ReactMethod
|
|
153
|
+
public void addConnectionStateListener(String eventName) {
|
|
154
|
+
// Required by React Native's event emitter contract.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@ReactMethod
|
|
158
|
+
public void removeConnectionStateListeners(double count) {
|
|
159
|
+
// Required by React Native's event emitter contract.
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Override
|
|
163
|
+
public void onHostResume() {
|
|
164
|
+
if (listening.get()) {
|
|
165
|
+
attachRootViewListeners();
|
|
166
|
+
registerPenInsertionReceiver();
|
|
167
|
+
connectSamsungRemoteIfAvailable();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Override
|
|
172
|
+
public void onHostPause() {
|
|
173
|
+
detachRootViewListeners();
|
|
174
|
+
unregisterPenInsertionReceiver();
|
|
175
|
+
disconnectSamsungRemote();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Override
|
|
179
|
+
public void onHostDestroy() {
|
|
180
|
+
listening.set(false);
|
|
181
|
+
detachRootViewListeners();
|
|
182
|
+
unregisterPenInsertionReceiver();
|
|
183
|
+
disconnectSamsungRemote();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private void attachRootViewListeners() {
|
|
187
|
+
Activity activity = reactContext.getCurrentActivity();
|
|
188
|
+
if (activity == null) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Window window = activity.getWindow();
|
|
193
|
+
if (window == null) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
View rootView = window.getDecorView();
|
|
198
|
+
if (rootView == null || rootView == attachedRootView) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
detachRootViewListeners();
|
|
203
|
+
attachedRootView = rootView;
|
|
204
|
+
|
|
205
|
+
rootView.setOnTouchListener(
|
|
206
|
+
new View.OnTouchListener() {
|
|
207
|
+
@Override
|
|
208
|
+
public boolean onTouch(View view, MotionEvent event) {
|
|
209
|
+
if (!isStylusEvent(event)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
emitMotionEvent(event);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
rootView.setOnHoverListener(
|
|
218
|
+
new View.OnHoverListener() {
|
|
219
|
+
@Override
|
|
220
|
+
public boolean onHover(View view, MotionEvent event) {
|
|
221
|
+
if (!isStylusEvent(event)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
|
|
225
|
+
|| event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE
|
|
226
|
+
|| event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
|
|
227
|
+
emitMotionEvent(event);
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private void detachRootViewListeners() {
|
|
235
|
+
if (attachedRootView == null) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
attachedRootView.setOnTouchListener(null);
|
|
240
|
+
attachedRootView.setOnHoverListener(null);
|
|
241
|
+
attachedRootView = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private void registerPenInsertionReceiver() {
|
|
245
|
+
if (!isSamsungDevice() || insertionReceiverRegistered) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
Intent stickyIntent =
|
|
251
|
+
reactContext.registerReceiver(insertionReceiver, new IntentFilter(SPEN_INSERT_ACTION));
|
|
252
|
+
insertionReceiverRegistered = true;
|
|
253
|
+
if (stickyIntent != null) {
|
|
254
|
+
updatePenInsertionState(stickyIntent, true);
|
|
255
|
+
}
|
|
256
|
+
} catch (Throwable t) {
|
|
257
|
+
Log.w(TAG, "Samsung S Pen insertion state is unavailable", t);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private void unregisterPenInsertionReceiver() {
|
|
262
|
+
if (!insertionReceiverRegistered) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
reactContext.unregisterReceiver(insertionReceiver);
|
|
268
|
+
} catch (Throwable t) {
|
|
269
|
+
Log.w(TAG, "Failed to unregister Samsung S Pen insertion listener", t);
|
|
270
|
+
} finally {
|
|
271
|
+
insertionReceiverRegistered = false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private String readPenInsertionState() {
|
|
276
|
+
if (!isSamsungDevice()) {
|
|
277
|
+
return "unknown";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (lastPenInsertionState != null) {
|
|
281
|
+
return lastPenInsertionState;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
Intent stickyIntent =
|
|
286
|
+
reactContext.registerReceiver(null, new IntentFilter(SPEN_INSERT_ACTION));
|
|
287
|
+
return updatePenInsertionState(stickyIntent, false);
|
|
288
|
+
} catch (Throwable t) {
|
|
289
|
+
Log.w(TAG, "Failed to read Samsung S Pen insertion state", t);
|
|
290
|
+
return "unknown";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private String updatePenInsertionState(@Nullable Intent intent, boolean shouldEmit) {
|
|
295
|
+
String state = "unknown";
|
|
296
|
+
if (intent != null && intent.hasExtra("penInsert")) {
|
|
297
|
+
state = intent.getBooleanExtra("penInsert", false) ? "inserted" : "detached";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
boolean changed = !state.equals(lastPenInsertionState);
|
|
301
|
+
lastPenInsertionState = state;
|
|
302
|
+
if (shouldEmit && changed) {
|
|
303
|
+
WritableMap payload = buildBasePayload("spen-insertion-state", System.currentTimeMillis());
|
|
304
|
+
payload.putString("insertionState", state);
|
|
305
|
+
payload.putMap("raw", buildRawMap("action", SPEN_INSERT_ACTION, "penInsert", "inserted".equals(state)));
|
|
306
|
+
emit(payload);
|
|
307
|
+
}
|
|
308
|
+
return state;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private void connectSamsungRemoteIfAvailable() {
|
|
312
|
+
if (!isSamsungDevice() || connecting.get()) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
if (spenRemote != null) {
|
|
318
|
+
boolean connected = getSamsungRemoteBoolean("isConnected", false);
|
|
319
|
+
if (connected && spenUnitManager != null) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
clearSamsungRemoteReferences();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
connecting.set(true);
|
|
327
|
+
|
|
328
|
+
Class<?> remoteClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote");
|
|
329
|
+
spenRemote = remoteClass.getMethod("getInstance").invoke(null);
|
|
330
|
+
installSamsungConnectionStateListener(remoteClass);
|
|
331
|
+
|
|
332
|
+
Class<?> callbackClass =
|
|
333
|
+
Class.forName("com.samsung.android.sdk.penremote.SpenRemote$ConnectionResultCallback");
|
|
334
|
+
spenConnectionCallback =
|
|
335
|
+
Proxy.newProxyInstance(
|
|
336
|
+
callbackClass.getClassLoader(),
|
|
337
|
+
new Class<?>[] {callbackClass},
|
|
338
|
+
new InvocationHandler() {
|
|
339
|
+
@Override
|
|
340
|
+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
|
341
|
+
String methodName = method.getName();
|
|
342
|
+
if ("onSuccess".equals(methodName) && args != null && args.length > 0) {
|
|
343
|
+
connecting.set(false);
|
|
344
|
+
handleSamsungConnectionSuccess(args[0]);
|
|
345
|
+
} else if ("onFailure".equals(methodName) && args != null && args.length > 0) {
|
|
346
|
+
connecting.set(false);
|
|
347
|
+
Log.w(TAG, "Samsung S Pen Remote connection failed: " + args[0]);
|
|
348
|
+
emitErrorEvent(args[0]);
|
|
349
|
+
clearSamsungRemoteReferences();
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
remoteClass
|
|
356
|
+
.getMethod("connect", Context.class, callbackClass)
|
|
357
|
+
.invoke(spenRemote, getConnectionContext(), spenConnectionCallback);
|
|
358
|
+
Log.i(TAG, "Samsung S Pen Remote connection requested");
|
|
359
|
+
} catch (Throwable t) {
|
|
360
|
+
connecting.set(false);
|
|
361
|
+
Log.w(TAG, "Samsung S Pen Remote SDK not available", t);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private void handleSamsungConnectionSuccess(Object unitManager) {
|
|
366
|
+
spenUnitManager = unitManager;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
Class<?> eventClass = Class.forName("com.samsung.android.sdk.penremote.SpenEvent");
|
|
370
|
+
Class<?> remoteClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote");
|
|
371
|
+
|
|
372
|
+
installSamsungConnectionStateListener(remoteClass);
|
|
373
|
+
|
|
374
|
+
SpenUnitManager typedManager = (SpenUnitManager) unitManager;
|
|
375
|
+
SpenEventListener typedButtonListener =
|
|
376
|
+
new SpenEventListener() {
|
|
377
|
+
@Override
|
|
378
|
+
public void onEvent(SpenEvent event) {
|
|
379
|
+
try {
|
|
380
|
+
emitSamsungButtonEvent(event, eventClass);
|
|
381
|
+
} catch (Throwable t) {
|
|
382
|
+
Log.e(TAG, "Failed to decode Samsung button event", t);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
SpenEventListener typedAirMotionListener =
|
|
387
|
+
new SpenEventListener() {
|
|
388
|
+
@Override
|
|
389
|
+
public void onEvent(SpenEvent event) {
|
|
390
|
+
try {
|
|
391
|
+
emitSamsungAirMotionEvent(event, eventClass);
|
|
392
|
+
} catch (Throwable t) {
|
|
393
|
+
Log.e(TAG, "Failed to decode Samsung Air Motion event", t);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
SpenUnit typedButtonUnit = typedManager.getUnit(SpenUnit.TYPE_BUTTON);
|
|
399
|
+
SpenUnit typedAirMotionUnit = typedManager.getUnit(SpenUnit.TYPE_AIR_MOTION);
|
|
400
|
+
if (typedButtonUnit != null) {
|
|
401
|
+
typedManager.registerSpenEventListener(typedButtonListener, typedButtonUnit);
|
|
402
|
+
}
|
|
403
|
+
if (typedAirMotionUnit != null) {
|
|
404
|
+
typedManager.registerSpenEventListener(typedAirMotionListener, typedAirMotionUnit);
|
|
405
|
+
}
|
|
406
|
+
buttonEventListener = typedButtonListener;
|
|
407
|
+
airMotionEventListener = typedAirMotionListener;
|
|
408
|
+
buttonUnit = typedButtonUnit;
|
|
409
|
+
airMotionUnit = typedAirMotionUnit;
|
|
410
|
+
Log.i(
|
|
411
|
+
TAG,
|
|
412
|
+
"Samsung S Pen Remote connected; button="
|
|
413
|
+
+ (buttonUnit != null)
|
|
414
|
+
+ ", airMotion="
|
|
415
|
+
+ (airMotionUnit != null));
|
|
416
|
+
emitConnectionStateEvent("connected", "connected");
|
|
417
|
+
} catch (Throwable t) {
|
|
418
|
+
Log.w(TAG, "Failed to register Samsung S Pen listeners", t);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private void installSamsungConnectionStateListener(Class<?> remoteClass) throws Exception {
|
|
423
|
+
if (spenConnectionStateListener != null) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
Class<?> stateListenerClass =
|
|
428
|
+
Class.forName("com.samsung.android.sdk.penremote.SpenRemote$ConnectionStateChangeListener");
|
|
429
|
+
spenConnectionStateListener =
|
|
430
|
+
Proxy.newProxyInstance(
|
|
431
|
+
stateListenerClass.getClassLoader(),
|
|
432
|
+
new Class<?>[] {stateListenerClass},
|
|
433
|
+
new InvocationHandler() {
|
|
434
|
+
@Override
|
|
435
|
+
public Object invoke(Object proxy, Method method, Object[] args) {
|
|
436
|
+
if ("onChange".equals(method.getName()) && args != null && args.length > 0) {
|
|
437
|
+
String state = mapConnectionState(args[0]);
|
|
438
|
+
Log.i(TAG, "Samsung S Pen Remote state changed: " + args[0] + " (" + state + ")");
|
|
439
|
+
emitConnectionStateEvent(state, String.valueOf(args[0]));
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
remoteClass
|
|
446
|
+
.getMethod("setConnectionStateChangeListener", stateListenerClass)
|
|
447
|
+
.invoke(spenRemote, spenConnectionStateListener);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private void emitSamsungButtonEvent(Object spenEvent, Class<?> eventClass) throws Exception {
|
|
451
|
+
Class<?> buttonEventClass = Class.forName("com.samsung.android.sdk.penremote.ButtonEvent");
|
|
452
|
+
Constructor<?> buttonCtor = buttonEventClass.getConstructor(eventClass);
|
|
453
|
+
Object buttonEvent = buttonCtor.newInstance(spenEvent);
|
|
454
|
+
int action = (Integer) buttonEventClass.getMethod("getAction").invoke(buttonEvent);
|
|
455
|
+
long timestamp = (Long) buttonEventClass.getMethod("getTimeStamp").invoke(buttonEvent);
|
|
456
|
+
WritableMap payload = buildBasePayload("spen-button", timestamp / 1000000.0);
|
|
457
|
+
payload.putInt("buttonState", action);
|
|
458
|
+
payload.putString("action", action == 0 ? "down" : "up");
|
|
459
|
+
payload.putMap("raw", buildRawMap("action", action, "eventClass", spenEvent.getClass().getName()));
|
|
460
|
+
Log.d(TAG, "Samsung button event: " + action);
|
|
461
|
+
emit(payload);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private void emitSamsungAirMotionEvent(Object spenEvent, Class<?> eventClass) throws Exception {
|
|
465
|
+
Class<?> airMotionEventClass = Class.forName("com.samsung.android.sdk.penremote.AirMotionEvent");
|
|
466
|
+
Constructor<?> airCtor = airMotionEventClass.getConstructor(eventClass);
|
|
467
|
+
Object airMotionEvent = airCtor.newInstance(spenEvent);
|
|
468
|
+
float deltaX = ((Number) airMotionEventClass.getMethod("getDeltaX").invoke(airMotionEvent)).floatValue();
|
|
469
|
+
float deltaY = ((Number) airMotionEventClass.getMethod("getDeltaY").invoke(airMotionEvent)).floatValue();
|
|
470
|
+
long timestamp = (Long) airMotionEventClass.getMethod("getTimeStamp").invoke(airMotionEvent);
|
|
471
|
+
WritableMap payload = buildBasePayload("spen-air-action", timestamp / 1000000.0);
|
|
472
|
+
WritableMap point = Arguments.createMap();
|
|
473
|
+
point.putDouble("x", deltaX);
|
|
474
|
+
point.putDouble("y", deltaY);
|
|
475
|
+
payload.putMap("point", point);
|
|
476
|
+
payload.putMap("raw", buildRawMap("deltaX", deltaX, "deltaY", deltaY, "eventClass", spenEvent.getClass().getName()));
|
|
477
|
+
Log.d(TAG, "Samsung Air Motion event: x=" + deltaX + ", y=" + deltaY);
|
|
478
|
+
emit(payload);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private void disconnectSamsungRemote() {
|
|
482
|
+
try {
|
|
483
|
+
if (spenUnitManager != null && buttonUnit != null) {
|
|
484
|
+
Class<?> managerClass = Class.forName("com.samsung.android.sdk.penremote.SpenUnitManager");
|
|
485
|
+
Class<?> unitClass = Class.forName("com.samsung.android.sdk.penremote.SpenUnit");
|
|
486
|
+
managerClass.getMethod("unregisterSpenEventListener", unitClass).invoke(spenUnitManager, buttonUnit);
|
|
487
|
+
}
|
|
488
|
+
if (spenUnitManager != null && airMotionUnit != null) {
|
|
489
|
+
Class<?> managerClass = Class.forName("com.samsung.android.sdk.penremote.SpenUnitManager");
|
|
490
|
+
Class<?> unitClass = Class.forName("com.samsung.android.sdk.penremote.SpenUnit");
|
|
491
|
+
managerClass.getMethod("unregisterSpenEventListener", unitClass).invoke(spenUnitManager, airMotionUnit);
|
|
492
|
+
}
|
|
493
|
+
if (spenRemote != null) {
|
|
494
|
+
Class<?> remoteClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote");
|
|
495
|
+
remoteClass.getMethod("setConnectionStateChangeListener", Class.forName("com.samsung.android.sdk.penremote.SpenRemote$ConnectionStateChangeListener")).invoke(spenRemote, new Object[] {null});
|
|
496
|
+
remoteClass.getMethod("disconnect", Context.class).invoke(spenRemote, getConnectionContext());
|
|
497
|
+
}
|
|
498
|
+
emitConnectionStateEvent("disconnected", "disconnected");
|
|
499
|
+
} catch (Throwable t) {
|
|
500
|
+
Log.w(TAG, "Failed to disconnect Samsung S Pen Remote SDK", t);
|
|
501
|
+
} finally {
|
|
502
|
+
connecting.set(false);
|
|
503
|
+
clearSamsungRemoteReferences();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private void clearSamsungRemoteReferences() {
|
|
508
|
+
spenRemote = null;
|
|
509
|
+
spenUnitManager = null;
|
|
510
|
+
spenConnectionCallback = null;
|
|
511
|
+
spenConnectionStateListener = null;
|
|
512
|
+
buttonEventListener = null;
|
|
513
|
+
airMotionEventListener = null;
|
|
514
|
+
buttonUnit = null;
|
|
515
|
+
airMotionUnit = null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private boolean isStylusEvent(MotionEvent event) {
|
|
519
|
+
int toolType = event.getToolType(0);
|
|
520
|
+
return toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private void emitMotionEvent(MotionEvent event) {
|
|
524
|
+
String eventName = mapMotionEventName(event.getActionMasked());
|
|
525
|
+
if (eventName == null) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
WritableMap payload = buildBasePayload(eventName, event.getEventTime());
|
|
530
|
+
WritableMap point = Arguments.createMap();
|
|
531
|
+
int pointerIndex = event.getActionIndex() >= 0 ? event.getActionIndex() : 0;
|
|
532
|
+
float pressure = event.getPressure(pointerIndex);
|
|
533
|
+
float hoverDistance = event.getAxisValue(MotionEvent.AXIS_DISTANCE, pointerIndex);
|
|
534
|
+
float tilt = getTilt(event, pointerIndex);
|
|
535
|
+
point.putDouble("x", event.getX(pointerIndex));
|
|
536
|
+
point.putDouble("y", event.getY(pointerIndex));
|
|
537
|
+
point.putDouble("pressure", pressure);
|
|
538
|
+
point.putDouble("hoverDistance", hoverDistance);
|
|
539
|
+
point.putDouble("tilt", tilt);
|
|
540
|
+
point.putString("toolType", mapToolType(event.getToolType(pointerIndex)));
|
|
541
|
+
payload.putMap("point", point);
|
|
542
|
+
payload.putMap(
|
|
543
|
+
"raw",
|
|
544
|
+
buildRawMap(
|
|
545
|
+
"source", "android-digitizer",
|
|
546
|
+
"actionMasked", event.getActionMasked(),
|
|
547
|
+
"pointerCount", event.getPointerCount(),
|
|
548
|
+
"actionIndex", event.getActionIndex()));
|
|
549
|
+
emit(payload);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private float getTilt(MotionEvent event, int pointerIndex) {
|
|
553
|
+
try {
|
|
554
|
+
return event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex);
|
|
555
|
+
} catch (Throwable ignored) {
|
|
556
|
+
try {
|
|
557
|
+
return event.getAxisValue(MotionEvent.AXIS_TILT);
|
|
558
|
+
} catch (Throwable ignoredAgain) {
|
|
559
|
+
return 0f;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
@Nullable
|
|
565
|
+
private String mapMotionEventName(int actionMasked) {
|
|
566
|
+
switch (actionMasked) {
|
|
567
|
+
case MotionEvent.ACTION_DOWN:
|
|
568
|
+
case MotionEvent.ACTION_POINTER_DOWN:
|
|
569
|
+
return "spen-down";
|
|
570
|
+
case MotionEvent.ACTION_MOVE:
|
|
571
|
+
return "spen-move";
|
|
572
|
+
case MotionEvent.ACTION_UP:
|
|
573
|
+
case MotionEvent.ACTION_POINTER_UP:
|
|
574
|
+
case MotionEvent.ACTION_CANCEL:
|
|
575
|
+
return "spen-up";
|
|
576
|
+
case MotionEvent.ACTION_HOVER_ENTER:
|
|
577
|
+
case MotionEvent.ACTION_HOVER_MOVE:
|
|
578
|
+
case MotionEvent.ACTION_HOVER_EXIT:
|
|
579
|
+
return "spen-hover";
|
|
580
|
+
default:
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private String mapToolType(int toolType) {
|
|
586
|
+
switch (toolType) {
|
|
587
|
+
case MotionEvent.TOOL_TYPE_STYLUS:
|
|
588
|
+
return "stylus";
|
|
589
|
+
case MotionEvent.TOOL_TYPE_ERASER:
|
|
590
|
+
return "stylus";
|
|
591
|
+
case MotionEvent.TOOL_TYPE_FINGER:
|
|
592
|
+
return "finger";
|
|
593
|
+
case MotionEvent.TOOL_TYPE_MOUSE:
|
|
594
|
+
return "mouse";
|
|
595
|
+
default:
|
|
596
|
+
return "unknown";
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private WritableMap buildBasePayload(String name, double timestamp) {
|
|
601
|
+
WritableMap payload = Arguments.createMap();
|
|
602
|
+
payload.putString("name", name);
|
|
603
|
+
payload.putDouble("timestamp", timestamp);
|
|
604
|
+
return payload;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private WritableMap buildRawMap(Object... keyValues) {
|
|
608
|
+
WritableMap raw = Arguments.createMap();
|
|
609
|
+
for (int i = 0; i + 1 < keyValues.length; i += 2) {
|
|
610
|
+
String key = String.valueOf(keyValues[i]);
|
|
611
|
+
Object value = keyValues[i + 1];
|
|
612
|
+
if (value instanceof Boolean) {
|
|
613
|
+
raw.putBoolean(key, (Boolean) value);
|
|
614
|
+
} else if (value instanceof Integer) {
|
|
615
|
+
raw.putInt(key, (Integer) value);
|
|
616
|
+
} else if (value instanceof Long) {
|
|
617
|
+
raw.putDouble(key, ((Long) value).doubleValue());
|
|
618
|
+
} else if (value instanceof Float) {
|
|
619
|
+
raw.putDouble(key, ((Float) value).doubleValue());
|
|
620
|
+
} else if (value instanceof Double) {
|
|
621
|
+
raw.putDouble(key, (Double) value);
|
|
622
|
+
} else if (value == null) {
|
|
623
|
+
raw.putNull(key);
|
|
624
|
+
} else {
|
|
625
|
+
raw.putString(key, String.valueOf(value));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return raw;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private WritableArray buildCapabilities() {
|
|
632
|
+
WritableArray capabilities = Arguments.createArray();
|
|
633
|
+
capabilities.pushString("stylusInput");
|
|
634
|
+
capabilities.pushString("hover");
|
|
635
|
+
capabilities.pushString("pressure");
|
|
636
|
+
if (isFeatureEnabledForRemote("button")) {
|
|
637
|
+
capabilities.pushString("button");
|
|
638
|
+
}
|
|
639
|
+
if (isFeatureEnabledForRemote("airMotion")) {
|
|
640
|
+
capabilities.pushString("airActions");
|
|
641
|
+
capabilities.pushString("airCommand");
|
|
642
|
+
}
|
|
643
|
+
return capabilities;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private boolean isSamsungDevice() {
|
|
647
|
+
String manufacturer = Build.MANUFACTURER == null ? "" : Build.MANUFACTURER;
|
|
648
|
+
String brand = Build.BRAND == null ? "" : Build.BRAND;
|
|
649
|
+
return manufacturer.toLowerCase(Locale.US).contains("samsung")
|
|
650
|
+
|| brand.toLowerCase(Locale.US).contains("samsung");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private boolean hasSamsungRemoteSdk() {
|
|
654
|
+
return classExists("com.samsung.android.sdk.penremote.SpenRemote");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private WritableMap buildSamsungSdkInfo() {
|
|
658
|
+
WritableMap sdk = Arguments.createMap();
|
|
659
|
+
boolean available = hasSamsungRemoteSdk();
|
|
660
|
+
sdk.putBoolean("available", available);
|
|
661
|
+
sdk.putBoolean("connected", isSamsungRemoteConnected());
|
|
662
|
+
if (available) {
|
|
663
|
+
sdk.putInt("versionCode", getSamsungRemoteInteger("getVersionCode", 0));
|
|
664
|
+
sdk.putString("versionName", getSamsungRemoteString("getVersionName", ""));
|
|
665
|
+
WritableMap featureFlags = Arguments.createMap();
|
|
666
|
+
featureFlags.putBoolean("button", isFeatureEnabledForRemote("button"));
|
|
667
|
+
featureFlags.putBoolean("airMotion", isFeatureEnabledForRemote("airMotion"));
|
|
668
|
+
sdk.putMap("featureFlags", featureFlags);
|
|
669
|
+
}
|
|
670
|
+
return sdk;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private boolean isSamsungRemoteConnected() {
|
|
674
|
+
return "connected".equals(lastConnectionState)
|
|
675
|
+
|| getSamsungRemoteBoolean("isConnected", false);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private boolean isFeatureEnabledForRemote(String feature) {
|
|
679
|
+
if (!hasSamsungRemoteSdk()) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
int featureType = mapFeatureNameToType(feature);
|
|
684
|
+
return getSamsungRemoteBoolean("isFeatureEnabled", false, new Class<?>[] {int.class}, new Object[] {featureType});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private int mapFeatureNameToType(String feature) {
|
|
688
|
+
if (!hasSamsungRemoteSdk()) {
|
|
689
|
+
return -1;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
Class<?> remoteClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote");
|
|
694
|
+
if ("airMotion".equals(feature)) {
|
|
695
|
+
return remoteClass.getField("FEATURE_TYPE_AIR_MOTION").getInt(null);
|
|
696
|
+
}
|
|
697
|
+
return remoteClass.getField("FEATURE_TYPE_BUTTON").getInt(null);
|
|
698
|
+
} catch (Throwable t) {
|
|
699
|
+
return -1;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private String mapConnectionState(Object stateValue) {
|
|
704
|
+
if (stateValue == null) {
|
|
705
|
+
return "unknown";
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
String normalized = String.valueOf(stateValue).trim().toUpperCase(Locale.US);
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
Class<?> stateClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote$State");
|
|
712
|
+
int connected = stateClass.getField("CONNECTED").getInt(null);
|
|
713
|
+
int disconnected = stateClass.getField("DISCONNECTED").getInt(null);
|
|
714
|
+
int disconnectedUnknown = stateClass.getField("DISCONNECTED_BY_UNKNOWN_REASON").getInt(null);
|
|
715
|
+
|
|
716
|
+
if (stateValue instanceof Number) {
|
|
717
|
+
int state = ((Number) stateValue).intValue();
|
|
718
|
+
if (state == connected) {
|
|
719
|
+
return "connected";
|
|
720
|
+
}
|
|
721
|
+
if (state == disconnected) {
|
|
722
|
+
return "disconnected";
|
|
723
|
+
}
|
|
724
|
+
if (state == disconnectedUnknown) {
|
|
725
|
+
return "disconnectedByUnknownReason";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (matchesStateName(normalized, "DISCONNECTED_BY_UNKNOWN_REASON")) {
|
|
730
|
+
return "disconnectedByUnknownReason";
|
|
731
|
+
}
|
|
732
|
+
if (matchesStateName(normalized, "DISCONNECTED")) {
|
|
733
|
+
return "disconnected";
|
|
734
|
+
}
|
|
735
|
+
if (matchesStateName(normalized, "CONNECTED")) {
|
|
736
|
+
return "connected";
|
|
737
|
+
}
|
|
738
|
+
} catch (Throwable ignored) {
|
|
739
|
+
// Fall through to generic mapping.
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (normalized.contains("DISCONNECTED_BY_UNKNOWN_REASON")) {
|
|
743
|
+
return "disconnectedByUnknownReason";
|
|
744
|
+
}
|
|
745
|
+
if (normalized.contains("DISCONNECTED")) {
|
|
746
|
+
return "disconnected";
|
|
747
|
+
}
|
|
748
|
+
if (normalized.contains("CONNECTED")) {
|
|
749
|
+
return "connected";
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return "unknown";
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private boolean matchesStateName(String normalizedValue, String expected) {
|
|
756
|
+
return normalizedValue.equals(expected)
|
|
757
|
+
|| normalizedValue.endsWith("." + expected)
|
|
758
|
+
|| normalizedValue.contains(expected);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private void emitConnectionStateEvent(String state, String rawState) {
|
|
762
|
+
lastConnectionState = state;
|
|
763
|
+
WritableMap payload = buildBasePayload("spen-connection-state", System.currentTimeMillis());
|
|
764
|
+
payload.putString("connectionState", state);
|
|
765
|
+
payload.putMap("raw", buildRawMap("state", state, "rawState", rawState));
|
|
766
|
+
emit(payload);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private void emitErrorEvent(Object errorValue) {
|
|
770
|
+
WritableMap payload = buildBasePayload("spen-error", System.currentTimeMillis());
|
|
771
|
+
payload.putString("errorCode", mapErrorCode(errorValue));
|
|
772
|
+
payload.putMap("raw", buildRawMap("error", String.valueOf(errorValue)));
|
|
773
|
+
emit(payload);
|
|
774
|
+
emitConnectionStateEvent(mapConnectionStateFromError(errorValue), String.valueOf(errorValue));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private String mapErrorCode(Object errorValue) {
|
|
778
|
+
if (errorValue == null) {
|
|
779
|
+
return "unknown";
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
Class<?> errorClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote$Error");
|
|
784
|
+
int value = ((Number) errorValue).intValue();
|
|
785
|
+
if (value == errorClass.getField("UNSUPPORTED_DEVICE").getInt(null)) {
|
|
786
|
+
return "unsupportedDevice";
|
|
787
|
+
}
|
|
788
|
+
if (value == errorClass.getField("CONNECTION_FAILED").getInt(null)) {
|
|
789
|
+
return "connectionFailed";
|
|
790
|
+
}
|
|
791
|
+
if (value == errorClass.getField("UNKNOWN").getInt(null)) {
|
|
792
|
+
return "unknown";
|
|
793
|
+
}
|
|
794
|
+
} catch (Throwable ignored) {
|
|
795
|
+
// Fall through to string-based fallback.
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
String normalized = String.valueOf(errorValue).toUpperCase(Locale.US);
|
|
799
|
+
if (normalized.contains("UNSUPPORTED")) {
|
|
800
|
+
return "unsupportedDevice";
|
|
801
|
+
}
|
|
802
|
+
if (normalized.contains("CONNECTION_FAILED")) {
|
|
803
|
+
return "connectionFailed";
|
|
804
|
+
}
|
|
805
|
+
return "unknown";
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private String mapConnectionStateFromError(Object errorValue) {
|
|
809
|
+
String errorCode = mapErrorCode(errorValue);
|
|
810
|
+
if ("unsupportedDevice".equals(errorCode)) {
|
|
811
|
+
return "unsupportedDevice";
|
|
812
|
+
}
|
|
813
|
+
if ("connectionFailed".equals(errorCode)) {
|
|
814
|
+
return "connectionFailed";
|
|
815
|
+
}
|
|
816
|
+
return "unknown";
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private boolean getSamsungRemoteBoolean(String methodName, boolean fallback) {
|
|
820
|
+
return getSamsungRemoteBoolean(methodName, fallback, new Class<?>[0], new Object[0]);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private boolean getSamsungRemoteBoolean(String methodName, boolean fallback, Class<?>[] parameterTypes, Object[] args) {
|
|
824
|
+
Object remote = getSamsungRemoteInstance();
|
|
825
|
+
if (remote == null) {
|
|
826
|
+
return fallback;
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
Object value = remote.getClass().getMethod(methodName, parameterTypes).invoke(remote, args);
|
|
830
|
+
return value instanceof Boolean ? (Boolean) value : fallback;
|
|
831
|
+
} catch (Throwable ignored) {
|
|
832
|
+
return fallback;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private int getSamsungRemoteInteger(String methodName, int fallback) {
|
|
837
|
+
Object remote = getSamsungRemoteInstance();
|
|
838
|
+
if (remote == null) {
|
|
839
|
+
return fallback;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
Object value = remote.getClass().getMethod(methodName).invoke(remote);
|
|
843
|
+
return value instanceof Number ? ((Number) value).intValue() : fallback;
|
|
844
|
+
} catch (Throwable ignored) {
|
|
845
|
+
return fallback;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private String getSamsungRemoteString(String methodName, String fallback) {
|
|
850
|
+
Object remote = getSamsungRemoteInstance();
|
|
851
|
+
if (remote == null) {
|
|
852
|
+
return fallback;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
Object value = remote.getClass().getMethod(methodName).invoke(remote);
|
|
856
|
+
return value == null ? fallback : String.valueOf(value);
|
|
857
|
+
} catch (Throwable ignored) {
|
|
858
|
+
return fallback;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
@Nullable
|
|
863
|
+
private Context getConnectionContext() {
|
|
864
|
+
Activity activity = reactContext.getCurrentActivity();
|
|
865
|
+
return activity != null ? activity : reactContext;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
@Nullable
|
|
869
|
+
private Object getSamsungRemoteInstance() {
|
|
870
|
+
if (spenRemote != null) {
|
|
871
|
+
return spenRemote;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (!hasSamsungRemoteSdk()) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
Class<?> remoteClass = Class.forName("com.samsung.android.sdk.penremote.SpenRemote");
|
|
880
|
+
spenRemote = remoteClass.getMethod("getInstance").invoke(null);
|
|
881
|
+
return spenRemote;
|
|
882
|
+
} catch (Throwable t) {
|
|
883
|
+
Log.w(TAG, "Failed to obtain Samsung S Pen Remote instance", t);
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private boolean classExists(String className) {
|
|
889
|
+
try {
|
|
890
|
+
Class.forName(className);
|
|
891
|
+
return true;
|
|
892
|
+
} catch (Throwable ignored) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private void emitStatusEvent(String status, String message) {
|
|
898
|
+
WritableMap payload = buildBasePayload("spen-status", System.currentTimeMillis());
|
|
899
|
+
payload.putMap("raw", buildRawMap("status", status, "message", message));
|
|
900
|
+
emit(payload);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private void emit(@NonNull WritableMap payload) {
|
|
904
|
+
if (reactContext.hasActiveCatalystInstance()) {
|
|
905
|
+
reactContext
|
|
906
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
907
|
+
.emit(EVENT_NAME, payload);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|