Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom cordova plugin creation for ionic2 project

Many of us would have gone through similar issues, but even after going through following most relevant links reference link1 and reference link2 , I am not able to resolve.

Issue:

Create a custom plugin (Cordova) in-order to use this in ionic2 project.

Expectation: This plugin will be able to interact with native features of IOS and Android. To be precise I am trying to access features of a native SDK (Aruba Internal Positioning SDK) using cordova into Ionic project.

Step 1 Initially created plugin as per reference link 1

Step 2 Created Ionic 2 project( created with this basic steps )

Step 3 JavaScript file in plugin was not able to refer and access in Ionic2 .

After googling , I found this discussion , where it is told to create interface in plugin itself because of the following reason.

import {myPluginName} from '../../../plugins/xxx/*.js'

Will not work because the plugin is not part of the ionic native bundle.

If you have a custom plugin, you can do a few things.

1) Make a PR to add it to ionic-native proper

2) Use the raw plugin API. You can use the raw plugin API without having it be part of Ionic Native. The plugin is on the window object, so you would target the api normally

window.plugin.myPlugin.myMethod()

According to the GITHUB Example project this way the interface should be implemented

interface CordovaPlugins {
  ZPLPrinter: ZPLPrinter;
}

interface ZPLPrinter {

  print(
    ipaddress: string,
    bclabels: any,
    printSuccess: (ip: string, labels: string[]) => void,
    printError: (message: string) => void): void;

}

Now I created a similar interface in my plugin which is the following in plugin's www folder

interface CordovaPlugins {
  Communicator: Communicator;
}

interface Communicator {

  getInfo(successCallback: any, errorCallback: any);

}

This interface would ideally target this method in JS file

Device.prototype.getInfo = function(successCallback, errorCallback) {
    console.log("device.js: getInfo function called");
    argscheck.checkArgs('fF', 'Device.getInfo', arguments);
    exec(successCallback, errorCallback, "Device", "getDeviceInfo", []);
};

Now I am stuck , as my Ionic project is not having typings folder itself.

In the sample Github Project, cordova packages are referred using typings folder . TypeScript File in project is referring Cordova using index.t.js

Import used to refer should go like

declare var cordova: Cordova;

Doubts:

  1. Am I in the wright direction of the process
  2. Is this the way to create Cordova plugin and use in ionic
  3. Why I am not able to get typings folder in Ionic2

EDIT 1:

After just adding the plugin without even referring in Ionic project, I tried to run in Android device. But it gave me the following error.

Main error is this

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.ionicframework.cutepuppypics234138/com.ionicframework.cutepuppypics234138.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void org.apache.cordova.CordovaPlugin.privateInitialize(java.lang.String, org.apache.cordova.CordovaInterface, org.apache.cordova.CordovaWebView, org.apache.cordova.CordovaPreferences)' on a null object reference

Why would this error be causing? Detailed logs have given below

12-08 16:10:49.079 20555-20555/? E/ApkAssets: Error while loading asset assets/natives_blob_64.bin: java.io.FileNotFoundException: assets/natives_blob_64.bin
12-08 16:10:49.079 20555-20555/? E/ApkAssets: Error while loading asset assets/snapshot_blob_64.bin: java.io.FileNotFoundException: assets/snapshot_blob_64.bin
12-08 16:10:49.682 20555-20555/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.ionicframework.cutepuppypics234138, PID: 20555
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.ionicframework.cutepuppypics234138/com.ionicframework.cutepuppypics234138.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void org.apache.cordova.CordovaPlugin.privateInitialize(java.lang.String, org.apache.cordova.CordovaInterface, org.apache.cordova.CordovaWebView, org.apache.cordova.CordovaPreferences)' on a null object reference
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2339)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2413)
   at android.app.ActivityThread.access$800(ActivityThread.java:155)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1317)
   at android.os.Handler.dispatchMessage(Handler.java:102)
   at android.os.Looper.loop(Looper.java:135)
   at android.app.ActivityThread.main(ActivityThread.java:5343)
   at java.lang.reflect.Method.invoke(Native Method)
   at java.lang.reflect.Method.invoke(Method.java:372)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:905)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:700)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void org.apache.cordova.CordovaPlugin.privateInitialize(java.lang.String, org.apache.cordova.CordovaInterface, org.apache.cordova.CordovaWebView, org.apache.cordova.CordovaPreferences)' on a null object reference
   at org.apache.cordova.PluginManager.getPlugin(PluginManager.java:171)
   at org.apache.cordova.PluginManager.startupPlugins(PluginManager.java:97)
   at org.apache.cordova.PluginManager.init(PluginManager.java:86)
   at org.apache.cordova.CordovaWebViewImpl.init(CordovaWebViewImpl.java:115)
   at org.apache.cordova.CordovaActivity.init(CordovaActivity.java:149)
   at org.apache.cordova.CordovaActivity.loadUrl(CordovaActivity.java:224)
   at com.ionicframework.cutepuppypics234138.MainActivity.onCreate(MainActivity.java:39)
   at android.app.Activity.performCreate(Activity.java:6010)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1129)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2292)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2413) 
   at android.app.ActivityThread.access$800(ActivityThread.java:155) 
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1317) 
   at android.os.Handler.dispatchMessage(Handler.java:102) 
   at android.os.Looper.loop(Looper.java:135) 
   at android.app.ActivityThread.main(ActivityThread.java:5343) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at java.lang.reflect.Method.invoke(Method.java:372) 
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:905) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:700) 
12-08 16:10:49.879 20656-20656/? E/SubDex: SubDex Config : .dex 2
12-08 16:10:50.285 20656-20656/? E/project: extsdcard==/storage/emulated/0/Android/data/com.cleanmaster.mguard/files
12-08 16:10:50.303 20656-20656/? E/project: extsdcard==/storage/emulated/0/Android/data/com.cleanmaster.mguard/files
like image 888
Sreehari Avatar asked Dec 05 '16 13:12

