Library wrapper guide

This guide describes how to use the Android API library wrapper. The library wrapper command-line tool generates C-language wrapper code for Java Android APIs, enabling you to integrate Java libraries into native C/C++ Android apps. For more details on the library wrapper, see Library wrapper for Android APIs.

This step-by-step guide demonstrates how to use the wrapper tool to integrate a Java library into a native Android app. For example purposes, this guide covers integrating the notification library of the androidx.core.app package. See Create a Notification to learn more about this library.

Prerequisites

This guide assumes you have an existing native Android project. It also uses the Gradle build system. If you don't have an existing project, create a new one in Android Studio using the Native C++ template.

The example code in this guide uses the directory root my_project/. Native code is located in my_project/app/src/main/cpp/, the default directory for Android Studio projects.

If you don't already have the library wrapper tool, download and unzip the package to the directory of your choice. This CLI tool requires the Java Runtime Environment (JRE).

Generate native code

When integrating a Java library, use the wrapper tool to generate a native code wrapper. The first step is to configure the wrapper.

Create the wrapper config

You create library wrapper configuration files to control the output of the native code generator. One feature of this file lets you specify the classes and methods to generate wrapper code.

Since there aren't many methods to wrap for the notifications library, you can define them directly in the custom_classes section. Create a new config.json resource anywhere in your project to define the methods. For example, you can create my_project/library_wrapper/config.json and paste the following sample configuration:

{
  "custom_classes": [
    {
      "class_name": "class java.lang.CharSequence"
    },
    {
      "class_name": "class java.lang.Object",
      "methods": [
        "java.lang.String toString()"
      ]
    },
    {
      "class_name": "class java.lang.String"
    },
    {
      "class_name": "class android.content.Context",
      "methods": [
        "java.lang.Object getSystemService(java.lang.String name)"
      ]
    },
    {
      "class_name": "class android.app.Notification"
    },
    {
      "class_name": "class android.app.NotificationManager",
      "methods": [
        "void createNotificationChannel(android.app.NotificationChannel channel)"
      ]
    },
    {
      "class_name": "class android.app.NotificationChannel",
      "methods": [
        "NotificationChannel(java.lang.String id, java.lang.CharSequence name, int importance)",
        "void setDescription(java.lang.String description)"
      ]
    },
    {
      "class_name": "class androidx.core.app.NotificationCompat"
    },
    {
      "class_name": "class androidx.core.app.NotificationCompat$Builder",
      "methods": [
        "Builder(android.content.Context context, java.lang.String channelId)",
        "androidx.core.app.NotificationCompat$Builder setContentText(java.lang.CharSequence text)",
        "androidx.core.app.NotificationCompat$Builder setContentTitle(java.lang.CharSequence title)",
        "androidx.core.app.NotificationCompat$Builder setSmallIcon(int icon)",
        "androidx.core.app.NotificationCompat$Builder setPriority(int pri)",
        "android.app.Notification build()"
      ]
    },
    {
      "class_name": "class androidx.core.app.NotificationManagerCompat",
      "methods": [
        "static androidx.core.app.NotificationManagerCompat from(android.content.Context context)",
        "void notify(int id, android.app.Notification notification)"
      ]
    }
  ]
}

In the preceding sample, you directly declare the Java classes and methods that require native wrapper code.

Run the library wrapper

With your wrapper config file defined, you're ready to use the tool to generate native wrapper code. Open a terminal to where you extracted the library wrapper and run the following command:

java -jar lw.jar \
  -o "my_project/app/src/main/cpp/native_wrappers" \
  -c "my_project/library_wrapper/config.json"

In the preceding sample you use the -c parameter to specify your wrapper config location, and the -o parameter to define the generated code directory. After running the tool, you should now have the generated code needed to call the Java-based notifications API from your native app.

Implement native notifications

In this section, you integrate the Android notifications library into your native app using your generated wrapper code. The first step is to update your project's app-level gradle.build resource (my_project/app/gradle.build).

Update gradle.build

  1. GNI is a support library required by the generated wrapper code. All projects using generated code should reference this library. To reference this library, add the following line to the dependencies section of build.gradle:

    implementation 'com.google.android.gms:play-services-gni-native-c:1.0.0-beta2'
    
  2. To enable prefab support, add the following code to the android section:

    buildFeatures {
      prefab true
    }
    
  3. To configure cmake, use the following cmake configuration in the android/defaultConfig section:

    externalNativeBuild {
      cmake {
          arguments '-DANDROID_STL=c++_shared'
      }
    }
    

