AltimitSystems / mv-android-client

RPG Maker MV unofficial Android client
http://www.hbgames.org/forums/viewtopic.php?f=48&t=79391
Apache License 2.0
114 stars 35 forks source link

API Extensions #2

Closed felixjones closed 5 years ago

felixjones commented 6 years ago

There are multiple attempts to add Android specific API extensions to the MV Android Client.

My thoughts right now is that it would be best to create a mechanism that makes it simpler and clearer on how to add API extensions to the MV Android Client.


The intention for the android-api is to start the (long and painful) journey of implementing node.js API equivalents in Android. I expect this would be a standard API for mv-android-client, so I will modify this branch to create a standard API Extensions mechanism, so features such as Google Play Services can be added in a more modular way.

Perhaps what we can do is make it so these API Extensions are their own libraries (https://developer.android.com/studio/projects/android-library.html) that come bundled with the MV Android Client, but they need to be added to the base project and activated for them to be included in the build itself. The app build.gradle file could be used as a way for listing out what API Extensions should be included in the project.

This moves potentially large dependencies such as Google Play Services out of the main project and also gives a clear, clean place to put all this code. The Javascript binding could also be stored in this library project.

The MV Android Client base code could detect the requested extensions, then add them through the use of reflection (to avoid the need to add imports for each of the libraries). The extensions would be given a context to use (from the activity, so extensions can retrieve the calling activity if desired) and they could optionally provide some Javascript code to run (similar to a standard MV Plugin) and a Javascript interface to bind (again, optional).

We would need to forward some Activity methods, such as onActivityResult for Google Play Services.


Thoughts, please?

JTM-rootstorm commented 6 years ago

At the very least, yeah, moving things off into their own libraries sounds like a good idea. I would, however, be a bit more interested in seeing the reflection idea go somewhere if it makes things easier in the long run.

Could you possibly explain more about the providing Js/JsInterface bit at the end there? I just want to make sure I'm on the same page with that as what I'm thinking is that the extention dev can provide a plugin that essentially acts as a bridge for the interfaces within the app but keeps that kind of config within MV and keeps editing of the android studio project minimal

felixjones commented 6 years ago

The JavascriptInterface system would be a mechanism for the extension to say "here's my JavascriptInterfaces, here's the names you should give them" and then the MV Android Client binds them when it needs to.

Perhaps the extensions will have a method that returns a mapping of JavascriptInterface names and objects - so the MV Android Client would call extension.getJavascriptInterfaces()


Later on, the MV Android Client will then query the loaded extensions for any Javascript code that needs to be ran - perhaps this is code that adds JS classes that utilise the JavascriptInterfaces.

There's an example of this in the current work done on the Android API branch: https://github.com/AltimitSystems/mv-android-client/blob/6601e0dc8409fc46f357d19ac314deed3bde015d/app/src/main/res/values/values_internal.xml#L43-L134 - this Javascript wraps the JavascriptInterface methods in a style that mimics the Node.JS require function. The JavascriptInterface is bound as "__a_client" earlier on.

Ideally, this Javascript code would not be in XML files like this. I think it should be moved out into .JS files, to make developing extensions a little easier. This would work perfectly fine (no qualms) if extensions were handled as their own libraries, so I think that would be the route to go down.

Perhaps the Javascript code would be queried from the Javascript interface objects themselves - javascriptInterface.getJavascript() or maybe the extension could also provide this method as extension.getJavascript() - or the extension could provide zero Javascript at all and its entire behaviour is handled by the names provided by the JavascriptInterface mapping.

The Javascript could just package up the JavascriptInterface into a handy library, to be used by MV Plugins, or it could be an MV Plugin itself - adding the Android functionality into the MV game (although I imagine this would be a bad idea as it would introduce Plugin compatibility issues - we should probably leave Plugin development for Plugin authors and only provide library interfaces for accessing Android features).


If you have your own ideas on how this mechanism can be designed, then I'd be glad to hear them. These are all ideas at the moment, no concrete plans (but I may start implementing some of these ideas to get a feel of their effectiveness).

JTM-rootstorm commented 6 years ago

Yeah, I'll agree that Js shouldn't be floating around in some XML; I'm already having a bit of a fit just imagining having to constaly format things like \' just to not break things.

I don't think us providing MV plugins would break things or bring compatability issues with anything as we'd only be interfacing with our stuff but, yeah, I guess leaving it to people who'd know what they're doing and the do's and don'ts would better ensure it doesn't happen. I agree that, at the very least, extensions should provide their own interfaces (and possibly documentation of some kind) for the authors to use

What you've got going in that XML sample looks fine, to be honest, and that looks the way to go. I do have a feeling that offloading stuff into Js files would mean having to do some file IO on the extension side to load the scripts in and then parse them before the webview launches (unless I'm wrong). If that's the case, then a standard location for extension Js files should probably be established so we're not having to worry about special file paths nor are we cluttering things up on the system.

