Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cordova plugin Android Activity - accessing resources

I am developing a Cordova plugin for Android and I am having difficulty overcoming accessing project resources from within an activity - the plugin should be project independent, but accessing the resources (e.g. R.java) is proving tricky.

My plugin, for now, is made up of two very simple classes: RedLaser.java and RedLaserScanner.java.

RedLaser.java

Inherits from CordovaPlugin and so contains the execute method and looks similar to the following.

public class RedLaser extends CordovaPlugin {
    private static final string SCAN_ACTION = "scan";

    public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException {
        if (action.equals(SCAN_ACTION)) {
            this.cordova.getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    scan(args, callbackContext);
                }
            });

            return true;
        }

        return false;
    }

    private void scan(JSONArray args, CallbackContext callbackContext) {
        Intent intent = new Intent(this.cordova.getActivity().getApplicationContext(), RedLaserScanner.class);
        this.cordova.startActivityForResult((CordovaPlugin) this, intent, 1);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        // Do something with the result
    }
}

RedLaserScanner.java

The RedLaserScanner contains the Android Activity logic and inherits from BarcodeScanActivity (which is a RedLaser SDK class, presumably itself inherits from Activity);

A very simple structure is as follows:

public class RedLaserScanner extends BarcodeScanActivity {
    @Override
    public void onCreate(Bundle icicle) {               
        super.onCreate(icicle);

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        setContentView(R.layout.preview_overlay_new_portrait);
    }
}

I am having trouble because I need to access the project's resources to access R.layout.preview_overlay_new_portrait (which are scatted in the Eclipse project) - but I cannot do this unless I import com.myProject.myApp.R - which makes my plugin have a dependency on the project itself.

I did some investigation and found cordova.getActivity().getResources() which seems useful, but this is not accessible from within my RedLaserScanner - because it does not inherit from CordovaPlugin.

Can somebody please help me with some pointers?

Thanks

like image 311
keldar Avatar asked Sep 30 '14 09:09

keldar


3 Answers

I just ran into the same issue and it turns out to be pretty easy to solve. RedLaserScanner extends an activity, so you can just call getResources() like this:

setContentView(getResources("preview_overlay_new_portrait", "layout", getPackageName()));
like image 123
Dave Wolfe Avatar answered Nov 20 '22 15:11

Dave Wolfe


Hooks can be used to replace source file contents to remove wrong imports and/or add the right imports of resources.

I created a script that do it without needing to specify the files. It tries to find source files (with .java extension), removes any resource import already in it and then put the right resources import (if needed), using the Cordova application package name.

This is the script:

#!/usr/bin/env node

/*
 * A hook to add resources class (R.java) import to Android classes which uses it.
 */

function getRegexGroupMatches(string, regex, index) {
    index || (index = 1)

    var matches = [];
    var match;
    if (regex.global) {
        while (match = regex.exec(string)) {
            matches.push(match[index]);
            console.log('Match:', match);
        }
    }
    else {
        if (match = regex.exec(string)) {
            matches.push(match[index]);
        }
    }

    return matches;
}

module.exports = function (ctx) {
    // If Android platform is not installed, don't even execute
    if (ctx.opts.cordova.platforms.indexOf('android') < 0)
        return;

    var fs = ctx.requireCordovaModule('fs'),
        path = ctx.requireCordovaModule('path'),
        Q = ctx.requireCordovaModule('q');

    var deferral = Q.defer();

    var platformSourcesRoot = path.join(ctx.opts.projectRoot, 'platforms/android/src');
    var pluginSourcesRoot = path.join(ctx.opts.plugin.dir, 'src/android');

    var androidPluginsData = JSON.parse(fs.readFileSync(path.join(ctx.opts.projectRoot, 'plugins', 'android.json'), 'utf8'));
    var appPackage = androidPluginsData.installed_plugins[ctx.opts.plugin.id]['PACKAGE_NAME'];

    fs.readdir(pluginSourcesRoot, function (err, files) {
        if (err) {
            console.error('Error when reading file:', err)
            deferral.reject();
            return
        }

        var deferrals = [];

        files.filter(function (file) { return path.extname(file) === '.java'; })
            .forEach(function (file) {
                var deferral = Q.defer();

                var filename = path.basename(file);
                var file = path.join(pluginSourcesRoot, filename);
                fs.readFile(file, 'utf-8', function (err, contents) {
                    if (err) {
                        console.error('Error when reading file:', err)
                        deferral.reject();
                        return
                    }

                    if (contents.match(/[^\.\w]R\./)) {
                        console.log('Trying to get packages from file:', filename);
                        var packages = getRegexGroupMatches(contents, /package ([^;]+);/);
                        for (var p = 0; p < packages.length; p++) {
                            try {
                                var package = packages[p];

                                var sourceFile = path.join(platformSourcesRoot, package.replace(/\./g, '/'), filename)
                                if (!fs.existsSync(sourceFile)) 
                                    throw 'Can\'t find file in installed platform directory: "' + sourceFile + '".';

                                var sourceFileContents = fs.readFileSync(sourceFile, 'utf8');
                                if (!sourceFileContents) 
                                    throw 'Can\'t read file contents.';

                                var newContents = sourceFileContents
                                    .replace(/(import ([^;]+).R;)/g, '')
                                    .replace(/(package ([^;]+);)/g, '$1 import ' + appPackage + '.R;');

                                fs.writeFileSync(sourceFile, newContents, 'utf8');
                                break;
                            }
                            catch (ex) {
                                console.log('Could not add import to "' +  filename + '" using package "' + package + '". ' + ex);
                            }
                        }
                    }
                });

                deferrals.push(deferral.promise);
            });

        Q.all(deferrals)
            .then(function() {
                console.log('Done with the hook!');
                deferral.resolve();
            })
    });

    return deferral.promise;
}

Just add as an after_plugin_install hook (for Android platform) in your plugin.xml:

<hook type="after_plugin_install" src="scripts/android/addResourcesClassImport.js" />

Hope it helps someone!

like image 4
Luan Avatar answered Nov 20 '22 14:11

Luan


I implemented a helper for this to keep things clean. It also helps when you create a plugin which takes config.xml arguments which you store in a string resource file in the plugin.

private int getAppResource(String name, String type) {
    return cordova.getActivity().getResources().getIdentifier(name, type, cordova.getActivity().getPackageName());
}

You can use it as follows:

getAppResource("app_name", "string");

That would return the string resource ID for app_name, the actually value still needs to be retrieved by calling:

this.activity.getString(getAppResource("app_name", "string"))

Or for the situation in the original question:

setContentView(getAppResource("preview_overlay_new_portrait", "layout"));

These days I just create a helper which returns the value immediately from the the helper:

private String getStringResource(String name) {
    return this.activity.getString(
        this.activity.getResources().getIdentifier(
          name, "string", this.activity.getPackageName()));
}

which in turn you'd call like this:

this.getStringResource("app_name");

I think it's important to point out that when you have the resource ID you're not always there yet.

like image 1
Thomas Avatar answered Nov 20 '22 13:11

Thomas