[JAVA] I will not hesitate anymore! Customize Cordova / Monaca Plugin --- Try to modify InAppBrowser Plugin

Overview

In Monaca / Cordova, you can use various plugins, but when developing apps, there are cases where standard plugins are not enough. In such a case, it is possible to modify the plug-in and add the necessary functions, but there is not much comprehensive information and it is often difficult. Therefore, as a sample of plug-in repair, I will introduce how to add functions to InAppBrowser.

To try out the contents of this article, you need an environment to run Cordova locally. Also, since the native code will be modified, Xcode is required for iOS and Android Studio is required for Android.

(Although it is very difficult, you can try it by embedding the cordova-plugin-inappbrowser plugin as a zip file using Monaca's Pro plan etc. and modifying the code in the plugin on Monaca IDE. )

What kind of renovation?

Just the other day, Mr. Teratail wanted to do a process that "only a specific URL does not move as it is", so let's think about this as a theme. (JavaScript --I want to prevent "do not move as it is" for a specific URL in Monaca InApp Browser (104255) | teratail)

As a requirement, the URL pattern is set in advance with a regular expression, and the page transition to the link destination is not performed only when the regular expression is matched. Also, when this function is activated, the unload event will be fired so that it can be acquired on the JS side.

As I will introduce later, this modification is available at https://github.com/knight9999/cordova-plugin-inappbrowser/tree/unload_list, so if you want to use it immediately, please try it. please look.

Concrete example

With this modification, the following test cases can be operated.

  1. Set " https: \ / \ / github \ .com \ / apache \ / cordova-plugin-inappbrowser \ / blob \ / master \ / README \ .md "as the URL pattern that prohibits page transitions. .. (I can't read README.md)
  2. In InAppBrowser, open https://github.com/apache/cordova-plugin-inappbrowser
  3. Even if I click the link to README.md, the page cannot be changed.
  4. The unload event fires

Programmatically it will look like this:

  document.getElementById('btn').addEventListener('click', function() {
    var ref = cordova.InAppBrowser.open('https://github.com/apache/cordova-plugin-inappbrowser', '_blank', 'location=yes', null, ['https:\/\/github\.com\/apache\/cordova-plugin-inappbrowser\/blob\/master\/README\.md']);
    ref.addEventListener('unload', function(json) { alert( json.url ); } );
  },false);

Allows you to put a list of regular expressions in the fifth parameter of cordova.InAppBrowser.open. If it matches this regex, it doesn't load the page and instead fires the unload event.

Creating a work app

Plugins do not work with plugins alone, so create a working app, embed the plugin in it, and develop it.

First, get cordova-plugin-inappbrowser from github. In a directory of your choice, do the following:

$ git clone https://github.com/apache/cordova-plugin-inappbrowser.git
$ git checkout 1.6.1

The cloned directory will now be [/ path / to / cordova-plugin-in appbrowser]. We will develop it based on version 1.6.1. (Version 1.7.x currently has a bug in iOS UIWebView, so this article is based on 1.6 series)

Next, prepare a working app that uses this plugin. Create another directory and do the following:

$ cordova create sample
$ cd sample
$ cordova platform add [email protected]
$ cordova platform add [email protected]

Here, cordova cli is 6.5.0. This is the same as the latest version of Monaca today. Also, the iOS platform version is 4.4.0 and the Android platform version is 6.2.3 in order to match the latest version of Monaca.

And then, install the cordova-plugin-in appbrowser that you cloned earlier.

$ cordova plugin add [/path/to/cordova-plugin-inappbrowser]

Now you have a development environment. From now on, we will work in this working app directory.

Operation check

First, let's check the operation. Either iOS or Android is fine, but for now, I'll try it on iOS.

Open the www / index.html file and add the button tag as follows:

Before correction

        <div class="app">
            <h1>Apache Cordova</h1>
            <div id="deviceready" class="blink">
                <p class="event listening">Connecting to Device</p>
                <p class="event received">Device is Ready</p>
            </div>
        </div>

Revised

        <div class="app">
            <h1>Apache Cordova</h1>
            <div id="deviceready" class="blink">
                <p class="event listening">Connecting to Device</p>
                <p class="event received">Device is Ready</p>
            </div>
            <button id="btn">Go Btn</button>
        </div>

Then, register the operation when you tap btn in js / index.js as follows.

Before correction

    onDeviceReady: function() {
        this.receivedEvent('deviceready');
    },

Revised

    onDeviceReady: function() {
        this.receivedEvent('deviceready');
        document.getElementById('btn').addEventListener('click', function() {
          var ref = cordova.InAppBrowser.open('https://github.com/apache/cordova-plugin-inappbrowser', '_blank', 'location=yes');
        },false);
    },

Since this is still a normal InAppBrowser, I haven't set the 5th argument of the open method.

$ cordova prepare ios

After that, open the project with Xcode and check the operation on the actual machine.

$ open platforms/ios/HelloCordova.xcworkspace

When the HelloCorodva screen appears, did you press the btn button below it to display the cordova-plugin-inappbrowser page on github?

Repair of JavaScript part

After confirming the operation, as a modification of the InAppBrowser plug-in, first modify the JavaScript part. The JavaScript part is common to Android and iOS, but first we will focus on iOS and modify it. Because when modifying a plugin, the operation test is repeated many times, so it is more efficient to work on what you are using on iOS, check it, and when it is completed, give feedback to the origin of the plugin. Because it's good. (I think so, but it may be different for some people)

The place to repair is usually /plugins/cordova-plugins-inappbrowser/www/inappbrowser.js, but in fact this is just the one that was installed when the plugin was added, so this is I will not fix it.

Also, what is actually running in Xcode is platforms / ios / www / plugins / cordova-plugins-inappbrowser / www / inappbrowser.js, but even if you rewrite this, it will be based on every time you do cordova prepare. I won't fix this either, as it will come back.

The javascript to be modified during plugin development is platforms / ios / platform_www / plugins / cordova-plugin-inappbrowser / www / inappbrowser.js. If you change this, every time you prepare cordova, it will be copied to platforms / ios / www / plugins / cordova-plugins-inappbrowser / www / inappbrowser.js running in Xcode.

Let's change this as follows.

Change before:

    function InAppBrowser() {
       this.channels = {
            'loadstart': channel.create('loadstart'),
            'loadstop' : channel.create('loadstop'),
            'loaderror' : channel.create('loaderror'),
            'exit' : channel.create('exit')
       };
    }

After change:

    function InAppBrowser() {
       this.channels = {
            'loadstart': channel.create('loadstart'),
            'loadstop' : channel.create('loadstop'),
            'loaderror' : channel.create('loaderror'),
            'unload' : channel.create('unload'),
            'exit' : channel.create('exit')
       };
    }

Added one unload to the channel.

Then change it as follows.

Before correction

    module.exports = function(strUrl, strWindowName, strWindowFeatures, callbacks) {
        // Don't catch calls that write to existing frames (e.g. named iframes).
        if (window.frames && window.frames[strWindowName]) {
            var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open');
            return origOpenFunc.apply(window, arguments);
        }

        strUrl = urlutil.makeAbsolute(strUrl);
        var iab = new InAppBrowser();

        callbacks = callbacks || {};
        for (var callbackName in callbacks) {
            iab.addEventListener(callbackName, callbacks[callbackName]);
        }

        var cb = function(eventname) {
           iab._eventHandler(eventname);
        };

        strWindowFeatures = strWindowFeatures || "";

        exec(cb, cb, "InAppBrowser", "open", [strUrl, strWindowName, strWindowFeatures]);
        return iab;
    };

Revised

    module.exports = function(strUrl, strWindowName, strWindowFeatures, callbacks, ignoreList) {
        // Don't catch calls that write to existing frames (e.g. named iframes).
        if (window.frames && window.frames[strWindowName]) {
            var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open');
            return origOpenFunc.apply(window, arguments);
        }

        strUrl = urlutil.makeAbsolute(strUrl);
        var iab = new InAppBrowser();

        callbacks = callbacks || {};
        for (var callbackName in callbacks) {
            iab.addEventListener(callbackName, callbacks[callbackName]);
        }

        ignoreList = ignoreList || [];

        var cb = function(eventname) {
           iab._eventHandler(eventname);
        };

        strWindowFeatures = strWindowFeatures || "";

        exec(cb, cb, "InAppBrowser", "open", [strUrl, strWindowName, strWindowFeatures, ignoreList]);
        return iab;
    };

Changed to receive ignoreList as the fifth parameter of function. And if it is empty, it is added as an empty array at the end of the fifth argument at exec.

This completes the modification of the JavaScript part.

Objective-C fix

Next, let's modify Objective-C. The target file is the platforms / ios / Hello Cordova / Plugins / cordova-plugin-inappbrowser / CDVInAppBrowser.m file. Open it in Xcode with assistance, not in a text editor, and fix it.

First, prepare _unloadList as an instance variable. This variable holds a list of regular expressions sent in JavaScript.

Before correction

 @interface CDVInAppBrowser () {
     NSInteger _previousStatusBarStyle;
 }

Revised

 @interface CDVInAppBrowser () {
     NSInteger _previousStatusBarStyle;
     NSArray<NSString *> *_unloadList;
 }

Next, in the open method, keep the list of regular expressions passed from JavaScript in _unloadList.

Before correction

- (void)open:(CDVInvokedUrlCommand*)command
{
    CDVPluginResult* pluginResult;

    NSString* url = [command argumentAtIndex:0];
    NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf];
    NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]];