Your completed build.gradle configuration should resemble the following:

android {
    ...

    buildFeatures {
        prefab true
    }

    defaultConfig {
        ...

        externalNativeBuild {
            cmake {
                arguments '-DANDROID_STL=c++_shared'
            }
        }
    }
}

dependencies {
    ...
    implementation 'com.google.android.gms:play-services-gni-native-c:1.0.0-beta2'
    ...
}

Modify CMakeLists

  1. Add the GNI library to your project's CMakeLists.txt (my_project/app/src/main/cpp/CMakeLists.txt) by adding the following line at the top level of the file:

    find_package(com.google.android.gms.gni.c REQUIRED CONFIG)
    
  2. Add the following line to the target_link_libraries section:

    PUBLIC com.google.android.gms.gni.c::gni_shared
    
  3. Add a reference to the generated code by adding the following line at the top level of the file:

    file(GLOB_RECURSE native_wrappers CONFIGURE_DEPENDS "native_wrappers/*.cpp" "native_wrappers/*.cc")
    
  4. Add these lines near the end of the file:

    include_directories(./native_wrappers/c)
    include_directories(./native_wrappers/cpp)
    

Your updated CMakeLists.txt resource should resemble the following sample:

cmake_minimum_required(VERSION 3.18.1)

project("my_project")

file(GLOB_RECURSE native_wrappers CONFIGURE_DEPENDS "native_wrappers/*.cpp" "native_wrappers/*.cc")

add_library(
        my_project
        SHARED
        native-lib.cpp
        ${native_wrappers}
        )

find_library(
        log-lib
        log)

find_package(com.google.android.gms.gni.c REQUIRED CONFIG)

target_link_libraries(
        my_project
        PUBLIC com.google.android.gms.gni.c::gni_shared
        ${log-lib})

include_directories(./native_wrappers/c)
include_directories(./native_wrappers/cpp)

