diff --git a/BUILD.gn b/BUILD.gn
index 86c63580228e3..49784eff36e03 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -168,6 +168,7 @@ group("unittests") {
public_deps = []
if (is_android) {
public_deps += [
+ "//flutter/impeller/toolkit/android:apk_unittests",
"//flutter/impeller/toolkit/android:unittests",
"//flutter/shell/platform/android:flutter_shell_native_unittests",
]
diff --git a/impeller/toolkit/android/BUILD.gn b/impeller/toolkit/android/BUILD.gn
index fd0dde36bd9bf..e0f83d652b573 100644
--- a/impeller/toolkit/android/BUILD.gn
+++ b/impeller/toolkit/android/BUILD.gn
@@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import("//flutter/testing/android/native_activity/native_activity.gni")
import("../../tools/impeller.gni")
config("public_android_config") {
@@ -37,13 +38,11 @@ test_fixtures("unittests_fixtures") {
fixtures = []
}
-executable("unittests") {
- assert(is_android)
+source_set("unittests_lib") {
+ visibility = [ ":*" ]
testonly = true
- output_name = "impeller_toolkit_android_unittests"
-
sources = [ "toolkit_android_unittests.cc" ]
deps = [
@@ -52,3 +51,24 @@ executable("unittests") {
"//flutter/testing",
]
}
+
+executable("unittests") {
+ assert(is_android)
+
+ testonly = true
+
+ output_name = "impeller_toolkit_android_unittests"
+
+ deps = [ ":unittests_lib" ]
+}
+
+native_activity_apk("apk_unittests") {
+ apk_name = "impeller_toolkit_android_unittests"
+
+ testonly = true
+
+ deps = [
+ ":unittests_lib",
+ "//flutter/testing/android/native_activity:gtest_activity",
+ ]
+}
diff --git a/testing/BUILD.gn b/testing/BUILD.gn
index 95b58199a714a..28dab99a0b301 100644
--- a/testing/BUILD.gn
+++ b/testing/BUILD.gn
@@ -21,6 +21,8 @@ source_set("testing_lib") {
"debugger_detection.h",
"display_list_testing.cc",
"display_list_testing.h",
+ "logger_listener.cc",
+ "logger_listener.h",
"mock_canvas.cc",
"mock_canvas.h",
"post_task_sync.cc",
diff --git a/testing/android/native_activity/AndroidManifest.xml.template b/testing/android/native_activity/AndroidManifest.xml.template
new file mode 100644
index 0000000000000..9f908332793a7
--- /dev/null
+++ b/testing/android/native_activity/AndroidManifest.xml.template
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testing/android/native_activity/BUILD.gn b/testing/android/native_activity/BUILD.gn
new file mode 100644
index 0000000000000..354864c1c97a3
--- /dev/null
+++ b/testing/android/native_activity/BUILD.gn
@@ -0,0 +1,41 @@
+# 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.
+
+# To create an native activity, deps in this source set in a
+# `native_activity_apk` target and make sure to add the implementation of
+# `NativeActivityMain` which returns a `flutter::NativeActivity` subclass.
+source_set("native_activity") {
+ assert(is_android)
+
+ sources = [
+ "native_activity.cc",
+ "native_activity.h",
+ ]
+
+ public_deps = [
+ "//flutter/fml",
+ "//flutter/impeller/toolkit/android",
+ ]
+
+ libs = [
+ "android",
+ "log",
+ ]
+}
+
+source_set("gtest_activity") {
+ assert(is_android)
+
+ testonly = true
+
+ sources = [
+ "gtest_activity.cc",
+ "gtest_activity.h",
+ ]
+
+ public_deps = [
+ ":native_activity",
+ "//flutter/testing:testing_lib",
+ ]
+}
diff --git a/testing/android/native_activity/README.md b/testing/android/native_activity/README.md
new file mode 100644
index 0000000000000..5ce198a06691d
--- /dev/null
+++ b/testing/android/native_activity/README.md
@@ -0,0 +1,31 @@
+Native Activity
+===============
+
+Executables packaged as native activities in an Android APK. These activities
+contain no Java code.
+
+To create an APK of your existing `exectuable` target, replace `exectuable` with
+`native_activity_apk` from the `native_activity.gni` template and give it an
+`apk_name`.
+
+## Example
+
+```
+native_activity_apk("apk_unittests") {
+ apk_name = "toolkit_unittests"
+
+ testonly = true
+
+ sources = [ "toolkit_android_unittests.cc" ]
+
+ deps = [
+ ":unittests_lib",
+ "//flutter/testing/android/native_activity:gtest_activity",
+ ]
+}
+```
+
+One of the translation units in must contain an implementation of
+`flutter::NativeActivityMain`. The `gtest_activity` target contains an
+implementation of an activity that run GoogleTests. That can be used off the
+shelf.
diff --git a/testing/android/native_activity/debug.keystore b/testing/android/native_activity/debug.keystore
new file mode 100644
index 0000000000000..df0714f9b96d2
Binary files /dev/null and b/testing/android/native_activity/debug.keystore differ
diff --git a/testing/android/native_activity/gtest_activity.cc b/testing/android/native_activity/gtest_activity.cc
new file mode 100644
index 0000000000000..4a678f36e6b98
--- /dev/null
+++ b/testing/android/native_activity/gtest_activity.cc
@@ -0,0 +1,49 @@
+// 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/testing/android/native_activity/gtest_activity.h"
+
+#include "flutter/impeller/toolkit/android/native_window.h"
+#include "flutter/testing/logger_listener.h"
+#include "flutter/testing/test_timeout_listener.h"
+
+namespace flutter {
+
+GTestActivity::GTestActivity(ANativeActivity* activity)
+ : NativeActivity(activity) {}
+
+GTestActivity::~GTestActivity() = default;
+
+static void StartTestSuite(const impeller::android::NativeWindow& window) {
+ auto timeout_listener = new flutter::testing::TestTimeoutListener(
+ fml::TimeDelta::FromSeconds(120u));
+ auto logger_listener = new flutter::testing::LoggerListener();
+
+ auto& listeners = ::testing::UnitTest::GetInstance()->listeners();
+
+ listeners.Append(timeout_listener);
+ listeners.Append(logger_listener);
+
+ int result = RUN_ALL_TESTS();
+
+ delete listeners.Release(timeout_listener);
+ delete listeners.Release(logger_listener);
+
+ FML_CHECK(result == 0);
+}
+
+// |NativeActivity|
+void GTestActivity::OnNativeWindowCreated(ANativeWindow* window) {
+ auto handle = std::make_shared(window);
+ background_thread_.GetTaskRunner()->PostTask(
+ [handle]() { StartTestSuite(*handle); });
+}
+
+std::unique_ptr NativeActivityMain(
+ ANativeActivity* activity,
+ std::unique_ptr saved_state) {
+ return std::make_unique(activity);
+}
+
+} // namespace flutter
diff --git a/testing/android/native_activity/gtest_activity.h b/testing/android/native_activity/gtest_activity.h
new file mode 100644
index 0000000000000..44a0a5eb74557
--- /dev/null
+++ b/testing/android/native_activity/gtest_activity.h
@@ -0,0 +1,40 @@
+// 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_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_
+#define FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_
+
+#include "flutter/fml/macros.h"
+#include "flutter/fml/thread.h"
+#include "flutter/testing/android/native_activity/native_activity.h"
+
+namespace flutter {
+
+//------------------------------------------------------------------------------
+/// @brief A native activity subclass an in implementation of
+/// `flutter::NativeActivityMain` that return it.
+///
+/// This class runs a Google Test harness on a background thread and
+/// redirects progress updates to `logcat` instead of STDOUT.
+///
+class GTestActivity final : public NativeActivity {
+ public:
+ explicit GTestActivity(ANativeActivity* activity);
+
+ ~GTestActivity() override;
+
+ GTestActivity(const GTestActivity&) = delete;
+
+ GTestActivity& operator=(const GTestActivity&) = delete;
+
+ // |NativeActivity|
+ void OnNativeWindowCreated(ANativeWindow* window) override;
+
+ private:
+ fml::Thread background_thread_;
+};
+
+} // namespace flutter
+
+#endif // FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_GTEST_ACTIVITY_H_
diff --git a/testing/android/native_activity/native_activity.cc b/testing/android/native_activity/native_activity.cc
new file mode 100644
index 0000000000000..37f7a1d7652e8
--- /dev/null
+++ b/testing/android/native_activity/native_activity.cc
@@ -0,0 +1,143 @@
+// 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/testing/android/native_activity/native_activity.h"
+
+#include "flutter/fml/message_loop.h"
+
+namespace flutter {
+
+NativeActivity::NativeActivity(ANativeActivity* activity)
+ : activity_(activity) {
+ fml::MessageLoop::EnsureInitializedForCurrentThread();
+
+ activity->instance = this;
+
+ activity->callbacks->onStart = [](ANativeActivity* activity) {
+ reinterpret_cast(activity->instance)->OnStart();
+ };
+ activity->callbacks->onStop = [](ANativeActivity* activity) {
+ reinterpret_cast(activity->instance)->OnStop();
+ };
+ activity->callbacks->onPause = [](ANativeActivity* activity) {
+ reinterpret_cast(activity->instance)->OnPause();
+ };
+ activity->callbacks->onResume = [](ANativeActivity* activity) {
+ reinterpret_cast(activity->instance)->OnResume();
+ };
+ activity->callbacks->onDestroy = [](ANativeActivity* activity) {
+ delete reinterpret_cast(activity->instance);
+ };
+ activity->callbacks->onSaveInstanceState = [](ANativeActivity* activity,
+ size_t* out_size) -> void* {
+ auto mapping = reinterpret_cast(activity->instance)
+ ->OnSaveInstanceState();
+ if (mapping == nullptr || mapping->GetMapping() == nullptr) {
+ *out_size = 0;
+ return nullptr;
+ }
+
+ // This will be `free`d by the framework.
+ auto copied = malloc(mapping->GetSize());
+ FML_CHECK(copied != nullptr)
+ << "Allocation failure while saving instance state.";
+ memcpy(copied, mapping->GetMapping(), mapping->GetSize());
+ *out_size = mapping->GetSize();
+ return copied;
+ };
+ activity->callbacks->onWindowFocusChanged = [](ANativeActivity* activity,
+ int has_focus) {
+ reinterpret_cast(activity->instance)
+ ->OnWindowFocusChanged(has_focus);
+ };
+ activity->callbacks->onNativeWindowCreated = [](ANativeActivity* activity,
+ ANativeWindow* window) {
+ reinterpret_cast(activity->instance)
+ ->OnNativeWindowCreated(window);
+ };
+ activity->callbacks->onNativeWindowResized = [](ANativeActivity* activity,
+ ANativeWindow* window) {
+ reinterpret_cast(activity->instance)
+ ->OnNativeWindowResized(window);
+ };
+ activity->callbacks->onNativeWindowRedrawNeeded =
+ [](ANativeActivity* activity, ANativeWindow* window) {
+ reinterpret_cast(activity->instance)
+ ->OnNativeWindowRedrawNeeded(window);
+ };
+ activity->callbacks->onNativeWindowDestroyed = [](ANativeActivity* activity,
+ ANativeWindow* window) {
+ reinterpret_cast(activity->instance)
+ ->OnNativeWindowDestroyed(window);
+ };
+ activity->callbacks->onInputQueueCreated = [](ANativeActivity* activity,
+ AInputQueue* queue) {
+ reinterpret_cast(activity->instance)
+ ->OnInputQueueCreated(queue);
+ };
+ activity->callbacks->onInputQueueDestroyed = [](ANativeActivity* activity,
+ AInputQueue* queue) {
+ reinterpret_cast(activity->instance)
+ ->OnInputQueueDestroyed(queue);
+ };
+ activity->callbacks->onConfigurationChanged = [](ANativeActivity* activity) {
+ reinterpret_cast(activity->instance)
+ ->OnConfigurationChanged();
+ };
+ activity->callbacks->onLowMemory = [](ANativeActivity* activity) {
+ reinterpret_cast(activity->instance)->OnLowMemory();
+ };
+}
+
+NativeActivity::~NativeActivity() = default;
+
+void NativeActivity::OnStart() {}
+
+void NativeActivity::OnStop() {}
+
+void NativeActivity::OnPause() {}
+
+void NativeActivity::OnResume() {}
+
+std::shared_ptr NativeActivity::OnSaveInstanceState() {
+ return nullptr;
+}
+
+void NativeActivity::OnWindowFocusChanged(bool has_focus) {}
+
+void NativeActivity::OnNativeWindowCreated(ANativeWindow* window) {}
+
+void NativeActivity::OnNativeWindowResized(ANativeWindow* window) {}
+
+void NativeActivity::OnNativeWindowRedrawNeeded(ANativeWindow* window) {}
+
+void NativeActivity::OnNativeWindowDestroyed(ANativeWindow* window) {}
+
+void NativeActivity::OnInputQueueCreated(AInputQueue* queue) {}
+
+void NativeActivity::OnInputQueueDestroyed(AInputQueue* queue) {}
+
+void NativeActivity::OnConfigurationChanged() {}
+
+void NativeActivity::OnLowMemory() {}
+
+void NativeActivity::Terminate() {
+ ANativeActivity_finish(activity_);
+}
+
+} // namespace flutter
+
+extern "C" __attribute__((visibility("default"))) void ANativeActivity_onCreate(
+ ANativeActivity* activity,
+ void* saved_state,
+ size_t saved_state_size) {
+ std::unique_ptr saved_state_mapping;
+ if (saved_state_size > 0u) {
+ saved_state_mapping = std::make_unique(
+ fml::MallocMapping::Copy(saved_state, saved_state_size));
+ }
+ flutter::NativeActivityMain(activity, std::move(saved_state_mapping))
+ .release(); // Will be freed when the frame calls the onDestroy. See the
+ // delete in that callback.
+}
diff --git a/testing/android/native_activity/native_activity.gni b/testing/android/native_activity/native_activity.gni
new file mode 100644
index 0000000000000..cbf4d3e75141d
--- /dev/null
+++ b/testing/android/native_activity/native_activity.gni
@@ -0,0 +1,92 @@
+# 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.
+
+import("//build/config/android/config.gni")
+import("//flutter/tools/templater/templater.gni")
+
+android_buildtools = "//third_party/android_tools/sdk/build-tools/34.0.0"
+aapt2 = "$android_buildtools/aapt2"
+zipalign = "$android_buildtools/zipalign"
+apksigner = "$android_buildtools/apksigner"
+android_jar = "//third_party/android_tools/sdk/platforms/android-34/android.jar"
+src_root = "//flutter/testing/android/native_activity"
+
+# A drop in replacement for an executable or shared library target. Providing a
+# (required) apk_name packages that native code into an APK suitable for
+# debugging.
+template("native_activity_apk") {
+ assert(defined(invoker.apk_name), "The name of the APK must be specified.")
+
+ invoker_apk_name = invoker.apk_name
+ apk_dylib_name = "lib$invoker_apk_name.so"
+
+ android_manifest_template = "$src_root/AndroidManifest.xml.template"
+ android_manifest = "$target_gen_dir/AndroidManifest.xml"
+
+ android_manifest_target_name = "android_manifest_$target_name"
+ templater(android_manifest_target_name) {
+ input = android_manifest_template
+ output = android_manifest
+ values = [ "--apk-library-name=$invoker_apk_name" ]
+ }
+
+ shared_library_target_name = "shared_library_$target_name"
+ shared_library(shared_library_target_name) {
+ forward_variables_from(invoker, "*", [ "output_name" ])
+ output_name = invoker_apk_name
+ }
+
+ apk_target_name = "apk_$target_name"
+ action(apk_target_name) {
+ forward_variables_from(invoker, [ "testonly" ])
+
+ script = "$src_root/native_activity_apk.py"
+
+ apk_path = "$root_build_dir/$invoker_apk_name.apk"
+
+ sources = [
+ "$root_build_dir/$apk_dylib_name",
+ aapt2,
+ android_jar,
+ android_manifest_template,
+ apksigner,
+ zipalign,
+ ]
+
+ outputs = [ apk_path ]
+
+ args = [
+ "--aapt2-bin",
+ rebase_path(aapt2, root_build_dir),
+ "--zipalign-bin",
+ rebase_path(zipalign, root_build_dir),
+ "--android-manifest",
+ rebase_path(android_manifest, root_build_dir),
+ "--android-jar",
+ rebase_path(android_jar, root_build_dir),
+ "--output-path",
+ rebase_path(apk_path, root_build_dir),
+ "--library",
+ rebase_path("$root_build_dir/$apk_dylib_name", root_build_dir),
+ "--apksigner-bin",
+ rebase_path(apksigner, root_build_dir),
+ "--keystore",
+ rebase_path("$src_root/debug.keystore", root_build_dir),
+ "--gen-dir",
+ rebase_path(target_gen_dir, root_build_dir),
+ "--android-abi",
+ android_app_abi,
+ ]
+ deps = [
+ ":$android_manifest_target_name",
+ ":$shared_library_target_name",
+ ]
+ }
+
+ group(target_name) {
+ forward_variables_from(invoker, [ "testonly" ])
+
+ deps = [ ":$apk_target_name" ]
+ }
+}
diff --git a/testing/android/native_activity/native_activity.h b/testing/android/native_activity/native_activity.h
new file mode 100644
index 0000000000000..d740d87486942
--- /dev/null
+++ b/testing/android/native_activity/native_activity.h
@@ -0,0 +1,81 @@
+// 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_TESTING_ANDROID_NATIVE_ACTIVITY_NATIVE_ACTIVITY_H_
+#define FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_NATIVE_ACTIVITY_H_
+
+#include
+
+#include "flutter/fml/macros.h"
+#include "flutter/fml/mapping.h"
+
+namespace flutter {
+
+//------------------------------------------------------------------------------
+/// @brief An instance of a native activity. Users of the
+/// `native_activity_apk` are meant to subclass this and return an
+/// instance of this subclass from `flutter::NativeActivityMain`.
+///
+/// All methods are called on the Android Platform main-thread.
+/// Subclasses will usually re-thread calls to a background thread
+/// for long running tasks as these will lead to ANRs on when
+/// invoked on the platform thread.
+///
+class NativeActivity {
+ public:
+ virtual ~NativeActivity();
+
+ //----------------------------------------------------------------------------
+ /// @brief Perform graceful termination of the activity. Will eventually
+ /// lead to the other activity lifecycle callback on the way to
+ /// termination.
+ ///
+ /// Can be called from any thread.
+ ///
+ void Terminate();
+
+ virtual void OnStart();
+
+ virtual void OnStop();
+
+ virtual void OnPause();
+
+ virtual void OnResume();
+
+ virtual std::shared_ptr OnSaveInstanceState();
+
+ virtual void OnWindowFocusChanged(bool has_focus);
+
+ virtual void OnNativeWindowCreated(ANativeWindow* window);
+
+ virtual void OnNativeWindowResized(ANativeWindow* window);
+
+ virtual void OnNativeWindowRedrawNeeded(ANativeWindow* window);
+
+ virtual void OnNativeWindowDestroyed(ANativeWindow* window);
+
+ virtual void OnInputQueueCreated(AInputQueue* queue);
+
+ virtual void OnInputQueueDestroyed(AInputQueue* queue);
+
+ virtual void OnConfigurationChanged();
+
+ virtual void OnLowMemory();
+
+ protected:
+ explicit NativeActivity(ANativeActivity* activity);
+
+ private:
+ ANativeActivity* activity_ = nullptr;
+
+ FML_DISALLOW_COPY_AND_ASSIGN(NativeActivity);
+};
+
+std::unique_ptr NativeActivityMain(
+ ANativeActivity* activity,
+ std::unique_ptr saved_state);
+
+} // namespace flutter
+
+#endif // FLUTTER_TESTING_ANDROID_NATIVE_ACTIVITY_NATIVE_ACTIVITY_H_
diff --git a/testing/android/native_activity/native_activity_apk.py b/testing/android/native_activity/native_activity_apk.py
new file mode 100644
index 0000000000000..23813ab969048
--- /dev/null
+++ b/testing/android/native_activity/native_activity_apk.py
@@ -0,0 +1,97 @@
+# 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.
+
+import sys
+
+import argparse
+import os
+import zipfile
+import subprocess
+
+
+def run_command_checked(command):
+ try:
+ subprocess.check_output(command, stderr=subprocess.STDOUT, text=True)
+ except subprocess.CalledProcessError as cpe:
+ print(cpe.output)
+ raise cpe
+
+
+def main():
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('--aapt2-bin', type=str, required=True, help='The path to the aapt2 binary.')
+ parser.add_argument(
+ '--zipalign-bin', type=str, required=True, help='The path to the zipalign binary.'
+ )
+ parser.add_argument(
+ '--apksigner-bin', type=str, required=True, help='The path to the apksigner binary.'
+ )
+ parser.add_argument(
+ '--android-manifest', type=str, required=True, help='The path to the AndroidManifest.xml.'
+ )
+ parser.add_argument('--android-jar', type=str, required=True, help='The path to android.jar.')
+ parser.add_argument('--output-path', type=str, required=True, help='The path to the output apk.')
+ parser.add_argument(
+ '--library', type=str, required=True, help='The path to the library to put in the apk.'
+ )
+ parser.add_argument(
+ '--keystore', type=str, required=True, help='The path to the debug keystore to sign the apk.'
+ )
+ parser.add_argument(
+ '--gen-dir', type=str, required=True, help='The directory for generated files.'
+ )
+ parser.add_argument(
+ '--android-abi', type=str, required=True, help='The android ABI of the library.'
+ )
+
+ args = parser.parse_args()
+
+ library_file = os.path.basename(args.library)
+ apk_name = os.path.basename(args.output_path)
+
+ unaligned_apk_path = os.path.join(args.gen_dir, '%s.unaligned' % apk_name)
+ unsigned_apk_path = os.path.join(args.gen_dir, '%s.unsigned' % apk_name)
+ apk_path = args.output_path
+
+ # Create the skeleton of the APK using aapt2.
+ aapt2_command = [
+ args.aapt2_bin,
+ 'link',
+ '-I',
+ args.android_jar,
+ '--manifest',
+ args.android_manifest,
+ '-o',
+ unaligned_apk_path,
+ ]
+ run_command_checked(aapt2_command)
+
+ # Stuff the library in the APK which is just a regular ZIP file. Libraries are not compressed.
+ with zipfile.ZipFile(unaligned_apk_path, 'a', compression=zipfile.ZIP_STORED) as zipf:
+ zipf.write(args.library, 'lib/%s/%s' % (args.android_abi, library_file))
+
+ # Align the dylib to a page boundary.
+ zipalign_command = [
+ args.zipalign_bin,
+ '-p', # Page align the dylib
+ '-f', # overwrite output if exists
+ '4', # 32-bit alignment
+ unaligned_apk_path,
+ unsigned_apk_path,
+ ]
+ run_command_checked(zipalign_command)
+
+ # Sign the APK.
+ apksigner_command = [
+ args.apksigner_bin, 'sign', '--ks', args.keystore, '--ks-pass', 'pass:android', '--out',
+ apk_path, unsigned_apk_path
+ ]
+ run_command_checked(apksigner_command)
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/testing/logger_listener.cc b/testing/logger_listener.cc
new file mode 100644
index 0000000000000..a4ce8408636d4
--- /dev/null
+++ b/testing/logger_listener.cc
@@ -0,0 +1,58 @@
+// 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/testing/logger_listener.h"
+
+namespace flutter::testing {
+
+LoggerListener::LoggerListener() = default;
+
+LoggerListener::~LoggerListener() = default;
+
+void testing::LoggerListener::OnTestStart(
+ const ::testing::TestInfo& test_info) {
+ FML_LOG(IMPORTANT) << ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>";
+ FML_LOG(IMPORTANT) << "Starting Test: " << test_info.test_suite_name() << ":"
+ << test_info.name();
+}
+
+std::string TestStatusAsString(const ::testing::TestResult* result) {
+ if (result == nullptr) {
+ return "UNKNOWN";
+ }
+ if (result->Passed()) {
+ return "PASSED";
+ }
+ if (result->Skipped()) {
+ return "SKIPPED";
+ }
+ if (result->Failed()) {
+ return "FAILED";
+ }
+ return "UNKNOWN";
+}
+
+std::string TestLabel(const ::testing::TestInfo& info) {
+ return std::string{info.test_suite_name()} + "." + info.name();
+}
+
+std::string TestTimeAsString(const ::testing::TestResult* result) {
+ if (result == nullptr) {
+ return "UNKNOWN";
+ }
+ return std::to_string(result->elapsed_time()) + " ms";
+}
+
+void testing::LoggerListener::OnTestEnd(const ::testing::TestInfo& info) {
+ FML_LOG(IMPORTANT) << "Test " << TestStatusAsString(info.result()) << " ("
+ << TestTimeAsString(info.result())
+ << "): " << TestLabel(info);
+ FML_LOG(IMPORTANT) << "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<";
+}
+
+void testing::LoggerListener::OnTestDisabled(const ::testing::TestInfo& info) {
+ FML_LOG(IMPORTANT) << "Test Disabled: " << TestLabel(info);
+}
+
+} // namespace flutter::testing
diff --git a/testing/logger_listener.h b/testing/logger_listener.h
new file mode 100644
index 0000000000000..92a4fafd1f504
--- /dev/null
+++ b/testing/logger_listener.h
@@ -0,0 +1,35 @@
+// 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_TESTING_LOGGER_LISTENER_H_
+#define FLUTTER_TESTING_LOGGER_LISTENER_H_
+
+#include "flutter/fml/logging.h"
+#include "flutter/testing/testing.h"
+
+namespace flutter::testing {
+
+class LoggerListener : public ::testing::EmptyTestEventListener {
+ public:
+ LoggerListener();
+
+ ~LoggerListener();
+
+ LoggerListener(const LoggerListener&) = delete;
+
+ LoggerListener& operator=(const LoggerListener&) = delete;
+
+ // |testing::EmptyTestEventListener|
+ void OnTestStart(const ::testing::TestInfo& test_info) override;
+
+ // |testing::EmptyTestEventListener|
+ void OnTestEnd(const ::testing::TestInfo& test_info) override;
+
+ // |testing::EmptyTestEventListener|
+ void OnTestDisabled(const ::testing::TestInfo& test_info) override;
+};
+
+} // namespace flutter::testing
+
+#endif // FLUTTER_TESTING_LOGGER_LISTENER_H_
diff --git a/tools/templater/BUILD.gn b/tools/templater/BUILD.gn
new file mode 100644
index 0000000000000..78bab468318f2
--- /dev/null
+++ b/tools/templater/BUILD.gn
@@ -0,0 +1,11 @@
+# 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.
+
+executable("templater") {
+ sources = [ "templater_main.cc" ]
+ deps = [
+ "//flutter/fml",
+ "//third_party/inja",
+ ]
+}
diff --git a/tools/templater/templater.gni b/tools/templater/templater.gni
new file mode 100644
index 0000000000000..fc4b47cb97df8
--- /dev/null
+++ b/tools/templater/templater.gni
@@ -0,0 +1,31 @@
+# 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.
+
+import("//build/compiled_action.gni")
+
+# Inflate the input template file using Inja and the specified values.
+template("templater") {
+ assert(defined(invoker.input), "The input template must be specified.")
+ assert(defined(invoker.output), "The output location must be defined.")
+ assert(
+ defined(invoker.values),
+ "The values referenced in the template must be specified. Use the --key=value format for each value.")
+
+ compiled_action(target_name) {
+ tool = "//flutter/tools/templater"
+
+ inputs = [ invoker.input ]
+ outputs = [ invoker.output ]
+
+ templater_input_path = rebase_path(invoker.input, root_build_dir)
+ templater_input_flag = "--templater-input=$templater_input_path"
+ templater_output_path = rebase_path(invoker.output, root_build_dir)
+ templater_output_flag = "--templater-output=$templater_output_path"
+
+ args = [
+ templater_input_flag,
+ templater_output_flag,
+ ] + invoker.values
+ }
+}
diff --git a/tools/templater/templater_main.cc b/tools/templater/templater_main.cc
new file mode 100644
index 0000000000000..4cd0f230d8857
--- /dev/null
+++ b/tools/templater/templater_main.cc
@@ -0,0 +1,73 @@
+// 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.
+
+// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/105732
+
+#include
+#include
+#include
+
+#include "flutter/fml/command_line.h"
+#include "flutter/fml/file.h"
+#include "flutter/fml/logging.h"
+#include "flutter/fml/mapping.h"
+#include "inja/inja.hpp"
+
+namespace flutter {
+
+bool TemplaterMain(const fml::CommandLine& command_line) {
+ std::string input_path;
+ std::string output_path;
+
+ if (!command_line.GetOptionValue("templater-input", &input_path)) {
+ FML_LOG(ERROR)
+ << "Input template path not specified. Use --templater-input.";
+ return false;
+ }
+ if (!command_line.GetOptionValue("templater-output", &output_path)) {
+ FML_LOG(ERROR)
+ << "Input template path not specified. Use --templater-output.";
+ return false;
+ }
+
+ auto input = fml::FileMapping::CreateReadOnly(input_path);
+ if (!input) {
+ FML_LOG(ERROR) << "Could not open input file: " << input_path;
+ return false;
+ }
+
+ nlohmann::json arguments;
+ for (const auto& option : command_line.options()) {
+ arguments[option.name] = option.value;
+ }
+ inja::Environment env;
+ auto rendered_template = env.render(
+ std::string_view{reinterpret_cast(input->GetMapping()),
+ input->GetSize()},
+ arguments);
+ auto output = fml::NonOwnedMapping{
+ reinterpret_cast(rendered_template.data()),
+ rendered_template.size()};
+
+ auto current_dir =
+ fml::OpenDirectory(std::filesystem::current_path().u8string().c_str(),
+ false, fml::FilePermission::kReadWrite);
+ if (!current_dir.is_valid()) {
+ FML_LOG(ERROR) << "Could not open current directory.";
+ return false;
+ }
+ if (!fml::WriteAtomically(current_dir, output_path.c_str(), output)) {
+ FML_LOG(ERROR) << "Could not write output to path: " << output_path;
+ return false;
+ }
+ return true;
+}
+
+} // namespace flutter
+
+int main(int argc, char const* argv[]) {
+ return flutter::TemplaterMain(fml::CommandLineFromArgcArgv(argc, argv))
+ ? EXIT_SUCCESS
+ : EXIT_FAILURE;
+}