AWS Developer Blog

Setting up an Android application with AWS SDK for C++

The AWS SDK for C++ can build and run on many different platforms, including Android. In this post, I walk you through building and running a sample application on an Android device.

Overview

I cover the following topics:

  • Building and installing the SDK for C++ and creating a desktop application that implements the basic functionality.
  • Setting up the environment for Android development.
  • Building and installing the SDK for C++ for Android and transplanting the desktop application to Android platform with the cross-compiled SDK.

Prerequisites

To get started, you need the following resources:

  • An AWS account
  • A GitHub environment to build and install AWS SDK for C++

To set up the application on the desktop

Follow these steps to set up the application on your desktop:

  • Create an Amazon Cognito identity pool
  • Build and test the desktop application

Create an Amazon Cognito identity pool

For this demo, I use unauthenticated identities, which typically belong to guest users. To learn about unauthenticated and authenticated identities and choose the one that fits your business, check out Using Identity Pools.

In the Amazon Cognito console, choose Manage identity pools, Create new identity pool. Enter an identity pool name, like “My Android App with CPP SDK”, and choose Enable access to unauthenticated identities.

Next, choose Create Pool, View Details. Two Role Summary sections should display, one for authenticated and the other for unauthenticated identities.

For the unauthenticated identities, choose View Policy Document, Edit. Under Action, add the following line:

s3: ListAllMyBuckets

After you have completed the preceding steps, the policy should read as follows:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListAllMyBuckets",
                "mobileanalytics:PutEvents",
                "cognito-sync:*"
            ],
            "Resource": "*"
        }
    ]
}

To finish the creation, choose Allow.

On the next page, in the Get AWS Credentials section, copy the identity pool ID and keep it somewhere to use later. Or, you can find it after you choose Edit identity pool in the Amazon Cognito console. The identity pool ID has the following format:

<region>:<uuid>

Build and test the desktop application

Before building an Android application, you can build a regular application with the SDK for C++ in your desktop environment for testing purpose. Then you must modify the source code, making the CMake script switch its target to Android.

Here’s how to build and install the SDK for C++ statically:

cd <workspace>
git clone https://github.com/aws/aws-sdk-cpp.git
mkdir build_sdk_desktop
cd build_sdk_desktop
cmake ../aws-sdk-cpp \
    -DBUILD_ONLY="identity-management;s3" \
    -DBUILD_SHARED_LIBS=OFF \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX="<workspace>/install_sdk_desktop"
cmake --build .
cmake --build . --target install

If you install the SDK for C++ successfully, you can find libaws-cpp-sdk-*.a (or aws-cpp-sdk-*.lib for Windows) under <workspace>/install_sdk_desktop/lib/ (or <workspace>/install_sdk_desktop/lib64).

Next, build the application and link it to the library that you built. Create a folder <workspace>/app_list_all_buckets and place two files under this directory:

  • main.cpp (source file)
  • CMakeLists.txt (CMake file)
// main.cpp
#include <iostream>
#include <aws/core/Aws.h>
#include <aws/core/utils/Outcome.h>
#include <aws/core/utils/logging/AWSLogging.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/core/auth/AWSCredentialsProvider.h>
#include <aws/identity-management/auth/CognitoCachingCredentialsProvider.h>
#include <aws/s3/S3Client.h>

using namespace Aws::Auth;
using namespace Aws::CognitoIdentity;
using namespace Aws::CognitoIdentity::Model;

static const char ALLOCATION_TAG[] = "ListAllBuckets";
static const char ACCOUNT_ID[] = "your-account-id";
static const char IDENTITY_POOL_ID[] = "your-cognito-identity-id";

