Using AssetManager class from Android NDK in a Flutter app

Issue

I’ve been trying to use the Android NDK’s AssetManager class in my Flutter app, that works with Google Oboe, to access to audio files. Following this example in the Oboe repository, I learned that they obtain the AssetManager from Java like this:

JNIEXPORT void JNICALL
Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onStart(JNIEnv *env, jobject instance,
                                                                     jobject jAssetManager) {

    AAssetManager *assetManager = AAssetManager_fromJava(env, jAssetManager);
    if (assetManager == nullptr) {
        LOGE("Could not obtain the AAssetManager");
        return;
    }

    game = std::make_unique<Game>(*assetManager);
    game->start();
}

Basically with the argument jAssetManager they pass from Java to the C++ functions, through the JNI interface. Now I’m not working with JNI because I’m using Flutter and Dart, and the way in Dart for communicating with C++ functions is through dart:ffi, but since the only way I can create an AssetManager is with AAssetManager_fromJava(env, jAssetManager), I need those two arguments that I can’t find a way to replace with Flutter and Dart.

I did some research and when I created the Flutter FFI plugin, apparently the Dart code communicates with Kotlin code, which then calls the native C++ functions.

Here’s my C++ function:

EXTERNC void *engine_create(void) {
    AAssetManager *assetManager = AAssetManager_fromJava(env, jAssetManager);   // ERROR: How do I get these?
    if (assetManager == nullptr) {
        LOGE("Could not obtain the AAssetManager");
        return nullptr;
    }   

    return new DSPAudioEngine(*assetManager);
}

Here’s the Dart wrapper for that function:

import 'dart:ffi';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart';

typedef oboe_engine_init = Pointer<Void> Function();
typedef OboeEngineInit = Pointer<Void> Function();

class FfiGoogleOboe {
  static const MethodChannel _channel =
      const MethodChannel('ffi_google_oboe');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static FfiGoogleOboe _instance;

  factory FfiGoogleOboe() {
    if (_instance == null) {
      _instance = FfiGoogleOboe._();
    }
    return _instance;
  }


  OboeEngineInit _engineInit;

  FfiGoogleOboe._() {
    final oboeLib = DynamicLibrary.open('libffi_google_oboe.so');

    _engineInit = oboeLib
        .lookup<NativeFunction<oboe_engine_init>>('engine_create')
        .asFunction();
  }

}

And here’s the Kotlin code I found in the FFI plugin implementation:

package g1_assd_2020.ffi_google_oboe

import androidx.annotation.NonNull;

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.Registrar
import android.content.res.AssetManager

/** FfiGoogleOboePlugin */
public class FfiGoogleOboePlugin: FlutterPlugin, MethodCallHandler {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private lateinit var channel : MethodChannel

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "ffi_google_oboe")
    channel.setMethodCallHandler(this);
  }

  // This static function is optional and equivalent to onAttachedToEngine. It supports the old
  // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
  // plugin registration via this function while apps migrate to use the new Android APIs
  // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
  //
  // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
  // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
  // depending on the user's project. onAttachedToEngine or registerWith must both be defined
  // in the same class.
  companion object {
    @JvmStatic
    fun registerWith(registrar: Registrar) {
      val channel = MethodChannel(registrar.messenger(), "ffi_google_oboe")
      channel.setMethodCallHandler(FfiGoogleOboePlugin())
    }
  }

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getPlatformVersion") {
      result.success("Android ${android.os.Build.VERSION.RELEASE}")
    } else {
      result.notImplemented()
    }
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}

Finally, here’s how the people from Oboe handle it using JNI and Java:

package com.google.oboe.sample.rhythmgame;

import android.content.Context;
import android.content.res.AssetManager;
import androidx.appcompat.app.AppCompatActivity;

import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.view.WindowManager;

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setDefaultStreamValues(this);
    }

    protected void onResume(){
        super.onResume();
        native_onStart(getAssets());
    }

    protected void onPause(){
        super.onPause();
        native_onStop();
    }

    static void setDefaultStreamValues(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
            AudioManager myAudioMgr = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
            String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
            int defaultSampleRate = Integer.parseInt(sampleRateStr);
            String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
            int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);

            native_setDefaultStreamValues(defaultSampleRate, defaultFramesPerBurst);
        }
    }

    private native void native_onStart(AssetManager assetManager);
    private native void native_onStop();
    private static native void native_setDefaultStreamValues(int defaultSampleRate,
                                                      int defaultFramesPerBurst);
}

Solution

Basically, you need to pass the AssetManager reference from your plugin’s Kotlin file to the C++ library. This answer explains how to make the Kotlin file call C++ code: Android: How to call ndk function from Kotlin?

You’ll want to use a methodChannel call to trigger this. You can get the AssetManager reference in the onAttachedToEngine method from flutterPluginBinding.applicationContext.assets.

Here’s an example Flutter plugin that reads an asset in a C++ library:
https://github.com/mikeperri/ndk_asset_manager_example/commit/533d28b33c1d22f89028f89691f78e907bf19db3

Answered By – Mike

Answer Checked By – Senaida (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.