Revised

- (void)open:(CDVInvokedUrlCommand*)command
{
    CDVPluginResult* pluginResult;

    NSString* url = [command argumentAtIndex:0];
    NSString* target = [command argumentAtIndex:1 withDefault:kInAppBrowserTargetSelf];
    NSString* options = [command argumentAtIndex:2 withDefault:@"" andClass:[NSString class]];
    _unloadList = [command argumentAtIndex:3 withDefault:@[] andClass:[NSArray<NSString *> class]];

Next, let's judge whether the URL specified at the time of page transition matches _unloadList, and if it matches, stop the page transition and fire the unload event. This can be implemented using the method webView: shouldStartLoadWithRequest: navigationType :. If this method returns YES, page transition can be performed, and if NO is returned, page transition will not occur.

Modify the end of this method as follows:

Before correction

return YES;

Revised

    __block BOOL unloadFlag = NO;
    NSString *urlStr = [url absoluteString];
    [_unloadList enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        NSError *error = nil;
        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:obj
                                                          options:NSRegularExpressionCaseInsensitive
                                                            error:&error];
        if (! error) {
            NSTextCheckingResult *match = [regex firstMatchInString:urlStr
                                                            options:0
                                                          range:NSMakeRange(0, urlStr.length)];
            if (match) {
                unloadFlag = YES;
            }
        }
    }];
    if (unloadFlag) {
        CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK
                                                      messageAsDictionary:@{@"type":@"unload", @"url":urlStr}];
        [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]];
        
        [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
        return NO;
    }

    return YES;