int main()
{
    Aws::SDKOptions options;
    options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug;
    Aws::InitAPI(options);

    Aws::Client::ClientConfiguration config;
    auto cognitoIdentityClient = Aws::MakeShared<CognitoIdentityClient>(ALLOCATION_TAG, Aws::MakeShared<AnonymousAWSCredentialsProvider>(ALLOCATION_TAG), config);
    auto cognitoCredentialsProvider = Aws::MakeShared<CognitoCachingAnonymousCredentialsProvider>(ALLOCATION_TAG, ACCOUNT_ID, IDENTITY_POOL_ID, cognitoIdentityClient);

    Aws::S3::S3Client s3Client(cognitoCredentialsProvider, config);
    auto listBucketsOutcome = s3Client.ListBuckets();
    Aws::StringStream ss;
    if (listBucketsOutcome.IsSuccess())
    {
        ss << "Buckets:" << std::endl;
        for (auto const& bucket : listBucketsOutcome.GetResult().GetBuckets())
        {
            ss << "  " << bucket.GetName() << std::endl;
        }
    }
    else
    {
        ss << "Failed to list buckets." << std::endl;
    }
    std::cout << ss.str() << std::endl;
    Aws::ShutdownAPI(options);
    return 0;
}
# CMakeLists.txt
cmake_minimum_required(VERSION 3.3)
set(CMAKE_CXX_STANDARD 11)

project(list_all_buckets LANGUAGES CXX)
find_package(AWSSDK REQUIRED COMPONENTS s3 identity-management)
add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} ${AWSSDK_LINK_LIBRARIES})

Build and test this desktop application with the following commands:

cd <workspace>
mkdir build_app_desktop
cd build_app_desktop
cmake ../app_list_all_buckets \
    -DBUILD_SHARED_LIBS=ON \
    -DCMAKE_PREFIX_PATH="<workspace>/install_sdk_desktop"
cmake --build .
./list_all_buckets # or ./Debug/list_all_buckets.exe for Windows

The output should read as follows:

Buckets:
  <bucket_1>
  <bucket_2>
  ...

Now you have a desktop application. You’ve accomplished this without touching anything related to Android. The next section covers Android instructions.

To set up the application on Android with AWS SDK for C++

Follow these steps to set up the application on Android with the SDK for C++:

  • Set up Android Studio
  • Cross-compile the SDK for C++ and the library
  • Build and run the application in Android Studio

Set up Android Studio

First, download and install Android Studio. For more detailed instructions, see the Android Studio Install documentation.

Next, open Android Studio and create a new project. On the Choose your project screen, as shown in the following screenshot, choose Native C++, Next.

Complete all fields. In the following example, you build the SDK for C++ with Android API level 19, so the Minimum API Level is “API 19: Android 4.4 (KitKat)”.

Choose C++ 11 for C++ Standard and choose Finish for the setup phase.

The first time that you open Android Studio, you might see “missing NDK and CMake” errors during automatic installation. Ignore these warnings for the moment. You install Android NDK and CMake manually. Or you can accept the license to install NDK and CMake within Android Studio. Doing so should suppress the warnings.

After you choose Finish, you should get a sample application with Android Studio. This application publishes some messages on the screens of your devices. For more details, see Create a new project with C++.

Starting from this sample, take the following steps to build your application:

First, cross-compile the SDK for C++ for Android.

Modify the source code and CMake script to build list_all_buckets as a shared object library (liblist_all_buckets.so) rather than an executable. This library has a function: listAllBuckets() to output all buckets.

Specify the path to the library in the module’s build.gradle file so that the Android application can find it.

Load this library in MainActivity by System.loadLibrary("list_all_buckets"), so that the Android application can use the listAllBuckets() function.

Call the listAllBuckets() function in OnCreate() function in MainActivity.

More details for each step will be given in the following sections.

Cross-compile the SDK for C++ and the library

Use Android NDK to cross-compile the SDK for C++. In this example, you are using version r19c. To find whether Android Studio has downloaded NDK by default, check the following:

  • Linux: ~/Android/Sdk/ndk-bundle
  • MacOS: ~/Library/Android/sdk/ndk-bundle
  • Windows: C:\Users\<username>\AppData\Local\Android\Sdk\ndk\<version>

