tests: Add tests for ScreenRecoveryUI.

In order to support that, this CL adds Paths::set_resource_dir() to
override the default resource dir ("/res/images/") that's only available
under recovery. Note that since there're external modules depending on
libminui, it adds a separate function of res_set_resource_dir(), instead
of requiring the dependency on libotautil for everyone.

Test: mmma -j bootable/recovery
Test: Run recovery_unit_test on marlin.
Change-Id: I0a7dcf4476808bea9e634eaffc9676f6cbaf92b7
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
index e47d705..ff8a35d 100644
--- a/tests/unit/screen_ui_test.cpp
+++ b/tests/unit/screen_ui_test.cpp
@@ -16,11 +16,19 @@
 
 #include <stddef.h>
 
+#include <functional>
+#include <map>
+#include <memory>
 #include <string>
 #include <vector>
 
+#include <android-base/logging.h>
 #include <gtest/gtest.h>
 
+#include "common/test_constants.h"
+#include "device.h"
+#include "otautil/paths.h"
+#include "private/resources.h"
 #include "screen_ui.h"
 
 static const std::vector<std::string> HEADERS{ "header" };
@@ -185,3 +193,162 @@
   ASSERT_EQ(0u, menu.MenuStart());
   ASSERT_EQ(3u, menu.MenuEnd());
 }
+
+static constexpr int kMagicAction = 101;
+
+enum class KeyCode : int {
+  TIMEOUT = -1,
+  NO_OP = 0,
+  UP = 1,
+  DOWN = 2,
+  ENTER = 3,
+  MAGIC = 1001,
+  LAST,
+};
+
+static const std::map<KeyCode, int> kKeyMapping{
+  // clang-format off
+  { KeyCode::NO_OP, Device::kNoAction },
+  { KeyCode::UP, Device::kHighlightUp },
+  { KeyCode::DOWN, Device::kHighlightDown },
+  { KeyCode::ENTER, Device::kInvokeItem },
+  { KeyCode::MAGIC, kMagicAction },
+  // clang-format on
+};
+
+class TestableScreenRecoveryUI : public ScreenRecoveryUI {
+ public:
+  int WaitKey() override;
+
+  void SetKeyBuffer(const std::vector<KeyCode>& buffer);
+
+  int KeyHandler(int key, bool visible) const;
+
+  bool GetRtlLocale() const {
+    return rtl_locale_;
+  }
+
+ private:
+  std::vector<KeyCode> key_buffer_;
+  size_t key_buffer_index_;
+};
+
+void TestableScreenRecoveryUI::SetKeyBuffer(const std::vector<KeyCode>& buffer) {
+  key_buffer_ = buffer;
+  key_buffer_index_ = 0;
+}
+
+int TestableScreenRecoveryUI::KeyHandler(int key, bool) const {
+  KeyCode key_code = static_cast<KeyCode>(key);
+  if (kKeyMapping.find(key_code) != kKeyMapping.end()) {
+    return kKeyMapping.at(key_code);
+  }
+  return Device::kNoAction;
+}
+
+int TestableScreenRecoveryUI::WaitKey() {
+  CHECK_LT(key_buffer_index_, key_buffer_.size());
+  return static_cast<int>(key_buffer_[key_buffer_index_++]);
+}
+
+class ScreenRecoveryUITest : public ::testing::Test {
+ protected:
+  const std::string kTestLocale = "en-US";
+  const std::string kTestRtlLocale = "ar";
+  const std::string kTestRtlLocaleWithSuffix = "ar_EG";
+
+  void SetUp() override {
+    ui_ = std::make_unique<TestableScreenRecoveryUI>();
+
+    std::string testdata_dir = from_testdata_base("");
+    Paths::Get().set_resource_dir(testdata_dir);
+    res_set_resource_dir(testdata_dir);
+
+    ASSERT_TRUE(ui_->Init(kTestLocale));
+  }
+
+  std::unique_ptr<TestableScreenRecoveryUI> ui_;
+};
+
+TEST_F(ScreenRecoveryUITest, Init) {
+  ASSERT_EQ(kTestLocale, ui_->GetLocale());
+  ASSERT_FALSE(ui_->GetRtlLocale());
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ASSERT_FALSE(ui_->WasTextEverVisible());
+}
+
+TEST_F(ScreenRecoveryUITest, ShowText) {
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ui_->ShowText(true);
+  ASSERT_TRUE(ui_->IsTextVisible());
+  ASSERT_TRUE(ui_->WasTextEverVisible());
+
+  ui_->ShowText(false);
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ASSERT_TRUE(ui_->WasTextEverVisible());
+}
+
+TEST_F(ScreenRecoveryUITest, RtlLocale) {
+  ASSERT_TRUE(ui_->Init(kTestRtlLocale));
+  ASSERT_TRUE(ui_->GetRtlLocale());
+
+  ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix));
+  ASSERT_TRUE(ui_->GetRtlLocale());
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu) {
+  ui_->SetKeyBuffer({
+      KeyCode::UP,
+      KeyCode::DOWN,
+      KeyCode::UP,
+      KeyCode::DOWN,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(3u, ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+                              std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                        std::placeholders::_1, std::placeholders::_2)));
+
+  ui_->SetKeyBuffer({
+      KeyCode::UP,
+      KeyCode::UP,
+      KeyCode::NO_OP,
+      KeyCode::NO_OP,
+      KeyCode::UP,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(2u, ui_->ShowMenu(HEADERS, ITEMS, 0, true,
+                              std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                        std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_NotMenuOnly) {
+  ui_->SetKeyBuffer({
+      KeyCode::MAGIC,
+  });
+  ASSERT_EQ(static_cast<size_t>(kMagicAction),
+            ui_->ShowMenu(HEADERS, ITEMS, 3, false,
+                          std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                    std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut) {
+  ui_->SetKeyBuffer({
+      KeyCode::TIMEOUT,
+  });
+  ASSERT_EQ(static_cast<size_t>(-1), ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) {
+  ui_->ShowText(true);
+  ui_->ShowText(false);
+  ASSERT_TRUE(ui_->WasTextEverVisible());
+
+  ui_->SetKeyBuffer({
+      KeyCode::TIMEOUT,
+      KeyCode::DOWN,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(4u, ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+                              std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                        std::placeholders::_1, std::placeholders::_2)));
+}