Skip to content

Commit 64b556e

Browse files
android: create native-gradle-node-folder sample
Adds a sample for a native Gradle project that starts the node runtime from a Node Project folder.
1 parent 17e4e18 commit 64b556e

File tree

174 files changed

+67732
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

174 files changed

+67732
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This is a collection of samples showcasing the use of [Node.js on Mobile](https:
44

55
It contains the following samples:
66
* Android: [Native Gradle Sample](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/android/native-gradle)
7+
* Android: [Native Gradle Sample using a Node Project folder](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/android/native-gradle-node-folder)
78
* iOS: [Native Xcode Sample](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/ios/native-xcode)
89
* iOS: [Native Xcode Sample using a Node Project folder](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/ios/native-xcode-node-folder)
910
* React-Native: [Suspend Resume Sample](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/react-native/SuspendResume)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea
5+
.DS_Store
6+
/build
7+
/captures
8+
.externalNativeBuild
9+
*.so
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# Native Gradle Sample using a Node Project folder
2+
3+
An Android Studio project that uses the [`Node.js on Mobile`]( https://github.com/janeasystems/nodejs-mobile) shared library, as an example of using a Node Project folder inside the Application.
4+
5+
The sample app runs the node.js engine in a background thread to start an HTTP server on port 3000. The app's Main Activity UI has a button to query the server and show the server's response (i.e. the `process.versions` value). Alternatively, it's also possible to access the server from a browser running on a different device connected to the same local network.
6+
7+
## How to run
8+
- Clone this project.
9+
- Download the Node.js on Mobile shared library from [here](https://github.com/janeasystems/nodejs-mobile/releases/download/nodejs-mobile-v0.1.4/nodejs-mobile-v0.1.4-android.zip).
10+
- Copy the `bin/` folder from inside the downloaded zip file to `app/libnode/bin` (There are `copy-libnode.so-here` files in each architecture's path for convenience). If it's been done correctly you'll end with the following paths for the binaries:
11+
- `app/libnode/bin/arm64-v8a/libnode.so`
12+
- `app/libnode/bin/armeabi-v7a/libnode.so`
13+
- `app/libnode/bin/x86/libnode.so`
14+
- `app/libnode/bin/x86_64/libnode.so`
15+
- In Android Studio import the `android/native-gradle/` gradle project. It will automatically check for dependencies and prompt you to install missing requirements (i.e. you may need to update the Android SDK build tools to the required version (25.0.3) and install CMake to compile the C++ file that bridges Java to the Node.js on Mobile library).
16+
- After the gradle build completes, run the app on a compatible device.
17+
18+
19+
## How the sample was developed
20+
21+
This sample was built on top of the [`native-gradle` sample from this repo](https://github.com/janeasystems/nodejs-mobile-samples/tree/master/android/native-gradle), with the same functionality, but uses a `nodejs-project` folder that contains the node part of the project.
22+
23+
### Create the `nodejs-project` folder
24+
25+
Create a `nodejs-project` folder inside the project, in Gradle's default folder for Android's application assets (`app/src/main/assets/nodejs-project`). Create the `main.js` and `package.json` files inside:
26+
27+
- `app/src/main/assets/nodejs-project/main.js` contents:
28+
```js
29+
var http = require('http');
30+
var versions_server = http.createServer( (request, response) => {
31+
response.end('Versions: ' + JSON.stringify(process.versions));
32+
});
33+
versions_server.listen(3000);
34+
console.log('The node project has started.');
35+
```
36+
37+
- `app/src/main/assets/nodejs-project/package.json` contents:
38+
```js
39+
{
40+
"name": "native-gradle-node-project",
41+
"version": "0.0.1",
42+
"description": "node part of the project",
43+
"main": "main.js",
44+
"author": "janeasystems",
45+
"license": ""
46+
}
47+
```
48+
49+
> Having a `nodejs-project` path with a `package.json` inside is helpful for using npm modules, by running `npm install {module_name}` inside `nodejs-project` so that the modules are also packaged with the application and made available at runtime.
50+
51+
### Copy the `nodejs-project` at runtime and start from there
52+
53+
To start the `Node.js` engine runtime with a file path, we need to first copy the project to somewhere in the Android file system, because the Android Application's APK is an archive file and `Node.js` won't be able to start running from there. For this purpose, we choose to copy the nodejs-project into the Application's `FilesDir`.
54+
55+
Add the helper functions to `app/src/main/java/com/yourorg/sample/MainActivity.java`:
56+
```java
57+
import android.content.Context;
58+
import android.content.res.AssetManager;
59+
60+
...
61+
62+
private static boolean deleteFolderRecursively(File file) {
63+
try {
64+
boolean res=true;
65+
for (File childFile : file.listFiles()) {
66+
if (childFile.isDirectory()) {
67+
res &= deleteFolderRecursively(childFile);
68+
} else {
69+
res &= childFile.delete();
70+
}
71+
}
72+
res &= file.delete();
73+
return res;
74+
} catch (Exception e) {
75+
e.printStackTrace();
76+
return false;
77+
}
78+
}
79+
80+
private static boolean copyAssetFolder(AssetManager assetManager, String fromAssetPath, String toPath) {
81+
try {
82+
String[] files = assetManager.list(fromAssetPath);
83+
boolean res = true;
84+
85+
if (files.length==0) {
86+
//If it's a file, it won't have any assets "inside" it.
87+
res &= copyAsset(assetManager,
88+
fromAssetPath,
89+
toPath);
90+
} else {
91+
new File(toPath).mkdirs();
92+
for (String file : files)
93+
res &= copyAssetFolder(assetManager,
94+
fromAssetPath + "/" + file,
95+
toPath + "/" + file);
96+
}
97+
return res;
98+
} catch (Exception e) {
99+
e.printStackTrace();
100+
return false;
101+
}
102+
}
103+
104+
private static boolean copyAsset(AssetManager assetManager, String fromAssetPath, String toPath) {
105+
InputStream in = null;
106+
OutputStream out = null;
107+
try {
108+
in = assetManager.open(fromAssetPath);
109+
new File(toPath).createNewFile();
110+
out = new FileOutputStream(toPath);
111+
copyFile(in, out);
112+
in.close();
113+
in = null;
114+
out.flush();
115+
out.close();
116+
out = null;
117+
return true;
118+
} catch(Exception e) {
119+
e.printStackTrace();
120+
return false;
121+
}
122+
}
123+
124+
private static void copyFile(InputStream in, OutputStream out) throws IOException {
125+
byte[] buffer = new byte[1024];
126+
int read;
127+
while ((read = in.read(buffer)) != -1) {
128+
out.write(buffer, 0, read);
129+
}
130+
}
131+
```
132+
133+
Before starting the node runtime, delete the previous `nodejs-project` and copy the current one into the `FilesDir` and start the runtime from there:
134+
```java
135+
new Thread(new Runnable() {
136+
@Override
137+
public void run() {
138+
//The path where we expect the node project to be at runtime.
139+
String nodeDir=getApplicationContext().getFilesDir().getAbsolutePath()+"/nodejs-project";
140+
//Recursively delete any existing nodejs-project.
141+
File nodeDirReference=new File(nodeDir);
142+
if (nodeDirReference.exists()) {
143+
deleteFolderRecursively(new File(nodeDir));
144+
}
145+
//Copy the node project from assets into the application's data path.
146+
copyAssetFolder(getApplicationContext().getAssets(), "nodejs-project", nodeDir);
147+
startNodeWithArguments(new String[]{"node",
148+
nodeDir+"/main.js"
149+
});
150+
}
151+
}).start();
152+
```
153+
154+
> Attention: Given the project folder can be overwritten, it should not be used for persistent data storage.
155+
156+
### Copy the `nodejs-project` only after an APK change
157+
158+
Recopying the `nodejs-project` at each Application's run can be expensive, so improve it by saving the last time the APK was updated on an Application Shared Preference and check if we need to delete and copy the `nodejs-project`.
159+
160+
Add the helper functions to `app/src/main/java/com/yourorg/sample/MainActivity.java`:
161+
```java
162+
import android.content.pm.PackageInfo;
163+
import android.content.pm.PackageManager;
164+
import android.content.SharedPreferences;
165+
166+
...
167+
168+
private boolean wasAPKUpdated() {
169+
SharedPreferences prefs = getApplicationContext().getSharedPreferences("NODEJS_MOBILE_PREFS", Context.MODE_PRIVATE);
170+
long previousLastUpdateTime = prefs.getLong("NODEJS_MOBILE_APK_LastUpdateTime", 0);
171+
long lastUpdateTime = 1;
172+
try {
173+
PackageInfo packageInfo = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0);
174+
lastUpdateTime = packageInfo.lastUpdateTime;
175+
} catch (PackageManager.NameNotFoundException e) {
176+
e.printStackTrace();
177+
}
178+
return (lastUpdateTime != previousLastUpdateTime);
179+
}
180+
181+
private void saveLastUpdateTime() {
182+
long lastUpdateTime = 1;
183+
try {
184+
PackageInfo packageInfo = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0);
185+
lastUpdateTime = packageInfo.lastUpdateTime;
186+
} catch (PackageManager.NameNotFoundException e) {
187+
e.printStackTrace();
188+
}
189+
SharedPreferences prefs = getApplicationContext().getSharedPreferences("NODEJS_MOBILE_PREFS", Context.MODE_PRIVATE);
190+
SharedPreferences.Editor editor = prefs.edit();
191+
editor.putLong("NODEJS_MOBILE_APK_LastUpdateTime", lastUpdateTime);
192+
editor.commit();
193+
}
194+
```
195+
196+
Change the code that starts the node runtime to check if it needs to delete the previous `nodejs-project` and copy the current one into the `FilesDir`:
197+
198+
```java
199+
new Thread(new Runnable() {
200+
@Override
201+
public void run() {
202+
//The path where we expect the node project to be at runtime.
203+
String nodeDir=getApplicationContext().getFilesDir().getAbsolutePath()+"/nodejs-project";
204+
if (wasAPKUpdated()) {
205+
//Recursively delete any existing nodejs-project.
206+
File nodeDirReference=new File(nodeDir);
207+
if (nodeDirReference.exists()) {
208+
deleteFolderRecursively(new File(nodeDir));
209+
}
210+
//Copy the node project from assets into the application's data path.
211+
copyAssetFolder(getApplicationContext().getAssets(), "nodejs-project", nodeDir);
212+
213+
saveLastUpdateTime();
214+
}
215+
startNodeWithArguments(new String[]{"node",
216+
nodeDir+"/main.js"
217+
});
218+
}
219+
}).start();
220+
```
221+
222+
### Redirect the stdout and stderr to logcat
223+
224+
The Node.js runtime and the Node.js `console` module use the process' `stdout` and `stderr` streams. Some code is needed to redirect those streams to the Android system log, so they can be viewed with logcat.
225+
This sample adds C++ code to manage the redirection by starting two background threads (one for `stdout` and the other for `stderr`), to provide a more pleasant Node.js debugging experience.
226+
227+
Add the helper functions to `app/src/main/cpp/native-lib.cpp`:
228+
```cc
229+
#include <pthread.h>
230+
#include <unistd.h>
231+
#include <android/log.h>
232+
233+
...
234+
235+
// Start threads to redirect stdout and stderr to logcat.
236+
int pipe_stdout[2];
237+
int pipe_stderr[2];
238+
pthread_t thread_stdout;
239+
pthread_t thread_stderr;
240+
const char *ADBTAG = "NODEJS-MOBILE";
241+
242+
void *thread_stderr_func(void*) {
243+
ssize_t redirect_size;
244+
char buf[2048];
245+
while((redirect_size = read(pipe_stderr[0], buf, sizeof buf - 1)) > 0) {
246+
//__android_log will add a new line anyway.
247+
if(buf[redirect_size - 1] == '\n')
248+
--redirect_size;
249+
buf[redirect_size] = 0;
250+
__android_log_write(ANDROID_LOG_ERROR, ADBTAG, buf);
251+
}
252+
return 0;
253+
}
254+
255+
void *thread_stdout_func(void*) {
256+
ssize_t redirect_size;
257+
char buf[2048];
258+
while((redirect_size = read(pipe_stdout[0], buf, sizeof buf - 1)) > 0) {
259+
//__android_log will add a new line anyway.
260+
if(buf[redirect_size - 1] == '\n')
261+
--redirect_size;
262+
buf[redirect_size] = 0;
263+
__android_log_write(ANDROID_LOG_INFO, ADBTAG, buf);
264+
}
265+
return 0;
266+
}
267+
268+
int start_redirecting_stdout_stderr() {
269+
//set stdout as unbuffered.
270+
setvbuf(stdout, 0, _IONBF, 0);
271+
pipe(pipe_stdout);
272+
dup2(pipe_stdout[1], STDOUT_FILENO);
273+
274+
//set stderr as unbuffered.
275+
setvbuf(stderr, 0, _IONBF, 0);
276+
pipe(pipe_stderr);
277+
dup2(pipe_stderr[1], STDERR_FILENO);
278+
279+
if(pthread_create(&thread_stdout, 0, thread_stdout_func, 0) == -1)
280+
return -1;
281+
pthread_detach(thread_stdout);
282+
283+
if(pthread_create(&thread_stderr, 0, thread_stderr_func, 0) == -1)
284+
return -1;
285+
pthread_detach(thread_stderr);
286+
287+
return 0;
288+
}
289+
```
290+
291+
Start the redirection right begore starting the Node.js runtime:
292+
```cc
293+
//Start threads to show stdout and stderr in logcat.
294+
if (start_redirecting_stdout_stderr()==-1) {
295+
__android_log_write(ANDROID_LOG_ERROR, ADBTAG, "Couldn't start redirecting stdout and stderr to logcat.");
296+
}
297+
298+
//Start node, with argc and argv.
299+
return jint(node::Start(argument_count,argv));
300+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# For more information about using CMake with Android Studio, read the
2+
# documentation: https://d.android.com/studio/projects/add-native-code.html
3+
4+
# Sets the minimum version of CMake required to build the native library.
5+
6+
cmake_minimum_required(VERSION 3.4.1)
7+
8+
# Creates and names a library, sets it as either STATIC
9+
# or SHARED, and provides the relative paths to its source code.
10+
# You can define multiple libraries, and CMake builds them for you.
11+
# Gradle automatically packages shared libraries with your APK.
12+
13+
add_library( # Sets the name of the library.
14+
native-lib
15+
16+
# Sets the library as a shared library.
17+
SHARED
18+
19+
# Provides a relative path to your source file(s).
20+
src/main/cpp/native-lib.cpp )
21+
22+
#Includes node's header files.
23+
include_directories(libnode/include/node/)
24+
25+
add_library( libnode
26+
SHARED
27+
IMPORTED )
28+
29+
set_target_properties( # Specifies the target library.
30+
libnode
31+
32+
# Specifies the parameter you want to define.
33+
PROPERTIES IMPORTED_LOCATION
34+
35+
# Provides the path to the library you want to import.
36+
${CMAKE_SOURCE_DIR}/libnode/bin/${ANDROID_ABI}/libnode.so )
37+
38+
# Searches for a specified prebuilt library and stores the path as a
39+
# variable. Because CMake includes system libraries in the search path by
40+
# default, you only need to specify the name of the public NDK library
41+
# you want to add. CMake verifies that the library exists before
42+
# completing its build.
43+
44+
find_library( # Sets the name of the path variable.
45+
log-lib
46+
47+
# Specifies the name of the NDK library that
48+
# you want CMake to locate.
49+
log )
50+
51+
# Specifies libraries CMake should link to your target library. You
52+
# can link multiple libraries, such as libraries you define in this
53+
# build script, prebuilt third-party libraries, or system libraries.
54+
55+
target_link_libraries( # Specifies the target library.
56+
native-lib
57+
58+
libnode
59+
60+
# Links the target library to the log library
61+
# included in the NDK.
62+
${log-lib} )

0 commit comments

Comments
 (0)