/* Copyright (C) 2017-2021 Martijn Braam, Clayton Craft <clayton@craftyguy.net>, et al. This file is part of osk-sdl. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "config.h" #include "draw_helpers.h" #include "keyboard.h" #include "luksdevice.h" #include "tooltip.h" #include "toggle.h" #include "util.h" #include <SDL2/SDL.h> #include <cmath> #include <cstdlib> #include <iostream> #include <list> #include <string> #include <sys/reboot.h> #include <unistd.h> bool lastUnlockingState = false; bool showPasswordError = false; constexpr char ErrorText[] = "Incorrect passphrase"; constexpr char EnterPassText[] = "Enter disk decryption passphrase"; constexpr char UnlockingDiskText[] = "Trying to unlock disk..."; int main(int argc, char **args) { std::vector<std::string> passphrase; Opts opts {}; Config config; SDL_Event event; SDL_Window *display = nullptr; SDL_Renderer *renderer = nullptr; SDL_Haptic *haptic = nullptr; int WIDTH = 480; int HEIGHT = 800; std::chrono::milliseconds repeat_delay { 25 }; // Keyboard key repeat rate in ms unsigned prev_keydown_ticks = 0; // Two sep. prev_ticks required for handling unsigned prev_text_ticks = 0; // textinput & keydown event types bool show_osk = true; static Uint32 renderEventType = SDL_RegisterEvents(1); static SDL_Event renderEvent { .type = renderEventType }; SDL_LogSetAllPriority(SDL_LOG_PRIORITY_ERROR); SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO); if (fetchOpts(argc, args, &opts)) { exit(EXIT_FAILURE); } if (opts.verbose) { SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO); } SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "osk-sdl v%s", VERSION); if (!config.Read(opts.confPath)) { SDL_LogError(SDL_LOG_CATEGORY_ERROR, "No valid config file specified, use -c [path]"); exit(EXIT_FAILURE); } if (!opts.confOverridePath.empty() && !config.Read(opts.confOverridePath)) { SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Config override file could not be loaded, continuing"); } LuksDevice luksDev(opts.luksDevName, opts.luksDevPath, renderEventType); atexit(SDL_Quit); /* * default to hiding the on-screen keyboard if a physical keyboard is present */ if (!opts.showKeyboard && (opts.noKeyboard || hasPhysKeyboard())) show_osk = false; SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "%sshowing on-screen keyboard", show_osk ? "" : "NOT "); Uint32 sdlFlags = SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER; /* * DirectFB does not work with haptic feedback, so disable it if using * the DirectFB backend */ if (isDirectFB()) { SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Using directfb, not enabling haptic feedback."); SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Using directfb, animations have been disabled."); } else { sdlFlags |= SDL_INIT_HAPTIC; } if (SDL_Init(sdlFlags) < 0) { SDL_LogError(SDL_LOG_CATEGORY_ERROR, "SDL_Init failed: %s", SDL_GetError()); exit(EXIT_FAILURE); } if (!opts.testMode) { // Switch to the resolution of the framebuffer if not running // in test mode. SDL_DisplayMode mode = { SDL_PIXELFORMAT_UNKNOWN, 0, 0, 0, nullptr }; if (SDL_GetDisplayMode(0, 0, &mode) != 0) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "SDL_GetDisplayMode failed: %s", SDL_GetError()); exit(EXIT_FAILURE); } WIDTH = mode.w; HEIGHT = mode.h; } /* * Set up display and renderer * Use windowed mode in test mode and device resolution otherwise */ Uint32 windowFlags = 0; if (opts.testMode) { windowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN; } else { windowFlags = SDL_WINDOW_FULLSCREEN; } display = SDL_CreateWindow("OSK SDL", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, windowFlags); if (display == nullptr) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not create window/display: %s", SDL_GetError()); exit(EXIT_FAILURE); } /* * Prefer using GLES, since it's better supported on mobile devices * than full GL. * NOTE: DirectFB's SW GLES implementation is broken, so don't try to * use GLES w/ DirectFB */ int rendererIndex = -1; if (!opts.noGLES && !isDirectFB()) rendererIndex = find_gles_driver_index(); renderer = SDL_CreateRenderer(display, rendererIndex, 0); if (renderer == nullptr) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not create renderer: %s", SDL_GetError()); exit(EXIT_FAILURE); } if (TTF_Init() == -1) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "TTF_Init: %s", TTF_GetError()); exit(EXIT_FAILURE); } int keyboardHeight = HEIGHT / 3 * 2; if (HEIGHT > WIDTH) { // Keyboard height is screen width / max number of keys per row * rows // Denominator below chosen to provide enough room for a 5 row layout without causing key height to // shrink too much keyboardHeight = WIDTH / 1.6; } int inputWidth, inputHeight; inputWidth = static_cast<int>(WIDTH * 0.9); if (!show_osk) { // Reduce height when no keyboard is shown inputHeight = config.inputBoxFontSize + 8; } else { inputHeight = static_cast<int>(WIDTH * 0.1); } if (SDL_SetRenderDrawColor(renderer, 255, 128, 0, SDL_ALPHA_OPAQUE) != 0) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not set background color: %s", SDL_GetError()); exit(EXIT_FAILURE); } if (SDL_RenderFillRect(renderer, nullptr) != 0) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not fill background color: %s", SDL_GetError()); exit(EXIT_FAILURE); } // Disable mouse cursor if not in testmode if (SDL_ShowCursor(opts.testMode) < 0) { SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, "Setting cursor visibility failed: %s", SDL_GetError()); // Not stopping here, this is a pretty recoverable error. } // Initialize haptic device if (SDL_WasInit(SDL_INIT_HAPTIC) != 0) { haptic = SDL_HapticOpen(0); if (haptic == nullptr) { SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Unable to open haptic device"); } else if (SDL_HapticRumbleInit(haptic) != 0) { SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Unable to initialize haptic device"); SDL_HapticClose(haptic); haptic = nullptr; } else { SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Initialized haptic device"); } } // Initialize virtual keyboard Keyboard keyboard(0, 1, WIDTH, keyboardHeight, &config, haptic); if (keyboard.init(renderer)) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Failed to initialize keyboard!"); exit(EXIT_FAILURE); } // Make SDL send text editing events for textboxes SDL_StartTextInput(); // Wallpaper is renderer draw color SDL_SetRenderDrawColor(renderer, config.wallpaper.r, config.wallpaper.g, config.wallpaper.b, 255); int inputBoxRadius = std::strtol(config.inputBoxRadius.c_str(), nullptr, 10); if (inputBoxRadius >= BEZIER_RESOLUTION || inputBoxRadius > inputHeight / 1.5) { SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, "inputbox-radius must be below %d and %f, it is %d", BEZIER_RESOLUTION, inputHeight / 1.5, inputBoxRadius); inputBoxRadius = 0; } // Initialize tooltip for password error Tooltip passErrorTooltip(TooltipType::error, inputWidth, inputHeight, inputBoxRadius, &config); if (passErrorTooltip.init(renderer, ErrorText)) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Failed to initialize passErrorTooltip!"); exit(EXIT_FAILURE); } Tooltip enterPassTooltip(TooltipType::info, inputWidth, inputHeight, inputBoxRadius, &config); if (enterPassTooltip.init(renderer, EnterPassText)) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Failed to initialize enterPassTooltip!"); exit(EXIT_FAILURE); } // Tooltip for unlocking (when animations are disabled) Tooltip unlockingTooltip(TooltipType::info, inputWidth, inputHeight, inputBoxRadius, &config); if (unlockingTooltip.init(renderer, UnlockingDiskText)) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Failed to initialize unlockingTooltip!"); exit(EXIT_FAILURE); } // Initialize toggle button for keyboard Toggle keyboardToggle(WIDTH/10, HEIGHT/15, &config); if (keyboardToggle.init(renderer, "osk")) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Failed to initialize keyboardToggle!"); exit(EXIT_FAILURE); } keyboardToggle.setVisible(!show_osk); argb inputBoxColor = config.inputBoxBackground; SDL_Surface *inputBox = make_input_box(inputWidth, inputHeight, &inputBoxColor, inputBoxRadius); SDL_Texture *inputBoxTexture = SDL_CreateTextureFromSurface(renderer, inputBox); SDL_FreeSurface(inputBox); int topHalf = static_cast<int>(HEIGHT - (keyboard.getHeight() * keyboard.getPosition())); SDL_Rect inputBoxRect = SDL_Rect { .x = WIDTH / 20, .y = static_cast<int>(topHalf / 3.5), .w = inputWidth, .h = inputHeight }; if (!show_osk) { inputBoxRect.y = static_cast<int>(topHalf / 2); } if (inputBoxTexture == nullptr) { SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not create input box texture: %s", SDL_GetError()); exit(EXIT_FAILURE); } SDL_RendererInfo rendererInfo; SDL_GetRendererInfo(renderer, &rendererInfo); // Start drawing keyboard when main loop starts SDL_PushEvent(&renderEvent); // The Main Loop. bool done = false; int cur_ticks = 0; while (luksDev.isLocked() && !done) { show_osk = !keyboardToggle.isVisible(); if (SDL_WaitEvent(&event)) { // an event was found switch (event.type) { // handle the keyboard case SDL_KEYDOWN: // handle repeat key events cur_ticks = SDL_GetTicks(); if ((cur_ticks - repeat_delay.count()) < prev_keydown_ticks) { continue; } showPasswordError = false; prev_keydown_ticks = cur_ticks; if (SDL_GetModState() & KMOD_CTRL) { if (event.key.keysym.sym == SDLK_u) { passphrase.clear(); SDL_PushEvent(&renderEvent); continue; } } switch (event.key.keysym.sym) { case SDLK_RETURN: if (!passphrase.empty() && !luksDev.unlockRunning()) { std::string pass = strVector2str(passphrase); luksDev.setPassphrase(pass); if (opts.keyscript) { done = true; } else { luksDev.unlock(); } } break; // SDLK_RETURN case SDLK_BACKSPACE: if (!passphrase.empty() && !luksDev.unlockRunning()) { passphrase.pop_back(); SDL_PushEvent(&renderEvent); continue; } break; // SDLK_BACKSPACE case SDLK_POWER: if (opts.testMode) { SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Power off requested, but ignoring because" " test mode is active!"); break; } SDL_LogInfo(SDL_LOG_CATEGORY_SYSTEM, "Power off!"); sync(); reboot(RB_POWER_OFF); SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to power off: %s", strerror(errno)); break; case SDLK_ESCAPE: goto QUIT; break; // SDLK_ESCAPE } SDL_PushEvent(&renderEvent); break; // SDL_KEYDOWN // handle touchscreen case SDL_FINGERDOWN: { // x and y values are normalized! auto xTouch = static_cast<unsigned>(event.tfinger.x * WIDTH); auto yTouch = static_cast<unsigned>(event.tfinger.y * HEIGHT); handleTapBegin(xTouch, yTouch, HEIGHT, keyboard); SDL_PushEvent(&renderEvent); break; // SDL_FINGERDOWN } case SDL_FINGERUP: { auto xTouch = static_cast<unsigned>(event.tfinger.x * WIDTH); auto yTouch = static_cast<unsigned>(event.tfinger.y * HEIGHT); handleTapEnd(xTouch, yTouch, HEIGHT, keyboard, keyboardToggle, luksDev, passphrase, opts.keyscript, showPasswordError, done); SDL_PushEvent(&renderEvent); break; // SDL_FINGERUP } // handle the mouse case SDL_MOUSEBUTTONDOWN: { handleTapBegin(event.button.x, event.button.y, HEIGHT, keyboard); SDL_PushEvent(&renderEvent); break; // SDL_MOUSEBUTTONDOWN } case SDL_MOUSEBUTTONUP: { handleTapEnd(event.button.x, event.button.y, HEIGHT, keyboard, keyboardToggle, luksDev, passphrase, opts.keyscript, showPasswordError, done); SDL_PushEvent(&renderEvent); break; // SDL_MOUSEBUTTONUP } // handle physical keyboard case SDL_TEXTINPUT: { // Don't display characters for hotkey input if (SDL_GetModState() & KMOD_CTRL) { if (strcmp(event.text.text, "u") == 0) { continue; } } /* * Only register text input if time since last text input has exceeded * the keyboard repeat delay rate */ showPasswordError = false; cur_ticks = SDL_GetTicks(); // Enable key repeat delay if ((cur_ticks - repeat_delay.count()) > prev_text_ticks) { prev_text_ticks = cur_ticks; if (!luksDev.unlockRunning()) { passphrase.emplace_back(event.text.text); SDL_PushEvent(&renderEvent); SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "Phys Keyboard Key Entered %s", event.text.text); } } break; // SDL_TEXTINPUT } case SDL_QUIT: SDL_Log("Quit requested, quitting."); exit(0); break; // SDL_QUIT } // switch event.type // Render event handler if (event.type == renderEventType) { /* NOTE ON MULTI BUFFERING / RENDERING MULTIPLE TIMES: We only re-schedule render events during animation, otherwise we render once and then do nothing for a long while. A single render may however never reach the screen, since SDL_RenderCopy() page flips and with multi buffering that may just fill the hidden backbuffer(s). Therefore, we need to render multiple times if not during animation to make sure it actually shows on screen during lengthy pauses. For software rendering (directfb backend), rendering twice seems to be the sweet spot. For accelerated rendering, we render 3 times to make sure updates show on screen for drivers that use triple buffering */ int render_times = 0; int max_render_times = (rendererInfo.flags & SDL_RENDERER_ACCELERATED) ? 3 : 2; while (render_times < max_render_times) { render_times++; SDL_RenderClear(renderer); // Hide keyboard if unlock luks thread is running keyboard.setTargetPosition(!luksDev.unlockRunning()); // When *not* using animations, so draw keyboard first so tooltip is positioned correctly from the start if (!config.animations && show_osk) { keyboard.draw(renderer, HEIGHT); } topHalf = static_cast<int>(HEIGHT - (keyboard.getHeight() * keyboard.getPosition())); inputBoxRect.y = static_cast<int>(topHalf / 3.5); // Only show either error tooltip, enter password tooltip, or password input box if (showPasswordError) { passErrorTooltip.draw(renderer, inputBoxRect.x, inputBoxRect.y); } else if (passphrase.size() == 0) { enterPassTooltip.draw(renderer, inputBoxRect.x, inputBoxRect.y); } else if (luksDev.unlockRunning() && !config.animations) { unlockingTooltip.draw(renderer, inputBoxRect.x, inputBoxRect.y); } else { SDL_RenderCopy(renderer, inputBoxTexture, nullptr, &inputBoxRect); draw_password_box_dots(renderer, &config, inputBoxRect, passphrase.size(), luksDev.unlockRunning()); } if (!show_osk) keyboardToggle.draw(renderer, WIDTH-(WIDTH/10), HEIGHT-(HEIGHT/15)); // When using animations, draw keyboard last so that key previews don't get drawn over by e.g. the input box if (config.animations && show_osk) { keyboard.draw(renderer, HEIGHT); } SDL_RenderPresent(renderer); if (keyboard.isInSlideAnimation()) { // No need to double-flip if we'll redraw more for animation // in a tiny moment anyway. break; } } if (lastUnlockingState != luksDev.unlockRunning()) { if (!luksDev.unlockRunning() && luksDev.isLocked()) { // Luks is finished and the password was wrong showPasswordError = true; passphrase.clear(); // Show default keyboard layer again on wrong passphrase keyboard.setActiveLayer(0); SDL_PushEvent(&renderEvent); } lastUnlockingState = luksDev.unlockRunning(); } // If any animations are enabled and running, continue to push render events to the // event queue bool keyboardIsAnimating = keyboard.isInSlideAnimation() && show_osk; if (config.animations && (luksDev.unlockRunning() || keyboardIsAnimating)) { SDL_PushEvent(&renderEvent); } } } // event handle loop } // main loop QUIT: if (inputBoxTexture) SDL_DestroyTexture(inputBoxTexture); keyboardToggle.cleanup(); passErrorTooltip.cleanup(); enterPassTooltip.cleanup(); unlockingTooltip.cleanup(); keyboard.cleanup(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(display); TTF_Quit(); SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER | SDL_INIT_HAPTIC); if (opts.keyscript) { std::string pass = strVector2str(passphrase); printf("%s", pass.c_str()); fflush(stdout); } return 0; }