Skip to content

Commit 3762649

Browse files
authored
Receive audio or files from WearOS (#41)
1 parent 35b1b8c commit 3762649

File tree

11 files changed

+446
-123
lines changed

11 files changed

+446
-123
lines changed

README.md

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,30 +53,75 @@ Add the following entry to your `android/app/src/main/AndroidManifest.xml` (full
5353

5454
## React Native API Documentation
5555

56+
The example of implementation available in the [CounterScreen](example/src/CounterScreen/index.android.tsx).
57+
5658
### Send Messages
5759

60+
https://mtford.co.uk/projects/react-native-watch-connectivity/docs/communication/
61+
5862
```js
5963
import { sendMessage } from 'react-native-wear-connectivity';
6064

61-
sendMessage({ text: 'Hello watch!' });
65+
sendMessage({ text: 'Hello watch!' }, (reply) => {
66+
console.log(reply); // {"text": "Hello React Native app!"}
67+
});
6268
```
6369

6470
### Receive Messages
6571

72+
https://mtford.co.uk/projects/react-native-watch-connectivity/docs/communication/
73+
6674
```js
6775
import { watchEvents } from 'react-native-wear-connectivity';
6876

69-
const unsubscribe = watchEvents.on('message', (message) => {
77+
const unsubscribe = watchEvents.on('message', (message, reply) => {
7078
console.log('received message from watch', message);
79+
/*
80+
* reply is not supported on Android
81+
* reply({ text: 'Thanks watch!' });
82+
*/
7183
});
7284
```
7385

7486
### Send Files
7587

88+
https://mtford.co.uk/projects/react-native-watch-connectivity/docs/files/
89+
90+
```js
91+
import { startFileTransfer } from 'react-native-wear-connectivity';
92+
93+
const metadata = {};
94+
95+
const { id } = await startFileTransfer('file:///path/to/file', metadata);
96+
97+
console.log(`Started a new file transfer with id ${id}`);
98+
```
99+
100+
### Monitor File Transfers
101+
102+
https://mtford.co.uk/projects/react-native-watch-connectivity/docs/files/
103+
76104
```js
77-
import { sendFile } from 'react-native-wear-connectivity';
105+
import { monitorFileTransfers } from 'react-native-wear-connectivity';
106+
107+
const cancel = monitorFileTransfers((event) => {
108+
const {
109+
type, // started | progress | finished | error
110+
completedUnitCount, // num bytes completed
111+
estimatedTimeRemaining,
112+
fractionCompleted,
113+
throughput, // Bit rate
114+
totalUnitCount, // total num. bytes
115+
url, // url of file being transferred
116+
metadata, // file metadata
117+
id, // id === transferId
118+
startTime, // time that the file transfer started
119+
endTime, // time that the file transfer ended
120+
error, // null or [Error] if the file transfer failed
121+
} = transferInfo;
122+
});
78123

79-
await sendFile(filePath);
124+
cancel();
80125
```
81126

82127
## Jetpack Compose API Documentation

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

Lines changed: 163 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.wearconnectivity;
22

3-
import android.util.Log;
3+
import android.webkit.MimeTypeMap;
4+
45
import com.facebook.common.logging.FLog;
6+
import com.facebook.react.bridge.Arguments;
7+
import com.facebook.react.bridge.LifecycleEventListener;
58
import com.facebook.react.bridge.Promise;
9+
import com.facebook.react.bridge.WritableMap;
610
import com.google.android.gms.tasks.Task;
711
import com.google.android.gms.wearable.Asset;
812
import com.google.android.gms.wearable.DataClient;
@@ -15,20 +19,38 @@
1519
import java.io.FileInputStream;
1620
import java.io.FileOutputStream;
1721
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.net.URLConnection;
24+
import java.io.BufferedInputStream;
25+
import java.io.IOException;
26+
27+
import androidx.annotation.NonNull;
28+
import com.facebook.react.modules.core.DeviceEventManagerModule;
29+
import com.google.android.gms.tasks.OnFailureListener;
30+
import com.google.android.gms.tasks.OnSuccessListener;
31+
import com.google.android.gms.wearable.DataEvent;
32+
import com.google.android.gms.wearable.DataEventBuffer;
33+
import com.google.android.gms.wearable.DataMap;
34+
import com.google.android.gms.wearable.DataMapItem;
1835

19-
public class WearConnectivityDataClient {
36+
public class WearConnectivityDataClient implements DataClient.OnDataChangedListener, LifecycleEventListener {
2037
private static final String TAG = "WearConnectivityDataClient";
2138
private DataClient dataClient;
2239
private static ReactApplicationContext reactContext;
40+
private String fileName = "unknown_file";
41+
private long startTime;
42+
private int totalBytes;
2343

2444
public WearConnectivityDataClient(ReactApplicationContext context) {
2545
dataClient = Wearable.getDataClient(context);
2646
reactContext = context;
47+
dataClient.addListener(this);
48+
context.addLifecycleEventListener(this);
2749
}
2850

2951
/**
3052
* Sends a file (as an Asset) using the DataClient API.
31-
* @param path to the file to be sent.
53+
* @param uri path to the file to be sent.
3254
*/
3355
public void sendFile(String uri, Promise promise) {
3456
File file = new File(uri);
@@ -37,7 +59,6 @@ public void sendFile(String uri, Promise promise) {
3759
FLog.w(TAG, "Failed to create asset from file.");
3860
return;
3961
}
40-
// Create a DataMapRequest with a defined path.
4162
PutDataMapRequest dataMapRequest = PutDataMapRequest.create("/file_transfer");
4263
dataMapRequest.getDataMap().putAsset("file", asset);
4364
dataMapRequest.getDataMap().putLong("timestamp", System.currentTimeMillis());
@@ -50,6 +71,28 @@ public void sendFile(String uri, Promise promise) {
5071
});
5172
}
5273

74+
@Override
75+
public void onDataChanged(@NonNull DataEventBuffer dataEvents) {
76+
for (DataEvent event : dataEvents) {
77+
if (event.getType() == DataEvent.TYPE_CHANGED) {
78+
DataItem item = event.getDataItem();
79+
if (item.getUri().getPath().equals("/file_transfer")) {
80+
DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
81+
// Extract metadata from the DataMap
82+
if (dataMap.containsKey("metadata")) {
83+
DataMap metadata = dataMap.getDataMap("metadata");
84+
fileName = metadata.getString("fileName", "unknown_file");
85+
}
86+
87+
Asset asset = dataMap.getAsset("file");
88+
if (asset != null) {
89+
receiveFile(asset);
90+
}
91+
}
92+
}
93+
}
94+
}
95+
5396
/**
5497
* Helper method to create an Asset from a file.
5598
* @param file the file to convert.
@@ -71,4 +114,120 @@ private Asset createAssetFromFile(File file) {
71114
private static ReactApplicationContext getReactContext() {
72115
return reactContext;
73116
}
117+
118+
private void receiveFile(Asset asset) {
119+
Task<DataClient.GetFdForAssetResponse> task = dataClient.getFdForAsset(asset);
120+
startTime = System.currentTimeMillis();
121+
122+
// Dispatch 'started' event
123+
dispatchFileTransferEvent("started", startTime, 0, 0, 0, 0, fileName, null);
124+
task.addOnSuccessListener(this::handleFileReceived)
125+
.addOnFailureListener(this::handleFileReceiveError);
126+
}
127+
128+
129+
/**
130+
* Dispatches a file transfer event to React Native.
131+
*/
132+
private void dispatchFileTransferEvent(
133+
String type, long startTime, long completedUnitCount, long estimatedTimeRemaining,
134+
float fractionCompleted, long throughput, String fileName, String errorMessage) {
135+
WritableMap event = Arguments.createMap();
136+
String correctPath = "/data/data/" + getReactContext().getPackageName() + "/files/" + fileName;
137+
FLog.w(TAG, "WatchFileReceived filePath: " + correctPath);
138+
event.putString("type", type);
139+
event.putString("url", correctPath);
140+
event.putString("id", fileName);
141+
event.putDouble("startTime", startTime);
142+
event.putDouble("endTime", type.equals("finished") ? System.currentTimeMillis() : 0);
143+
event.putDouble("completedUnitCount", completedUnitCount);
144+
event.putDouble("estimatedTimeRemaining", estimatedTimeRemaining);
145+
event.putDouble("fractionCompleted", fractionCompleted);
146+
event.putDouble("throughput", throughput);
147+
event.putMap("metadata", getFileMetadata(fileName)); // Get metadata if available
148+
if (errorMessage != null) {
149+
event.putString("error", errorMessage);
150+
} else {
151+
event.putNull("error");
152+
}
153+
154+
getReactContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
155+
.emit("FileTransferEvent", event);
156+
}
157+
158+
/**
159+
* Retrieves metadata associated with a file.
160+
*/
161+
private WritableMap getFileMetadata(String fileName) {
162+
WritableMap metadata = Arguments.createMap();
163+
metadata.putString("fileName", fileName);
164+
metadata.putString("fileType", MimeTypeMap.getFileExtensionFromUrl(fileName));
165+
return metadata;
166+
}
167+
168+
private void handleFileReceived(DataClient.GetFdForAssetResponse response) {
169+
InputStream is = response.getInputStream();
170+
if (is == null) {
171+
FLog.w(TAG, "WatchFileReceiveError: InputStream is null");
172+
return;
173+
}
174+
175+
try {
176+
File file = new File(getReactContext().getFilesDir(), fileName);
177+
totalBytes = response.getInputStream().available();
178+
179+
saveFile(is, file);
180+
dispatchFileTransferEvent("finished", startTime, totalBytes, 0, 1.0f, 0, fileName, null);
181+
} catch (IOException e) {
182+
dispatchFileTransferEvent("error", startTime, 0, 0, 0, 0, fileName, e.getMessage());
183+
}
184+
}
185+
186+
private void handleFileReceiveError(@NonNull Exception e) {
187+
dispatchFileTransferEvent("error", startTime, 0, 0, 0, 0, fileName, e.toString());
188+
}
189+
190+
private void dispatchEvent(String eventName, String body) {
191+
getReactContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
192+
.emit(eventName, body);
193+
}
194+
195+
private void saveFile(InputStream is, File file) throws IOException {
196+
FileOutputStream fos = new FileOutputStream(file);
197+
byte[] buffer = new byte[1024];
198+
int bytesRead;
199+
long completedBytes = 0;
200+
201+
while ((bytesRead = is.read(buffer)) != -1) {
202+
fos.write(buffer, 0, bytesRead);
203+
completedBytes += bytesRead;
204+
205+
// Calculate progress metrics
206+
float fractionCompleted = (float) completedBytes / totalBytes;
207+
long elapsedTime = System.currentTimeMillis() - startTime;
208+
long estimatedTimeRemaining = (long) ((1 - fractionCompleted) * elapsedTime / fractionCompleted);
209+
long throughput = completedBytes * 8 / (elapsedTime + 1); // Avoid division by zero
210+
211+
// Dispatch 'progress' event
212+
dispatchFileTransferEvent("progress", startTime, completedBytes, estimatedTimeRemaining, fractionCompleted, throughput, fileName, null);
213+
}
214+
fos.flush();
215+
fos.close();
216+
is.close();
217+
}
218+
219+
@Override
220+
public void onHostResume() {
221+
// do nothing
222+
}
223+
224+
@Override
225+
public void onHostPause() {
226+
// do nothing
227+
}
228+
229+
@Override
230+
public void onHostDestroy() {
231+
dataClient.removeListener(this);
232+
}
74233
}

0 commit comments

Comments
 (0)