Implement notification logic

  1. Open or create the source file where you would like to implement notification capabilities. In this file, include the header file gni.h and define a new ShowNativeNotification() function:

    #include "gni/gni.h"
    
    void ShowNativeNotification(JNIEnv *env, jobject main_activity, int icon_id) {
      // Get the JavaVM from the JNIEnv.
      JavaVM *java_vm;
      env->GetJavaVM(&java_vm);
    
      // Initialize the GNI runtime. This function needs to be called before any
      // call to the generated code.
      GniCore_init(java_vm, main_activity);
    }
    
  2. Define notification-specific constant values, and the notification handler functions CharSequenceFromCString() and CreateNotification():

    C

    const int32_t IMPORTANCE_HIGH = 4;  // NotificationManager.IMPORTANCE_HIGH
    const int32_t PRIORITY_MAX = 2;  // NotificationCompat.PRIORITY_MAX
    const int32_t NOTIFICATION_ID = 123;  // User defined notification id.
    
    // Convert a C string into CharSequence.
    CharSequence *CharSequenceFromCString(const char *text) {
       String *string = String_fromCString(text);
       // Cast String to CharSequence. In Java, a String implements CharSequence.
       CharSequence *result = GNI_CAST(CharSequence, String, string);
       // Casting creates a new object, so it needs to be destroyed as normal.
       String_destroy(string);
       return result;
    }
    
    // Create a notification.
    Notification *
    CreateNotification(Context *context, String *channel_id,
                       const char *title, const char *content,
                       int32_t icon_id) {
       // Convert C strings to CharSequence.
       CharSequence *title_chars = CharSequenceFromCString(title);
       CharSequence *content_chars = CharSequenceFromCString(content);
    
       // Create a NotificationCompat.Builder and set all required properties.
       NotificationCompat_Builder *notification_builder =
           NotificationCompat_Builder_construct(context, channel_id);
       NotificationCompat_Builder_setContentTitle(notification_builder,
                                                  title_chars);
       NotificationCompat_Builder_setContentText(notification_builder,
                                                 content_chars);
       NotificationCompat_Builder_setSmallIcon(notification_builder, icon_id);
       NotificationCompat_Builder_setPriority(notification_builder,
                                              PRIORITY_MAX);
    
       // Build a notification.
       Notification *notification =
           NotificationCompat_Builder_build(notification_builder);
    
       // Clean up allocated objects.
       NotificationCompat_Builder_destroy(notification_builder);
       CharSequence_destroy(title_chars);
       CharSequence_destroy(content_chars);
    
       return notification;
    }
    

    C++

    const int32_t IMPORTANCE_HIGH = 4;  // NotificationManager.IMPORTANCE_HIGH
    const int32_t PRIORITY_MAX = 2;  // NotificationCompat.PRIORITY_MAX
    const int32_t NOTIFICATION_ID = 123;  // User defined notification id.
    
    // Convert a C string into CharSequence.
    CharSequence *CharSequenceFromCString(const char *text) {
       String *string = String_fromCString(text);
       // Cast String to CharSequence. In Java, a String implements CharSequence.
       CharSequence *result = new CharSequence(string->GetImpl());
       // Casting creates a new object, so it needs to be destroyed as normal.
       String::destroy(string);
       return result;
    }
    
    // Create a notification.
    Notification&
    CreateNotification(Context *context, String *channel_id, const char *title,
                       const char *content, int32_t icon_id) {
       // Convert C strings to CharSequence.
       CharSequence *title_chars = CharSequenceFromCString(title);
       CharSequence *content_chars = CharSequenceFromCString(content);
    
       // Create a NotificationCompat.Builder and set all required properties.
    
       NotificationCompat::Builder *notification_builder = new NotificationCompat::Builder(*context, *channel_id);
       notification_builder->setContentTitle(*title_chars);
       notification_builder->setContentText(*content_chars);
       notification_builder->setSmallIcon(icon_id);
       notification_builder->setPriority(PRIORITY_MAX);
    
       // Build a notification.
       Notification& notification = notification_builder->build();
    
       // Clean up allocated objects.
       NotificationCompat::Builder::destroy(notification_builder);
       CharSequence::destroy(title_chars);
       CharSequence::destroy(content_chars);
    
       return notification;
    }
    

    Some functions of the notifications library take the CharSequence instead of String. The CharSequenceFromCString() function enables conversion between these objects. The function CreateNotification() uses the wrapped version of Java NotificationCompat.Builder to create a notification.

  3. Add logic to create a notification channel by pasting in the following function, CreateNotificationChannel():

    C

    void CreateNotificationChannel(Context *context, String *channel_id) {
       CharSequence *channel_name = CharSequenceFromCString("channel name");
       String *channel_description = String_fromCString("channel description");
       String *system_service_name = String_fromCString("notification");
       NotificationChannel *channel =
           NotificationChannel_construct(channel_id, channel_name,
                                         IMPORTANCE_HIGH);
       NotificationChannel_setDescription(channel, channel_description);
    
       Object *notification_manager_as_object =
           Context_getSystemService(context, system_service_name);
       NotificationManager *notification_manager =
           GNI_CAST(NotificationManager, Object,
                    notification_manager_as_object);
    
       NotificationManager_createNotificationChannel(notification_manager,
                                                     channel);
    
       CharSequence_destroy(channel_name);
       String_destroy(channel_description);
       String_destroy(system_service_name);
       NotificationChannel_destroy(channel);
       Object_destroy(notification_manager_as_object);
       NotificationManager_destroy(notification_manager);
    }
    

    C++

    void CreateNotificationChannel(Context *context, String *channel_id) {
       CharSequence *channel_name = CharSequenceFromCString("channel name");
       String *channel_description = String_fromCString("channel description");
       String *system_service_name = String_fromCString("notification");
       NotificationChannel *channel =
           new NotificationChannel(*channel_id, *channel_name, IMPORTANCE_HIGH);
       channel->setDescription(*channel_description);
    
       Object& notification_manager_as_object =
           context->getSystemService(*system_service_name);
       NotificationManager *notification_manager =
           new NotificationManager(notification_manager_as_object.GetImpl());
    
       notification_manager->createNotificationChannel(*channel);
    
       CharSequence::destroy(channel_name);
       String::destroy(channel_description);
       String::destroy(system_service_name);
       NotificationChannel::destroy(channel);
       Object::destroy(&notification_manager_as_object);
       NotificationManager::destroy(notification_manager);
    }
    
  4. Update the ShowNativeNotification() function you created earlier to call CreateNotificationChannel(). Add the following code to the end of ShowNativeNotification():

    C

    void ShowNativeNotification(JNIEnv *env, jobject main_activity, int icon_id) {
     // ...
    
     // Create a Context object by wrapping an existing JNI reference.
     Context *context = Context_wrapJniReference(main_activity);
    
     // Create a String object.
     String *channel_id = String_fromCString("new_messages");
    
     // Create a notification channel.
     CreateNotificationChannel(context, channel_id);
    
     // Create a notification with a given title, content, and icon.
     Notification *notification =
         CreateNotification(context, channel_id, "My Native Notification",
                            "Hello!", icon_id);
    
     // Create a notification manager and use it to show the notification.
     NotificationManagerCompat *notification_manager =
         NotificationManagerCompat_from(context);
     NotificationManagerCompat_notify(notification_manager, NOTIFICATION_ID,
                                      notification);
    
     // Destroy all objects.
     Context_destroy(context);
     String_destroy(channel_id);
     Notification_destroy(notification);
     NotificationManagerCompat_destroy(notification_manager);
    }
    

    C++

    void ShowNativeNotification(JNIEnv *env, jobject main_activity, int icon_id) {
       // Get the JavaVM from the JNIEnv.
       JavaVM *java_vm;
       env->GetJavaVM(&java_vm);
    
       // Initialize the GNI runtime. This function needs to be called before any
       // call to the generated code.
       GniCore::Init(java_vm, main_activity);
    
       // Create a Context object by wrapping an existing JNI reference.
       Context *context = new Context(main_activity);
    
       // Create a String object.
       String *channel_id = String_fromCString("new_messages");
    
       // Create a notification channel.
       CreateNotificationChannel(context, channel_id);
    
       // Create a notification with a given title, content, and icon.
       Notification& notification =
           CreateNotification(context, channel_id, "My Native Notification",
                              "Hello!", icon_id);
    
       // Create a notification manager and use it to show the notification.
       NotificationManagerCompat& notification_manager =
           NotificationManagerCompat::from(*context);
       notification_manager.notify(NOTIFICATION_ID, notification);
    
       // Destroy all objects.
       Context::destroy(context);
       String::destroy(channel_id);
       Notification::destroy(&notification);
       NotificationManagerCompat::destroy(&notification_manager);
    }   
  5. With your logic defined, trigger a notification by calling ShowNativeNotification() at an appropriate location in your project.