felixjones commented 6 years ago

When I say breaking Plugin compatibility, I am imagining a scenario where - as a hypothetical example - we add Google Sign In capability and create a Plugin that adds this as a menu option in the Scene_Title - however the end user has decided to use a custom Scene_Title implementation that does not conform to the expected vanilla implementation (which is entirely fair on their part), and thus compatibility is broken.

By drawing a line in the sand and saying "we provide the JS library, Plugin authors write the implementation" we're moving the responsibility of maintaining Plugin compatibility to 'someone else'. It may be slightly frustrating for users to require an additional Plugin to access Android specific features, but at that point it is no longer our problem to solve.


The WebView will load as normal, the RPG Maker engine will load, then the JS is injected.

The JS code would be read into memory before the WebView has loaded, just to make sure it's ready to be injected and won't cause a stall at a more annoying time.

I don't think a standard location is actually needed. If the extension simply provides JS source code, then the extension could load that code from wherever it wants - be it a file, an XML string, a Java String or a JS file pulled from an online server.

That being said, for the sake of consistency I think all the bundled extensions should store their JS sources in the same way. Probably in the individual extension res/raw/ folder.


I'll get to modifying the android-api branch today to implement some of these ideas we've discussed.

felixjones commented 6 years ago

I've made a preliminary version of the Extension API: https://github.com/AltimitSystems/mv-android-client/tree/android-api