Alternately, download the Android NDK directly.

To cross-compile SDK for C++, run the following code:

cd <workspace>
mkdir build_sdk_android
cd build_sdk_anrdoid
cmake ../aws-sdk-cpp -DNDK_DIR="<path-to-android-ndk>" \
    -DBUILD_SHARED_LIBS=OFF \
    -DCMAKE_BUILD_TYPE=Release \
    -DCUSTOM_MEMORY_MANAGEMENT=ON \
    -DTARGET_ARCH=ANDROID \
    -DANDROID_NATIVE_API_LEVEL=19 \
    -DBUILD_ONLY="identity-management;s3" \
    -DCMAKE_INSTALL_PREFIX="<workspace>/install_sdk_android"
cmake --build . --target CURL # This step is only required on Windows.
cmake --build .
cmake --build . --target install

On Windows, you might see the error message: “CMAKE_SYSTEM_NAME is ‘Android’ but ‘NVIDIA Nsight Tegra Visual Studio Edition’ is not installed.” In that case, install Ninja and change the generator from Visual Studio to Ninja by passing -GNinja as another parameter to your CMake command.

To build list_all_buckets as an Android-targeted dynamic object library, you must change the source code and CMake script. More specifically, you must alter the source code as follows:

Replace main() function with Java_com_example_mynativecppapplication_MainActivity_listAllBuckets(). In the Android application, the Java code calls this function through JNI (Java Native Interface). You may have a different function name, based on your package name and activity name. For this demo, the package name is com.example.mynativecppapplication, the activity name is MainActivity, and the actual function called by Java code is called listAllBuckets().

Enable LogcatLogSystem, so that you can debug your Android application and see the output in the logcat console.

Your Android devices or emulators may miss CA certificates. So, you should push them to your devices and specify the path in client configuration. In this example, use CA certificates extracted from Mozilla in PEM format.

Download the certificate bundle.

Push this file to your Android devices:

# Change directory to the location of adb
cd <path-to-android-sdk>/platform-tools
# Replace "com.example.mynativecppapplication" with your package name
./adb shell mkdir -p /sdcard/Android/data/com.example.mynativecppapplication/certs
# push the PEM file to your devices
./adb push cacert.pem /sdcard/Android/data/com.example.mynativecppapplication/certs

Specify the path in the client configuration:

config.caFile = "/sdcard/Android/data/com.example.mynativecppapplication/certs/cacert.pem";

The complete source code looks like the following:

// main.cpp
#if __ANDROID__
#include <android/log.h>
#include <jni.h>
#include <aws/core/platform/Android.h>
#include <aws/core/utils/logging/android/LogcatLogSystem.h>
#endif
#include <iostream>
#include <aws/core/Aws.h>
#include <aws/core/utils/Outcome.h>
#include <aws/core/utils/logging/AWSLogging.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/core/auth/AWSCredentialsProvider.h>
#include <aws/identity-management/auth/CognitoCachingCredentialsProvider.h>
#include <aws/s3/S3Client.h>

using namespace Aws::Auth;
using namespace Aws::CognitoIdentity;
using namespace Aws::CognitoIdentity::Model;

static const char ALLOCATION_TAG[] = "ListAllBuckets";
static const char ACCOUNT_ID[] = "your-account-id";
static const char IDENTITY_POOL_ID[] = "your-cognito-identity-id";