That's all there is to it. The __block specifier is included because this variable is (possibly) modified from within the block syntax.

Operation check

Let's change the app side to use the new InAppBrowser

Modify www / index.js as follows:

    onDeviceReady: function() {
        this.receivedEvent('deviceready');
        document.getElementById('btn').addEventListener('click', function() {
          var ref = cordova.InAppBrowser.open('https://github.com/apache/cordova-plugin-inappbrowser', '_blank', 'location=yes', null, ['https:\/\/github\.com\/apache\/cordova-plugin-inappbrowser\/blob\/master\/README\.md']);
          ref.addEventListener('unload', function(json) { alert( json.url ); } );
        },false);
    },

Now, the README.md specified in the 5th argument of the open method cannot be opened, and the unload event will be fired at that time.

Feedback to plugins

The following two files have been modified in the work app this time.

platforms/ios/platform_www/plugins/cordova-plugin-inappbrowser/www/inappbrowser.js platforms/ios/HelloCordova/Plugins/cordova-plugin-inappbrowser/CDVInAppBrowser.m

These two changes must be reflected in the InAppBrowser plugin. In the InAppBroweser plugin directory, the above corresponds to:

www/inappbrowser.js src/iOS/CDVInAppBrowser.m

To put it simply, I think it's okay to overwrite it entirely, but you can't overwrite inappbrowser.js.

platforms/ios/platform_www/plugins/cordova-plugin-inappbrowser/www/inappbrowser.js When I open

cordova.define("cordova-plugin-inappbrowser.inappbrowser", function(require, exports, module) {
 //The contents of the code
});

However, please overwrite only this "contents of the code" as www / inappbrowser.js. In other words, the development app platforms / ios / platform_www / plugins / cordova-plugin-inappbrowser / www / inappbrowser.js The cordova-plugin-inappbrowser plugin www / inappbrowser.js is the one without the first and last lines of.

For CDVInAppBrowser.m, you can overwrite and copy as it is.

Summary so far

The modified version is https://github.com/knight9999/cordova-plugin-inappbrowser/tree/unload_list.

Compatible with Android