This comes with an example extension "Android API" which adds requireAndroid to Javascript (similar to Node's require function, but only parts of path and fs are implemented right now).

I think all the built-in extensions should have "lib" in their starting name to keep them alphabetically below the "app" module.

There are some 'gotchas'to look out for:

It should be possible to remove these limitations by adding some extra complexity (users would need to enter in two strings for an Extension, the module name and package name). I haven't bothered with this yet as this is preliminary - we might still realise that this system sucks and go with something else.

Extensions must have a top-level class named "Extension" - this is the mechanism that will register it with the MV Android Client.

The signature of this class is as such:

public class Extension {
    public Extension(Context context) {
        ...
    }

    public Map<String, Object> getJavascriptInterfaces() {
        ...
    }

    public String[] getJavascriptSources() {
        ...
    }

    public int[] getRequestCodes() {
        ...
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        ...
    }
}

These methods are all optional, however onActivityResult depends on the request code being in the array returned by getRequestCodes.

The Context object given in the constructor is guaranteed to be the WebPlayerActivity context. This Stack Overflow answer shows how you'd get an Activity from this: https://stackoverflow.com/a/46205896/2123663

The mapping returned by getJavascriptInterfaces is the name of the Javascript interface + the object itself. This is used literally as: addJavascriptInterface( mapping.key, mapping.value )

The String array returned by getJavascriptSources is executed sequentially - so make sure your dependencies are in the right order.

The extension can then be activated by adding it to the list of extensions in the build.gradle (app) file:

/**
 * Extensions
 */
def EXTENSIONS = [ 'libandroidapi', 'libawesome' ]

This example inserts the dependencies into gradle as \:libandroidapi and \:libawesome and adds systems.altimit.libandroidapi.Extension and systems.altimit.libawesome.Extension as Extension searches.

The way Javascript source files are handled is decided by the extension, however I think the official extensions should store them all in res/raw/ for consistency.

JTM-rootstorm commented 6 years ago

Sorry for the wait, been busy this weekend

I think I slightly easier way to go about this would to use an Interface that all extensions must implement so you can just call methods directly instead of using getMethod() as all implementations must have those methods defined in one way or another. The methods that aren't needed can still be left blank in the extension.

This might also mean no longer having to make use of

 public boolean respondsToRequestCode(int activityRequestCode)

as either the extension makes use of the requestCode (with its own switch-case or set of if-statements on its end if it uses multiple) or wont and does nothing with it anyways; as it stands it would just be adding another set of if-statements on top of others should this idea go through.

The classes could be loaded in a similar fashion as this StackOverflow answer provides and then perhaps just have the classes take on their library names (ie libandroidapi would rest in libandroidapi.java/.class). That second bit is just personal preference as I'd rather not have a bunch of Extension.java/.class floating about in various packages but ends up getting the same result anyways.

So I think this may speed up execution as we're no longer having to do a bunch of method lookups as the ones we want are guaranteed to exist if we force an Interface or Abstract Class and the only exception handling we'd most likely have to worry about is where you load the class in from the BuildConfig. We just need all extensions to extend something like

public abstract AbstractExtention {
    public AbstractExtention(Context context) {
        ...
    }

    /* Maybe use this to init instead of constructor */
    public void initContext(Context context) {
        ...
    }

    /* Perhaps keep the context in this parent class and use this method for the children */
    protected final Context getParentContext() {
        ...
    }

    public abstract Map<String, Object> getJavascriptInterfaces() {
        ...
    }

    public abstract String[] getJavascriptSources() {
        ...
    }

    public abstract int[] getRequestCodes() {
        ...
    }

    public abstract void onActivityResult(int requestCode, int resultCode, Intent data) {
        ...
    }
}

and we'd be good to go. Idk, just some thoughts to help streamline things on our end

felixjones commented 6 years ago

My original attempt was with interfaces, however that meant creating an extra dependency project between the client and the extensions, otherwise there'd be a cyclic dependency.

I also tried with abstract classes to allow some of the methods to be optional, which then led to the current implementation.

The reason for having respondsToRequestCode is to prevent leaking of information, rather than any actual binding. I can remove it entirely with the current system. It exists due to over engineering for a problem that probably isn't even a real problem (security of request codes??? Within private apps???). I'll probably remove it anyway.

I wanted to avoid adding another module to the project, but I will make a new version with the API library dependency and see how that feels.

Personally, I don't mind the use of reflection in the client, so long as the extension developer does not need to use it.

JTM-rootstorm commented 6 years ago

Personally, I think it's alright if the extensions have a dependency from the main app as, after all, they're extending from it, but yeah that's up to you; either one works in the end and the extension dev is gonna be calling the same methods the same way anyways. I was just looking at code and thought 'hey it's basically but with some extra', didn't know it had evolved from straight from that.

Part of me also prefers force having the method and it do nothing (just returns or w/e) vs ignoring thrown exceptions, but that could also be my bad habit of premature optimization coming back out (which could probably be said about a lot of recommendations/ideas I come up with) since not having it in try-catches would really only be slightly faster in the case of this app while creating a bit of coupling where there currently isnt any

I also realized, while trying to sleep, that having the needed classes called Extension (as per how you load them in) is going to be the way to go. Well, unless you feel like figuring out regular expressions/regex to pull, say, LibAndroidApi from libandroidapi but I also have no idea if that'll even work without creating a bunch of problems/making a mess /end_rambling

felixjones commented 6 years ago

I have one idea that avoids the classes being named Extension (and being top-level). The app build.gradle extensions list is not very flexible and has those caveats I mentioned in an earlier post above - the solution I have in mind for these caveats is to force the end user to actually write out the package name and the module name as pairs in the extension list - this could be changed so rather than the package name, the full class name is required.

def EXTENSIONS = [
    ['libandroidapi', 'systems.altimit.libandroidapi.Extension'],
    ['libawesome', 'package.custom.awesome.AwesomeExt']
]

Certainly a bit more typing required, more stuff to potentially get messed up by the end user, but it removes the caveats and provides a way to avoid naming the classes Extension.

The reflection API isn't good enough to search packages for classes; that was my other idea to have the package search for a classes with a particular annotation (from the shared library API). Not going to happen.

I wasn't concerned about performance as I made sure the reflection is mostly used to query on startup. The only area where performance of reflection is potentially a problem is the onActivityResult call, however you have other - more serious - performance issues if that is getting called multiple times in a short time-frame. These try blocks are also within the areas where the code is only called once (or is onActivityResult related).

I once again like the idea of using interfaces because it conforms to what Java developers expect. Using reflection this way is usually done by massive 3rd party libraries inside their black-box (the big Android 3rd party libs do this), not by a core run-time where you can modify the source code directly (this project). Actually, by using interfaces we can call the methods directly rather than cache their result (the current implementation) - so we could use that performance gain for something.


The extensions won't have a dependency on the main app as that causes a cyclic dependency issue (main app depends on extensions, which each depend on the main app). The main app and extensions will have to depend on a separate API module - this is what I wanted to avoid but I think I was just trying to oversimplify.

The API module will probably be called something like clientapi. So we have app depending on clientapi and any extensions, then each extension also depending on clientapi.


I did also consider going about this via a more "Androidy" way - with Services and Broadcasts - but that requires a heck of a lot more code for writing Extensions. I believe the reflection system we have is far nicer to deal with.

felixjones commented 6 years ago

https://github.com/AltimitSystems/mv-android-client/tree/android-api

I've gone ahead and made the changes discussed. There is now a clientapi module, that Extensions must depend on to be recognised by the run-time.

Extensions must extend the abstract class "AbstractExtension". There is a constructor that expects a non-null context, this is to force implementations to create a constructor that expects a valid context - which is what reflection expects. onActivityResult is an optional override, however getJavascriptInterfaces and getJavascriptSources are now mandatory. getRequestCodes is completely removed - "don't be a dolt: return early from onActivityResult".

The app build.gradle extensions list now looks like this:

/**
 * Extensions
 */
def EXTENSIONS = [
        [ module: 'libandroidapi', extension: 'systems.altimit.libandroidapi.AndroidAPI' ],
        [ module: 'libawesome', extension: 'package.custom.awesome.AwesomeExt' ],
]

Hopefully it's obvious enough on how it should be used. If any extensions are included, then a dependency for the clientapi project is automagically added to the main app.


On a side note, JavascriptInterface is very slow to call, I've had issues with it being incredibly slow before, so I've added a really dumb key mapping system to store the results of the example libandroidapi calls. This should probably be changed to a smarter caching system that cleans itself up when it needs to.

Extension developers should avoid JavascriptInterface calls as much as possible (yeah that means everyone needs to write more Javascript).


Another moderately large issue is that CrossWalk won't work with this. CrossWalk uses a different JavascriptInterface package name to the vanilla WebView, so compatibility is broken - sadly. I can't think of an easy way around this. Extensions is just a feature that CrossWalk won't receive.

JTM-rootstorm commented 6 years ago

EDIT: WOW I take so long to type and think that you threw another response in


Would it be possible for the DEFs (or something) that hold lib name/package-dirs to be in the extension build.gradles and it still be read by the main app? Basically, a way to where the extension dev just has to worry about adding the name/package-dir to their build.gradle and not have to worry about what's in others? Googling leads me to believe we could just call something like

EXTENSIONS.add(["libsausage", "butcher.shop.meatpatties.Sausage"])

in our gradle files, but I'd like to be sure

Beyond that, I dont think I've any other questions/concerns as far as where the codebase is concerned


Yeah, even looking at Messengers, which seem to be the easiest way to implement a Bound Service, it still looks like a good bit of extra work to get it set up and going. It also doesn't keep things as decoupled as they currently are as something like this

public void sayHello(View v) {
        if (!mBound) return;
        // Create and send a message to the service, using a supported 'what' value
        Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0);
        try {
            mService.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

would probably need one of the following done

None of which I find appealing. I also don't have much of an idea on how you would even load them in unless you'd also be able to use the existing DEF for that one

The upside, though? Being able to send Objects down to the extension should it be desired (maybe as a way of delivering any needed data)


As for caching, unless you dont want any more dependencies pulled in, perhaps Google's Guava could be looked at since it includes some caching functionality


Crosswalk not being able to have nice things isn't much of a surprise since it's no longer being maintained, but I'm wondering if we can just stack android's @JavascriptInterface stuff on top of Crosswalk's for all methods that need it.

I know in C++ this would be kinda easy since you could toggle a #define flag to switch what @JavascriptInterface is pointing at (either android's or crosswalk's), but I don't know if Java can do something similar at the moment. If it can, it may be able to solve that problem

UPDATE: Probably worth looking at. I don't know if this is agnostic from the 'spring' stuff it concerns, but it's something

felixjones commented 6 years ago

I like the idea of the libraries registering their classes with the list of EXTENSIONS automatically when the dependency is added, that would simplify things a lot. I'll investigate if this is possible, my first thoughts are "no" because gradle is pretty restrictive when it comes to cross module communication (which I thoroughly investigated when attempting to add CrossWalk compatibility - hopefully I missed something).

The problem with CrossWalk's JavascriptInterface annotation is that supporting it means adding crosswalk as a dependency for extensions, which means a massive amount of bloat added to APKs - even if they don't use CrossWalk. If possible, I'd have the clientapi be the module that holds the webview/crosswalk switch so libs and main app can detect if CrossWalk is available and switch their build files accordingly.

I really do miss compile-time macros right now.


The caching requirement is not on the Java side of the code, it's on the Javascript side of the code, so no Java library will help here. I already investigated caching on Java and it didn't help at all - it's definitely the JavascriptInterface bridge that causes the big performance hit, which makes sense as it needs to halt the WebView entirely while it does whatever it needs to in single-threaded Java.


I'll give the def idea another shot with extensions, if I'm unsuccessful then what we have in the current android-api tree is what we'll likely stick with.

felixjones commented 6 years ago

Alrighty, I investigated a bit more. I'm confident there's not going to be an easy way to have the EXTENSIONS def modified by the libraries when they are included in the app module. Global variables work nicely on build, but for some reason as soon as you launch/debug/whatever gradle will change the build order and ruin everything...Soooo close to it working, yet gradle just doesn't want to play ball.

I also tested out manually having CrossWalk switches for the libs (which involves a load of bloat written in the lib gradle files), which actually worked out quite well (libs will swap flavors to whatever is selected on the parent - if there is a matching name) - that is, up until the JavascriptInterface annotations were needed. There's no way to inherit an annotation, so I can't make a shared annotation that Extension methods could use. Basically; CrossWalk must be added as a dependency. It's definitely a thing that would be easily solved by C style compile time macros...

JTM-rootstorm commented 6 years ago

Ah man, well RIP in peace.

felixjones commented 6 years ago

Android API branch is now merged into master.

JTM-rootstorm commented 6 years ago

Alrighty, I'll get to hammerin

JTM-rootstorm commented 6 years ago

So, to keep within Google's recommendation on handling sign-ins to Play Services I've had to modify the AbstractExtention and the WebPlayerActivity to pass along the 'onResume()' call if that's alright. I tried to find a way to manage to get the same results without having to do that but it lead to a lot of unavoidable code duplication and I can't use statics since Android Studio complains about memory leaks since anything I need to be static uses a Context/Activity

Those changes just amount to a for-each-loop similar to what you have going for 'onActivityResult' and the requisite method signature for the AbstractExtention, though, so it's not anything major.

If that's all fine by you, then I believe I can get a pull request out with my implementation of the Play Services once I finish testing things on my end (and I swear I better not have to create a new game page in the Play Console because that's mildly painful)

While I'm thinking about it, though, would it be possible to use stuff from your AndroidAPI lib to save some things in the app's storage? Should be nothing more than a JSON file used to keep track of unlocked/incremented achievements while not connected should a connection be established at a later time so the player can get their unlocks and stuff

felixjones commented 6 years ago

Yes I did expect something like a requirement for onResume/onPause/onStart/onStop to crop up eventually. Could you please implement all three of these callbacks in the same way that onActivityResult is implemented? I think having all available as optional overrides is ideal. Perhaps may be worth having onDestroy & onRestart too.

I don't think a lib should depend on another lib if it can be avoided. Let Plugin developers handle dependency woes. Implement storage in a way that makes sense for your system. It is my understanding that Google Play Achievements handles offline achievements itself, so you shouldn't need to worry about keeping an offline ledger JSON; you should be able to query achievement state regardless of connectivity - AchievementsClient.load gets a buffer of all the achievements and their states for the current player, you should be able to avoid storage with this.

JTM-rootstorm commented 6 years ago

Alrighty, I'll get the rest of the 'onX' methods implemented.

And I wasn't sure if GPlay handled it on it's own as the examples they keep pointing me towards has a sort of 'outbox' implemented to handle it. Bleh, I'll let it be and see what issues crop up, if any, but will take a look at that load method you mention

EDIT: maaan, the tutorials I've been following (Google's themselves, even!) have hidden quite a number of functions from me..