Sreehari


People also ask

How do I make my own Cordova plugin?

So, you must install your application on a device to use any off-platform Cordova plugins. To do so, navigate to the Development tab and switch from DEVELOPMENT to the NATIVE PLATFORMS section under your mobile project module. Then click GENERATE ANDROID APP.

How do I add a plugin to config XML Cordova?

When adding plugins or platforms, use the --save flag to add them to config. xml. Ex: cordova platform add android --save. Existing projects can use cordova plugin save and cordova platform save commands to save all previously installed plugins and platforms into your project's config.


3 Answers

After many trial and errors I found the solution.

I am putting down below details for future reference to any one who is trying a similar stuff!

Issues with code was as follows (my plugin name is Inject)

  • Inject\www\Inject.js was not having essential function for installing the plugin
  • Any method name mentioned in Inject.js should be same till Inject\src\Inject.java as there are options in execute method to refer different method name based on tag's received

Steps:

  1. Use plugman to create skeleton of plugin Reference Link
  2. Instead of using names like cordova-plugin-am-i-late , use cordova.plugin.Inject . I faced compile/run time issues using - symbol
  3. Add targeted platform plugman platform add --platform_name android
  4. Verify plugin.xml comparing the same reference
  5. Include permissions in plugin.xml if required
  6. Now verify Inject.js its missing essential method calls. Currently its having only the following code.

var exec = require('cordova/exec');
exports.coolMethod = function(arg0, success, error) {
exec(success, error, "Inject", "coolMethod", [arg0]);
};
  1. But we need to include below also to make it install and work

function Inject(){
}
Inject.install = function () {
  if (!window.plugins) {
    window.plugins = {};
  }

  window.plugins.Inject = new Inject();
  return window.plugins.Inject;
};

cordova.addConstructor(Inject.install);
  1. For example I am targeting to show a Toast message for which below is my complete Inject.js file

function Inject(){
}

Inject.prototype.coolMethod = function (options, successCallback, errorCallback) {
  cordova.exec(successCallback, errorCallback, "Inject", "coolMethod", []);
};

Inject.install = function () {
  if (!window.plugins) {
    window.plugins = {};
  }

  window.plugins.Inject = new Inject();
  return window.plugins.Inject;
};

cordova.addConstructor(Inject.install);
  1. Now lets implement our Inject.java

public class Inject extends CordovaPlugin {

private static final int GRAVITY_CENTER = Gravity.CENTER_VERTICAL|Gravity.CENTER_HORIZONTAL;
private static final String TAG = "InjectCordovaPlugin";
String messageReceived;

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        Log.v(TAG, "execute , action =" + action);
        if (action.equals("coolMethod")) {
            String message = args.getString(0);
            Log.v(TAG, "coolMethod called with message =" + message);
            this.coolMethod(message, callbackContext);
            return true;
        }

