UNPKG

generator-cordova-plugin-bb10

Version:

Generator for BB10 Cordova Plugins

458 lines (335 loc) 22.6 kB
Template for BlackBerry 10 Cordova Plugins ========================================== BlackBerry 10 Cordova Plugins create a bridge between JavaScript code in an HTML5 application and C/C++ and/or Qt code in the plugin, allowing access to native libraries, which are not part of the HTML5 specification. The BlackBerry 10 Plugins included with the Cordova SDK also follow the same or similar format as shown in this template. Plugins offer a JavaScript API which communicates through an interface to native methods. This interface can send and receive data through strings, or data that can be represented as a string such as JSON and Base64. This template includes examples for communicating in several ways: 1. A function that returns a string from a native method. 2. A function that sends a string to, and gets another back, from a native method. 3. A function that calls a native method with a JSON object input, and provides a callback function, which is fired by a native method, and receives JSON data. 4. Defining a property that is linked to native methods when retrieved or set. 5. Starting and Stopping a native thread, with a callback function that receives events from the native thread. ## Building and Testing the Sample First build the Native portion of the plugin in Momentics, then add the plugin to the sample project ### Momentics NDK setup #### Importing the Template 1. Open the Momentics IDE. Navigate to the workbench and from the program menu select File -> Import and choose "Existing Projects into Workspace". 2. Choose "Select root directory: " and browse to the _/plugin/src/blackberry10/native_ directory that was just generated. Select the project name you entered in the Projects list and uncheck "Copy projects into workspace". Click Finish. 3. Follow these next steps to build the template plugin to be sure the setup is working. #### How to build your native Plugin 1. Right click your project and select the Clean Project option. 2. Right click your project again and select Build Configurations -> Build Selected... . 3. A window will appear that shows all the available build configurations for the project. Select __device__ and __simulator__ and click ok. 4. You should see the shared libraries (lib<ProjectName>.so files e.g. libMMSPlugin.so) generated in the folders for each Build Configuration that you selected. ###Cordova Instructions: 1. Change to the /sample direcotry that was generated. 3. Run the __bbndk-env.bat__ or __bbndk-env.sh__ script if the NDK or BB-Tools are not on your PATH. 4. Add the _blackberry10_ platform to your project by running __cordova platform add blackberry10__ 5. Then add the plugin to this new project with __cordova plugin add ../plugin__ 6. Finally, run __cordova run__ Then you can call the methods with the namespace __cordova.plugins.<%= apiBase %>__, and that should appear in WebInspector as an Object so you can see what APIs are available. The example APIs included will be detailed later in this guide. This is a screenshot of the test data being displayed in the Hello World sample app: ![Screenshot](screenshot.png) ### Using the Plugin in an Application To use the plugin in another project, that's been created with Cordova, run __cordova plugin add <path to this Template folder>/plugin__. All the methods in the plugin will be prefixed by that feature name, so a method called _test()_ supplied in the _community.templateplugin_ plugin will be called in JavaScript like so: ```javascript cordova.plugins.<%= apiBase %>.test(); ``` Depending on the plugin, these methods can also have return values, take arguments, or supply callback methods. To remove the plugin, run __cordova plugin rm <%= pluginName %>__ __PLEASE NOTE:__ You will need to remove and re-add the plugin on each build cycle where you make changes. You can edit the individual files, but they get scattered across various folders. It's safer to edit them in the plugin directory and then do a quick (maybe script it?) cycle of removing and adding the plugin, so the installation process is followed. Follow the steps above to: 1. [Build the native portion](#how-to-build-your-native-plugin), and 2. [Use the plugin in your test app](#using-the-plugin-in-an-application). 3. Remember to remove and add the plugin again with the cordova or webworks commands. ## Architecture of a Plugin Plugins are organized into several files, each with a specific role. 1. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/plugin.xml">plugin.xml</a> - defines the namespace of the plugin lists the files that need to be included. 2. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/www/client.js">client.js</a> - the client file defines the API that can be called by an application. It calls to functions in index.js using the Cordova framework. It also connects callback functions to the events that fire them. 3. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/src/blackberry10/index.js">index.js</a> - This is the main controller of the plugin. It receives calls from the client.js through Cordova, and calls the appropriate methods in the included JNEXT object, which does the communication to the Native side of JNEXT. 4. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/src/blackberry10/native/src/template_js.hpp">template_js.hpp</a> - C++ header for the JNEXT code. You should rarely have to edit this aside from renaming. 6. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/src/blackberry10/native/src/template_js.cpp">template_js.cpp</a> - C++ code for the JNEXT plugin. Besides renaming, the primary editing of this will be to edit the InvokeMethod function to call the appropriate methods in template_ndk.cpp, given the command and callbackId sent in from index.js and the Template object. 7. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/src/blackberry10/native/src/template_ndk.hpp">template_ndk.hpp</a> - C++ header for the native code. Where you will need to list method signatures for the extenion methods called from the template_js.cpp file, and any private methods or variables. 8. <a href="/blackberry/WebWorks-Community-APIs/tree/master/BB10-Cordova/Template/plugin/src/blackberry10/native/src/template_ndk.cpp">template_ndk.cpp</a> - C++ native code. All the main native code will go here typically. ## Communication end to end Plugin methods can be synchronous or asynchronous, send arguments, receive return values, and provide callback methods. In client.js we define the _ID variable for the name of the plugin, and the exec method from Cordova which will do the communication: ```javascript var _self = {}, _ID = "<%= pluginName %>", exec = cordova.require("cordova/exec"); ``` The basic format of all cordova client-side calls is ```javascript exec(success, fail, pluginId, method, args); ``` In most cases, we'll define success and fail inline. #### Synchronous Methods a simple Synchronous method call looks like this, in client.js: ```javascript _self.testInput = function (input, onSuccess, onFail) { exec(function (result) { invokeCallback(onSuccess, result); }, function(error) { invokeCallback(onFail, error); }, _ID, "test", { input: input }); return result; }; ``` In index.js, the client-side call maps to ```javascript test: function (success, fail, args, env) { var result = new PluginResult(args, env); args = JSON.parse(decodeURIComponent(args["input"])); result.ok(template.getInstance().testInput(result.callbackId, args), false); }, ``` Where the success and fail callbacks were sent in from client.js, and args is an object passed in to the exec method, but it has been converted by JSON.stringify() and encoded as a URIcomponent, so you must call ```javascript JSON.parse(decodeURIComponent(args["input"])); ``` To communicate through Cordova, a new PluginResult is created, and we fire the result.ok method, and pass in the result of the appropriate JNEXT function. In order to handle times where there is a callback, we will always include the callbackId when sending in arguments. So, the method in the JNEXT.Template object looks like the following: ```javascript self.testInput = function (callbackId, input) { return JNEXT.invoke(self.m_id, "testStringInput " + callbackId + " " + input); }; ``` Note: If input is a JSON object, we need to convert it to a string first with JSON.stringify(input). Now the communication passes into the native layer. JNEXT.invoke eventually calls into: ```cpp string TemplateJS::InvokeMethod(const string& command) { // format must be: "command callbackId params" size_t commandIndex = command.find_first_of(" "); std::string strCommand = command.substr(0, commandIndex); size_t callbackIndex = command.find_first_of(" ", commandIndex + 1); std::string callbackId = command.substr(commandIndex + 1, callbackIndex - commandIndex - 1); std::string arg = command.substr(callbackIndex + 1, command.length()); // ... } else if (strCommand == "testStringInput") { return m_pTemplateController->templateTestString(arg); } //... } ``` The method name and callbackId are stripped off the front of the command sent in, and the rest is sent in as an argument for the appropriate method. Note the use of the pointer to the TemplateNDK object, so we separate our regular native code from the JNEXT code. Finally, we call the native method that contains the functionality we are providing to our WebWorks application, and in this example, it takes in the arguments, appends them to a string and returns the result to WebWorks. ```cpp // Take in input and return a value std::string TemplateNDK::templateTestString(const std::string& inputString) { return "Template Test Function, got: " + inputString; } ``` If the input is a JSON object, we need to parse it first, as shown here: ```cpp // Parse the arg string as JSON Json::Reader reader; Json::Value root; bool parse = reader.parse(inputString, root); ``` To return JSON, use ```cpp Json::FastWriter writer; Json::Value root; root["result"] = "result value"; return writer.write(root); ``` #### Asynchronous Methods An Asynchronous method will typically provide a callback function to receive the results of a method, but this is not necessarily the case for all situations. The sequence is much the same as that of Synchronous Methods with these differences: Asynchronous method call, with both input and a callback function: ```javascript _self.testAsync = function (input, callback) { var success = function (data, response) { var json = JSON.parse(data); callback(json); }, fail = function (data, response) { console.log("Error: " + data); }; exec(success, fail, _ID, "testAsync", { input: input }); }; ``` In index.js we're going to keep a record of the PluginResult so we can call the callback on it later. And right now, we'll call into JNEXT for the native code, but return noResult(true) right now. The true value tells Cordova to keep our callback around. ```javascript testAsync: function (success, fail, args, env) { var result = new PluginResult(args, env); resultObjs[result.callbackId] = result; args = JSON.parse(decodeURIComponent(args["input"])); template.getInstance().testAsync(result.callbackId, args); result.noResult(true); }, ``` Now the code proceeds as for Synchronous events, until the template_ndk.cpp method, where instead of returning the result directly, the NotifyEvent method is used instead, but we'll be passing in the callbackId as well this time instead of ignoring it. The callbackId is what we'll use to communicate back through JNEXT: ```cpp // Asynchronous callback with JSON data input and output void TemplateNDK::templateTestAsync(const std::string& callbackId, const std::string& inputString) { // Parse the arg string as JSON Json::FastWriter writer; Json::Reader reader; Json::Value root; bool parse = reader.parse(inputString, root); if (!parse) { Json::Value error; error["result"] = "Cannot parse JSON object"; m_pParent->NotifyEvent(callbackId + " " + writer.write(error)); } else { root["result"] = root["value1"].asInt() + root["value2"].asInt(); m_pParent->NotifyEvent(callbackId + " " + writer.write(root)); } } ``` This event passes through the JNEXT plugin and is received on the onEvent(strData) method of the JNEXT.Template object. In the case of a standard asynchronous one-time event, we'll lookup the result and call callbackOk(data, false) so the callback is removed from Cordova, and we'll delete our record as well. However, if we need to keep that callback around for further events, we'll need some way of tracking that. In this example, there is only one callback that we need to keep around ever. : ```javascript // Fired by the Event framework (used by asynchronous callbacks) self.onEvent = function (strData) { var arData = strData.split(" "), callbackId = arData[0], result = resultObjs[callbackId], data = arData.slice(1, arData.length).join(" "); if (result) { if (callbackId != threadCallback) { result.callbackOk(data, false); delete resultObjs[callbackId]; } else { result.callbackOk(data, true); } } ... }; ``` result.callbackOk(data, false) will call the success method that we registered at the beginning of the flow, in client.js. #### Callbacks for multiple events The Asynchronous methods above use one-time callbacks to receive their results. Sometimes it is important to receive multiple events on a callback, such as when listening to a thread or longer process. This is done as above, simply retaining our PluginResult registry until we don't need it any longer, and if necessary, we can return a value immediately but also keep the callback as so: ```javascript startThread: function (success, fail, args, env) { var result = new PluginResult(args, env); if (!threadCallback) { threadCallback = result.callbackId; resultObjs[result.callbackId] = result; result.ok(template.getInstance().startThread(result.callbackId), true); } else { result.error(template.getInstance().startThread(result.callbackId), false); } }, ``` At the native layer, the NotifyEvent method can be called multiple times and be received by the callback in this way. The same success method will get called for the immediate result, and all the subsequent callbacks. #### Defining Properties Properties can be defined on the plugin so that code like: ```javascript cordova.plugins.<%= apiBase %>.templateProperty; ``` returns a value, and it can be set by the following code: ```javascript cordova.plugins.<%= apiBase %>.templateProperty = value; ``` The following code defines a property called templateProperty, backed by the plugin method of the same name for setting or retrieving the value. The getter or setter could be removed to restrict what the property responds to. __Note that properties only work on BlackBerry 10. Avoid using these if you intend to do cross platform plugins.__ ```javascript Object.defineProperty(_self, "templateProperty", { get: function () { var result, success = function (data, response) { result = data; }, fail = function (data, response) { console.log("Error: " + data); }; exec(success, fail, _ID, "templateProperty", null); return result; }, set: function (arg) { var result, success = function (data, response) { result = data; }, fail = function (data, response) { console.log("Error: " + data); }; exec(success, fail, _ID, "templateProperty", {"value": arg }); return result; } }); ``` The property calls are the same as synchronous calls, but are adapted to handle both input and no input on the same function in index.js: ```javascript templateProperty: function (success, fail, args, env) { var result = new PluginResult(args, env); var value; if (args && args["value"]) { value = JSON.parse(decodeURIComponent(args["value"])); template.getInstance().templateProperty(result.callbackId, value); result.noResult(false); } else { result.ok(template.getInstance().templateProperty(), false); } }, ``` ## Debugging Debugging plugins is difficult, but there are some ways that can be effective. If you open WebInspector to the __first__ WebView, instead of the second, you will be able to see and interact with the index.js code and the inner parts of the extension. Opening a tab on the __second__ WebView, where you normally would, at the same time will allow you to inspect the whole flow of calls within JavaScript. Simple JavaScript alerts also work in the plugin files and this can be invaluable to check the value of some data being passed around. Since plugins deal with strings, it's easy to check. ### Logging at the Native level with slogger2 You will notice there is a __Logger__ class in the project which has an instance created in __<%= projectLower %>_js.cpp__ and is used in many of the methods of __<%= projectLower %>_ndk.cpp__. This class wraps the slogger2 logging capabilities of BlackBerry 10 for easy logging integration in your Plugins. From your template_ndk.cpp file, you can call these convenience methods to write out a simple string to the log buffer. Appropriate log levels and buffers are created by the Logger class so you don't need to do anything else. ```cpp m_pParent->getLog()->debug(const char* message); m_pParent->getLog()->info(const char* message); m_pParent->getLog()->notice(const char* message); m_pParent->getLog()->warn(const char* message); m_pParent->getLog()->error(const char* message); m_pParent->getLog()->critical(const char* message); ``` These methods wrap the __[slog2c()](https://developer.blackberry.com/native/reference/core/com.qnx.doc.neutrino.lib_ref/topic/s/slog2c.html)__ method which is the fastest logging method, but accepts only character arrays. The Logger creates two different sized buffers: One larger for high frequency, but lower priority messages, and one smaller for low frequency but higher priority messages. The methods above choose the appropriate buffer to use. If you need to send more complicated log messages use the __[slog2fa()](https://developer.blackberry.com/native/reference/core/com.qnx.doc.neutrino.lib_ref/topic/s/slog2fa.html)__ method, and you can access the buffers through these methods: ```cpp slog2_buffer_t hiPriorityBuffer(); slog2_buffer_t lowPriorityBuffer(); ``` So, for example, you could do the following: ```cpp #include <slog2.h> int some_number = 1; slog2fa(m_pParent->getLog()->lowPriorityBuffer(), 0, SLOG2_INFO, "string:%s, some_number:%d", SLOG2_FA_STRING( "Hello world" ), SLOG2_FA_SIGNED( some_number ), SLOG2_FA_END ); ``` Slogger2 data can be seen either over an SSH connection to the device, and running slog2info (pass in -w to have the command wait and print out messages as they arrive), or even better in Momentics by right clicking on a Target in the Target Navigator view, and choosing __Open Device Log__. ### Including Libraries Libraries are added through the Add Library Wizard. Here's how to find it: 1. Right click on the project, and select properties, then go to C/C++ Build -> Settings. 1. Choose Tool Settings, and under QCC Linker, select Libraries 1. Click on Open Add Library Wizard ![Add Library](addLibrary.png) 1. Click Okay to the build settings change. 1. When prompted choose where the library is that you want to add. For any platform library, you will want the first option. 1. Find the library in the list. In this example we've searched for and found the QtCore library ![QtCore](QtCore.png) 1. Click Next, then Finish. Now the necessary library paths and include directories should be added. For some Qt based libraries, there are some additional steps. #### Special steps for including QtCore and other Qt libraries If you follow the instructions above and use the Add Library Wizard to add QtCore, there are some additional steps. When you build you'll see the following message: ``` C:\bbndk10.2\host_10_2_0_15\win32\x86\usr\bin\ntox86-ld: cannot find -lQtCore ``` Two additional library paths need to be added for the Linker: ``` ${QNX_TARGET}/${CPUVARDIR}/usr/lib ${QNX_TARGET}/${CPUVARDIR}/usr/lib/qt4/lib ``` __Repeat these steps so the same settings exist on both the device and simulator build configurations__ ![Library Paths](librarypaths.png) Additionally, you should add this include directory for the Compiler Preprocessor: ``` ${QNX_TARGET}/usr/include/qt4 ``` __Repeat these steps so the same settings exist on both the device and simulator build configurations__ ![Preprocessor](preprocessor.png) ### Common Issues If you are getting a message saying the application can not load your .so file, it's nearly always a linking problem. Your code may build in the IDE, but not actually link on the device. Make sure you've included all your dependencies and includes properly, _on the build that you have loaded_. Also, make sure that you've loaded the device build on a device, and the simulator on a simulator. If your application launches but quickly freezes and then is closed, you may be using a BPS API but haven't called ```cpp bps_initialize(); ``` It is required for things like LED, Vibration, Audio and many others. See [more information here](http://developer.blackberry.com/native/reference/bb10/com.qnx.doc.bps.lib_ref/com.qnx.doc.bps.lib_ref/topic/bps_initialize.html). ## Disclaimer THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.