#ifdef __ANDROID__
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_mynativecppapplication_MainActivity_listAllBuckets(JNIEnv* env, jobject classRef, jobject context)
#else
int main()
#endif
{
    Aws::SDKOptions options;
#ifdef __ANDROID__
    AWS_UNREFERENCED_PARAM(classRef);
    AWS_UNREFERENCED_PARAM(context);
    Aws::Utils::Logging::InitializeAWSLogging(Aws::MakeShared<Aws::Utils::Logging::LogcatLogSystem>(ALLOCATION_TAG, Aws::Utils::Logging::LogLevel::Debug));
#else
    options.loggingOptions.logLevel = Aws::Utils::Logging::LogLevel::Debug;
#endif
    Aws::InitAPI(options);

    Aws::Client::ClientConfiguration config;
#ifdef __ANDROID__
    config.caFile = "/sdcard/Android/data/com.example.mynativecppapplication/certs/cacert.pem";
#endif
    auto cognitoIdentityClient = Aws::MakeShared<CognitoIdentityClient>(ALLOCATION_TAG, Aws::MakeShared<AnonymousAWSCredentialsProvider>(ALLOCATION_TAG), config);
    auto cognitoCredentialsProvider = Aws::MakeShared<CognitoCachingAnonymousCredentialsProvider>(ALLOCATION_TAG, ACCOUNT_ID, IDENTITY_POOL_ID, cognitoIdentityClient);

    Aws::S3::S3Client s3Client(cognitoCredentialsProvider, config);
    auto listBucketsOutcome = s3Client.ListBuckets();
    Aws::StringStream ss;
    if (listBucketsOutcome.IsSuccess())
    {
        ss << "Buckets:" << std::endl;
        for (auto const& bucket : listBucketsOutcome.GetResult().GetBuckets())
        {
            ss << "  " << bucket.GetName() << std::endl;
        }
    }
    else
    {
        ss << "Failed to list buckets." << std::endl;
    }

#if __ANDROID__
    std::string allBuckets(ss.str().c_str());
    Aws::ShutdownAPI(options);
    return env->NewStringUTF(allBuckets.c_str());
#else
    std::cout << ss.str() << std::endl;
    Aws::ShutdownAPI(options);
    return 0;
#endif
}

Next, make the following changes for the CMake script:

  • Set the default values for the parameters used for the Android build, including:
    • The default Android API Level is 19
    • The default Android ABI is armeabi-v7a
    • Use libc++ as the standard library by default
    • Use android.toolchain.cmake supplied by Android NDK by default
  • Build list_all_buckets as a library rather than an executable
  • Link to the external libraries built in the previous step: zlib, ssl, crypto, and curl
# CMakeLists.txt
cmake_minimum_required(VERSION 3.3)
set(CMAKE_CXX_STANDARD 11)

if(TARGET_ARCH STREQUAL "ANDROID") 
    if(NOT NDK_DIR)
        set(NDK_DIR $ENV{ANDROID_NDK})
    endif()
    if(NOT IS_DIRECTORY "${NDK_DIR}")
        message(FATAL_ERROR "Could not find Android NDK (${NDK_DIR}); either set the ANDROID_NDK environment variable or pass the path in via -DNDK_DIR=..." )
    endif()
if(NOT CMAKE_TOOLCHAIN_FILE)
    set(CMAKE_TOOLCHAIN_FILE "${NDK_DIR}/build/cmake/android.toolchain.cmake")
endif()

if(NOT ANDROID_ABI)
    set(ANDROID_ABI "armeabi-v7a")
    message(STATUS "Android ABI: none specified, defaulting to ${ANDROID_ABI}")
else()
    message(STATUS "Android ABI: ${ANDROID_ABI}")
endif()

if(BUILD_SHARED_LIBS)
    set(ANDROID_STL "c++_shared")
else()
    set(ANDROID_STL "c++_static")
endif()

if(NOT ANDROID_NATIVE_API_LEVEL)
    set(ANDROID_NATIVE_API_LEVEL "android-19")
    message(STATUS "Android API Level: none specified, defaulting to ${ANDROID_NATIVE_API_LEVEL}")
else()
    message(STATUS "Android API Level: ${ANDROID_NATIVE_API_LEVEL}")