        return false;
    }
    private void coolMethod(String message, CallbackContext callbackContext) {
    Log.v(TAG, "Inject's coolMethod called ,message="+message);
    messageReceived = message;
        if (message != null && message.length() > 0) {
            cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {

            final android.widget.Toast toast = android.widget.Toast.makeText(
              cordova.getActivity().getWindow().getContext(),
              messageReceived,
              android.widget.Toast.LENGTH_LONG 
                );
                toast.setGravity(GRAVITY_CENTER, 0, 0);
                toast.show();
            }
            });
            callbackContext.success(message);
        } else {
            callbackContext.error("Expected one non-empty string argument.");
        }
    }
}
  1. Just to make this clear I'm putting out my final plugin.xml

<?xml version='1.0' encoding='utf-8'?>
<plugin id="cordova.plugin.Inject" 
    version="1" 
    xmlns="http://apache.org/cordova/ns/plugins/1.0" 
    xmlns:android="http://schemas.android.com/apk/res/android">
<name>Inject</name>
<js-module name="Inject" src="www/Inject.js">
    <clobbers target="window.plugins.Inject"/>
</js-module>
<platform name="android">
<config-file parent="/*" target="res/xml/config.xml">
<feature name="Inject">
<param name="android-package" 
        value="cordova.plugin.Inject.Inject" />
        </feature>
</config-file>
<config-file parent="/*" target="AndroidManifest.xml">
</config-file>
<source-file src="src/android/Inject.java" 
target-dir="src/cordova.plugin.Inject/Inject" />
</platform>
</plugin>
  1. Now create a sample Ionic2 project and add Android/IOS platform
  2. Add the created plugin using cordova plugin add command
  3. Navigate to ionic project in Nodejs command prompt and give this command (refer Plugin folder base after add ) cordova plugin add D:\PluginTrial\Inject
  4. The added plugin should be populated under Ionic2Project\plugins folder
  5. Call this function using window object. Below is my home.ts

import { Component } from '@angular/core';

import { NavController, Platform } from 'ionic-angular';

declare var window: any;
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  constructor(public navCtrl: NavController, private platform: Platform) {

  }
    showToast(message, position) {
        this.platform.ready().then(() => {
            window.plugins.Inject.coolMethod(message, "short", position);
        });
    }
}
  1. Now refer this showToast method in home.html file

<button ion-button (click)="showToast('Yo Man! Its working ', 'center')">Default</button>
  1. That's it. You should be able to test the plugin successfully! Happy Coding

Reference: Reference One , Reference Two , Reference Three

like image 85
Sreehari Avatar answered Oct 04 '22 02:10

Sreehari


Your plugin needs to look like this:

In: /[custom plugin name]/js/custom_plugin.js

var CustomPlugin = function(){};

CustomPlugin.someFunction = function(){
    console.log("someFunction starts");

    return new Promise(function(resolve,reject){
    cordova.exec(
        resolve,
        reject,
        [PLUGIN_NAME],
        [ACTION_ON_NATIVE_SIDE],
        []
    );

    });
    console.log("someFunction stops");
}

.... more functions

module.exports = CustomPlugin;

In: /[custom plugin name]/src/[android]||[ios] , the classes with native code.

And in: /[custom plugin name]/plugin.xml (this is an example, settings have to be adjusted to your case):

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="[CustomPlugin]"
    version="1.0.0">
    <name>CustomPlugin</name>
    <description>...</description>
    <license>...</license>
    <author>...</author>

    <engines>
        <engine name="cordova" version=">=6.0.0" />
    </engines>



    <js-module src="www/js/custom_plugin.js" name="CustomPlugin">
        <clobbers target="CustomPlugin" />
    </js-module>


    <platform name="ios">
        <config-file target="config.xml" parent="/*">
            <preference name="orientation" value="portrait"/>
            <feature name="CustomPlugin">
                <param name="ios-package" value="CustomPlugin" />
                <param name="onload" value="true"/>
            </feature>
        </config-file>

        <header-file src="src/ios/CustomPlugin.h" />
        <source-file src="src/ios/CustomPlugin.m" />
        <!--framework src="QuartzCore.framework" /> 
        <framework src="AssetsLibrary.framework" />
        <framework src="CoreGraphics.framework" />
        <framework src="MobileCoreServices.framework" /-->
    </platform>

    <platform name="android">
        <config-file target="res/xml/config.xml" parent="widget">
            <preference name="orientation" value="portrait"/>
            <feature name="CustomPlugin" >
                <param name="android-package" value="[package name].CustomPlugin"/>
                <param name="onload" value="true"/>
            </feature>
        </config-file>

        <config-file target="AndroidManifest.xml" parent="/*">
            <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
            <uses-permission android:name="..." />
            <uses-feature android:name="..." />
        </config-file>

        <source-file src="src/android/CustomPlugin.java" target-dir="[package folder directory organization like: com.android.something]" />
        <source-file ... />
        <source-file src="src/android/custom_plugin.xml" target-dir="res/layout" />

    </platform>

