1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #include <stddef.h> 18 #include <stdio.h> 19 20 #include <functional> 21 #include <map> 22 #include <memory> 23 #include <string> 24 #include <vector> 25 26 #include <android-base/file.h> 27 #include <android-base/logging.h> 28 #include <android-base/stringprintf.h> 29 #include <gtest/gtest.h> 30 #include <gtest/gtest_prod.h> 31 32 #include "common/test_constants.h" 33 #include "minui/minui.h" 34 #include "otautil/paths.h" 35 #include "private/resources.h" 36 #include "recovery_ui/device.h" 37 #include "recovery_ui/screen_ui.h" 38 39 static const std::vector<std::string> HEADERS{ "header" }; 40 static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" }; 41 42 // TODO(xunchang) check if some draw functions are called when drawing menus. 43 class MockDrawFunctions : public DrawInterface { 44 void SetColor(UIElement /* element */) const override {} 45 void DrawHighlightBar(int /* x */, int /* y */, int /* width */, 46 int /* height */) const override {} 47 int DrawHorizontalRule(int /* y */) const override { 48 return 0; 49 } 50 int DrawTextLine(int /* x */, int /* y */, const std::string& /* line */, 51 bool /* bold */) const override { 52 return 0; 53 } 54 void DrawSurface(const GRSurface* /* surface */, int /* sx */, int /* sy */, int /* w */, 55 int /* h */, int /* dx */, int /* dy */) const override {} 56 void DrawFill(int /* x */, int /* y */, int /* w */, int /* h */) const override {} 57 void DrawTextIcon(int /* x */, int /* y */, const GRSurface* /* surface */) const override {} 58 int DrawTextLines(int /* x */, int /* y */, 59 const std::vector<std::string>& /* lines */) const override { 60 return 0; 61 } 62 int DrawWrappedTextLines(int /* x */, int /* y */, 63 const std::vector<std::string>& /* lines */) const override { 64 return 0; 65 } 66 }; 67 68 class ScreenUITest : public testing::Test { 69 protected: 70 MockDrawFunctions draw_funcs_; 71 }; 72 73 TEST_F(ScreenUITest, StartPhoneMenuSmoke) { 74 TextMenu menu(false, 10, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); 75 ASSERT_FALSE(menu.scrollable()); 76 ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); 77 ASSERT_EQ(5u, menu.ItemsCount()); 78 79 std::string message; 80 ASSERT_FALSE(menu.ItemsOverflow(&message)); 81 for (size_t i = 0; i < menu.ItemsCount(); i++) { 82 ASSERT_EQ(ITEMS[i], menu.TextItem(i)); 83 } 84 85 ASSERT_EQ(0, menu.selection()); 86 } 87 88 TEST_F(ScreenUITest, StartWearMenuSmoke) { 89 TextMenu menu(true, 10, 8, HEADERS, ITEMS, 1, 20, draw_funcs_); 90 ASSERT_TRUE(menu.scrollable()); 91 ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); 92 ASSERT_EQ(5u, menu.ItemsCount()); 93 94 std::string message; 95 ASSERT_FALSE(menu.ItemsOverflow(&message)); 96 for (size_t i = 0; i < menu.ItemsCount() - 1; i++) { 97 ASSERT_EQ(ITEMS[i], menu.TextItem(i)); 98 } 99 // Test of the last item is truncated 100 ASSERT_EQ("12345678", menu.TextItem(4)); 101 ASSERT_EQ(1, menu.selection()); 102 } 103 104 TEST_F(ScreenUITest, StartPhoneMenuItemsOverflow) { 105 TextMenu menu(false, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); 106 ASSERT_FALSE(menu.scrollable()); 107 ASSERT_EQ(1u, menu.ItemsCount()); 108 109 std::string message; 110 ASSERT_FALSE(menu.ItemsOverflow(&message)); 111 for (size_t i = 0; i < menu.ItemsCount(); i++) { 112 ASSERT_EQ(ITEMS[i], menu.TextItem(i)); 113 } 114 115 ASSERT_EQ(0u, menu.MenuStart()); 116 ASSERT_EQ(1u, menu.MenuEnd()); 117 } 118 119 TEST_F(ScreenUITest, StartWearMenuItemsOverflow) { 120 TextMenu menu(true, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); 121 ASSERT_TRUE(menu.scrollable()); 122 ASSERT_EQ(5u, menu.ItemsCount()); 123 124 std::string message; 125 ASSERT_TRUE(menu.ItemsOverflow(&message)); 126 ASSERT_EQ("Current item: 1/5", message); 127 128 for (size_t i = 0; i < menu.ItemsCount(); i++) { 129 ASSERT_EQ(ITEMS[i], menu.TextItem(i)); 130 } 131 132 ASSERT_EQ(0u, menu.MenuStart()); 133 ASSERT_EQ(1u, menu.MenuEnd()); 134 } 135 136 TEST_F(ScreenUITest, PhoneMenuSelectSmoke) { 137 int sel = 0; 138 TextMenu menu(false, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_); 139 // Mimic down button 10 times (2 * items size) 140 for (int i = 0; i < 10; i++) { 141 sel = menu.Select(++sel); 142 ASSERT_EQ(sel, menu.selection()); 143 144 // Wraps the selection for unscrollable menu when it reaches the boundary. 145 int expected = (i + 1) % 5; 146 ASSERT_EQ(expected, menu.selection()); 147 148 ASSERT_EQ(0u, menu.MenuStart()); 149 ASSERT_EQ(5u, menu.MenuEnd()); 150 } 151 152 // Mimic up button 10 times 153 for (int i = 0; i < 10; i++) { 154 sel = menu.Select(--sel); 155 ASSERT_EQ(sel, menu.selection()); 156 157 int expected = (9 - i) % 5; 158 ASSERT_EQ(expected, menu.selection()); 159 160 ASSERT_EQ(0u, menu.MenuStart()); 161 ASSERT_EQ(5u, menu.MenuEnd()); 162 } 163 } 164 165 TEST_F(ScreenUITest, WearMenuSelectSmoke) { 166 int sel = 0; 167 TextMenu menu(true, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_); 168 // Mimic pressing down button 10 times (2 * items size) 169 for (int i = 0; i < 10; i++) { 170 sel = menu.Select(++sel); 171 ASSERT_EQ(sel, menu.selection()); 172 173 // Stops the selection at the boundary if the menu is scrollable. 174 int expected = std::min(i + 1, 4); 175 ASSERT_EQ(expected, menu.selection()); 176 177 ASSERT_EQ(0u, menu.MenuStart()); 178 ASSERT_EQ(5u, menu.MenuEnd()); 179 } 180 181 // Mimic pressing up button 10 times 182 for (int i = 0; i < 10; i++) { 183 sel = menu.Select(--sel); 184 ASSERT_EQ(sel, menu.selection()); 185 186 int expected = std::max(3 - i, 0); 187 ASSERT_EQ(expected, menu.selection()); 188 189 ASSERT_EQ(0u, menu.MenuStart()); 190 ASSERT_EQ(5u, menu.MenuEnd()); 191 } 192 } 193 194 TEST_F(ScreenUITest, WearMenuSelectItemsOverflow) { 195 int sel = 1; 196 TextMenu menu(true, 3, 20, HEADERS, ITEMS, sel, 20, draw_funcs_); 197 ASSERT_EQ(5u, menu.ItemsCount()); 198 199 // Scroll the menu to the end, and check the start & end of menu. 200 for (int i = 0; i < 3; i++) { 201 sel = menu.Select(++sel); 202 ASSERT_EQ(i + 2, sel); 203 ASSERT_EQ(static_cast<size_t>(i), menu.MenuStart()); 204 ASSERT_EQ(static_cast<size_t>(i + 3), menu.MenuEnd()); 205 } 206 207 // Press down button one more time won't change the MenuStart() and MenuEnd(). 208 sel = menu.Select(++sel); 209 ASSERT_EQ(4, sel); 210 ASSERT_EQ(2u, menu.MenuStart()); 211 ASSERT_EQ(5u, menu.MenuEnd()); 212 213 // Scroll the menu to the top. 214 // The expected menu sel, start & ends are: 215 // sel 3, start 2, end 5 216 // sel 2, start 2, end 5 217 // sel 1, start 1, end 4 218 // sel 0, start 0, end 3 219 for (int i = 0; i < 4; i++) { 220 sel = menu.Select(--sel); 221 ASSERT_EQ(3 - i, sel); 222 ASSERT_EQ(static_cast<size_t>(std::min(3 - i, 2)), menu.MenuStart()); 223 ASSERT_EQ(static_cast<size_t>(std::min(6 - i, 5)), menu.MenuEnd()); 224 } 225 226 // Press up button one more time won't change the MenuStart() and MenuEnd(). 227 sel = menu.Select(--sel); 228 ASSERT_EQ(0, sel); 229 ASSERT_EQ(0u, menu.MenuStart()); 230 ASSERT_EQ(3u, menu.MenuEnd()); 231 } 232 233 TEST_F(ScreenUITest, GraphicMenuSelection) { 234 auto image = GRSurface::Create(50, 50, 50, 1); 235 auto header = image->Clone(); 236 std::vector<const GRSurface*> items = { 237 image.get(), 238 image.get(), 239 image.get(), 240 }; 241 GraphicMenu menu(header.get(), items, 0, draw_funcs_); 242 243 ASSERT_EQ(0, menu.selection()); 244 245 int sel = 0; 246 for (int i = 0; i < 3; i++) { 247 sel = menu.Select(++sel); 248 ASSERT_EQ((i + 1) % 3, sel); 249 ASSERT_EQ(sel, menu.selection()); 250 } 251 252 sel = 0; 253 for (int i = 0; i < 3; i++) { 254 sel = menu.Select(--sel); 255 ASSERT_EQ(2 - i, sel); 256 ASSERT_EQ(sel, menu.selection()); 257 } 258 } 259 260 TEST_F(ScreenUITest, GraphicMenuValidate) { 261 auto image = GRSurface::Create(50, 50, 50, 1); 262 auto header = image->Clone(); 263 std::vector<const GRSurface*> items = { 264 image.get(), 265 image.get(), 266 image.get(), 267 }; 268 269 ASSERT_TRUE(GraphicMenu::Validate(200, 200, header.get(), items)); 270 271 // Menu exceeds the horizontal boundary. 272 auto wide_surface = GRSurface::Create(300, 50, 300, 1); 273 ASSERT_FALSE(GraphicMenu::Validate(299, 200, wide_surface.get(), items)); 274 275 // Menu exceeds the vertical boundary. 276 items.emplace_back(image.get()); 277 ASSERT_FALSE(GraphicMenu::Validate(200, 249, header.get(), items)); 278 } 279 280 static constexpr int kMagicAction = 101; 281 282 enum class KeyCode : int { 283 TIMEOUT = -1, 284 NO_OP = 0, 285 UP = 1, 286 DOWN = 2, 287 ENTER = 3, 288 MAGIC = 1001, 289 LAST, 290 }; 291 292 static const std::map<KeyCode, int> kKeyMapping{ 293 // clang-format off 294 { KeyCode::NO_OP, Device::kNoAction }, 295 { KeyCode::UP, Device::kHighlightUp }, 296 { KeyCode::DOWN, Device::kHighlightDown }, 297 { KeyCode::ENTER, Device::kInvokeItem }, 298 { KeyCode::MAGIC, kMagicAction }, 299 // clang-format on 300 }; 301 302 class TestableScreenRecoveryUI : public ScreenRecoveryUI { 303 public: 304 int WaitKey() override; 305 306 void SetKeyBuffer(const std::vector<KeyCode>& buffer); 307 308 int KeyHandler(int key, bool visible) const; 309 310 private: 311 FRIEND_TEST(DISABLED_ScreenRecoveryUITest, Init); 312 FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocale); 313 FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix); 314 FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation); 315 FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation); 316 317 std::vector<KeyCode> key_buffer_; 318 size_t key_buffer_index_; 319 }; 320 321 void TestableScreenRecoveryUI::SetKeyBuffer(const std::vector<KeyCode>& buffer) { 322 key_buffer_ = buffer; 323 key_buffer_index_ = 0; 324 } 325 326 int TestableScreenRecoveryUI::KeyHandler(int key, bool) const { 327 KeyCode key_code = static_cast<KeyCode>(key); 328 if (kKeyMapping.find(key_code) != kKeyMapping.end()) { 329 return kKeyMapping.at(key_code); 330 } 331 return Device::kNoAction; 332 } 333 334 int TestableScreenRecoveryUI::WaitKey() { 335 if (IsKeyInterrupted()) { 336 return static_cast<int>(RecoveryUI::KeyError::INTERRUPTED); 337 } 338 339 CHECK_LT(key_buffer_index_, key_buffer_.size()); 340 return static_cast<int>(key_buffer_[key_buffer_index_++]); 341 } 342 343 class DISABLED_ScreenRecoveryUITest : public ::testing::Test { 344 protected: 345 const std::string kTestLocale = "en-US"; 346 const std::string kTestRtlLocale = "ar"; 347 const std::string kTestRtlLocaleWithSuffix = "ar-EG"; 348 349 void SetUp() override { 350 has_graphics_ = gr_init() == 0; 351 gr_exit(); 352 353 if (has_graphics_) { 354 ui_ = std::make_unique<TestableScreenRecoveryUI>(); 355 } 356 357 testdata_dir_ = from_testdata_base(""); 358 Paths::Get().set_resource_dir(testdata_dir_); 359 res_set_resource_dir(testdata_dir_); 360 } 361 362 bool has_graphics_; 363 std::unique_ptr<TestableScreenRecoveryUI> ui_; 364 std::string testdata_dir_; 365 }; 366 367 #define RETURN_IF_NO_GRAPHICS \ 368 do { \ 369 if (!has_graphics_) { \ 370 GTEST_LOG_(INFO) << "Test skipped due to no available graphics device"; \ 371 return; \ 372 } \ 373 } while (false) 374 375 TEST_F(DISABLED_ScreenRecoveryUITest, Init) { 376 RETURN_IF_NO_GRAPHICS; 377 378 ASSERT_TRUE(ui_->Init(kTestLocale)); 379 ASSERT_EQ(kTestLocale, ui_->GetLocale()); 380 ASSERT_FALSE(ui_->rtl_locale_); 381 ASSERT_FALSE(ui_->IsTextVisible()); 382 ASSERT_FALSE(ui_->WasTextEverVisible()); 383 } 384 385 TEST_F(DISABLED_ScreenRecoveryUITest, dtor_NotCallingInit) { 386 ui_.reset(); 387 ASSERT_FALSE(ui_); 388 } 389 390 TEST_F(DISABLED_ScreenRecoveryUITest, ShowText) { 391 RETURN_IF_NO_GRAPHICS; 392 393 ASSERT_TRUE(ui_->Init(kTestLocale)); 394 ASSERT_FALSE(ui_->IsTextVisible()); 395 ui_->ShowText(true); 396 ASSERT_TRUE(ui_->IsTextVisible()); 397 ASSERT_TRUE(ui_->WasTextEverVisible()); 398 399 ui_->ShowText(false); 400 ASSERT_FALSE(ui_->IsTextVisible()); 401 ASSERT_TRUE(ui_->WasTextEverVisible()); 402 } 403 404 TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocale) { 405 RETURN_IF_NO_GRAPHICS; 406 407 ASSERT_TRUE(ui_->Init(kTestRtlLocale)); 408 ASSERT_TRUE(ui_->rtl_locale_); 409 } 410 411 TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix) { 412 RETURN_IF_NO_GRAPHICS; 413 414 ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix)); 415 ASSERT_TRUE(ui_->rtl_locale_); 416 } 417 418 TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu) { 419 RETURN_IF_NO_GRAPHICS; 420 421 ASSERT_TRUE(ui_->Init(kTestLocale)); 422 ui_->SetKeyBuffer({ 423 KeyCode::UP, 424 KeyCode::DOWN, 425 KeyCode::UP, 426 KeyCode::DOWN, 427 KeyCode::ENTER, 428 }); 429 ASSERT_EQ(3u, ui_->ShowMenu(HEADERS, ITEMS, 3, true, 430 std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), 431 std::placeholders::_1, std::placeholders::_2))); 432 433 ui_->SetKeyBuffer({ 434 KeyCode::UP, 435 KeyCode::UP, 436 KeyCode::NO_OP, 437 KeyCode::NO_OP, 438 KeyCode::UP, 439 KeyCode::ENTER, 440 }); 441 ASSERT_EQ(2u, ui_->ShowMenu(HEADERS, ITEMS, 0, true, 442 std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), 443 std::placeholders::_1, std::placeholders::_2))); 444 } 445 446 TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_NotMenuOnly) { 447 RETURN_IF_NO_GRAPHICS; 448 449 ASSERT_TRUE(ui_->Init(kTestLocale)); 450 ui_->SetKeyBuffer({ 451 KeyCode::MAGIC, 452 }); 453 ASSERT_EQ(static_cast<size_t>(kMagicAction), 454 ui_->ShowMenu(HEADERS, ITEMS, 3, false, 455 std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), 456 std::placeholders::_1, std::placeholders::_2))); 457 } 458 459 TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_TimedOut) { 460 RETURN_IF_NO_GRAPHICS; 461 462 ASSERT_TRUE(ui_->Init(kTestLocale)); 463 ui_->SetKeyBuffer({ 464 KeyCode::TIMEOUT, 465 }); 466 ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT), 467 ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr)); 468 } 469 470 TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) { 471 RETURN_IF_NO_GRAPHICS; 472 473 ASSERT_TRUE(ui_->Init(kTestLocale)); 474 ui_->ShowText(true); 475 ui_->ShowText(false); 476 ASSERT_TRUE(ui_->WasTextEverVisible()); 477 478 ui_->SetKeyBuffer({ 479 KeyCode::TIMEOUT, 480 KeyCode::DOWN, 481 KeyCode::ENTER, 482 }); 483 ASSERT_EQ(4u, ui_->ShowMenu(HEADERS, ITEMS, 3, true, 484 std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), 485 std::placeholders::_1, std::placeholders::_2))); 486 } 487 488 TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenuWithInterrupt) { 489 RETURN_IF_NO_GRAPHICS; 490 491 ASSERT_TRUE(ui_->Init(kTestLocale)); 492 ui_->SetKeyBuffer({ 493 KeyCode::UP, 494 KeyCode::DOWN, 495 KeyCode::UP, 496 KeyCode::DOWN, 497 KeyCode::ENTER, 498 }); 499 500 ui_->InterruptKey(); 501 ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED), 502 ui_->ShowMenu(HEADERS, ITEMS, 3, true, 503 std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), 504 std::placeholders::_1, std::placeholders::_2))); 505 506 ui_->SetKeyBuffer({ 507 KeyCode::UP, 508 KeyCode::UP, 509 KeyCode::NO_OP, 510 KeyCode::NO_OP, 511 KeyCode::UP, 512 KeyCode::ENTER, 513 }); 514 ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED), 515 ui_->ShowMenu(HEADERS, ITEMS, 0, true, 516 std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), 517 std::placeholders::_1, std::placeholders::_2))); 518 } 519 520 TEST_F(DISABLED_ScreenRecoveryUITest, LoadAnimation) { 521 RETURN_IF_NO_GRAPHICS; 522 523 ASSERT_TRUE(ui_->Init(kTestLocale)); 524 // Make a few copies of loop00000.png from testdata. 525 std::string image_data; 526 ASSERT_TRUE(android::base::ReadFileToString(testdata_dir_ + "/loop00000.png", &image_data)); 527 528 std::vector<std::string> tempfiles; 529 TemporaryDir resource_dir; 530 for (const auto& name : { "00002", "00100", "00050" }) { 531 tempfiles.push_back(android::base::StringPrintf("%s/loop%s.png", resource_dir.path, name)); 532 ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back())); 533 } 534 for (const auto& name : { "00", "01" }) { 535 tempfiles.push_back(android::base::StringPrintf("%s/intro%s.png", resource_dir.path, name)); 536 ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back())); 537 } 538 Paths::Get().set_resource_dir(resource_dir.path); 539 540 ui_->LoadAnimation(); 541 542 ASSERT_EQ(2u, ui_->intro_frames_.size()); 543 ASSERT_EQ(3u, ui_->loop_frames_.size()); 544 545 for (const auto& name : tempfiles) { 546 ASSERT_EQ(0, unlink(name.c_str())); 547 } 548 } 549 550 TEST_F(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation) { 551 RETURN_IF_NO_GRAPHICS; 552 553 ASSERT_TRUE(ui_->Init(kTestLocale)); 554 // We need a dir that doesn't contain any animation. However, using TemporaryDir will give 555 // leftovers since this is a death test where TemporaryDir::~TemporaryDir() won't be called. 556 Paths::Get().set_resource_dir("/proc/self"); 557 558 ::testing::FLAGS_gtest_death_test_style = "threadsafe"; 559 ASSERT_EXIT(ui_->LoadAnimation(), ::testing::KilledBySignal(SIGABRT), ""); 560 } 561 562 #undef RETURN_IF_NO_GRAPHICS 563