Unable to Override the Built-in Library

As suggested by vknow360 in the post: Is the extension-template project deprecating?, I repost my original issue on https://github.com/mit-cml/extension-template/issues/9 to here.

Continued from that post, I've already tried to replace the corresponding jar files in the "lib/appinventor" folder, but this method didn't work. I've also tried deleting the "build" folder before compiling, but this didn't work either.

The aix file is able to produced. But when it's imported and used to produce an apk file, and when run on an Android device. A following error message is shown:

java.lang.NoSuchMethodError: No virtual method setSmallIcon(Landroidx/core/graphics/drawable/IconCompat;)Landroidx/core/app/NotificationCompat$Builder; in class Landroidx/core/app/NotificationCompat$Builder; or its super classes (declaration of 'androidx.core.app.NotificationCompat$Builder' appears in base.apk)

Noted that this is a "NoSuchMethodError", and I immediately recognized the older version of the class was still being used (instead of the new version) when compiling to the apk file.

Any more suggestion?

Is latest version of lib added to aix?

Yes, pretty sure. If the new version is not used (to replace the old version), I can't even comply the codes to the aix file.

The App Inventor build servers are the ultimate source of truth on the matter. They include all of the .jar files needed by the core components (anything identified by the @UsesLibraries annotation in appinventor-sources/appinventor/components/src/). If you attempt to include a newer version of the library you're somewhat beholden to whether the app developer includes a component that references that library or not. If not, then you're fine and your extension will compile into the app successfully. If so, then the build server will include its version of the library, which will conflict with the version you've used in your extension, and the compilation will fail because the dex utility does not allow duplicate versions of the same class file (even if the files are byte-for-byte identical).

According to my own experience, the compilation doesn't fail, but the complied app just includes the old version only. So, when the app is run, and a method that only the newer version has is called upon, it keeps complaining no such method.

Anyway, plainly speaking, is there any way I can use the newer version instead?

Repackage the jar, it should work.

Repackage it? I actually replaced it completely, it still continue to use the old version.

The short answer is no, you cannot override the libraries included in App Inventor. The longer answer is that under certain circumstances it is possible but it's really a niche situation and you'll probably have a bad time.

The @UsesLibraries annotation is key to how App Inventor manages the inclusion of dependencies for components. For the components App Inventor provides, the set of required libraries is built based on the @UsesLibraries annotations for components used in the project. So if your project uses types A, B, and C, then the set of libraries included will be the union of libraries identified by the @UsesLibraries annotation on the classes A, B, and C. There are also some libraries always included (e.g., appcompat) because they are used everywhere and thus aren't tied to a particular component. This list of critical libraries can be found in Compiler.java.

For extensions, however, the libraries are bundled into the extension itself. Extensions include both a JAR file, which contains the union of all dependencies and your extension's classes, and a DEX file, which contains the compiled versions that are linked into the companion during live testing. During the extension compilation, the system looks at the list of libraries in the @UsesLibraries annotation and pulls the named libraries into the JAR file (which is then used to make the DEX file).

This can lead to a few different situations:

  1. If you replaced an existing library provided by App Inventor but did not name it in the @UsesLibraries annotation, your extension will compile and be able to reference methods defined in the newer version, but the new library code won't be included in your extension due to the lack of annotation and so at app compile time or in the companion the older version provided by App Inventor is found and used. Based on your description this is the most likely scenario but it's really a violation of the rules of creating an App Inventor component so it's not surprising that it doesn't work.
  2. If you replaced an existing library and included it in the list of @UsesLibraries annotation, then the library code will get bundled into your extension. This is fine so long as the person using your extension never includes a component in their app that makes use of the other version of the library. This will cause the build server to include both the old and new classes, which the dx utility will balk at during compilation since that violates one of its constraints.
  3. There is a special scenario where you can replace android.jar with a newer Android runtime (i.e., to use APIs in newer Android versions that App Inventor doesn't compile against yet). This will work (do not list android.jar in @UsesLibraries though) but you should gate your use of the new API based on android.os.Build.VERSION.SDK_INT so that the extension doesn't crash the app on older versions of Android.
  4. In some cases, you can make use of a utility called jarjar that allows you to repackage the classes in the library into a different package name that won't conflict with App Inventor's built in version of the library. Generally this is the most sane approach if you really need to use the new library in question and at least used to be recommended by Google as a way of packaging versions of libraries that conflict with those provided by Android itself, since classes provided by the Android runtime get priority over those provided by your APK.
1 Like

Not really when you have ProGuard. It's just a matter* of adding the following rule to the lib/proguard/proguard.cfg file...

-repackageclasses 'com/example/package'   # This can be anything except the package name of the conflicting library

...in your extension template, and all the classes of your extension and its libraries will be repackaged under, for e.g., com.example.package package when compiled with ProGuard enabled (like this: ant extensions -Dproguard=1). No need to use jarjar.

Also, you get the added benefits of using ProGuard like decreased extension size and code optimizations.

*In some cases, this might not work unless you replace this rule in the lib/proguard/proguard.cfg file:

-keep public class * {
    public protected *;
}

...with this:

-keep public class your.extension.package** {
    public *;
 }
-keeppackagenames gnu.kawa**, gnu.expr**
1 Like

Thanks for all the suggestions. I will consider them carefully and try if the situation fits.