From cd7a24c1b9732b12b47ae2ecfb25f2086e80a83c Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 27 Feb 2025 17:59:42 -0800 Subject: [PATCH] [Android] Use java for looking up Android API level. (#163558) android_get_device_api_level is an API 24 API, whereas flutter supports down to API 21. We have other places in the engine that use this API but they're not on paths that work < 29. Even though the ndk API seems to work on older devices we should still probably not rely on it... --- .../shell/platform/android/flutter_main.cc | 15 ++-- .../shell/platform/android/flutter_main.h | 6 +- .../flutter/embedding/engine/FlutterJNI.java | 16 ++-- .../engine/loader/FlutterLoader.java | 3 +- .../platform_view_android_unittests.cc | 15 ++-- .../engine/loader/FlutterLoaderTest.java | 73 +++++++++++++++++-- 6 files changed, 96 insertions(+), 32 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/flutter_main.cc b/engine/src/flutter/shell/platform/android/flutter_main.cc index 7f5e50d3ae..7f6d807550 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.cc +++ b/engine/src/flutter/shell/platform/android/flutter_main.cc @@ -95,7 +95,8 @@ void FlutterMain::Init(JNIEnv* env, jstring kernelPath, jstring appStoragePath, jstring engineCachesPath, - jlong initTimeMillis) { + jlong initTimeMillis, + jint api_level) { std::vector args; args.push_back("flutter"); for (auto& arg : fml::jni::StringArrayToVector(env, jargs)) { @@ -118,8 +119,12 @@ void FlutterMain::Init(JNIEnv* env, "Dart DevTools."); } } + // The API level must be provided from java, as the NDK function + // android_get_device_api_level() is only available on API 24 and greater, and + // Flutter still supports 21, 22, and 23. - AndroidRenderingAPI android_rendering_api = SelectedRenderingAPI(settings); + AndroidRenderingAPI android_rendering_api = + SelectedRenderingAPI(settings, api_level); switch (android_rendering_api) { case AndroidRenderingAPI::kSoftware: case AndroidRenderingAPI::kSkiaOpenGLES: @@ -233,7 +238,7 @@ bool FlutterMain::Register(JNIEnv* env) { { .name = "nativeInit", .signature = "(Landroid/content/Context;[Ljava/lang/String;Ljava/" - "lang/String;Ljava/lang/String;Ljava/lang/String;J)V", + "lang/String;Ljava/lang/String;Ljava/lang/String;JI)V", .fnPtr = reinterpret_cast(&Init), }, { @@ -271,7 +276,8 @@ bool FlutterMain::IsKnownBadSOC(std::string_view hardware) { // static AndroidRenderingAPI FlutterMain::SelectedRenderingAPI( - const flutter::Settings& settings) { + const flutter::Settings& settings, + int api_level) { if (settings.enable_software_rendering) { FML_CHECK(!settings.enable_impeller) << "Impeller does not support software rendering. Either disable " @@ -301,7 +307,6 @@ AndroidRenderingAPI FlutterMain::SelectedRenderingAPI( // Even if this check returns true, Impeller may determine it cannot use // Vulkan for some other reason, such as a missing required extension or // feature. - int api_level = android_get_device_api_level(); if (api_level < kMinimumAndroidApiLevelForVulkan) { return kVulkanUnsupportedFallback; } diff --git a/engine/src/flutter/shell/platform/android/flutter_main.h b/engine/src/flutter/shell/platform/android/flutter_main.h index f6e27bcdc9..2354caa8c9 100644 --- a/engine/src/flutter/shell/platform/android/flutter_main.h +++ b/engine/src/flutter/shell/platform/android/flutter_main.h @@ -26,7 +26,8 @@ class FlutterMain { flutter::AndroidRenderingAPI GetAndroidRenderingAPI(); static AndroidRenderingAPI SelectedRenderingAPI( - const flutter::Settings& settings); + const flutter::Settings& settings, + int api_level); static bool IsDeviceEmulator(std::string_view product_model); @@ -47,7 +48,8 @@ class FlutterMain { jstring kernelPath, jstring appStoragePath, jstring engineCachesPath, - jlong initTimeMillis); + jlong initTimeMillis, + jint api_level); void SetupDartVMServiceUriCallback(JNIEnv* env); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index d67f5daa65..be6ac39e68 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -180,7 +180,8 @@ public class FlutterJNI { @Nullable String bundlePath, @NonNull String appStoragePath, @NonNull String engineCachesPath, - long initTimeMillis); + long initTimeMillis, + int apiLevel); /** * Perform one time initialization of the Dart VM and Flutter engine. @@ -193,6 +194,7 @@ public class FlutterJNI { * @param appStoragePath The path to the application data directory. * @param engineCachesPath The path to the application cache directory. * @param initTimeMillis The time, in milliseconds, taken for initialization. + * @param apiLevel The current Android API level. */ public void init( @NonNull Context context, @@ -200,13 +202,14 @@ public class FlutterJNI { @Nullable String bundlePath, @NonNull String appStoragePath, @NonNull String engineCachesPath, - long initTimeMillis) { + long initTimeMillis, + int apiLevel) { if (FlutterJNI.initCalled) { Log.w(TAG, "FlutterJNI.init called more than once"); } FlutterJNI.nativeInit( - context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis); + context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis, apiLevel); FlutterJNI.initCalled = true; } @@ -247,7 +250,7 @@ public class FlutterJNI { * VM Service URI for the VM instance. * *

Its value is set by the native engine once {@link #init(Context, String[], String, String, - * String, long)} is run. + * String, long, int)} is run. */ @Nullable public static String getVMServiceUri() { @@ -258,7 +261,7 @@ public class FlutterJNI { * VM Service URI for the VM instance. * *

Its value is set by the native engine once {@link #init(Context, String[], String, String, - * String, long)} is run. + * String, long, int)} is run. * * @deprecated replaced by {@link #getVMServiceUri()}. */ @@ -448,7 +451,8 @@ public class FlutterJNI { * #attachToNative()}. * *

Static methods that should be only called once such as {@link #init(Context, String[], - * String, String, String, long)} shouldn't be called again on the spawned FlutterJNI instance. + * String, String, String, long, int)} shouldn't be called again on the spawned FlutterJNI + * instance. */ @UiThread @NonNull diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 96654be4fe..b269697e4a 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -390,7 +390,8 @@ public class FlutterLoader { kernelPath, result.appStoragePath, result.engineCachesPath, - initTimeMillis); + initTimeMillis, + Integer.valueOf(android.os.Build.VERSION.SDK_INT)); initialized = true; } catch (Exception e) { diff --git a/engine/src/flutter/shell/platform/android/platform_view_android_unittests.cc b/engine/src/flutter/shell/platform/android/platform_view_android_unittests.cc index 646337716e..94124428df 100644 --- a/engine/src/flutter/shell/platform/android/platform_view_android_unittests.cc +++ b/engine/src/flutter/shell/platform/android/platform_view_android_unittests.cc @@ -21,15 +21,10 @@ TEST(AndroidPlatformView, DISABLED_SelectsVulkanBasedOnApiLevel) { settings.enable_software_rendering = false; settings.enable_impeller = true; - int api_level = android_get_device_api_level(); - EXPECT_GT(api_level, 0); - if (api_level >= 29) { - EXPECT_EQ(FlutterMain::SelectedRenderingAPI(settings), - AndroidRenderingAPI::kImpellerVulkan); - } else { - EXPECT_EQ(FlutterMain::SelectedRenderingAPI(settings), - AndroidRenderingAPI::kImpellerOpenGLES); - } + EXPECT_EQ(FlutterMain::SelectedRenderingAPI(settings, 29), + AndroidRenderingAPI::kImpellerVulkan); + EXPECT_EQ(FlutterMain::SelectedRenderingAPI(settings, 24), + AndroidRenderingAPI::kImpellerOpenGLES); } TEST(AndroidPlatformView, SoftwareRenderingNotSupportedWithImpeller) { @@ -37,7 +32,7 @@ TEST(AndroidPlatformView, SoftwareRenderingNotSupportedWithImpeller) { settings.enable_software_rendering = true; settings.enable_impeller = true; - ASSERT_DEATH(FlutterMain::SelectedRenderingAPI(settings), ""); + ASSERT_DEATH(FlutterMain::SelectedRenderingAPI(settings, 29), ""); } TEST(AndroidPlatformView, FallsBackToGLESonEmulator) { diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java index d8697b7e40..373fca5992 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java @@ -9,6 +9,7 @@ import static io.flutter.Build.API_LEVELS; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.eq; @@ -99,7 +100,14 @@ public class FlutterLoaderTest { final String oldGenHeapArg = "--old-gen-heap-size=" + oldGenHeapSizeMegaBytes; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertTrue(arguments.contains(oldGenHeapArg)); } @@ -122,7 +130,14 @@ public class FlutterLoaderTest { "--resource-cache-max-bytes-threshold=" + resourceCacheMaxBytesThreshold; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertTrue(arguments.contains(resourceCacheMaxBytesThresholdArg)); } @@ -140,7 +155,14 @@ public class FlutterLoaderTest { final String leakVMArg = "--leak-vm=true"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertTrue(arguments.contains(leakVMArg)); } @@ -162,7 +184,14 @@ public class FlutterLoaderTest { final String leakVMArg = "--leak-vm=false"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertTrue(arguments.contains(leakVMArg)); } @@ -191,7 +220,14 @@ public class FlutterLoaderTest { final String enableImpellerArg = "--enable-impeller"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertFalse(arguments.contains(enableImpellerArg)); } @@ -209,7 +245,14 @@ public class FlutterLoaderTest { final String enableVulkanValidationArg = "--enable-vulkan-validation"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertFalse(arguments.contains(enableVulkanValidationArg)); } @@ -231,7 +274,14 @@ public class FlutterLoaderTest { final String enableImpellerArg = "--enable-impeller=true"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertTrue(arguments.contains(enableImpellerArg)); } @@ -253,7 +303,14 @@ public class FlutterLoaderTest { final String disabledControlArg = "--enable-surface-control"; ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); verify(mockFlutterJNI, times(1)) - .init(eq(ctx), shellArgsCaptor.capture(), anyString(), anyString(), anyString(), anyLong()); + .init( + eq(ctx), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong(), + anyInt()); List arguments = Arrays.asList(shellArgsCaptor.getValue()); assertTrue(arguments.contains(disabledControlArg)); }