</plugin>

then you add you plugin with the CLI: ionic plugin add [folder of your plugin]

In your Ionic project, in the classes (angular2 directives) where you want to use your plugin, write before the @Component section: declare var CustomPlugin: any;. Then in that class, you can use your plugin by refering to CustomPlugin that is exported with module.exports = CustomPlugin; from the file: /[custom plugin name]/js/custom_plugin.js.

TO ANSWER EDIT 1 OF THE QUESTION, HERE SOME DETAILS OF THE ANDROID PART: In the android plugin project (once platform android has been added and built at least once, with ionic CLI), in android studio (2.2.2), when looking at the build project under "[my project]\platforms\android":

In the hierarchy, the MainActivity file is autogenerated under: "android\java\com\ionicframework.[my project name + a large number]\MainActivity":

  package com.ionicframework.[my project name + a large number];

import android.os.Bundle;
import org.apache.cordova.*;

public class MainActivity extends CordovaActivity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // enable Cordova apps to be started in the background
        Bundle extras = getIntent().getExtras();
        if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
            moveTaskToBack(true);
        }

        // Set by <content src="index.html" /> in config.xml
        loadUrl(launchUrl);
    }
}

For my custom plugin (not going into details here) under "android\java[package of the custom plugin]:

package [package of the custom plugin];

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;

// + other imports needed

public class CustomPlugin extends CordovaPlugin  {

    private static CallbackContext customDeferredCallback;

    public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException {
//all the thing corresponding to your case need to end if with either:
//   if OK: callbackContext.success(); return true;  ;
//   if not OK: callbackContext.error(error.getMessage()); return false;
//   if JAVA returns a result to JS do: actionBindListener(callbackContext);



}

    private boolean actionBindListener(final CallbackContext callbackContext){
    cordova.getThreadPool().execute(new Runnable() {
            public void run(){
                try{
                    customDeferredCallback= callbackContext;
                }catch(Exception e){e.printStackTrace();}
            }
        });
        return true;
    }


    //in your program when you get the result you want to send back to javascript call this function
    public static void sendResultToJS(res){
        JSONObject eventData = new JSONObject();
        try {
            eventData.put("CUSTOM_KEY", res);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, eventData);
        pluginResult.setKeepCallback(true);
        try{
                customDeferredCallback.sendPluginResult(pluginResult);
        } catch(NullPointerException e){
            e.printStackTrace();
        }
    }
}

And finally android\manifests\manifests.xml looks like that:

<?xml version='1.0' encoding='utf-8'?>
<manifest android:hardwareAccelerated="true" android:versionCode="1" android:versionName="0.0.1" package="com.ionicframework.[project name + large number]" xmlns:android="http://schemas.android.com/apk/res/android">
    <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
    <uses-permission android:name="android.permission.INTERNET" />
    <application android:hardwareAccelerated="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:supportsRtl="true">
        <activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" android:label="@string/activity_name" android:launchMode="singleTop" android:name="MainActivity" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:windowSoftInputMode="adjustResize">
            <intent-filter android:label="@string/launcher_name">
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:exported="true" android:name="com.adobe.phonegap.push.PushHandlerActivity" />
        <receiver android:name="com.adobe.phonegap.push.BackgroundActionButtonHandler" />
        <receiver android:exported="true" android:name="com.google.android.gms.gcm.GcmReceiver" android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="${applicationId}" />
            </intent-filter>
        </receiver>
        <service android:exported="false" android:name="com.adobe.phonegap.push.GCMIntentService">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            </intent-filter>
        </service>
        <service android:exported="false" android:name="com.adobe.phonegap.push.PushInstanceIDListenerService">
            <intent-filter>
                <action android:name="com.google.android.gms.iid.InstanceID" />
            </intent-filter>
        </service>
        <service android:exported="false" android:name="com.adobe.phonegap.push.RegistrationIntentService" />
    </application>
    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
    <permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature" />
</manifest>
like image 30
nyluje Avatar answered Oct 04 '22 01:10

nyluje


As far as typings is concerned, it is no longer used. All or most typescript declarations are moved to npm itself and you install them as npm install @types/package_name. https://www.npmjs.com/~types https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/README.md If you need typings folder you could try

npm install typings

you can also referance type declararions through

// <reference path="" />

in typescript

like image 41
Suraj Rao Avatar answered Oct 04 '22 00:10

Suraj Rao