From 85ad0174ceb09d5f9b4e8718653de278e5f3108e Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Thu, 14 Mar 2024 19:38:28 -0700 Subject: [PATCH 1/7] A native Android unit-testing harness. Sets up rules to create an APK that is comprised of solely native code. Existing executable targets (like GTests) can then use this to run on Android devices while having access to activities, windows, etc.. This allows for broader test coverage. Basically, anything that needed an ANativeWindow could only be tested in an integration test. Executables that need access to the native activity must provide an implementation of `NativeActivityMain` that returns a custom subclass of `flutter::NativeActivity`. The `native_activity_apk` reads like an `executable` or `shared_library` target. Just one that packages that executable in an APK. The APK is built using the Android Tools and does not use Gradle. Creating a new APK after invalidating some code takes ~200ms on my machine. The edit, compile, run cycle for only a tiny bit worse than testing on the host. Builds on top of this new infrastructure to create a `GTestActivity` that runs an existing test suites. This works really well except the GTest suite logs to `STDOUT` whereas the engine logs to `logcat`. To quickly work around this, a custom test status listener has been wired up. This only displays the test results to logcat today but a similar mechanism can be used to talk to the test runner in the host. I will wire this up in an upcoming patch as there is no hooks into this from CI right now. Creates an APK variant of the `impeller_toolkit_android_unittests` harness. --- BUILD.gn | 1 + impeller/toolkit/android/BUILD.gn | 28 +++- testing/BUILD.gn | 2 + .../AndroidManifest.xml.template | 20 +++ testing/android/native_activity/BUILD.gn | 41 +++++ testing/android/native_activity/README.md | 5 + .../android/native_activity/debug.keystore | Bin 0 -> 2618 bytes .../android/native_activity/gtest_activity.cc | 49 ++++++ .../android/native_activity/gtest_activity.h | 33 ++++ .../native_activity/native_activity.cc | 142 ++++++++++++++++++ .../native_activity/native_activity.gni | 92 ++++++++++++ .../android/native_activity/native_activity.h | 64 ++++++++ .../native_activity/native_activity_apk.py | 88 +++++++++++ testing/logger_listener.cc | 58 +++++++ testing/logger_listener.h | 35 +++++ tools/templater/BUILD.gn | 11 ++ tools/templater/main.cc | 71 +++++++++ tools/templater/templater.gni | 31 ++++ 18 files changed, 767 insertions(+), 4 deletions(-) create mode 100644 testing/android/native_activity/AndroidManifest.xml.template create mode 100644 testing/android/native_activity/BUILD.gn create mode 100644 testing/android/native_activity/README.md create mode 100644 testing/android/native_activity/debug.keystore create mode 100644 testing/android/native_activity/gtest_activity.cc create mode 100644 testing/android/native_activity/gtest_activity.h create mode 100644 testing/android/native_activity/native_activity.cc create mode 100644 testing/android/native_activity/native_activity.gni create mode 100644 testing/android/native_activity/native_activity.h create mode 100644 testing/android/native_activity/native_activity_apk.py create mode 100644 testing/logger_listener.cc create mode 100644 testing/logger_listener.h create mode 100644 tools/templater/BUILD.gn create mode 100644 tools/templater/main.cc create mode 100644 tools/templater/templater.gni 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..1d531aa230518 --- /dev/null +++ b/testing/android/native_activity/README.md @@ -0,0 +1,5 @@ +Native Activity +=============== + +Executables packaged as native activities in an Android APK. These activities +contain no Java code. diff --git a/testing/android/native_activity/debug.keystore b/testing/android/native_activity/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..df0714f9b96d27c4abac51776c167e1919d81e71 GIT binary patch literal 2618 zcma)8XH=655=}x92t|0*2Vy9p29+=LV(17+2_Qv!uhKgTf}9>36BDJSl@q9uqG%TR(AOWJ|lwe399;R>!-U1wdO~7aX6F};BOAQD? z8FSU*$|o^XdWemNz~tGT&=XxT3kZat4g%%`sNod<-Uva!K)^LP!a7PF>_7$uqoH*5 zt@kS5GFM*g*oVK#7ev=U@z6KI^+1#0_m}SM*?7%^r-TI_rH{(7^lj3aa{ZM>(f-@b zf2(2C{95ERgc)gD1c(#ydl-g+MUw=Fd=FZS1UV86bn!u@VwTa{>G+fJHt$b zUZ>i&Av@*UuIwcHP+wR3!WL#+aNyDKkq-NQo~;?xk}vj%OXU1-99B$?wW_F{d;1kW z?)$uC(6Uhr>la+RJ62#^HYS!8W+GB1H~jcnkhcV3T2K%h{3D8e(G0GbSVtWtmolU^ zW~ezLlsQALbzD-NLQbspP4g@s~RmBtK{j-eX!O?El2Qs znYr1DYl+fPUPx07a?@cRgfVetWu;cj2&GE!@*oU7nJ5Ulhy}gDvd>t{dHd8e?dRY&{MjT;C*(6iTIi`MC6Tqy}f6 zdjN=5Ijr4weWR#bzu9v{&5Ov(%J}f@S)OnJmD3RQm$Shf$!V$0!ys%#x7@1%0pYuV zX6zc8Df;=;c6c#cTZf)_S6of{U z+$op?%F{GdF7{l>D^@KMZH#*uLa{@Ze3B)+J^2$vs;d`I8`dP3ax2IHL}AT#STT zp(QthK`S|)=z}(WU*}I|x!?DM>mqlFWjz4F@ruKEJUZxJBDbNInQdu`UVqlWFKY5(&SHlkpg8Q@lZbGqj;>bReJN$t7 zXlBP5B{a{HJ0<$U*+@A^J$-|9hRTA^=2Nb7jW?0apa%u9^ zLEe_u(>VdAwKZncHRO_Cz<%Fe@1X4ladP#V~z))I;HNUxwY_>mS#AMhEcnOVV$F4UJ@Q zYh?Bv)=`2xJrB`J;1TvJ=MWtiQ7C0AdnSB7hViDFNW2;9nwgcp;wbD}X2a3;}~K)8=mm{J)HQaG;Y7($l=~!#A%_ z99}g}v~q*~KgO*XzXEmfe<&x*_su*2>EsxWC-eF_UiuHCh=9~n`g#_ptUo{NMQ|J| zArpK;ilA2`8GOvJW+_WnYwpqhfpc>L9LnxtB2P(I&TXoC!|PVpx(#=3uJilZZMRI_ zv%$gs-mdO0DK23hp5+-aT`#2Xan;tL@>A{^YJytrN|E2uc=SmY%n@`F+_y82)Qiut zY!shIQ6;}MSbiaq8J$mmcGpd140(=Qj_J%>d8tafPp^A4 z^e#VVNve1&E52lA>p-XoUE(x9ixj{7{lwKQ`FpP^PEKU{<7iCUf^;sF343V%gwHZ| zo|Y%T=u2u+6T@)=Lk^g%E~&^nE1`JS*m)ETd+(CkWPqKmYb)KCo~YG8^^e%sKAb%S zok!702WJ^hm4;unF+VBI8jVgszE~63okIC3uq}K8~1NtafLk^Q*ph ze>P*nD{0Gz>fXJR3#2fMg-0X(y4wef$>M#Co)h}jEXt%pZ_d}VZlPy3;WxAFHidBM zZo~{@*EVuV6Pmv|&o-V2Gt508hDaOvpr(uY1dH5?a@u3u-qbemdVPuol`8A^#Aa}K zoQ8yHWq+Q~*(aCVt6OPlN%FWF`%|G;Lw%9RC7B27){bOBd0T~5ku-Zh#h7Q9b<~o4|Shrj@(GQo3L|~&v1oTOY~s}i4+-0HM50l zY4CPSRmtM6G=n%7!!FMBUxZS3KVR+pIWUtPt#eTD*lDw$r|${IFc+I;y2R#?G>7Eq z*|g$srKRyXxU*4k%{*<&>5NJ@pS@iX6LnS8qMwKCP?|DCZt$6+J`1v1E_DpSay=$R z>)JJ!z%IHDp4<-ZvU0}y;)K<+GMVZeEn-L_PkG#yUj6U~E}u3I$Q3Msdr8h-Vd7nL znoi=O{ZHy+j*Ii;TOAAa%j}=`n}yP~(>8Xln>+2ncTy#bKd5^*)1%I~LWk;Sm`xp~ zW)Hg8cZvkxp1JL~XryyV$2+Fz5lSI)dv{GtQ zp_zvCcF1JO!{Q~nlyz}FPeBK#9tTiHN+Z68rd;<>KbMnpj&5BOkMn%>f1Uh40dan- A6aWAK literal 0 HcmV?d00001 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..5435383087cb6 --- /dev/null +++ b/testing/android/native_activity/gtest_activity.h @@ -0,0 +1,33 @@ +// 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 { + +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..456204173d0f8 --- /dev/null +++ b/testing/android/native_activity/native_activity.cc @@ -0,0 +1,142 @@ +// 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; + } + // The framework is going to call free for us. That's why we copied to a + // malloc mapping. + auto malloc_mapping = + std::make_unique( + fml::MallocMapping::Copy(mapping->GetMapping(), mapping->GetSize())) + .release(); + *out_size = malloc_mapping->GetSize(); + return const_cast(malloc_mapping->GetMapping()); + }; + 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(); +} 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..693ac819dba22 --- /dev/null +++ b/testing/android/native_activity/native_activity.h @@ -0,0 +1,64 @@ +// 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 { + +class NativeActivity { + public: + virtual ~NativeActivity(); + + 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..f0d7bbef9937d --- /dev/null +++ b/testing/android/native_activity/native_activity_apk.py @@ -0,0 +1,88 @@ +# 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 errno +import os +import zipfile +import subprocess + +def RunCommandChecked(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) + + unaligned_apk_path = os.path.join(args.gen_dir, '%s.unaligned' % library_file) + unsigned_apk_path = os.path.join(args.gen_dir, '%s.unsigned' % library_file) + 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, + ] + RunCommandChecked(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, + ] + RunCommandChecked(zipalign_command) + + # Sign the APK. + apksigner_command = [ + args.apksigner_bin, + 'sign', + '--ks', + args.keystore, + '--ks-pass', + 'pass:android', + '--out', + apk_path, + unsigned_apk_path + ] + RunCommandChecked(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..5a4263bbf5152 --- /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_LISTERNER_H_ +#define FLUTTER_TESTING_LOGGER_LISTERNER_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_LISTERNER_H_ diff --git a/tools/templater/BUILD.gn b/tools/templater/BUILD.gn new file mode 100644 index 0000000000000..e2cdd3291b505 --- /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 = [ "main.cc" ] + deps = [ + "//flutter/fml", + "//third_party/inja", + ] +} diff --git a/tools/templater/main.cc b/tools/templater/main.cc new file mode 100644 index 0000000000000..640b1d16cf6c7 --- /dev/null +++ b/tools/templater/main.cc @@ -0,0 +1,71 @@ +// 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 +#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; +} 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 + } +} From a0a5597dd07adb022187cde99c702577f465222e Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Sat, 16 Mar 2024 23:26:55 -0700 Subject: [PATCH 2/7] Format. --- .../native_activity/native_activity_apk.py | 55 +++++++++++-------- testing/logger_listener.h | 6 +- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/testing/android/native_activity/native_activity_apk.py b/testing/android/native_activity/native_activity_apk.py index f0d7bbef9937d..60f51e46c48d9 100644 --- a/testing/android/native_activity/native_activity_apk.py +++ b/testing/android/native_activity/native_activity_apk.py @@ -10,6 +10,7 @@ import zipfile import subprocess + def RunCommandChecked(command): try: subprocess.check_output(command, stderr=subprocess.STDOUT, text=True) @@ -17,19 +18,34 @@ def RunCommandChecked(command): 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( + '--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.') + 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() @@ -52,37 +68,30 @@ def main(): ] RunCommandChecked(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, + args.zipalign_bin, + '-p', # Page align the dylib + '-f', # overwrite output if exists + '4', # 32-bit alignment + unaligned_apk_path, + unsigned_apk_path, ] RunCommandChecked(zipalign_command) # Sign the APK. apksigner_command = [ - args.apksigner_bin, - 'sign', - '--ks', - args.keystore, - '--ks-pass', - 'pass:android', - '--out', - apk_path, - unsigned_apk_path + args.apksigner_bin, 'sign', '--ks', args.keystore, '--ks-pass', 'pass:android', '--out', + apk_path, unsigned_apk_path ] RunCommandChecked(apksigner_command) return 0 + if __name__ == '__main__': sys.exit(main()) diff --git a/testing/logger_listener.h b/testing/logger_listener.h index 5a4263bbf5152..92a4fafd1f504 100644 --- a/testing/logger_listener.h +++ b/testing/logger_listener.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef FLUTTER_TESTING_LOGGER_LISTERNER_H_ -#define FLUTTER_TESTING_LOGGER_LISTERNER_H_ +#ifndef FLUTTER_TESTING_LOGGER_LISTENER_H_ +#define FLUTTER_TESTING_LOGGER_LISTENER_H_ #include "flutter/fml/logging.h" #include "flutter/testing/testing.h" @@ -32,4 +32,4 @@ class LoggerListener : public ::testing::EmptyTestEventListener { } // namespace flutter::testing -#endif // FLUTTER_TESTING_LOGGER_LISTERNER_H_ +#endif // FLUTTER_TESTING_LOGGER_LISTENER_H_ From 74440978ccc9bbf4fa941a5a803ebdec39b86df9 Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Sun, 17 Mar 2024 13:41:47 -0700 Subject: [PATCH 3/7] Lints. --- .../android/native_activity/native_activity_apk.py | 11 +++++------ tools/templater/main.cc | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/testing/android/native_activity/native_activity_apk.py b/testing/android/native_activity/native_activity_apk.py index 60f51e46c48d9..f91d84841b6fc 100644 --- a/testing/android/native_activity/native_activity_apk.py +++ b/testing/android/native_activity/native_activity_apk.py @@ -5,13 +5,12 @@ import sys import argparse -import errno import os import zipfile import subprocess -def RunCommandChecked(command): +def run_command_checked(command): try: subprocess.check_output(command, stderr=subprocess.STDOUT, text=True) except subprocess.CalledProcessError as cpe: @@ -66,10 +65,10 @@ def main(): '-o', unaligned_apk_path, ] - RunCommandChecked(aapt2_command) + 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: + 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. @@ -81,14 +80,14 @@ def main(): unaligned_apk_path, unsigned_apk_path, ] - RunCommandChecked(zipalign_command) + 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 ] - RunCommandChecked(apksigner_command) + run_command_checked(apksigner_command) return 0 diff --git a/tools/templater/main.cc b/tools/templater/main.cc index 640b1d16cf6c7..4cd0f230d8857 100644 --- a/tools/templater/main.cc +++ b/tools/templater/main.cc @@ -2,6 +2,8 @@ // 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 From 40b391be072934b4b45c1c73cd9bb4e2527bf341 Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Sun, 17 Mar 2024 20:40:26 -0700 Subject: [PATCH 4/7] Fix intermediate names. --- testing/android/native_activity/native_activity_apk.py | 5 +++-- tools/templater/BUILD.gn | 2 +- tools/templater/{main.cc => templater_main.cc} | 0 3 files changed, 4 insertions(+), 3 deletions(-) rename tools/templater/{main.cc => templater_main.cc} (100%) diff --git a/testing/android/native_activity/native_activity_apk.py b/testing/android/native_activity/native_activity_apk.py index f91d84841b6fc..23813ab969048 100644 --- a/testing/android/native_activity/native_activity_apk.py +++ b/testing/android/native_activity/native_activity_apk.py @@ -49,9 +49,10 @@ def main(): 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' % library_file) - unsigned_apk_path = os.path.join(args.gen_dir, '%s.unsigned' % library_file) + 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. diff --git a/tools/templater/BUILD.gn b/tools/templater/BUILD.gn index e2cdd3291b505..78bab468318f2 100644 --- a/tools/templater/BUILD.gn +++ b/tools/templater/BUILD.gn @@ -3,7 +3,7 @@ # found in the LICENSE file. executable("templater") { - sources = [ "main.cc" ] + sources = [ "templater_main.cc" ] deps = [ "//flutter/fml", "//third_party/inja", diff --git a/tools/templater/main.cc b/tools/templater/templater_main.cc similarity index 100% rename from tools/templater/main.cc rename to tools/templater/templater_main.cc From 88d10b630bcbfa5821422bc595961b304f3c5af2 Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Mon, 18 Mar 2024 11:52:57 -0700 Subject: [PATCH 5/7] Docs. --- testing/android/native_activity/README.md | 26 +++++++++++++++++++ .../android/native_activity/gtest_activity.h | 7 +++++ .../android/native_activity/native_activity.h | 17 ++++++++++++ 3 files changed, 50 insertions(+) diff --git a/testing/android/native_activity/README.md b/testing/android/native_activity/README.md index 1d531aa230518..5ce198a06691d 100644 --- a/testing/android/native_activity/README.md +++ b/testing/android/native_activity/README.md @@ -3,3 +3,29 @@ 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/gtest_activity.h b/testing/android/native_activity/gtest_activity.h index 5435383087cb6..44a0a5eb74557 100644 --- a/testing/android/native_activity/gtest_activity.h +++ b/testing/android/native_activity/gtest_activity.h @@ -11,6 +11,13 @@ 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); diff --git a/testing/android/native_activity/native_activity.h b/testing/android/native_activity/native_activity.h index 693ac819dba22..d740d87486942 100644 --- a/testing/android/native_activity/native_activity.h +++ b/testing/android/native_activity/native_activity.h @@ -12,10 +12,27 @@ 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(); From dd85af2512878427a3842e1ba59f5fafe6cef508 Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Mon, 18 Mar 2024 14:47:14 -0700 Subject: [PATCH 6/7] Jasons comments. --- .../android/native_activity/native_activity.cc | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/testing/android/native_activity/native_activity.cc b/testing/android/native_activity/native_activity.cc index 456204173d0f8..3bf914a19513a 100644 --- a/testing/android/native_activity/native_activity.cc +++ b/testing/android/native_activity/native_activity.cc @@ -37,14 +37,13 @@ NativeActivity::NativeActivity(ANativeActivity* activity) *out_size = 0; return nullptr; } - // The framework is going to call free for us. That's why we copied to a - // malloc mapping. - auto malloc_mapping = - std::make_unique( - fml::MallocMapping::Copy(mapping->GetMapping(), mapping->GetSize())) - .release(); - *out_size = malloc_mapping->GetSize(); - return const_cast(malloc_mapping->GetMapping()); + + // 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()); + return copied; }; activity->callbacks->onWindowFocusChanged = [](ANativeActivity* activity, int has_focus) { @@ -138,5 +137,6 @@ extern "C" __attribute__((visibility("default"))) void ANativeActivity_onCreate( fml::MallocMapping::Copy(saved_state, saved_state_size)); } flutter::NativeActivityMain(activity, std::move(saved_state_mapping)) - .release(); + .release(); // Will be freed when the frame calls the onDestroy. See the + // delete in that callback. } From 41aa0ff8ae5afc1717d01aa422abd61af823368a Mon Sep 17 00:00:00 2001 From: Chinmay Garde Date: Mon, 18 Mar 2024 14:53:35 -0700 Subject: [PATCH 7/7] Derp. --- testing/android/native_activity/native_activity.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/android/native_activity/native_activity.cc b/testing/android/native_activity/native_activity.cc index 3bf914a19513a..37f7a1d7652e8 100644 --- a/testing/android/native_activity/native_activity.cc +++ b/testing/android/native_activity/native_activity.cc @@ -43,6 +43,7 @@ NativeActivity::NativeActivity(ANativeActivity* activity) 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,