|
| 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 | +``` |
0 commit comments