JTM-rootstorm commented 6 years ago

This about up to par with how you expect extensions to be laid out? (Also having someone look over the code may reveal things I've either done incorrectly or can do better at this point as I'd rather have it looked at now and not before I've added half a dozen complex things)

The requested changes to implement the rest of the methods has also been done, and I've modified the EXTENSIONS def in the main app's build.gradle to be

def EXTENSIONS = [
         [ module: 'libandroidapi', extension: 'systems.altimit.libandroidapi.AndroidAPI' ]
        //,[ module: 'libgoogleplay', extension: 'systems.altimit.libgoogleplay.GooglePlay' ]
]

since that isn't going to be readily visible unless you clicked around a good bit.

As for 'AchievementClients.load', it does help if the player signs out or gets signed out but the data gets lost if the player/system closes/kills the app. It does help that if the player has no network, but is signed in to GPlay on their device that things are stored there so at least that's half the problem solved.

I'm pretty sure I'm overcomplicating things; just serializing what I need and outputting to JSON or some binary format will do just fine in fixing that

felixjones commented 6 years ago

Yes that looks like an acceptable way for the extension to be laid out.

I believe gradle allows having dangling commas in array items, so you can move the commas to the end of the entries, rather than after the comment (makes more sense in English grammar).

def EXTENSIONS = [
    [ module: 'libandroidapi', extension: 'systems.altimit.libandroidapi.AndroidAPI' ],
    // [ module: 'libgoogleplay', extension: 'systems.altimit.libgoogleplay.GooglePlay' ],
]

I would have expected Google to store achievements on the device immediately when they're unlocked and synchronise in its own time, rather than depend on the game not being closed by the user. Even when I type that out it still sounds like the behaviour that should logically be happening.

Do the Google official guides mention anything about having to write local achievement storage? To me it sounds weird to require a local storage format that duplicates the data that the Google account should already be handling.

I also spot what looks like a small attempt at cloud save files? now this would be a great feature, however it may be worth doing what I did with the local Android file system storage in the Android API and just make it generic "cloud storage" and let Plugin authors implement it for save files.

Very excited by what you're adding here.

JTM-rootstorm commented 6 years ago

I was not aware Gradle could do that.. that certainly makes things cleaner

The google account handles it, yes, but only if the player has signed in or has signed in before losing network connectivity. The user not being signed in is where I can't seem to find anything. If they haven't signed in at least once, though, then none of it matters; nothing will be populated on that end (and shows me that I forgot to check and see if my caches are empty before querying their contents.. whoops)

The Google official stuff doesn't mention anything about handling things if a user has signed out, either, but some (read: one or two) SO answers say that the way it's currently implemented (via 'cacheAchievements' method) is more-or-less how it should be kept until the user can sign in again. It goes comletely dark, however, on what should be done if the user doesn't sign back in and the app is killed which is why I feel falling back to a local file would be a safty net of sorts.

TL;DR It's basically for those who signed in at least once before signing out, as then GPlay doesn't keep track of stuff for them at that point I think.. Doesn't quite say.

And yeah, I'm poking at cloud saves. I haven't gone through the Google stuff completely on that one so I can't say what'd be required on that end outside of permissions. I don't think I can quite genericize Snapshots, though, as it's a part of GPlay used specifically for game saves. Though I can probably have a method that just hands of a list/array of Snapshot objects for an author to work with as far as working them into a native MV UI as it also has a 'load' method

felixjones commented 5 years ago

I am considering removing the API Extensions system to simplify the project. It seems very under utilised and I believe that libraries are more likely to be developed if they were not tied to main branch. Wouldn't need to be pulled in for any updates.

The extensions mechanism was a good concept, but Android Studio projects are not suitable for this. Would be better to rely on traditional AAR libraries with some sort of tutorial for each library on how it should be integrated.

felixjones commented 5 years ago

@Lakaroth please create a new issue with your problem

Lakaroth commented 5 years ago

@felixjones do it. Sorry it's my first post on github

felixjones commented 5 years ago

I've made the executive decision to remove the extensions system entirely. Extensions should now be handled by their own AAR libraries by Plugin authors. 741198303a7f77037d9031a82cc9e228687448f9

Any new mechanism for similar behaviour should be made as a new issue of discussion.