endif()

    list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_PREFIX_PATH})
endif()

project(list_all_buckets LANGUAGES CXX)
find_package(AWSSDK REQUIRED COMPONENTS s3 identity-management)
if(TARGET_ARCH STREQUAL "ANDROID")
    set(SUFFIX so)
    add_library(zlib STATIC IMPORTED)
    set_target_properties(zlib PROPERTIES IMPORTED_LOCATION ${EXTERNAL_DEPS}/zlib/lib/libz.a)
    add_library(ssl STATIC IMPORTED)
    set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ${EXTERNAL_DEPS}/openssl/lib/libssl.a)
    add_library(crypto STATIC IMPORTED)
    set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${EXTERNAL_DEPS}/openssl/lib/libcrypto.a)
    add_library(curl STATIC IMPORTED)
    set_target_properties(curl PROPERTIES IMPORTED_LOCATION ${EXTERNAL_DEPS}/curl/lib/libcurl.a)
    add_library(${PROJECT_NAME} "main.cpp")
else()
    add_executable(${PROJECT_NAME} "main.cpp")
endif()
target_link_libraries(${PROJECT_NAME} ${AWSSDK_LINK_LIBRARIES})

Finally, build this library with the following command:

cd <workspace>
mkdir build_app_android
cd build_app_android
cmake ../app_list_all_buckets \
    -DNDK_DIR="<path-to-android-ndk>" \
    -DBUILD_SHARED_LIBS=ON \
    -DTARGET_ARCH=ANDROID \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_PREFIX_PATH="<workspace>/install_sdk_android" \
    -DEXTERNAL_DEPS="<workspace>/build_sdk_android/external"
cmake --build .

This build results in the shared library liblist_all_buckets.so under <workspace>/build_app_android/. It’s time to switch to Android Studio.

Build and run the application in Android Studio

First, the application must find the library (liblist_all_buckets.so) that you built and the standard library (libc++_shared.so). The default search path for JNI libraries is app/src/main/jniLibs/<android-abi>. Create a directory called: <your-android-application-root>/app/src/main/jniLibs/armeabi-v7a/ and copy the following files to this directory:

<workspace>/build_app_android/liblist_all_buckets.so

  • For Linux: <android-ndk>/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so
  • For MacOS: <android-ndk>/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so
  • For Windows: <android-ndk>/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so

Next, open the build.gradle file for your module and remove the externalNativeBuild{} block, because you are using prebuilt libraries, instead of building the source with the Android application.

Then, edit MainActivity.java, which is under app/src/main/java/<package-name>/. Replace all native-libs with list_all_buckets and replace all stringFromJNI() with listAllBuckets(). The whole Java file looks like the following code example:

// MainActivity.java
package com.example.mynativecppapplication;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'list_all_buckets' library on application startup.
    static {
        System.loadLibrary("list_all_buckets");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(listAllBuckets());
    }

    /**
    * A native method that is implemented by the 'list_all_buckets' native library,
    * which is packaged with this application.
    */
    public native String listAllBuckets();
}

Finally, don’t forget to grant internet access permission to your application by adding the following lines in the AndroidManifest.xml, located at app/src/main/:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mynativecppapplication">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    ...
</manifest>

To run the application on Android emulator, make sure that the CPU/ABI is armeabi-v7a for the system image. That’s what you specified when you cross-compiled the SDK and list_all_buckets library for the Android platform.

Run this application by choosing the Run icon or choosing Run, Run [app]. You should see that the application lists all buckets, as shown in the following screenshot.

Summary

With Android Studio and its included tools, you can cross-compile the AWS SDK for C++ and build a sample Android application to get temporary Amazon Cognito credentials and list all S3 buckets. Starting from this simple application, AWS hopes to see more exciting integrations with the SDK for C++.

As always, AWS welcomes all feedback or comment. Feel free to open an issue on GitHub if you have questions or submit a pull request to contribute.