Run the app

Compile and run the code that calls ShowNativeNotification(). A simple notification should appear at the top of the screen of your test device.

Generate wrappers from JARs

In the previous example, you manually defined Java classes and methods requiring native code in a wrapper config file. For scenarios where you need to access large sections of an API, its more efficient to provide one or more library JARs to the wrapper tool. The wrapper then generates wrappers for all public symbols it finds in the JAR.

The following example wraps the entire Notifications API by providing a library JAR.

Get the required JARs

The Notification API is a part of the androidx.core package, available from the Google Maven repository. Download the library aar file and unpack it to a directory of your choice. Locate the classes.jar file.

The classes.jar file contains many classes beyond our required notifications library. If you provide the library wrapper with just classes.jar, the tool generates native code for every class in the JAR, which is inefficient and unnecessary for our project. To solve this, provide a filter file to the wrapper configuration to restrict code generation to the JAR's notification classes.

Define an allow filter

Filter files are plain text files you provide to your library wrapper configuration. They allow you to define which classes to include (or exclude) from JAR files provided to the library wrapper.

In your project, create a file titled allowed-symbols.txt and paste in the following line:

androidx.core.app.NotificationCompat*

When used as an allow filter, the preceding code specifies that only symbols whose name starts with androidx.core.app.NotificationCompat are wrapped.

Run the library wrapper

Open a terminal to the JAR directory and run the following command:

java -jar lw.jar \
 -i classes.jar \
 -o "./generated-jar" \
 -c "./config.json" \
 -fa allowed-symbols.txt \
 --skip_deprecated_symbols

The preceding sample command generates wrapper code for your filtered classes to the directory generated-jar/.

Support

If you find an issue with the library wrapper, please let us know.

Browse bugs File a bug
Engineering
Documentation