If you are a development app, remove the cordova-plugin-inappbrowser for development that has already been installed, and re-install the collected cordova-plugin-inappbrowser again.

$ cordova plugin rm cordova-plugin-inappbrowser
$ cordova plugin add [/path/to/cordova-plugin-inappbrowser]

Now, the modified JavaScript is reflected on the Android side as well. Check out platforms / android / platform_www / plugins / cordova-plugin-inappbrowser / www / inappbrowser.js.

And prepare once.

$ cordova prepare android

About Android Studio

I don't think Cordova-Android 6.2.3 can be built successfully with the latest version of Android Studio 3.0 series. In that case, please download and unzip gradle-3.2-all.zip from https://services.gradle.org/distributions/ so that you can use it from Android Studio. (On Mac, it will be placed under /Application/Android Studio.app/Contents/gradle)

If you get an EACCESS error with cordova compile etc., use the -d command to display the details of the error and deal with it.

$ cordova compile android -d

Also, when you open it in Android Studio, if you get a message to downgrade gradle, please downgrade.

Java code modification

Then open the platforms / android directory in Android Studio. Launch Android Studio and open the platforms / android directory with the Open an existing Android Studio project.

The target file to be modified is platforms / android / src / org / apache / cordova / inappbrowser / InAppBrowser.java.

First, I want to use JSONArray, so I'll import it.

Revised

import org.json.JSONArray;

Then, immediately after the class declaration of InAppBrowser, define unloadList as an instance variable as follows.

Before correction

    private boolean shouldPauseInAppBrowser = false;
    private boolean useWideViewPort = true;

Revised

    private boolean shouldPauseInAppBrowser = false;
    private boolean useWideViewPort = true;
    private JSONArray unloadList;

Then, when it is called by the open method from JavaScript, the array assigned by the 5th argument is stored in JSONArray. Modify the execute method of the InAppBrowser class.

Before correction

            final String target = t;
            final HashMap<String, Boolean> features = parseFeature(args.optString(2));

Revised

            final String target = t;
            final HashMap<String, Boolean> features = parseFeature(args.optString(2));
            unloadList = args.optJSONArray(3);

And finally, modify the shouldOverrideUrlLoading method of the InAppBrowserClient class (located in the InAppBrowser.java file). Now, if it matches the regular expression, it returns an unload event to prevent page transitions. Add a judgment process at the end of this method.

Before correction

            return false;

Revised

            boolean unloadFlag = false;
            for (int i=0;i<unloadList.length();i++) {
                String regex = unloadList.optString(i);
                if (url.matches(regex)) {
                    unloadFlag = true;
                }
            }
            if (unloadFlag) {
                try {
                    JSONObject obj = new JSONObject();
                    obj.put("type", "unload");
                    obj.put("url", url);
                    sendUpdate(obj, true);
                } catch (JSONException ex) {
                    LOG.e(LOG_TAG, "URI passed in has caused a JSON error.");
                }
                return true;
            }

            return false;

It is similar to webView: shouldStartLoadWithRequest: navigationType: on iOS, but note that the boolean value returned by shouldOverrideUrlLoading on Android is reversed. A method with similar functionality, but not compatible with iOS and Android. It's a different feature of each framework.

This is complete.

As with iOS, in order to reflect this in the plugin, this development app

platforms/android/src/org/apache/cordova/inappbrowser/InAppBrowser.java

Cordova-plugin-inappbrowser in the plugin directory

src/android/InAppBrowser.java

Please overwrite.

Summary so far

The Android fix is also reflected in https://github.com/knight9999/cordova-plugin-inappbrowser/tree/unload_list. Also, the plugin version is 1.6.1-unload.

Summary

Using InAppBrowser as a theme, I introduced how to modify Cordova / Monaca plugin.

Plugin development / modification work requires knowledge of native code such as Java and Objecitve-C, knowledge of Cocoa framework and Android framework, CordovaLib mechanism, cordova plugin configuration file mechanism, hooks. This is a difficult task because there are various requirements such as knowledge about scripts. (For the hook script, I wrote an article on https://qiita.com/KNaito/items/65587f5d51974e8b4adf, so please refer to it as well.)

However, the development of plugins will broaden your horizons and give you an idea of how to break down the functionality of your app. In addition, the plug-in can be reused and will be an asset for future application development, so please give it a try.

Recommended Posts

I will not hesitate anymore! Customize Cordova / Monaca Plugin --- Try to modify InAppBrowser Plugin
I will not hesitate anymore! RSpec introduction flow