Using AssetManager class from Android NDK in a Flutter app
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
Facundo Farall
Updated on November 28, 2022Comments
-
Facundo Farall over 1 year
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 anAssetManager
is withAAssetManager_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); }