Refactor NDK helpers some more, add methods for SurfaceControl/Transaction, tests (#50540)

Adds more dynamic method lookups in service of https://github.com/flutter/flutter/issues/143105

Moves the TU out to FML so that Impeller can more easily use it.

Adds checking on `AHardwareBuffer_getId` so that it checks the return value before returning what is potentially garbage.

Adds some smoke tests to make sure these things actually work/look up meaningful symbols. Test is in the shell because we have testing infra for this kind of thing there.
diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files
index 396430f..96588c1 100644
--- a/ci/licenses_golden/excluded_files
+++ b/ci/licenses_golden/excluded_files
@@ -100,6 +100,7 @@
 ../../../flutter/fml/message_loop_task_queues_unittests.cc
 ../../../flutter/fml/message_loop_unittests.cc
 ../../../flutter/fml/paths_unittests.cc
+../../../flutter/fml/platform/android/ndk_helpers_unittests.cc
 ../../../flutter/fml/platform/darwin/cf_utils_unittests.mm
 ../../../flutter/fml/platform/darwin/scoped_nsobject_arc_unittests.mm
 ../../../flutter/fml/platform/darwin/scoped_nsobject_unittests.mm
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 3095959..1b17db6 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -4832,6 +4832,8 @@
 ORIGIN: ../../../flutter/fml/platform/android/jni_weak_ref.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/fml/platform/android/message_loop_android.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/fml/platform/android/message_loop_android.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/fml/platform/android/ndk_helpers.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/fml/platform/android/ndk_helpers.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/fml/platform/android/paths_android.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/fml/platform/android/paths_android.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/fml/platform/android/scoped_java_ref.cc + ../../../flutter/LICENSE
@@ -6474,8 +6476,6 @@
 ORIGIN: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/library_loader.cc + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/shell/platform/android/ndk_helpers.cc + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/shell/platform/android/ndk_helpers.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/platform_message_handler_android.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/platform_message_response_android.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/android/platform_message_response_android.h + ../../../flutter/LICENSE
@@ -7663,6 +7663,8 @@
 FILE: ../../../flutter/fml/platform/android/jni_weak_ref.h
 FILE: ../../../flutter/fml/platform/android/message_loop_android.cc
 FILE: ../../../flutter/fml/platform/android/message_loop_android.h
+FILE: ../../../flutter/fml/platform/android/ndk_helpers.cc
+FILE: ../../../flutter/fml/platform/android/ndk_helpers.h
 FILE: ../../../flutter/fml/platform/android/paths_android.cc
 FILE: ../../../flutter/fml/platform/android/paths_android.h
 FILE: ../../../flutter/fml/platform/android/scoped_java_ref.cc
@@ -9324,8 +9326,6 @@
 FILE: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.cc
 FILE: ../../../flutter/shell/platform/android/jni/platform_view_android_jni.h
 FILE: ../../../flutter/shell/platform/android/library_loader.cc
-FILE: ../../../flutter/shell/platform/android/ndk_helpers.cc
-FILE: ../../../flutter/shell/platform/android/ndk_helpers.h
 FILE: ../../../flutter/shell/platform/android/platform_message_handler_android.cc
 FILE: ../../../flutter/shell/platform/android/platform_message_handler_android.h
 FILE: ../../../flutter/shell/platform/android/platform_message_response_android.cc
diff --git a/fml/BUILD.gn b/fml/BUILD.gn
index 3267884..088c391 100644
--- a/fml/BUILD.gn
+++ b/fml/BUILD.gn
@@ -184,6 +184,8 @@
       "platform/android/jni_weak_ref.h",
       "platform/android/message_loop_android.cc",
       "platform/android/message_loop_android.h",
+      "platform/android/ndk_helpers.cc",
+      "platform/android/ndk_helpers.h",
       "platform/android/paths_android.cc",
       "platform/android/paths_android.h",
       "platform/android/scoped_java_ref.cc",
diff --git a/fml/platform/android/ndk_helpers.cc b/fml/platform/android/ndk_helpers.cc
new file mode 100644
index 0000000..6726743
--- /dev/null
+++ b/fml/platform/android/ndk_helpers.cc
@@ -0,0 +1,258 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "fml/platform/android/ndk_helpers.h"
+
+#include "fml/logging.h"
+#include "fml/native_library.h"
+
+#include <android/hardware_buffer.h>
+#include <dlfcn.h>
+
+namespace flutter {
+
+namespace {
+
+#define DECLARE_TYPES(ret, name, args) \
+  typedef ret(*fp_##name) args;        \
+  ret(*_##name) args = nullptr
+
+DECLARE_TYPES(int,
+              AHardwareBuffer_allocate,
+              (const AHardwareBuffer_Desc* desc, AHardwareBuffer** outBuffer));
+DECLARE_TYPES(int,
+              AHardwareBuffer_isSupported,
+              (const AHardwareBuffer_Desc* desc));
+DECLARE_TYPES(AHardwareBuffer*,
+              AHardwareBuffer_fromHardwareBuffer,
+              (JNIEnv * env, jobject hardwareBufferObj));
+DECLARE_TYPES(void, AHardwareBuffer_release, (AHardwareBuffer * buffer));
+DECLARE_TYPES(void,
+              AHardwareBuffer_describe,
+              (AHardwareBuffer * buffer, AHardwareBuffer_Desc* desc));
+DECLARE_TYPES(int,
+              AHardwareBuffer_getId,
+              (AHardwareBuffer * buffer, uint64_t* outId));
+
+DECLARE_TYPES(bool, ATrace_isEnabled, (void));
+
+DECLARE_TYPES(ASurfaceControl*,
+              ASurfaceControl_createFromWindow,
+              (ANativeWindow * parent, const char* debug_name));
+DECLARE_TYPES(void,
+              ASurfaceControl_release,
+              (ASurfaceControl * surface_control));
+DECLARE_TYPES(ASurfaceTransaction*, ASurfaceTransaction_create, (void));
+DECLARE_TYPES(void,
+              ASurfaceTransaction_delete,
+              (ASurfaceTransaction * surface_transaction));
+DECLARE_TYPES(void,
+              ASurfaceTransaction_apply,
+              (ASurfaceTransaction * surface_transaction));
+DECLARE_TYPES(void,
+              ASurfaceTransaction_setBuffer,
+              (ASurfaceTransaction * transaction,
+               ASurfaceControl* surface_control,
+               AHardwareBuffer* buffer,
+               int acquire_fence_fd));
+
+DECLARE_TYPES(AChoreographer*, AChoreographer_getInstance, (void));
+DECLARE_TYPES(void,
+              AChoreographer_postFrameCallback,
+              (AChoreographer * choreographer,
+               AChoreographer_frameCallback callbackk,
+               void* data));
+DECLARE_TYPES(void,
+              AChoreographer_postFrameCallback64,
+              (AChoreographer * choreographer,
+               AChoreographer_frameCallback64 callbackk,
+               void* data));
+
+DECLARE_TYPES(EGLClientBuffer,
+              eglGetNativeClientBufferANDROID,
+              (AHardwareBuffer * buffer));
+
+#undef DECLARE_TYPES
+
+std::once_flag init_once;
+
+void InitOnceCallback() {
+  static fml::RefPtr<fml::NativeLibrary> android =
+      fml::NativeLibrary::Create("libandroid.so");
+  FML_CHECK(android.get() != nullptr);
+  static fml::RefPtr<fml::NativeLibrary> egl =
+      fml::NativeLibrary::Create("libEGL.so");
+  FML_CHECK(egl.get() != nullptr);
+
+#define LOOKUP(lib, func) \
+  _##func = lib->ResolveFunction<fp_##func>(#func).value_or(nullptr)
+
+  LOOKUP(egl, eglGetNativeClientBufferANDROID);
+
+  LOOKUP(android, AHardwareBuffer_fromHardwareBuffer);
+  LOOKUP(android, AHardwareBuffer_release);
+  LOOKUP(android, AHardwareBuffer_getId);
+  LOOKUP(android, AHardwareBuffer_describe);
+  LOOKUP(android, AHardwareBuffer_allocate);
+  LOOKUP(android, AHardwareBuffer_isSupported);
+  LOOKUP(android, ATrace_isEnabled);
+  LOOKUP(android, AChoreographer_getInstance);
+  if (_AChoreographer_getInstance) {
+    LOOKUP(android, AChoreographer_postFrameCallback64);
+    if (!_AChoreographer_postFrameCallback64) {
+      LOOKUP(android, AChoreographer_postFrameCallback);
+    }
+  }
+
+  LOOKUP(android, ASurfaceControl_createFromWindow);
+  LOOKUP(android, ASurfaceControl_release);
+  LOOKUP(android, ASurfaceTransaction_apply);
+  LOOKUP(android, ASurfaceTransaction_create);
+  LOOKUP(android, ASurfaceTransaction_delete);
+  LOOKUP(android, ASurfaceTransaction_setBuffer);
+#undef LOOKUP
+}
+
+}  // namespace
+
+void NDKHelpers::Init() {
+  std::call_once(init_once, InitOnceCallback);
+}
+
+bool NDKHelpers::ATrace_isEnabled() {
+  if (_ATrace_isEnabled) {
+    return _ATrace_isEnabled();
+  }
+  return false;
+}
+
+ChoreographerSupportStatus NDKHelpers::ChoreographerSupported() {
+  if (_AChoreographer_postFrameCallback64) {
+    return ChoreographerSupportStatus::kSupported64;
+  }
+  if (_AChoreographer_postFrameCallback) {
+    return ChoreographerSupportStatus::kSupported32;
+  }
+  return ChoreographerSupportStatus::kUnsupported;
+}
+
+AChoreographer* NDKHelpers::AChoreographer_getInstance() {
+  FML_CHECK(_AChoreographer_getInstance);
+  return _AChoreographer_getInstance();
+}
+
+void NDKHelpers::AChoreographer_postFrameCallback(
+    AChoreographer* choreographer,
+    AChoreographer_frameCallback callback,
+    void* data) {
+  FML_CHECK(_AChoreographer_postFrameCallback);
+  return _AChoreographer_postFrameCallback(choreographer, callback, data);
+}
+
+void NDKHelpers::AChoreographer_postFrameCallback64(
+    AChoreographer* choreographer,
+    AChoreographer_frameCallback64 callback,
+    void* data) {
+  FML_CHECK(_AChoreographer_postFrameCallback64);
+  return _AChoreographer_postFrameCallback64(choreographer, callback, data);
+}
+
+bool NDKHelpers::HardwareBufferSupported() {
+  const bool r = _AHardwareBuffer_fromHardwareBuffer != nullptr;
+  return r;
+}
+
+AHardwareBuffer* NDKHelpers::AHardwareBuffer_fromHardwareBuffer(
+    JNIEnv* env,
+    jobject hardwareBufferObj) {
+  FML_CHECK(_AHardwareBuffer_fromHardwareBuffer != nullptr);
+  return _AHardwareBuffer_fromHardwareBuffer(env, hardwareBufferObj);
+}
+
+void NDKHelpers::AHardwareBuffer_release(AHardwareBuffer* buffer) {
+  FML_CHECK(_AHardwareBuffer_release != nullptr);
+  _AHardwareBuffer_release(buffer);
+}
+
+void NDKHelpers::AHardwareBuffer_describe(AHardwareBuffer* buffer,
+                                          AHardwareBuffer_Desc* desc) {
+  FML_CHECK(_AHardwareBuffer_describe != nullptr);
+  _AHardwareBuffer_describe(buffer, desc);
+}
+
+std::optional<HardwareBufferKey> NDKHelpers::AHardwareBuffer_getId(
+    AHardwareBuffer* buffer) {
+  if (_AHardwareBuffer_getId == nullptr) {
+    return std::nullopt;
+  }
+  HardwareBufferKey outId;
+  int result = _AHardwareBuffer_getId(buffer, &outId);
+  if (result == 0) {
+    return outId;
+  }
+  return std::nullopt;
+}
+
+EGLClientBuffer NDKHelpers::eglGetNativeClientBufferANDROID(
+    AHardwareBuffer* buffer) {
+  FML_CHECK(_eglGetNativeClientBufferANDROID != nullptr);
+  return _eglGetNativeClientBufferANDROID(buffer);
+}
+
+bool NDKHelpers::SurfaceControlAndTransactionSupported() {
+  return _ASurfaceControl_createFromWindow && _ASurfaceControl_release &&
+         _ASurfaceTransaction_create && _ASurfaceTransaction_apply &&
+         _ASurfaceTransaction_delete && _ASurfaceTransaction_setBuffer;
+}
+
+ASurfaceControl* NDKHelpers::ASurfaceControl_createFromWindow(
+    ANativeWindow* parent,
+    const char* debug_name) {
+  FML_CHECK(_ASurfaceControl_createFromWindow);
+  return _ASurfaceControl_createFromWindow(parent, debug_name);
+}
+
+void NDKHelpers::ASurfaceControl_release(ASurfaceControl* surface_control) {
+  FML_CHECK(_ASurfaceControl_release);
+  return _ASurfaceControl_release(surface_control);
+}
+
+ASurfaceTransaction* NDKHelpers::ASurfaceTransaction_create() {
+  FML_CHECK(_ASurfaceTransaction_create);
+  return _ASurfaceTransaction_create();
+}
+
+void NDKHelpers::ASurfaceTransaction_delete(
+    ASurfaceTransaction* surface_transaction) {
+  FML_CHECK(_ASurfaceTransaction_delete);
+  _ASurfaceTransaction_delete(surface_transaction);
+}
+
+void NDKHelpers::ASurfaceTransaction_apply(
+    ASurfaceTransaction* surface_transaction) {
+  FML_CHECK(_ASurfaceTransaction_apply);
+  _ASurfaceTransaction_apply(surface_transaction);
+}
+
+void NDKHelpers::ASurfaceTransaction_setBuffer(ASurfaceTransaction* transaction,
+                                               ASurfaceControl* surface_control,
+                                               AHardwareBuffer* buffer,
+                                               int acquire_fence_fd) {
+  FML_CHECK(_ASurfaceTransaction_setBuffer);
+  _ASurfaceTransaction_setBuffer(transaction, surface_control, buffer,
+                                 acquire_fence_fd);
+}
+
+int NDKHelpers::AHardwareBuffer_isSupported(const AHardwareBuffer_Desc* desc) {
+  FML_CHECK(_AHardwareBuffer_isSupported);
+  return _AHardwareBuffer_isSupported(desc);
+}
+
+int NDKHelpers::AHardwareBuffer_allocate(const AHardwareBuffer_Desc* desc,
+                                         AHardwareBuffer** outBuffer) {
+  FML_CHECK(_AHardwareBuffer_allocate);
+  return _AHardwareBuffer_allocate(desc, outBuffer);
+}
+
+}  // namespace flutter
diff --git a/fml/platform/android/ndk_helpers.h b/fml/platform/android/ndk_helpers.h
new file mode 100644
index 0000000..9299776
--- /dev/null
+++ b/fml/platform/android/ndk_helpers.h
@@ -0,0 +1,100 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_FML_PLATFORM_ANDROID_NDK_HELPERS_H_
+#define FLUTTER_FML_PLATFORM_ANDROID_NDK_HELPERS_H_
+
+#include <EGL/egl.h>
+#include <android/choreographer.h>
+#include <android/hardware_buffer.h>
+#include <android/surface_control.h>
+#include <android/trace.h>
+#include <jni.h>
+#include <optional>
+
+namespace flutter {
+
+using HardwareBufferKey = uint64_t;
+
+enum class ChoreographerSupportStatus {
+  // Unavailable, API level < 24.
+  kUnsupported,
+  // Available, but only with postFrameCallback.
+  kSupported32,
+  // Available, but only with postFrameCallback64.
+  kSupported64,
+};
+
+// A collection of NDK functions that are available depending on the version of
+// the Android SDK we are linked with at runtime.
+class NDKHelpers {
+ public:
+  // Safe to call multiple times.
+  // Normally called from JNI_OnLoad.
+  static void Init();
+
+  // API Version 23
+  static bool ATrace_isEnabled();
+
+  // API Version 24
+  static ChoreographerSupportStatus ChoreographerSupported();
+  static AChoreographer* _Nullable AChoreographer_getInstance();
+  // Deprecated in 29, available since 24.
+  static void AChoreographer_postFrameCallback(
+      AChoreographer* _Nonnull choreographer,
+      AChoreographer_frameCallback _Nonnull callback,
+      void* _Nullable data);
+
+  // API Version 26
+  static bool HardwareBufferSupported();
+  static AHardwareBuffer* _Nonnull AHardwareBuffer_fromHardwareBuffer(
+      JNIEnv* _Nonnull env,
+      jobject _Nonnull hardwareBufferObj);
+  static void AHardwareBuffer_release(AHardwareBuffer* _Nonnull buffer);
+  static void AHardwareBuffer_describe(AHardwareBuffer* _Nonnull buffer,
+                                       AHardwareBuffer_Desc* _Nullable desc);
+  static int AHardwareBuffer_allocate(
+      const AHardwareBuffer_Desc* _Nonnull desc,
+      AHardwareBuffer* _Nullable* _Nullable outBuffer);
+  static EGLClientBuffer _Nonnull eglGetNativeClientBufferANDROID(
+      AHardwareBuffer* _Nonnull buffer);
+
+  // API Version 29
+  static int AHardwareBuffer_isSupported(
+      const AHardwareBuffer_Desc* _Nonnull desc);
+
+  static void AChoreographer_postFrameCallback64(
+      AChoreographer* _Nonnull choreographer,
+      AChoreographer_frameCallback64 _Nonnull callback,
+      void* _Nullable data);
+
+  static bool SurfaceControlAndTransactionSupported();
+
+  static ASurfaceControl* _Nonnull ASurfaceControl_createFromWindow(
+      ANativeWindow* _Nonnull parent,
+      const char* _Nullable debug_name);
+  static void ASurfaceControl_release(
+      ASurfaceControl* _Nonnull surface_control);
+
+  static ASurfaceTransaction* _Nonnull ASurfaceTransaction_create();
+  static void ASurfaceTransaction_delete(
+      ASurfaceTransaction* _Nonnull surface_transaction);
+  static void ASurfaceTransaction_apply(
+      ASurfaceTransaction* _Nonnull surface_transaction);
+  static void ASurfaceTransaction_setBuffer(
+      ASurfaceTransaction* _Nonnull transaction,
+      ASurfaceControl* _Nonnull surface_control,
+      AHardwareBuffer* _Nonnull buffer,
+      int acquire_fence_fd);
+
+  // API Version 31
+
+  // Returns std::nullopt on API version 26 - 30.
+  static std::optional<HardwareBufferKey> AHardwareBuffer_getId(
+      AHardwareBuffer* _Nonnull buffer);
+};
+
+}  // namespace flutter
+
+#endif  // FLUTTER_FML_PLATFORM_ANDROID_NDK_HELPERS_H_
diff --git a/fml/platform/android/ndk_helpers_unittests.cc b/fml/platform/android/ndk_helpers_unittests.cc
new file mode 100644
index 0000000..e6cab42
--- /dev/null
+++ b/fml/platform/android/ndk_helpers_unittests.cc
@@ -0,0 +1,121 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "fml/message_loop.h"
+#include "fml/platform/android/ndk_helpers.h"
+
+#include "gtest/gtest.h"
+
+namespace flutter {
+namespace testing {
+namespace android {
+
+class NdkHelpersTest : public ::testing::Test {
+ public:
+  void SetUp() override { NDKHelpers::Init(); }
+
+  static void OnVsync(int64_t frame_nanos, void* data) {}
+  static void OnVsync32(
+      long frame_nanos,  // NOLINT - compat for deprecated call
+      void* data) {}
+};
+
+TEST_F(NdkHelpersTest, ATrace) {
+  ASSERT_GT(android_get_device_api_level(), 22);
+  EXPECT_FALSE(NDKHelpers::ATrace_isEnabled());
+}
+
+TEST_F(NdkHelpersTest, AChoreographer32) {
+  if (android_get_device_api_level() >= 29) {
+    GTEST_SKIP() << "This test is for less than API 29.";
+  }
+
+  EXPECT_EQ(NDKHelpers::ChoreographerSupported(),
+            ChoreographerSupportStatus::kSupported32);
+
+  EXPECT_FALSE(NDKHelpers::AChoreographer_getInstance());
+
+  fml::MessageLoop::EnsureInitializedForCurrentThread();
+
+  EXPECT_TRUE(NDKHelpers::AChoreographer_getInstance());
+
+  NDKHelpers::AChoreographer_postFrameCallback(
+      NDKHelpers::AChoreographer_getInstance(), &OnVsync32, nullptr);
+}
+
+TEST_F(NdkHelpersTest, AChoreographer64) {
+  if (android_get_device_api_level() < 29) {
+    GTEST_SKIP() << "This test is for API 29 and above.";
+  }
+
+  EXPECT_EQ(NDKHelpers::ChoreographerSupported(),
+            ChoreographerSupportStatus::kSupported64);
+
+  EXPECT_FALSE(NDKHelpers::AChoreographer_getInstance());
+
+  fml::MessageLoop::EnsureInitializedForCurrentThread();
+
+  EXPECT_TRUE(NDKHelpers::AChoreographer_getInstance());
+
+  NDKHelpers::AChoreographer_postFrameCallback64(
+      NDKHelpers::AChoreographer_getInstance(), &OnVsync, nullptr);
+}
+
+TEST_F(NdkHelpersTest, HardwareBuffer) {
+  if (android_get_device_api_level() < 26) {
+    GTEST_SKIP() << "Test requires at least API 26.";
+  }
+
+  ASSERT_TRUE(NDKHelpers::HardwareBufferSupported());
+
+  AHardwareBuffer_Desc desc{
+      .width = 4,
+      .height = 4,
+      .layers = 1,
+      .format = AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
+  };
+  if (android_get_device_api_level() >= 29) {
+    EXPECT_TRUE(NDKHelpers::AHardwareBuffer_isSupported(&desc));
+  }
+
+  AHardwareBuffer* buffer = nullptr;
+  // AHardwareBuffer_allocate returns 0 on success.
+  EXPECT_EQ(NDKHelpers::AHardwareBuffer_allocate(&desc, &buffer), 0);
+  EXPECT_TRUE(buffer);
+
+  AHardwareBuffer_Desc out_desc = {};
+  NDKHelpers::AHardwareBuffer_describe(buffer, &out_desc);
+  EXPECT_EQ(desc.width, out_desc.width);
+  EXPECT_EQ(desc.height, out_desc.height);
+  EXPECT_EQ(desc.layers, out_desc.layers);
+  EXPECT_EQ(desc.format, out_desc.format);
+
+  auto id = NDKHelpers::AHardwareBuffer_getId(buffer);
+  if (android_get_device_api_level() >= 31) {
+    EXPECT_TRUE(id.has_value());
+  } else {
+    EXPECT_FALSE(id.has_value());
+  }
+
+  NDKHelpers::AHardwareBuffer_release(buffer);
+}
+
+TEST_F(NdkHelpersTest, SurfaceTransaction) {
+  if (android_get_device_api_level() < 29) {
+    GTEST_SKIP() << "Test requires at least API 29.";
+  }
+  EXPECT_TRUE(NDKHelpers::SurfaceControlAndTransactionSupported());
+
+  // Need ANativeWindow to create ASurfaceControl and set a buffer to the
+  // transaction. Just create/apply/delete as a smoke test.
+
+  ASurfaceTransaction* transaction = NDKHelpers::ASurfaceTransaction_create();
+  EXPECT_TRUE(transaction);
+  NDKHelpers::ASurfaceTransaction_apply(transaction);
+  NDKHelpers::ASurfaceTransaction_delete(transaction);
+}
+
+}  // namespace android
+}  // namespace testing
+}  // namespace flutter
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 2142960..a6c4cb6 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -42,6 +42,7 @@
   visibility = [ "*" ]
   testonly = true
   sources = [
+    "//flutter/fml/platform/android/ndk_helpers_unittests.cc",
     "android_context_gl_impeller_unittests.cc",
     "android_context_gl_unittests.cc",
     "android_shell_holder_unittests.cc",
@@ -53,6 +54,7 @@
   public_configs = [ "//flutter:config" ]
   deps = [
     ":flutter_shell_native_src",
+    "//flutter/fml",
     "//flutter/shell/platform/android/jni:jni_mock",
     "//third_party/googletest:gmock",
     "//third_party/googletest:gtest",
@@ -113,8 +115,6 @@
     "image_lru.cc",
     "image_lru.h",
     "library_loader.cc",
-    "ndk_helpers.cc",
-    "ndk_helpers.h",
     "platform_message_handler_android.cc",
     "platform_message_handler_android.h",
     "platform_message_response_android.cc",
diff --git a/shell/platform/android/flutter_main.cc b/shell/platform/android/flutter_main.cc
index 8a2e9ea..6ad57a7 100644
--- a/shell/platform/android/flutter_main.cc
+++ b/shell/platform/android/flutter_main.cc
@@ -18,13 +18,13 @@
 #include "flutter/fml/native_library.h"
 #include "flutter/fml/paths.h"
 #include "flutter/fml/platform/android/jni_util.h"
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/fml/platform/android/paths_android.h"
 #include "flutter/fml/size.h"
 #include "flutter/lib/ui/plugins/callback_cache.h"
 #include "flutter/runtime/dart_vm.h"
 #include "flutter/shell/common/shell.h"
 #include "flutter/shell/common/switches.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 #include "third_party/dart/runtime/include/dart_tools_api.h"
 #include "txt/platform.h"
 
diff --git a/shell/platform/android/image_external_texture.cc b/shell/platform/android/image_external_texture.cc
index da6f558..855b56d 100644
--- a/shell/platform/android/image_external_texture.cc
+++ b/shell/platform/android/image_external_texture.cc
@@ -4,8 +4,9 @@
 #include <android/hardware_buffer_jni.h>
 #include <android/sensor.h>
 
+#include "flutter/fml/platform/android/jni_util.h"
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/shell/platform/android/jni/platform_view_android_jni.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 
 namespace flutter {
 
diff --git a/shell/platform/android/image_external_texture_gl.cc b/shell/platform/android/image_external_texture_gl.cc
index 1dea2df..6a676e9 100644
--- a/shell/platform/android/image_external_texture_gl.cc
+++ b/shell/platform/android/image_external_texture_gl.cc
@@ -8,11 +8,11 @@
 #include <android/sensor.h>
 
 #include "flutter/common/graphics/texture.h"
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/impeller/core/formats.h"
 #include "flutter/impeller/display_list/dl_image_impeller.h"
 #include "flutter/impeller/toolkit/egl/image.h"
 #include "flutter/impeller/toolkit/gles/texture.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 #include "third_party/skia/include/core/SkAlphaType.h"
 #include "third_party/skia/include/core/SkColorType.h"
 #include "third_party/skia/include/gpu/ganesh/SkImageGanesh.h"
diff --git a/shell/platform/android/image_external_texture_gl.h b/shell/platform/android/image_external_texture_gl.h
index e3040d5..8aeec74 100644
--- a/shell/platform/android/image_external_texture_gl.h
+++ b/shell/platform/android/image_external_texture_gl.h
@@ -17,8 +17,8 @@
 #include "flutter/impeller/toolkit/egl/image.h"
 #include "flutter/impeller/toolkit/gles/texture.h"
 
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/shell/platform/android/android_context_gl_skia.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 
 namespace flutter {
 
diff --git a/shell/platform/android/image_external_texture_vk.cc b/shell/platform/android/image_external_texture_vk.cc
index 5eaf470..9af0683 100644
--- a/shell/platform/android/image_external_texture_vk.cc
+++ b/shell/platform/android/image_external_texture_vk.cc
@@ -2,6 +2,7 @@
 #include "flutter/shell/platform/android/image_external_texture_vk.h"
 #include <cstdint>
 
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/impeller/core/formats.h"
 #include "flutter/impeller/core/texture_descriptor.h"
 #include "flutter/impeller/display_list/dl_image_impeller.h"
@@ -9,7 +10,6 @@
 #include "flutter/impeller/renderer/backend/vulkan/command_buffer_vk.h"
 #include "flutter/impeller/renderer/backend/vulkan/command_encoder_vk.h"
 #include "flutter/impeller/renderer/backend/vulkan/texture_vk.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 
 namespace flutter {
 
diff --git a/shell/platform/android/image_lru.h b/shell/platform/android/image_lru.h
index 34fa97c..28a31cd 100644
--- a/shell/platform/android/image_lru.h
+++ b/shell/platform/android/image_lru.h
@@ -9,7 +9,7 @@
 #include <cstddef>
 
 #include "display_list/image/dl_image.h"
-#include "shell/platform/android/ndk_helpers.h"
+#include "fml/platform/android/ndk_helpers.h"
 
 namespace flutter {
 
diff --git a/shell/platform/android/library_loader.cc b/shell/platform/android/library_loader.cc
index 103a3ae..9f3691d 100644
--- a/shell/platform/android/library_loader.cc
+++ b/shell/platform/android/library_loader.cc
@@ -3,9 +3,9 @@
 // found in the LICENSE file.
 
 #include "flutter/fml/platform/android/jni_util.h"
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/shell/platform/android/android_image_generator.h"
 #include "flutter/shell/platform/android/flutter_main.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 #include "flutter/shell/platform/android/platform_view_android.h"
 #include "flutter/shell/platform/android/vsync_waiter_android.h"
 
diff --git a/shell/platform/android/ndk_helpers.cc b/shell/platform/android/ndk_helpers.cc
deleted file mode 100644
index 65fc936..0000000
--- a/shell/platform/android/ndk_helpers.cc
+++ /dev/null
@@ -1,217 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "flutter/shell/platform/android/ndk_helpers.h"
-
-#include "fml/native_library.h"
-
-#include "flutter/fml/logging.h"
-
-#include <android/hardware_buffer.h>
-#include <dlfcn.h>
-
-namespace flutter {
-
-namespace {
-
-typedef AHardwareBuffer* (*fp_AHardwareBuffer_fromHardwareBuffer)(
-    JNIEnv* env,
-    jobject hardwareBufferObj);
-typedef void (*fp_AHardwareBuffer_acquire)(AHardwareBuffer* buffer);
-typedef void (*fp_AHardwareBuffer_release)(AHardwareBuffer* buffer);
-typedef void (*fp_AHardwareBuffer_describe)(AHardwareBuffer* buffer,
-                                            AHardwareBuffer_Desc* desc);
-typedef void (*fp_AHardwareBuffer_getId)(AHardwareBuffer* buffer,
-                                         uint64_t* outId);
-
-typedef bool (*fp_ATrace_isEnabled)(void);
-
-typedef AChoreographer* (*fp_AChoreographer_getInstance)(void);
-typedef void (*fp_AChoreographer_postFrameCallback)(
-    AChoreographer* choreographer,
-    AChoreographer_frameCallback callbackk,
-    void* data);
-typedef void (*fp_AChoreographer_postFrameCallback64)(
-    AChoreographer* choreographer,
-    AChoreographer_frameCallback64 callbackk,
-    void* data);
-
-typedef EGLClientBuffer (*fp_eglGetNativeClientBufferANDROID)(
-    AHardwareBuffer* buffer);
-
-AHardwareBuffer* (*_AHardwareBuffer_fromHardwareBuffer)(
-    JNIEnv* env,
-    jobject hardwareBufferObj) = nullptr;
-void (*_AHardwareBuffer_acquire)(AHardwareBuffer* buffer) = nullptr;
-void (*_AHardwareBuffer_release)(AHardwareBuffer* buffer) = nullptr;
-void (*_AHardwareBuffer_describe)(AHardwareBuffer* buffer,
-                                  AHardwareBuffer_Desc* desc) = nullptr;
-void (*_AHardwareBuffer_getId)(AHardwareBuffer* buffer,
-                               uint64_t* outId) = nullptr;
-bool (*_ATrace_isEnabled)() = nullptr;
-AChoreographer* (*_AChoreographer_getInstance)() = nullptr;
-void (*_AChoreographer_postFrameCallback)(
-    AChoreographer* choreographer,
-    AChoreographer_frameCallback callbackk,
-    void* data) = nullptr;
-void (*_AChoreographer_postFrameCallback64)(
-    AChoreographer* choreographer,
-    AChoreographer_frameCallback64 callbackk,
-    void* data) = nullptr;
-
-EGLClientBuffer (*_eglGetNativeClientBufferANDROID)(AHardwareBuffer* buffer) =
-    nullptr;
-
-std::once_flag init_once;
-
-void InitOnceCallback() {
-  static fml::RefPtr<fml::NativeLibrary> android =
-      fml::NativeLibrary::Create("libandroid.so");
-  FML_CHECK(android.get() != nullptr);
-  static fml::RefPtr<fml::NativeLibrary> egl =
-      fml::NativeLibrary::Create("libEGL.so");
-  FML_CHECK(egl.get() != nullptr);
-  _eglGetNativeClientBufferANDROID =
-      egl->ResolveFunction<fp_eglGetNativeClientBufferANDROID>(
-             "eglGetNativeClientBufferANDROID")
-          .value_or(nullptr);
-  _AHardwareBuffer_fromHardwareBuffer =
-      android
-          ->ResolveFunction<fp_AHardwareBuffer_fromHardwareBuffer>(
-              "AHardwareBuffer_fromHardwareBuffer")
-          .value_or(nullptr);
-  _AHardwareBuffer_acquire = android
-                                 ->ResolveFunction<fp_AHardwareBuffer_acquire>(
-                                     "AHardwareBuffer_acquire")
-                                 .value_or(nullptr);
-  _AHardwareBuffer_release = android
-                                 ->ResolveFunction<fp_AHardwareBuffer_release>(
-                                     "AHardwareBuffer_release")
-                                 .value_or(nullptr);
-  _AHardwareBuffer_getId =
-      android
-          ->ResolveFunction<fp_AHardwareBuffer_getId>("AHardwareBuffer_getId")
-          .value_or(nullptr);
-  _AHardwareBuffer_describe =
-      android
-          ->ResolveFunction<fp_AHardwareBuffer_describe>(
-              "AHardwareBuffer_describe")
-          .value_or(nullptr);
-
-  _ATrace_isEnabled =
-      android->ResolveFunction<fp_ATrace_isEnabled>("ATrace_isEnabled")
-          .value_or(nullptr);
-
-  _AChoreographer_getInstance =
-      android
-          ->ResolveFunction<fp_AChoreographer_getInstance>(
-              "AChoreographer_getInstance")
-          .value_or(nullptr);
-  if (_AChoreographer_getInstance) {
-    _AChoreographer_postFrameCallback64 =
-        android
-            ->ResolveFunction<fp_AChoreographer_postFrameCallback64>(
-                "AChoreographer_postFrameCallback64")
-            .value_or(nullptr);
-#if FML_ARCH_CPU_64_BITS
-    if (!_AChoreographer_postFrameCallback64) {
-      _AChoreographer_postFrameCallback =
-          android
-              ->ResolveFunction<fp_AChoreographer_postFrameCallback>(
-                  "AChoreographer_postFrameCallback")
-              .value_or(nullptr);
-    }
-#endif
-  }
-}
-
-}  // namespace
-
-void NDKHelpers::Init() {
-  std::call_once(init_once, InitOnceCallback);
-}
-
-bool NDKHelpers::ATrace_isEnabled() {
-  if (_ATrace_isEnabled) {
-    return _ATrace_isEnabled();
-  }
-  return false;
-}
-
-ChoreographerSupportStatus NDKHelpers::ChoreographerSupported() {
-  if (_AChoreographer_postFrameCallback64) {
-    return ChoreographerSupportStatus::kSupported64;
-  }
-  if (_AChoreographer_postFrameCallback) {
-    return ChoreographerSupportStatus::kSupported32;
-  }
-  return ChoreographerSupportStatus::kUnsupported;
-}
-
-AChoreographer* NDKHelpers::AChoreographer_getInstance() {
-  FML_CHECK(_AChoreographer_getInstance);
-  return _AChoreographer_getInstance();
-}
-
-void NDKHelpers::AChoreographer_postFrameCallback(
-    AChoreographer* choreographer,
-    AChoreographer_frameCallback callback,
-    void* data) {
-  FML_CHECK(_AChoreographer_postFrameCallback);
-  return _AChoreographer_postFrameCallback(choreographer, callback, data);
-}
-
-void NDKHelpers::AChoreographer_postFrameCallback64(
-    AChoreographer* choreographer,
-    AChoreographer_frameCallback64 callback,
-    void* data) {
-  FML_CHECK(_AChoreographer_postFrameCallback64);
-  return _AChoreographer_postFrameCallback64(choreographer, callback, data);
-}
-
-bool NDKHelpers::HardwareBufferSupported() {
-  const bool r = _AHardwareBuffer_fromHardwareBuffer != nullptr;
-  return r;
-}
-
-AHardwareBuffer* NDKHelpers::AHardwareBuffer_fromHardwareBuffer(
-    JNIEnv* env,
-    jobject hardwareBufferObj) {
-  FML_CHECK(_AHardwareBuffer_fromHardwareBuffer != nullptr);
-  return _AHardwareBuffer_fromHardwareBuffer(env, hardwareBufferObj);
-}
-
-void NDKHelpers::AHardwareBuffer_acquire(AHardwareBuffer* buffer) {
-  FML_CHECK(_AHardwareBuffer_acquire != nullptr);
-  _AHardwareBuffer_acquire(buffer);
-}
-
-void NDKHelpers::AHardwareBuffer_release(AHardwareBuffer* buffer) {
-  FML_CHECK(_AHardwareBuffer_release != nullptr);
-  _AHardwareBuffer_release(buffer);
-}
-
-void NDKHelpers::AHardwareBuffer_describe(AHardwareBuffer* buffer,
-                                          AHardwareBuffer_Desc* desc) {
-  FML_CHECK(_AHardwareBuffer_describe != nullptr);
-  _AHardwareBuffer_describe(buffer, desc);
-}
-
-std::optional<HardwareBufferKey> NDKHelpers::AHardwareBuffer_getId(
-    AHardwareBuffer* buffer) {
-  if (_AHardwareBuffer_getId == nullptr) {
-    return std::nullopt;
-  }
-  HardwareBufferKey outId;
-  _AHardwareBuffer_getId(buffer, &outId);
-  return outId;
-}
-
-EGLClientBuffer NDKHelpers::eglGetNativeClientBufferANDROID(
-    AHardwareBuffer* buffer) {
-  FML_CHECK(_eglGetNativeClientBufferANDROID != nullptr);
-  return _eglGetNativeClientBufferANDROID(buffer);
-}
-
-}  // namespace flutter
diff --git a/shell/platform/android/ndk_helpers.h b/shell/platform/android/ndk_helpers.h
deleted file mode 100644
index 5b7856a..0000000
--- a/shell/platform/android/ndk_helpers.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef FLUTTER_SHELL_PLATFORM_ANDROID_NDK_HELPERS_H_
-#define FLUTTER_SHELL_PLATFORM_ANDROID_NDK_HELPERS_H_
-
-#include "flutter/fml/native_library.h"
-#include "flutter/fml/platform/android/jni_util.h"
-
-#include "flutter/impeller/toolkit/egl/egl.h"
-
-#include <android/choreographer.h>
-#include <android/hardware_buffer.h>
-#include <android/surface_control.h>
-#include <android/trace.h>
-
-namespace flutter {
-
-using HardwareBufferKey = uint64_t;
-
-enum class ChoreographerSupportStatus {
-  // Unavailable, API level < 24.
-  kUnsupported,
-  // Available, but only with postFrameCallback.
-  kSupported32,
-  // Available, but only with postFrameCallback64.
-  kSupported64,
-};
-
-// A collection of NDK functions that are available depending on the version of
-// the Android SDK we are linked with at runtime.
-class NDKHelpers {
- public:
-  // Safe to call multiple times.
-  // Normally called from JNI_OnLoad.
-  static void Init();
-
-  // API Version 23
-  static bool ATrace_isEnabled();
-
-  // API Version 24
-  static ChoreographerSupportStatus ChoreographerSupported();
-  static AChoreographer* AChoreographer_getInstance();
-  // Deprecated in 29, available since 24.
-  static void AChoreographer_postFrameCallback(
-      AChoreographer* choreographer,
-      AChoreographer_frameCallback callback,
-      void* data);
-
-  // API Version 26
-  static bool HardwareBufferSupported();
-  static AHardwareBuffer* AHardwareBuffer_fromHardwareBuffer(
-      JNIEnv* env,
-      jobject hardwareBufferObj);
-  static void AHardwareBuffer_acquire(AHardwareBuffer* buffer);
-  static void AHardwareBuffer_release(AHardwareBuffer* buffer);
-  static void AHardwareBuffer_describe(AHardwareBuffer* buffer,
-                                       AHardwareBuffer_Desc* desc);
-  static EGLClientBuffer eglGetNativeClientBufferANDROID(
-      AHardwareBuffer* buffer);
-
-  // API Version 29
-  static void AChoreographer_postFrameCallback64(
-      AChoreographer* choreographer,
-      AChoreographer_frameCallback64 callback,
-      void* data);
-
-  // API Version 31
-
-  // Returns std::nullopt on API version 26 - 30.
-  static std::optional<HardwareBufferKey> AHardwareBuffer_getId(
-      AHardwareBuffer* buffer);
-};
-
-}  // namespace flutter
-
-#endif  // FLUTTER_SHELL_PLATFORM_ANDROID_NDK_HELPERS_H_
diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc
index 7d13c10..b6d5dfc 100644
--- a/shell/platform/android/platform_view_android_jni_impl.cc
+++ b/shell/platform/android/platform_view_android_jni_impl.cc
@@ -12,7 +12,7 @@
 #include <sstream>
 #include <utility>
 
-#include "flutter/shell/platform/android/ndk_helpers.h"
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "include/android/SkImageAndroid.h"
 #include "unicode/uchar.h"
 
diff --git a/shell/platform/android/vsync_waiter_android.cc b/shell/platform/android/vsync_waiter_android.cc
index 7b7f136..d0a8db3 100644
--- a/shell/platform/android/vsync_waiter_android.cc
+++ b/shell/platform/android/vsync_waiter_android.cc
@@ -10,10 +10,10 @@
 #include "flutter/common/task_runners.h"
 #include "flutter/fml/logging.h"
 #include "flutter/fml/platform/android/jni_util.h"
+#include "flutter/fml/platform/android/ndk_helpers.h"
 #include "flutter/fml/platform/android/scoped_java_ref.h"
 #include "flutter/fml/size.h"
 #include "flutter/fml/trace_event.h"
-#include "flutter/shell/platform/android/ndk_helpers.h"
 
 namespace flutter {