Skip to content

Commit c36db4d

Browse files
authored
Add support for file transfers (#39)
1 parent 4063eb8 commit c36db4d

File tree

8 files changed

+334
-89
lines changed

8 files changed

+334
-89
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ const unsubscribe = watchEvents.on('message', (message) => {
7171
});
7272
```
7373

74+
### Send Files
75+
76+
```js
77+
import { sendFile } from 'react-native-wear-connectivity';
78+
79+
await sendFile(filePath);
80+
```
81+
7482
## Jetpack Compose API Documentation
7583

7684
### Send Messages
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.wearconnectivity;
2+
3+
import android.util.Log;
4+
import com.facebook.common.logging.FLog;
5+
import com.facebook.react.bridge.Promise;
6+
import com.google.android.gms.tasks.Task;
7+
import com.google.android.gms.wearable.Asset;
8+
import com.google.android.gms.wearable.DataClient;
9+
import com.google.android.gms.wearable.DataItem;
10+
import com.google.android.gms.wearable.PutDataMapRequest;
11+
import com.google.android.gms.wearable.PutDataRequest;
12+
import com.google.android.gms.wearable.Wearable;
13+
import com.facebook.react.bridge.ReactApplicationContext;
14+
import java.io.File;
15+
import java.io.FileInputStream;
16+
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
19+
public class WearConnectivityDataClient {
20+
private static final String TAG = "WearConnectivityDataClient";
21+
private DataClient dataClient;
22+
private static ReactApplicationContext reactContext;
23+
24+
public WearConnectivityDataClient(ReactApplicationContext context) {
25+
dataClient = Wearable.getDataClient(context);
26+
reactContext = context;
27+
}
28+
29+
/**
30+
* Sends a file (as an Asset) using the DataClient API.
31+
* @param path to the file to be sent.
32+
*/
33+
public void sendFile(String uri, Promise promise) {
34+
File file = new File(uri);
35+
Asset asset = createAssetFromFile(file);
36+
if (asset == null) {
37+
FLog.w(TAG, "Failed to create asset from file.");
38+
return;
39+
}
40+
// Create a DataMapRequest with a defined path.
41+
PutDataMapRequest dataMapRequest = PutDataMapRequest.create("/file_transfer");
42+
dataMapRequest.getDataMap().putAsset("file", asset);
43+
dataMapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis());
44+
PutDataRequest request = dataMapRequest.asPutDataRequest();
45+
Task<DataItem> task = dataClient.putDataItem(request);
46+
task.addOnSuccessListener(dataItem -> {
47+
promise.resolve("File sent successfully via DataClient.");
48+
}).addOnFailureListener(e -> {
49+
promise.reject("File sending failed: " + e);
50+
});
51+
}
52+
53+
/**
54+
* Helper method to create an Asset from a file.
55+
* @param file the file to convert.
56+
* @return the resulting Asset, or null if an error occurred.
57+
*/
58+
private Asset createAssetFromFile(File file) {
59+
try {
60+
FileInputStream fileInputStream = new FileInputStream(file);
61+
byte[] byteArray = new byte[(int) file.length()];
62+
fileInputStream.read(byteArray);
63+
fileInputStream.close();
64+
return Asset.createFromBytes(byteArray);
65+
} catch (IOException e) {
66+
e.printStackTrace();
67+
return null;
68+
}
69+
}
70+
71+
private static ReactApplicationContext getReactContext() {
72+
return reactContext;
73+
}
74+
}

android/src/main/java/com/wearconnectivity/WearConnectivityModule.java

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
package com.wearconnectivity;
22

3-
import android.app.ActivityManager;
4-
import android.content.Context;
53
import android.os.Build;
64
import android.util.Log;
75
import androidx.annotation.NonNull;
8-
import androidx.annotation.Nullable;
96
import com.facebook.common.logging.FLog;
107
import com.facebook.react.bridge.Arguments;
118
import com.facebook.react.bridge.Callback;
129
import com.facebook.react.bridge.JSONArguments;
1310
import com.facebook.react.bridge.LifecycleEventListener;
11+
import com.facebook.react.bridge.Promise;
1412
import com.facebook.react.bridge.ReactApplicationContext;
15-
import com.facebook.react.bridge.ReactContext;
1613
import com.facebook.react.bridge.ReactMethod;
1714
import com.facebook.react.bridge.ReadableMap;
1815
import com.facebook.react.bridge.WritableMap;
19-
import com.facebook.react.modules.core.DeviceEventManagerModule;
2016
import com.google.android.gms.common.ConnectionResult;
2117
import com.google.android.gms.tasks.OnFailureListener;
2218
import com.google.android.gms.tasks.OnSuccessListener;
@@ -27,6 +23,7 @@
2723
import com.google.android.gms.wearable.Node;
2824
import com.google.android.gms.wearable.NodeClient;
2925
import com.google.android.gms.wearable.Wearable;
26+
3027
import java.util.List;
3128
import org.json.JSONException;
3229
import org.json.JSONObject;
@@ -43,7 +40,10 @@ public class WearConnectivityModule extends WearConnectivitySpec
4340
private static ReactApplicationContext reactContext;
4441
public static final String NAME = "WearConnectivity";
4542
private static final String TAG = "react-native-wear-connectivity ";
46-
private final MessageClient client;
43+
private final MessageClient messageClient;
44+
private final WearConnectivityDataClient dataClient;
45+
private boolean isListenerAdded = false;
46+
4747
private String CLIENT_ADDED =
4848
TAG + "onMessageReceived listener added when activity is created. Client receives messages.";
4949
private String NO_NODES_FOUND = TAG + "sendMessage failed. No connected nodes found.";
@@ -61,9 +61,10 @@ public class WearConnectivityModule extends WearConnectivitySpec
6161
super(context);
6262
reactContext = context;
6363
context.addLifecycleEventListener(this);
64-
client = Wearable.getMessageClient(context);
64+
messageClient = Wearable.getMessageClient(context);
65+
dataClient = new WearConnectivityDataClient(context);
6566
Log.d(TAG, CLIENT_ADDED);
66-
client.addListener(this);
67+
messageClient.addListener(this);
6768
}
6869

6970
@Override
@@ -72,32 +73,19 @@ public String getName() {
7273
return NAME;
7374
}
7475

75-
private List<Node> retrieveNodes(Callback errorCb) {
76-
try {
77-
int result = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getReactApplicationContext());
78-
ConnectionResult connectionResult = new ConnectionResult(result);
79-
if (!connectionResult.isSuccess()) {
80-
errorCb.invoke( MISSING_GOOGLE_PLAY_SERVICES + connectionResult.getErrorMessage());
81-
return null;
82-
}
83-
NodeClient nodeClient = Wearable.getNodeClient(getReactApplicationContext());
84-
try {
85-
Tasks.await(GoogleApiAvailability.getInstance().checkApiAvailability(nodeClient));
86-
} catch (Exception e) {
87-
errorCb.invoke(INSTALL_GOOGLE_PLAY_WEARABLE + e);
88-
return null;
89-
}
90-
return Tasks.await(nodeClient.getConnectedNodes());
91-
} catch (Exception e) {
92-
errorCb.invoke(RETRIEVE_NODES_FAILED + e);
93-
return null;
76+
@ReactMethod
77+
public void sendFile(String fileName, Promise promise) {
78+
if (dataClient != null) {
79+
dataClient.sendFile(fileName, promise);
80+
} else {
81+
promise.reject("E_SEND_FAILED", "Failed to send file");
9482
}
9583
}
9684

9785
@ReactMethod
9886
public void sendMessage(ReadableMap messageData, Callback replyCb, Callback errorCb) {
9987
List<Node> connectedNodes = retrieveNodes(errorCb);
100-
if (connectedNodes != null && connectedNodes.size() > 0 && client != null) {
88+
if (connectedNodes != null && connectedNodes.size() > 0 && messageClient != null) {
10189
for (Node connectedNode : connectedNodes) {
10290
if (connectedNode.isNearby()) {
10391
sendMessageToClient(messageData, connectedNode, replyCb, errorCb);
@@ -122,7 +110,7 @@ private void sendMessageToClient(
122110
try {
123111
// the last parameter is for file transfer (for ex. audio)
124112
JSONObject messageJSON = new JSONObject(messageData.toHashMap());
125-
Task<Integer> sendTask = client.sendMessage(node.getId(), messageJSON.toString(), null);
113+
Task<Integer> sendTask = messageClient.sendMessage(node.getId(), messageJSON.toString(), null);
126114
sendTask.addOnSuccessListener(onSuccessListener);
127115
sendTask.addOnFailureListener(onFailureListener);
128116
} catch (Exception e) {
@@ -155,28 +143,52 @@ public void onMessageReceived(MessageEvent messageEvent) {
155143
}
156144
}
157145

158-
public static ReactApplicationContext getReactContext() {
159-
return reactContext;
160-
}
161-
162146
@Override
163147
public void onHostResume() {
164-
if (client != null) {
165-
Log.d(TAG, ADD_CLIENT);
166-
client.addListener(this);
148+
if (messageClient != null && !isListenerAdded) {
149+
Log.d(TAG, "Adding listener on host resume");
150+
messageClient.addListener(this);
151+
isListenerAdded = true;
167152
}
168153
}
169154

170155
@Override
171156
public void onHostPause() {
172-
// Log.d(TAG, REMOVE_CLIENT);
173-
// removed this to allow to send updates when app is in the background
174-
// client.removeListener(this);
157+
Log.d(TAG, "onHostPause: leaving listener active for background events");
175158
}
176159

177160
@Override
178161
public void onHostDestroy() {
179-
Log.d(TAG, REMOVE_CLIENT);
180-
client.removeListener(this);
162+
if (messageClient != null && isListenerAdded) {
163+
Log.d(TAG, "Removing listener on host destroy");
164+
messageClient.removeListener(this);
165+
isListenerAdded = false;
166+
}
167+
}
168+
169+
private List<Node> retrieveNodes(Callback errorCb) {
170+
try {
171+
int result = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getReactContext());
172+
ConnectionResult connectionResult = new ConnectionResult(result);
173+
if (!connectionResult.isSuccess()) {
174+
errorCb.invoke( MISSING_GOOGLE_PLAY_SERVICES + connectionResult.getErrorMessage());
175+
return null;
176+
}
177+
NodeClient nodeClient = Wearable.getNodeClient(getReactContext());
178+
try {
179+
Tasks.await(GoogleApiAvailability.getInstance().checkApiAvailability(nodeClient));
180+
} catch (Exception e) {
181+
errorCb.invoke(INSTALL_GOOGLE_PLAY_WEARABLE + e);
182+
return null;
183+
}
184+
return Tasks.await(nodeClient.getConnectedNodes());
185+
} catch (Exception e) {
186+
errorCb.invoke(RETRIEVE_NODES_FAILED + e);
187+
return null;
188+
}
189+
}
190+
191+
private static ReactApplicationContext getReactContext() {
192+
return reactContext;
181193
}
182194
}

example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
},
1212
"dependencies": {
1313
"react": "18.3.1",
14-
"react-native": "^0.77.0"
14+
"react-native": "^0.77.1",
15+
"react-native-image-picker": "^8.1.0"
1516
},
1617
"devDependencies": {
1718
"@babel/core": "^7.25.2",

example/src/CounterScreen/index.android.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import React, { useEffect } from 'react';
2-
import { View, StyleSheet, Text, Button } from 'react-native';
3-
import { sendMessage, watchEvents } from 'react-native-wear-connectivity';
2+
import { View, StyleSheet, Text, Button, Image } from 'react-native';
3+
import { launchImageLibrary } from 'react-native-image-picker';
4+
import {
5+
sendFile,
6+
sendMessage,
7+
watchEvents,
8+
} from 'react-native-wear-connectivity';
49
import type {
510
ReplyCallback,
611
ErrorCallback,
@@ -32,6 +37,24 @@ function CounterScreen() {
3237
sendMessage(json, onSuccess, onError);
3338
};
3439

40+
const sendFileToWear = async () => {
41+
try {
42+
// @ts-ignore
43+
const result = await launchImageLibrary();
44+
if (!result.assets || result.assets.length === 0) {
45+
console.log('No asset selected');
46+
return;
47+
}
48+
const asset = result.assets[0] || { uri: undefined };
49+
if (asset.uri) {
50+
const filePath = asset.uri.replace('file://', '');
51+
await sendFile(filePath);
52+
}
53+
} catch (error) {
54+
console.error('Error in sendFileToWear:', error);
55+
}
56+
};
57+
3558
return (
3659
<View style={styles.container}>
3760
<Text style={styles.counter}>{count}</Text>
@@ -40,6 +63,7 @@ function CounterScreen() {
4063
title="increase counter"
4164
onPress={sendMessageToWear}
4265
/>
66+
<Button title="send file" onPress={sendFileToWear} />
4367
</View>
4468
);
4569
}

src/NativeWearConnectivity.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ export type SendMessage = (
1212
errCb: ErrorCallback
1313
) => void;
1414

15+
export type SendFile = (file: string) => Promise<any>;
16+
1517
export interface Spec extends TurboModule {
1618
sendMessage: SendMessage;
19+
sendFile: SendFile;
1720
}
1821

1922
export default TurboModuleRegistry.getEnforcing<Spec>('WearConnectivity');

src/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { AppRegistry } from 'react-native';
22
import { NativeModules, Platform } from 'react-native';
33
import { watchEvents } from './subscriptions';
44
import { sendMessage } from './messages';
5-
import type { ReplyCallback, ErrorCallback } from './NativeWearConnectivity';
5+
import type {
6+
ReplyCallback,
7+
ErrorCallback,
8+
SendFile,
9+
} from './NativeWearConnectivity';
610
import { DeviceEventEmitter } from 'react-native';
711

812
const LINKING_ERROR =
@@ -29,7 +33,11 @@ const WearConnectivity = WearConnectivityModule
2933
}
3034
);
3135

32-
export { sendMessage, watchEvents, WearConnectivity };
36+
const sendFile: SendFile = (file) => {
37+
return WearConnectivity.sendFile(file);
38+
};
39+
40+
export { sendFile, sendMessage, watchEvents, WearConnectivity };
3341
export type { ReplyCallback, ErrorCallback };
3442

3543
type WearParameters = {

0 commit comments

Comments
 (0)