@haykmkrtich/react-native-patriot-native 1.0.5 → 1.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 +3 -3
- package/android/build.gradle +39 -9
- package/android/gradle.properties +21 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/com/patriotnative/NativePatriotNativeSpec.java +47 -0
- package/android/src/main/java/com/patriotnative/PatriotNativePackage.java +29 -25
- package/android/src/newarch/java/com/patriotnative/PatriotNativeModule.java +201 -0
- package/android/src/oldarch/java/com/patriotnative/PatriotNativeModule.java +198 -0
- package/index.ts +79 -10
- package/package.json +30 -2
- package/src/NativePatriotNative.ts +16 -0
- package/android/src/main/java/com/patriotnative/PatriotNativeModule.java +0 -116
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
## ✨ What's New in v1.0.5
|
|
10
10
|
|
|
11
11
|
- 🔍 **Device Detection**: New `getConnectedWatchProperties()` function
|
|
12
|
-
- 📱 **Multi-Platform Support**: Detect WearOS, Fitbit, and other wearable devices
|
|
12
|
+
- 📱 **Multi-Platform Support**: Detect WearOS, Fitbit, and other wearable devices
|
|
13
13
|
- 🏷️ **Device Information**: Get name, platform, type, and unique ID
|
|
14
14
|
- 🛡️ **Enhanced Error Handling**: Improved disconnection detection
|
|
15
15
|
|
|
@@ -112,7 +112,7 @@ dependencies {
|
|
|
112
112
|
### WearOS Development Best Practices
|
|
113
113
|
|
|
114
114
|
> ⚠️ **Important**: For Google Play Console compliance, create **two applications** with identical package names:
|
|
115
|
-
> - 📱 Mobile companion app (React Native)
|
|
115
|
+
> - 📱 Mobile companion app (React Native)
|
|
116
116
|
> - ⌚ WearOS watch face app
|
|
117
117
|
|
|
118
118
|
This ensures proper functionality and distribution through Google Play Store.
|
|
@@ -201,4 +201,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
201
201
|
|
|
202
202
|
Made with ❤️ for the React Native community
|
|
203
203
|
|
|
204
|
-
</div>
|
|
204
|
+
</div>
|
package/android/build.gradle
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
|
-
|
|
2
1
|
apply plugin: 'com.android.library'
|
|
3
2
|
apply plugin: 'com.facebook.react'
|
|
4
3
|
|
|
5
4
|
android {
|
|
6
|
-
|
|
5
|
+
compileSdk 35
|
|
7
6
|
namespace "com.patriotnative"
|
|
7
|
+
|
|
8
8
|
defaultConfig {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
minSdk 24
|
|
10
|
+
targetSdk 35
|
|
11
|
+
|
|
12
|
+
ndk {
|
|
13
|
+
abiFilters "arm64-v8a", "x86_64"
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
compileOptions {
|
|
14
|
-
sourceCompatibility JavaVersion.
|
|
15
|
-
targetCompatibility JavaVersion.
|
|
18
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
19
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
16
20
|
}
|
|
21
|
+
|
|
22
|
+
packaging {
|
|
23
|
+
pickFirst "**/libc++_shared.so"
|
|
24
|
+
pickFirst "**/libjsc.so"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
publishing {
|
|
28
|
+
singleVariant("release") {
|
|
29
|
+
withSourcesJar()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
sourceSets {
|
|
34
|
+
main {
|
|
35
|
+
if (project.hasProperty("newArchEnabled") && project.property("newArchEnabled") == "true") {
|
|
36
|
+
java.srcDirs += ["src/newarch/java"]
|
|
37
|
+
} else {
|
|
38
|
+
java.srcDirs += ["src/oldarch/java"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
react {
|
|
45
|
+
libraryName = "PatriotNative"
|
|
46
|
+
codegenJavaPackageName = "com.patriotnative"
|
|
17
47
|
}
|
|
18
48
|
|
|
19
49
|
dependencies {
|
|
20
|
-
implementation "com.facebook.react:react-android
|
|
21
|
-
implementation 'androidx.annotation:annotation:1.
|
|
22
|
-
implementation 'com.google.android.gms:play-services-wearable:18.
|
|
50
|
+
implementation "com.facebook.react:react-android"
|
|
51
|
+
implementation 'androidx.annotation:annotation:1.7.0'
|
|
52
|
+
implementation 'com.google.android.gms:play-services-wearable:18.2.0'
|
|
23
53
|
implementation 'androidx.wear:wear-remote-interactions:1.0.0'
|
|
24
54
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# React Native Patriot Native Android Configuration
|
|
2
|
+
|
|
3
|
+
# Enable 16KB page size support for Google Play requirements
|
|
4
|
+
android.experimental.enableArtProfiles=true
|
|
5
|
+
android.experimental.r8.dex-startup-optimization=true
|
|
6
|
+
|
|
7
|
+
# React Native optimizations
|
|
8
|
+
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
|
9
|
+
org.gradle.parallel=true
|
|
10
|
+
org.gradle.configureondemand=true
|
|
11
|
+
org.gradle.daemon=true
|
|
12
|
+
|
|
13
|
+
# Android build optimizations
|
|
14
|
+
android.useAndroidX=true
|
|
15
|
+
android.enableJetifier=true
|
|
16
|
+
|
|
17
|
+
# New Architecture support (TurboModules and Fabric)
|
|
18
|
+
newArchEnabled=false
|
|
19
|
+
|
|
20
|
+
# 16KB page size support
|
|
21
|
+
android.bundle.enableUncompressedNativeLibs=false
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
5
|
+
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
|
|
6
|
+
|
|
7
|
+
<application android:extractNativeLibs="false" />
|
|
8
|
+
|
|
9
|
+
</manifest>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
|
|
3
|
+
*
|
|
4
|
+
* Do not edit this file as changes may cause incorrect behavior and will be lost
|
|
5
|
+
* once the code is regenerated.
|
|
6
|
+
*
|
|
7
|
+
* @generated by codegen project: GenerateModuleJavaSpec.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
package com.patriotnative;
|
|
11
|
+
|
|
12
|
+
import com.facebook.proguard.annotations.DoNotStrip;
|
|
13
|
+
import com.facebook.react.bridge.Promise;
|
|
14
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
15
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
16
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
17
|
+
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
|
|
18
|
+
|
|
19
|
+
public abstract class NativePatriotNativeSpec extends ReactContextBaseJavaModule implements TurboModule {
|
|
20
|
+
|
|
21
|
+
public static final String NAME = "PatriotNative";
|
|
22
|
+
|
|
23
|
+
public NativePatriotNativeSpec(ReactApplicationContext reactContext) {
|
|
24
|
+
super(reactContext);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Override
|
|
28
|
+
public String getName() {
|
|
29
|
+
return NAME;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
@DoNotStrip
|
|
34
|
+
public abstract void installWatchface(String packageName, Promise promise);
|
|
35
|
+
|
|
36
|
+
@ReactMethod
|
|
37
|
+
@DoNotStrip
|
|
38
|
+
public abstract void getConnectedDevices(Promise promise);
|
|
39
|
+
|
|
40
|
+
@ReactMethod
|
|
41
|
+
@DoNotStrip
|
|
42
|
+
public abstract void isAppInstalledOnWatch(String packageName, Promise promise);
|
|
43
|
+
|
|
44
|
+
@ReactMethod
|
|
45
|
+
@DoNotStrip
|
|
46
|
+
public abstract void sendMessageToWatch(String nodeId, String path, String data, Promise promise);
|
|
47
|
+
}
|
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import com.facebook.react.
|
|
6
|
-
import com.facebook.react.bridge.
|
|
7
|
-
import com.facebook.react.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import java.util.
|
|
11
|
-
import java.util.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
package com.patriotnative;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
public class PatriotNativePackage implements ReactPackage {
|
|
15
|
+
|
|
16
|
+
@NonNull
|
|
17
|
+
@Override
|
|
18
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
19
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
20
|
+
modules.add(new PatriotNativeModule(reactContext));
|
|
21
|
+
return modules;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@NonNull
|
|
25
|
+
@Override
|
|
26
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
27
|
+
return Collections.emptyList();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.patriotnative;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.widget.Toast;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.bridge.Arguments;
|
|
9
|
+
import com.facebook.react.bridge.Promise;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
12
|
+
import com.facebook.react.bridge.WritableArray;
|
|
13
|
+
import com.facebook.react.bridge.WritableMap;
|
|
14
|
+
|
|
15
|
+
import com.google.android.gms.wearable.CapabilityClient;
|
|
16
|
+
import com.google.android.gms.wearable.CapabilityInfo;
|
|
17
|
+
import com.google.android.gms.wearable.Node;
|
|
18
|
+
import com.google.android.gms.wearable.NodeClient;
|
|
19
|
+
import com.google.android.gms.wearable.Wearable;
|
|
20
|
+
import com.google.android.gms.tasks.Task;
|
|
21
|
+
import com.google.android.gms.tasks.Tasks;
|
|
22
|
+
|
|
23
|
+
import java.util.Set;
|
|
24
|
+
import java.util.concurrent.ExecutionException;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* NewArch implementation of PatriotNative (TurboModule) for React Native 0.77+
|
|
28
|
+
*/
|
|
29
|
+
public class PatriotNativeModule extends NativePatriotNativeSpec {
|
|
30
|
+
private static final String NAME = "PatriotNative";
|
|
31
|
+
private static final String PLAY_STORE_APP_URI = "market://details?id=";
|
|
32
|
+
|
|
33
|
+
private final ReactApplicationContext reactContext;
|
|
34
|
+
|
|
35
|
+
public PatriotNativeModule(ReactApplicationContext reactContext) {
|
|
36
|
+
super(reactContext);
|
|
37
|
+
this.reactContext = reactContext;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Override
|
|
41
|
+
@NonNull
|
|
42
|
+
public String getName() {
|
|
43
|
+
return NAME;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private Context getContext() {
|
|
47
|
+
Context context = getCurrentActivity();
|
|
48
|
+
if (context == null) {
|
|
49
|
+
context = getReactApplicationContext();
|
|
50
|
+
}
|
|
51
|
+
return context;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Override
|
|
55
|
+
@ReactMethod
|
|
56
|
+
public void installWatchface(String packageName, Promise promise) {
|
|
57
|
+
try {
|
|
58
|
+
Context context = getContext();
|
|
59
|
+
|
|
60
|
+
NodeClient nodeClient = Wearable.getNodeClient(context);
|
|
61
|
+
Task<Set<Node>> nodesTask = nodeClient.getConnectedNodes();
|
|
62
|
+
Set<Node> nodes = Tasks.await(nodesTask);
|
|
63
|
+
|
|
64
|
+
if (nodes.isEmpty()) {
|
|
65
|
+
promise.reject("NO_NODES", "No connected WearOS devices found");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
String appUri = PLAY_STORE_APP_URI + packageName;
|
|
70
|
+
boolean installationSent = false;
|
|
71
|
+
|
|
72
|
+
for (Node node : nodes) {
|
|
73
|
+
try {
|
|
74
|
+
Task<Integer> sendTask = Wearable.getMessageClient(context)
|
|
75
|
+
.sendMessage(node.getId(), "/install_watchface", appUri.getBytes());
|
|
76
|
+
Tasks.await(sendTask);
|
|
77
|
+
installationSent = true;
|
|
78
|
+
} catch (Exception e) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (installationSent) {
|
|
84
|
+
if (getCurrentActivity() != null) {
|
|
85
|
+
final Context finalContext = context;
|
|
86
|
+
getCurrentActivity().runOnUiThread(() ->
|
|
87
|
+
Toast.makeText(finalContext, "Check your watch for installation prompt", Toast.LENGTH_LONG).show()
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
promise.resolve(null);
|
|
91
|
+
} else {
|
|
92
|
+
promise.reject("INSTALL_FAILED", "Failed to send installation request to any connected device");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
96
|
+
promise.reject("INSTALL_FAILED", "Failed to communicate with WearOS device: " + e.getMessage());
|
|
97
|
+
} catch (Exception e) {
|
|
98
|
+
promise.reject("INSTALL_FAILED", "Unexpected error: " + e.getMessage());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Override
|
|
103
|
+
@ReactMethod
|
|
104
|
+
public void getConnectedDevices(Promise promise) {
|
|
105
|
+
try {
|
|
106
|
+
Context context = getContext();
|
|
107
|
+
|
|
108
|
+
NodeClient nodeClient = Wearable.getNodeClient(context);
|
|
109
|
+
Task<Set<Node>> nodesTask = nodeClient.getConnectedNodes();
|
|
110
|
+
Set<Node> nodes = Tasks.await(nodesTask);
|
|
111
|
+
|
|
112
|
+
CapabilityClient capabilityClient = Wearable.getCapabilityClient(context);
|
|
113
|
+
|
|
114
|
+
WritableArray devicesArray = Arguments.createArray();
|
|
115
|
+
|
|
116
|
+
for (Node node : nodes) {
|
|
117
|
+
WritableMap deviceMap = Arguments.createMap();
|
|
118
|
+
deviceMap.putString("id", node.getId());
|
|
119
|
+
deviceMap.putString("displayName", node.getDisplayName());
|
|
120
|
+
deviceMap.putBoolean("isNearby", node.isNearby());
|
|
121
|
+
deviceMap.putString("type", "watch");
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
Task<CapabilityInfo> capabilityTask = capabilityClient.getCapability(
|
|
125
|
+
"wear_app_runtime", CapabilityClient.FILTER_REACHABLE);
|
|
126
|
+
CapabilityInfo capabilityInfo = Tasks.await(capabilityTask);
|
|
127
|
+
|
|
128
|
+
boolean isWearOS = false;
|
|
129
|
+
for (Node capableNode : capabilityInfo.getNodes()) {
|
|
130
|
+
if (capableNode.getId().equals(node.getId())) {
|
|
131
|
+
isWearOS = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
deviceMap.putString("platform", isWearOS ? "wearOS" : "unknown");
|
|
136
|
+
} catch (Exception e) {
|
|
137
|
+
deviceMap.putString("platform", "wearOS");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
devicesArray.pushMap(deviceMap);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
promise.resolve(devicesArray);
|
|
144
|
+
|
|
145
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
146
|
+
promise.reject("DETECTION_FAILED", "Failed to get connected devices: " + e.getMessage());
|
|
147
|
+
} catch (Exception e) {
|
|
148
|
+
promise.reject("DETECTION_FAILED", "Unexpected error: " + e.getMessage());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Override
|
|
153
|
+
@ReactMethod
|
|
154
|
+
public void isAppInstalledOnWatch(String packageName, Promise promise) {
|
|
155
|
+
try {
|
|
156
|
+
Context context = getContext();
|
|
157
|
+
|
|
158
|
+
CapabilityClient capabilityClient = Wearable.getCapabilityClient(context);
|
|
159
|
+
Task<CapabilityInfo> capabilityTask = capabilityClient.getCapability(
|
|
160
|
+
packageName, CapabilityClient.FILTER_ALL);
|
|
161
|
+
CapabilityInfo capabilityInfo = Tasks.await(capabilityTask);
|
|
162
|
+
|
|
163
|
+
Set<Node> capableNodes = capabilityInfo.getNodes();
|
|
164
|
+
|
|
165
|
+
WritableMap result = Arguments.createMap();
|
|
166
|
+
result.putBoolean("isInstalled", !capableNodes.isEmpty());
|
|
167
|
+
|
|
168
|
+
WritableArray nodeIds = Arguments.createArray();
|
|
169
|
+
for (Node node : capableNodes) {
|
|
170
|
+
nodeIds.pushString(node.getId());
|
|
171
|
+
}
|
|
172
|
+
result.putArray("installedOnNodes", nodeIds);
|
|
173
|
+
|
|
174
|
+
promise.resolve(result);
|
|
175
|
+
|
|
176
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
177
|
+
promise.reject("CHECK_FAILED", "Failed to check app installation: " + e.getMessage());
|
|
178
|
+
} catch (Exception e) {
|
|
179
|
+
promise.reject("CHECK_FAILED", "Unexpected error: " + e.getMessage());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Override
|
|
184
|
+
@ReactMethod
|
|
185
|
+
public void sendMessageToWatch(String nodeId, String path, String data, Promise promise) {
|
|
186
|
+
try {
|
|
187
|
+
Context context = getContext();
|
|
188
|
+
|
|
189
|
+
Task<Integer> sendTask = Wearable.getMessageClient(context)
|
|
190
|
+
.sendMessage(nodeId, path, data.getBytes());
|
|
191
|
+
Tasks.await(sendTask);
|
|
192
|
+
|
|
193
|
+
promise.resolve(null);
|
|
194
|
+
|
|
195
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
196
|
+
promise.reject("MESSAGE_FAILED", "Failed to send message: " + e.getMessage());
|
|
197
|
+
} catch (Exception e) {
|
|
198
|
+
promise.reject("MESSAGE_FAILED", "Unexpected error: " + e.getMessage());
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
package com.patriotnative;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.widget.Toast;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.bridge.Arguments;
|
|
9
|
+
import com.facebook.react.bridge.Promise;
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
12
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
13
|
+
import com.facebook.react.bridge.WritableArray;
|
|
14
|
+
import com.facebook.react.bridge.WritableMap;
|
|
15
|
+
|
|
16
|
+
import com.google.android.gms.wearable.CapabilityClient;
|
|
17
|
+
import com.google.android.gms.wearable.CapabilityInfo;
|
|
18
|
+
import com.google.android.gms.wearable.Node;
|
|
19
|
+
import com.google.android.gms.wearable.NodeClient;
|
|
20
|
+
import com.google.android.gms.wearable.Wearable;
|
|
21
|
+
import com.google.android.gms.tasks.Task;
|
|
22
|
+
import com.google.android.gms.tasks.Tasks;
|
|
23
|
+
|
|
24
|
+
import java.util.Set;
|
|
25
|
+
import java.util.concurrent.ExecutionException;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* OldArch implementation of PatriotNative (legacy bridge) for React Native < 0.77
|
|
29
|
+
*/
|
|
30
|
+
public class PatriotNativeModule extends ReactContextBaseJavaModule {
|
|
31
|
+
private static final String NAME = "PatriotNative";
|
|
32
|
+
private static final String PLAY_STORE_APP_URI = "market://details?id=";
|
|
33
|
+
|
|
34
|
+
private final ReactApplicationContext reactContext;
|
|
35
|
+
|
|
36
|
+
public PatriotNativeModule(ReactApplicationContext reactContext) {
|
|
37
|
+
super(reactContext);
|
|
38
|
+
this.reactContext = reactContext;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Override
|
|
42
|
+
@NonNull
|
|
43
|
+
public String getName() {
|
|
44
|
+
return NAME;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private Context getContext() {
|
|
48
|
+
Context context = getCurrentActivity();
|
|
49
|
+
if (context == null) {
|
|
50
|
+
context = getReactApplicationContext();
|
|
51
|
+
}
|
|
52
|
+
return context;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@ReactMethod
|
|
56
|
+
public void installWatchface(String packageName, Promise promise) {
|
|
57
|
+
try {
|
|
58
|
+
Context context = getContext();
|
|
59
|
+
|
|
60
|
+
NodeClient nodeClient = Wearable.getNodeClient(context);
|
|
61
|
+
Task<Set<Node>> nodesTask = nodeClient.getConnectedNodes();
|
|
62
|
+
Set<Node> nodes = Tasks.await(nodesTask);
|
|
63
|
+
|
|
64
|
+
if (nodes.isEmpty()) {
|
|
65
|
+
promise.reject("NO_NODES", "No connected WearOS devices found");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
String appUri = PLAY_STORE_APP_URI + packageName;
|
|
70
|
+
boolean installationSent = false;
|
|
71
|
+
|
|
72
|
+
for (Node node : nodes) {
|
|
73
|
+
try {
|
|
74
|
+
Task<Integer> sendTask = Wearable.getMessageClient(context)
|
|
75
|
+
.sendMessage(node.getId(), "/install_watchface", appUri.getBytes());
|
|
76
|
+
Tasks.await(sendTask);
|
|
77
|
+
installationSent = true;
|
|
78
|
+
} catch (Exception e) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (installationSent) {
|
|
84
|
+
if (getCurrentActivity() != null) {
|
|
85
|
+
final Context finalContext = context;
|
|
86
|
+
getCurrentActivity().runOnUiThread(() ->
|
|
87
|
+
Toast.makeText(finalContext, "Check your watch for installation prompt", Toast.LENGTH_LONG).show()
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
promise.resolve(null);
|
|
91
|
+
} else {
|
|
92
|
+
promise.reject("INSTALL_FAILED", "Failed to send installation request to any connected device");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
96
|
+
promise.reject("INSTALL_FAILED", "Failed to communicate with WearOS device: " + e.getMessage());
|
|
97
|
+
} catch (Exception e) {
|
|
98
|
+
promise.reject("INSTALL_FAILED", "Unexpected error: " + e.getMessage());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@ReactMethod
|
|
103
|
+
public void getConnectedDevices(Promise promise) {
|
|
104
|
+
try {
|
|
105
|
+
Context context = getContext();
|
|
106
|
+
|
|
107
|
+
NodeClient nodeClient = Wearable.getNodeClient(context);
|
|
108
|
+
Task<Set<Node>> nodesTask = nodeClient.getConnectedNodes();
|
|
109
|
+
Set<Node> nodes = Tasks.await(nodesTask);
|
|
110
|
+
|
|
111
|
+
CapabilityClient capabilityClient = Wearable.getCapabilityClient(context);
|
|
112
|
+
|
|
113
|
+
WritableArray devicesArray = Arguments.createArray();
|
|
114
|
+
|
|
115
|
+
for (Node node : nodes) {
|
|
116
|
+
WritableMap deviceMap = Arguments.createMap();
|
|
117
|
+
deviceMap.putString("id", node.getId());
|
|
118
|
+
deviceMap.putString("displayName", node.getDisplayName());
|
|
119
|
+
deviceMap.putBoolean("isNearby", node.isNearby());
|
|
120
|
+
deviceMap.putString("type", "watch");
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
Task<CapabilityInfo> capabilityTask = capabilityClient.getCapability(
|
|
124
|
+
"wear_app_runtime", CapabilityClient.FILTER_REACHABLE);
|
|
125
|
+
CapabilityInfo capabilityInfo = Tasks.await(capabilityTask);
|
|
126
|
+
|
|
127
|
+
boolean isWearOS = false;
|
|
128
|
+
for (Node capableNode : capabilityInfo.getNodes()) {
|
|
129
|
+
if (capableNode.getId().equals(node.getId())) {
|
|
130
|
+
isWearOS = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
deviceMap.putString("platform", isWearOS ? "wearOS" : "unknown");
|
|
135
|
+
} catch (Exception e) {
|
|
136
|
+
deviceMap.putString("platform", "wearOS");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
devicesArray.pushMap(deviceMap);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
promise.resolve(devicesArray);
|
|
143
|
+
|
|
144
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
145
|
+
promise.reject("DETECTION_FAILED", "Failed to get connected devices: " + e.getMessage());
|
|
146
|
+
} catch (Exception e) {
|
|
147
|
+
promise.reject("DETECTION_FAILED", "Unexpected error: " + e.getMessage());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@ReactMethod
|
|
152
|
+
public void isAppInstalledOnWatch(String packageName, Promise promise) {
|
|
153
|
+
try {
|
|
154
|
+
Context context = getContext();
|
|
155
|
+
|
|
156
|
+
CapabilityClient capabilityClient = Wearable.getCapabilityClient(context);
|
|
157
|
+
Task<CapabilityInfo> capabilityTask = capabilityClient.getCapability(
|
|
158
|
+
packageName, CapabilityClient.FILTER_ALL);
|
|
159
|
+
CapabilityInfo capabilityInfo = Tasks.await(capabilityTask);
|
|
160
|
+
|
|
161
|
+
Set<Node> capableNodes = capabilityInfo.getNodes();
|
|
162
|
+
|
|
163
|
+
WritableMap result = Arguments.createMap();
|
|
164
|
+
result.putBoolean("isInstalled", !capableNodes.isEmpty());
|
|
165
|
+
|
|
166
|
+
WritableArray nodeIds = Arguments.createArray();
|
|
167
|
+
for (Node node : capableNodes) {
|
|
168
|
+
nodeIds.pushString(node.getId());
|
|
169
|
+
}
|
|
170
|
+
result.putArray("installedOnNodes", nodeIds);
|
|
171
|
+
|
|
172
|
+
promise.resolve(result);
|
|
173
|
+
|
|
174
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
175
|
+
promise.reject("CHECK_FAILED", "Failed to check app installation: " + e.getMessage());
|
|
176
|
+
} catch (Exception e) {
|
|
177
|
+
promise.reject("CHECK_FAILED", "Unexpected error: " + e.getMessage());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@ReactMethod
|
|
182
|
+
public void sendMessageToWatch(String nodeId, String path, String data, Promise promise) {
|
|
183
|
+
try {
|
|
184
|
+
Context context = getContext();
|
|
185
|
+
|
|
186
|
+
Task<Integer> sendTask = Wearable.getMessageClient(context)
|
|
187
|
+
.sendMessage(nodeId, path, data.getBytes());
|
|
188
|
+
Tasks.await(sendTask);
|
|
189
|
+
|
|
190
|
+
promise.resolve(null);
|
|
191
|
+
|
|
192
|
+
} catch (ExecutionException | InterruptedException e) {
|
|
193
|
+
promise.reject("MESSAGE_FAILED", "Failed to send message: " + e.getMessage());
|
|
194
|
+
} catch (Exception e) {
|
|
195
|
+
promise.reject("MESSAGE_FAILED", "Unexpected error: " + e.getMessage());
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,16 +1,85 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
// Import TurboModule spec for RN 0.77+ compatibility
|
|
4
|
+
let PatriotNativeModule: any;
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
6
|
+
try {
|
|
7
|
+
// Try to use TurboModule first (RN 0.77+)
|
|
8
|
+
PatriotNativeModule = require('./src/NativePatriotNative').default;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
// Fallback to legacy NativeModules (RN < 0.77)
|
|
11
|
+
PatriotNativeModule = NativeModules.PatriotNative;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
if (!PatriotNativeModule) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`PatriotNative module is not properly linked. Please check your installation:
|
|
17
|
+
1. Run 'npx react-native clean' and rebuild
|
|
18
|
+
2. Ensure Android dependencies are properly installed
|
|
19
|
+
3. Check that your React Native version is 0.60+`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
export interface ConnectedDevice {
|
|
24
|
+
id: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
isNearby: boolean;
|
|
27
|
+
type: string;
|
|
28
|
+
platform: string;
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
export
|
|
16
|
-
|
|
31
|
+
export interface AppInstallStatus {
|
|
32
|
+
isInstalled: boolean;
|
|
33
|
+
installedOnNodes: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Install a watch face on connected WearOS devices.
|
|
38
|
+
*/
|
|
39
|
+
export const installWatchface = (packageName: string): Promise<void> => {
|
|
40
|
+
if (Platform.OS !== 'android') {
|
|
41
|
+
return Promise.reject(new Error('PatriotNative is only supported on Android'));
|
|
42
|
+
}
|
|
43
|
+
return PatriotNativeModule.installWatchface(packageName);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all connected WearOS devices with full details (id, name, proximity, type, platform).
|
|
48
|
+
* Returns an empty array if no devices are connected.
|
|
49
|
+
*/
|
|
50
|
+
export const getConnectedDevices = (): Promise<ConnectedDevice[]> => {
|
|
51
|
+
if (Platform.OS !== 'android') {
|
|
52
|
+
return Promise.reject(new Error('PatriotNative is only supported on Android'));
|
|
53
|
+
}
|
|
54
|
+
return PatriotNativeModule.getConnectedDevices();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a specific app/capability is installed on connected watches.
|
|
59
|
+
*/
|
|
60
|
+
export const isAppInstalledOnWatch = (packageName: string): Promise<AppInstallStatus> => {
|
|
61
|
+
if (Platform.OS !== 'android') {
|
|
62
|
+
return Promise.reject(new Error('PatriotNative is only supported on Android'));
|
|
63
|
+
}
|
|
64
|
+
return PatriotNativeModule.isAppInstalledOnWatch(packageName);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Send a custom message to a specific watch node.
|
|
69
|
+
*/
|
|
70
|
+
export const sendMessageToWatch = (nodeId: string, path: string, data: string = ''): Promise<void> => {
|
|
71
|
+
if (Platform.OS !== 'android') {
|
|
72
|
+
return Promise.reject(new Error('PatriotNative is only supported on Android'));
|
|
73
|
+
}
|
|
74
|
+
return PatriotNativeModule.sendMessageToWatch(nodeId, path, data);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Backward compatibility
|
|
78
|
+
/** @deprecated Use getConnectedDevices() instead. Returns the first device or { isDisconnected: true }. */
|
|
79
|
+
export const getConnectedWatchProperties = async (): Promise<ConnectedDevice & { isDisconnected?: boolean }> => {
|
|
80
|
+
const devices = await getConnectedDevices();
|
|
81
|
+
if (devices.length === 0) {
|
|
82
|
+
return { isDisconnected: true } as any;
|
|
83
|
+
}
|
|
84
|
+
return devices[0];
|
|
85
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haykmkrtich/react-native-patriot-native",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"files": [
|
|
6
6
|
"index.ts",
|
|
7
|
+
"src/",
|
|
7
8
|
"android/"
|
|
8
9
|
],
|
|
9
|
-
"keywords": [
|
|
10
|
+
"keywords": [
|
|
11
|
+
"react-native",
|
|
12
|
+
"wearos",
|
|
13
|
+
"watchface",
|
|
14
|
+
"16kb-pages",
|
|
15
|
+
"rn-0.77",
|
|
16
|
+
"turbomodule"
|
|
17
|
+
],
|
|
10
18
|
"author": "Hayk Mkrtich",
|
|
11
19
|
"license": "MIT",
|
|
12
20
|
"repository": {
|
|
@@ -15,5 +23,25 @@
|
|
|
15
23
|
},
|
|
16
24
|
"publishConfig": {
|
|
17
25
|
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react-native": ">=0.60.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=16.0.0"
|
|
32
|
+
},
|
|
33
|
+
"react-native": {
|
|
34
|
+
"android": {
|
|
35
|
+
"sourceDir": "android",
|
|
36
|
+
"packageImportPath": "import com.patriotnative.PatriotNativePackage;"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"codegenConfig": {
|
|
40
|
+
"name": "PatriotNative",
|
|
41
|
+
"type": "modules",
|
|
42
|
+
"jsSrcsDir": "src",
|
|
43
|
+
"android": {
|
|
44
|
+
"javaPackageName": "com.patriotnative"
|
|
45
|
+
}
|
|
18
46
|
}
|
|
19
47
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is the JavaScript spec for PatriotNative TurboModule
|
|
3
|
+
* This file is required for React Native 0.77+ Codegen support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TurboModule } from 'react-native';
|
|
7
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
8
|
+
|
|
9
|
+
export interface Spec extends TurboModule {
|
|
10
|
+
installWatchface(packageName: string): Promise<void>;
|
|
11
|
+
getConnectedDevices(): Promise<Object>;
|
|
12
|
+
isAppInstalledOnWatch(packageName: string): Promise<Object>;
|
|
13
|
+
sendMessageToWatch(nodeId: string, path: string, data: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('PatriotNative');
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
package com.patriotnative;
|
|
3
|
-
|
|
4
|
-
import android.content.Intent;
|
|
5
|
-
import android.net.Uri;
|
|
6
|
-
import android.os.Handler;
|
|
7
|
-
import android.os.Looper;
|
|
8
|
-
import android.widget.Toast;
|
|
9
|
-
|
|
10
|
-
import androidx.annotation.NonNull;
|
|
11
|
-
import androidx.wear.remote.interactions.RemoteActivityHelper;
|
|
12
|
-
|
|
13
|
-
import com.facebook.react.bridge.ReactApplicationContext;
|
|
14
|
-
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
15
|
-
import com.facebook.react.bridge.ReactMethod;
|
|
16
|
-
import com.facebook.react.bridge.Promise;
|
|
17
|
-
|
|
18
|
-
import com.google.android.gms.tasks.Task;
|
|
19
|
-
import com.google.android.gms.tasks.Tasks;
|
|
20
|
-
import com.google.android.gms.wearable.Node;
|
|
21
|
-
import com.google.android.gms.wearable.Wearable;
|
|
22
|
-
|
|
23
|
-
import java.util.List;
|
|
24
|
-
import java.util.concurrent.Executors;
|
|
25
|
-
import com.facebook.react.bridge.WritableMap;
|
|
26
|
-
import com.facebook.react.bridge.Arguments;
|
|
27
|
-
|
|
28
|
-
public class PatriotNativeModule extends ReactContextBaseJavaModule {
|
|
29
|
-
private final ReactApplicationContext reactContext;
|
|
30
|
-
|
|
31
|
-
public PatriotNativeModule(ReactApplicationContext context) {
|
|
32
|
-
super(context);
|
|
33
|
-
this.reactContext = context;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@NonNull
|
|
37
|
-
@Override
|
|
38
|
-
public String getName() {
|
|
39
|
-
return "PatriotNative";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@ReactMethod
|
|
43
|
-
public void installWatchface(String packageName, Promise promise) {
|
|
44
|
-
new Thread(() -> {
|
|
45
|
-
try {
|
|
46
|
-
Task<List<Node>> nodeListTask = Wearable.getNodeClient(reactContext).getConnectedNodes();
|
|
47
|
-
List<Node> nodes = Tasks.await(nodeListTask);
|
|
48
|
-
|
|
49
|
-
if (nodes.isEmpty()) {
|
|
50
|
-
showToast("Watch not connected");
|
|
51
|
-
promise.reject("NO_NODES", "No connected WearOS device found.");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (Node node : nodes) {
|
|
56
|
-
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
57
|
-
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
|
58
|
-
intent.setData(Uri.parse("market://details?id=" + packageName));
|
|
59
|
-
|
|
60
|
-
RemoteActivityHelper helper = new RemoteActivityHelper(reactContext, Executors.newSingleThreadExecutor());
|
|
61
|
-
helper.startRemoteActivity(intent, node.getId());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
showToast("Check your watch");
|
|
65
|
-
promise.resolve(null);
|
|
66
|
-
} catch (Exception e) {
|
|
67
|
-
promise.reject("INSTALL_FAILED", e.getMessage());
|
|
68
|
-
}
|
|
69
|
-
}).start();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@ReactMethod
|
|
73
|
-
public void getConnectedWatchProperties(Promise promise) {
|
|
74
|
-
Executors.newSingleThreadExecutor().execute(() -> {
|
|
75
|
-
try {
|
|
76
|
-
List<Node> nodes = Tasks.await(Wearable.getNodeClient(reactContext).getConnectedNodes());
|
|
77
|
-
if (nodes == null || nodes.isEmpty()) {
|
|
78
|
-
WritableMap result = Arguments.createMap();
|
|
79
|
-
result.putBoolean("isDisconnected", true);
|
|
80
|
-
promise.resolve(result);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (Node node : nodes) {
|
|
85
|
-
String displayName = node.getDisplayName().toLowerCase();
|
|
86
|
-
|
|
87
|
-
// Простой фильтр: часы по ключевым словам
|
|
88
|
-
if (displayName.contains("watch") || displayName.contains("fitbit") || displayName.contains("wear")) {
|
|
89
|
-
WritableMap map = Arguments.createMap();
|
|
90
|
-
map.putString("id", node.getId());
|
|
91
|
-
map.putString("displayName", node.getDisplayName());
|
|
92
|
-
map.putBoolean("isNearby", node.isNearby());
|
|
93
|
-
map.putString("type", "watch");
|
|
94
|
-
map.putString("platform", displayName.contains("fitbit") ? "fitbit" : "wearOS");
|
|
95
|
-
|
|
96
|
-
promise.resolve(map);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Если ни одно устройство не подошло
|
|
102
|
-
WritableMap result = Arguments.createMap();
|
|
103
|
-
result.putBoolean("isDisconnected", true);
|
|
104
|
-
promise.resolve(result);
|
|
105
|
-
} catch (Exception e) {
|
|
106
|
-
promise.reject("WATCH_DETECTION_FAILED", e.getMessage());
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
private void showToast(String msg) {
|
|
113
|
-
new Handler(Looper.getMainLooper()).post(() ->
|
|
114
|
-
Toast.makeText(reactContext, msg, Toast.LENGTH_SHORT).show());
|
|
115
|
-
}
|
|
116
|
-
}
|