/* Copyright (c) 2019-2025, Arm Limited and Contributors * * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "android_platform.h" #include #include #include #include #include "common/error.h" #include #include #include #include #include #include "apps.h" #include "common/strings.h" #include "core/util/logging.hpp" #include "platform/android/android_window.h" #include "platform/input_events.h" extern "C" { JNIEXPORT jobjectArray JNICALL Java_com_khronos_vulkan_1samples_SampleLauncherActivity_getSamples(JNIEnv *env, jobject thiz) { auto sample_list = apps::get_samples(); jclass c = env->FindClass("com/khronos/vulkan_samples/model/Sample"); jmethodID constructor = env->GetMethodID(c, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V"); jobjectArray j_sample_list = env->NewObjectArray(sample_list.size(), c, 0); for (int sample_index = 0; sample_index < sample_list.size(); sample_index++) { const apps::SampleInfo *sample_info = reinterpret_cast(sample_list[sample_index]); jstring id = env->NewStringUTF(sample_info->id.c_str()); jstring category = env->NewStringUTF(sample_info->category.c_str()); jstring author = env->NewStringUTF(sample_info->author.c_str()); jstring name = env->NewStringUTF(sample_info->name.c_str()); jstring desc = env->NewStringUTF(sample_info->description.c_str()); jobjectArray j_tag_list = env->NewObjectArray(sample_info->tags.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); for (int tag_index = 0; tag_index < sample_info->tags.size(); ++tag_index) { env->SetObjectArrayElement(j_tag_list, tag_index, env->NewStringUTF(sample_info->tags[tag_index].c_str())); } env->SetObjectArrayElement(j_sample_list, sample_index, env->NewObject(c, constructor, id, category, author, name, desc, j_tag_list)); } return j_sample_list; } } namespace vkb { namespace { inline std::tm thread_safe_time(const std::time_t time) { std::tm result; std::mutex mtx; std::lock_guard lock(mtx); result = *std::localtime(&time); return result; } inline KeyCode translate_key_code(int key) { static const std::unordered_map key_lookup = { {AKEYCODE_SPACE, KeyCode::Space}, {AKEYCODE_APOSTROPHE, KeyCode::Apostrophe}, {AKEYCODE_COMMA, KeyCode::Comma}, {AKEYCODE_MINUS, KeyCode::Minus}, {AKEYCODE_PERIOD, KeyCode::Period}, {AKEYCODE_SLASH, KeyCode::Slash}, {AKEYCODE_0, KeyCode::_0}, {AKEYCODE_1, KeyCode::_1}, {AKEYCODE_2, KeyCode::_2}, {AKEYCODE_3, KeyCode::_3}, {AKEYCODE_4, KeyCode::_4}, {AKEYCODE_5, KeyCode::_5}, {AKEYCODE_6, KeyCode::_6}, {AKEYCODE_7, KeyCode::_7}, {AKEYCODE_8, KeyCode::_8}, {AKEYCODE_9, KeyCode::_9}, {AKEYCODE_SEMICOLON, KeyCode::Semicolon}, {AKEYCODE_EQUALS, KeyCode::Equal}, {AKEYCODE_A, KeyCode::A}, {AKEYCODE_B, KeyCode::B}, {AKEYCODE_C, KeyCode::C}, {AKEYCODE_D, KeyCode::D}, {AKEYCODE_E, KeyCode::E}, {AKEYCODE_F, KeyCode::F}, {AKEYCODE_G, KeyCode::G}, {AKEYCODE_H, KeyCode::H}, {AKEYCODE_I, KeyCode::I}, {AKEYCODE_J, KeyCode::J}, {AKEYCODE_K, KeyCode::K}, {AKEYCODE_L, KeyCode::L}, {AKEYCODE_M, KeyCode::M}, {AKEYCODE_N, KeyCode::N}, {AKEYCODE_O, KeyCode::O}, {AKEYCODE_P, KeyCode::P}, {AKEYCODE_Q, KeyCode::Q}, {AKEYCODE_R, KeyCode::R}, {AKEYCODE_S, KeyCode::S}, {AKEYCODE_T, KeyCode::T}, {AKEYCODE_U, KeyCode::U}, {AKEYCODE_V, KeyCode::V}, {AKEYCODE_W, KeyCode::W}, {AKEYCODE_X, KeyCode::X}, {AKEYCODE_Y, KeyCode::Y}, {AKEYCODE_Z, KeyCode::Z}, {AKEYCODE_LEFT_BRACKET, KeyCode::LeftBracket}, {AKEYCODE_BACKSLASH, KeyCode::Backslash}, {AKEYCODE_RIGHT_BRACKET, KeyCode::RightBracket}, {AKEYCODE_ESCAPE, KeyCode::Escape}, {AKEYCODE_BACK, KeyCode::Back}, {AKEYCODE_ENTER, KeyCode::Enter}, {AKEYCODE_TAB, KeyCode::Tab}, {AKEYCODE_DEL, KeyCode::Backspace}, {AKEYCODE_INSERT, KeyCode::Insert}, {AKEYCODE_DEL, KeyCode::DelKey}, {AKEYCODE_SYSTEM_NAVIGATION_RIGHT, KeyCode::Right}, {AKEYCODE_SYSTEM_NAVIGATION_LEFT, KeyCode::Left}, {AKEYCODE_SYSTEM_NAVIGATION_DOWN, KeyCode::Down}, {AKEYCODE_SYSTEM_NAVIGATION_UP, KeyCode::Up}, {AKEYCODE_PAGE_UP, KeyCode::PageUp}, {AKEYCODE_PAGE_DOWN, KeyCode::PageDown}, {AKEYCODE_HOME, KeyCode::Home}, {AKEYCODE_CAPS_LOCK, KeyCode::CapsLock}, {AKEYCODE_SCROLL_LOCK, KeyCode::ScrollLock}, {AKEYCODE_NUM_LOCK, KeyCode::NumLock}, {AKEYCODE_BREAK, KeyCode::Pause}, {AKEYCODE_F1, KeyCode::F1}, {AKEYCODE_F2, KeyCode::F2}, {AKEYCODE_F3, KeyCode::F3}, {AKEYCODE_F4, KeyCode::F4}, {AKEYCODE_F5, KeyCode::F5}, {AKEYCODE_F6, KeyCode::F6}, {AKEYCODE_F7, KeyCode::F7}, {AKEYCODE_F8, KeyCode::F8}, {AKEYCODE_F9, KeyCode::F9}, {AKEYCODE_F10, KeyCode::F10}, {AKEYCODE_F11, KeyCode::F11}, {AKEYCODE_F12, KeyCode::F12}, {AKEYCODE_NUMPAD_0, KeyCode::KP_0}, {AKEYCODE_NUMPAD_1, KeyCode::KP_1}, {AKEYCODE_NUMPAD_2, KeyCode::KP_2}, {AKEYCODE_NUMPAD_3, KeyCode::KP_3}, {AKEYCODE_NUMPAD_4, KeyCode::KP_4}, {AKEYCODE_NUMPAD_5, KeyCode::KP_5}, {AKEYCODE_NUMPAD_6, KeyCode::KP_6}, {AKEYCODE_NUMPAD_7, KeyCode::KP_7}, {AKEYCODE_NUMPAD_8, KeyCode::KP_8}, {AKEYCODE_NUMPAD_9, KeyCode::KP_9}, {AKEYCODE_NUMPAD_DOT, KeyCode::KP_Decimal}, {AKEYCODE_NUMPAD_DIVIDE, KeyCode::KP_Divide}, {AKEYCODE_NUMPAD_MULTIPLY, KeyCode::KP_Multiply}, {AKEYCODE_NUMPAD_SUBTRACT, KeyCode::KP_Subtract}, {AKEYCODE_NUMPAD_ADD, KeyCode::KP_Add}, {AKEYCODE_NUMPAD_ENTER, KeyCode::KP_Enter}, {AKEYCODE_NUMPAD_EQUALS, KeyCode::KP_Equal}, {AKEYCODE_SHIFT_LEFT, KeyCode::LeftShift}, {AKEYCODE_CTRL_LEFT, KeyCode::LeftControl}, {AKEYCODE_ALT_LEFT, KeyCode::LeftAlt}, {AKEYCODE_SHIFT_RIGHT, KeyCode::RightShift}, {AKEYCODE_CTRL_RIGHT, KeyCode::RightControl}, {AKEYCODE_ALT_RIGHT, KeyCode::RightAlt}}; auto key_it = key_lookup.find(key); if (key_it == key_lookup.end()) { return KeyCode::Unknown; } return key_it->second; } inline KeyAction translate_key_action(int action) { if (action == AKEY_STATE_DOWN) { return KeyAction::Down; } else if (action == AKEY_STATE_UP) { return KeyAction::Up; } return KeyAction::Unknown; } inline MouseButton translate_mouse_button(int button) { if (button < 3) { return static_cast(button); } return MouseButton::Unknown; } inline MouseAction translate_mouse_action(int action) { if (action == AMOTION_EVENT_ACTION_DOWN) { return MouseAction::Down; } else if (action == AMOTION_EVENT_ACTION_UP) { return MouseAction::Up; } else if (action == AMOTION_EVENT_ACTION_MOVE) { return MouseAction::Move; } return MouseAction::Unknown; } inline TouchAction translate_touch_action(int action) { action &= AMOTION_EVENT_ACTION_MASK; if (action == AMOTION_EVENT_ACTION_DOWN || action == AMOTION_EVENT_ACTION_POINTER_DOWN) { return TouchAction::Down; } else if (action == AMOTION_EVENT_ACTION_UP || action == AMOTION_EVENT_ACTION_POINTER_UP) { return TouchAction::Up; } else if (action == AMOTION_EVENT_ACTION_CANCEL) { return TouchAction::Cancel; } else if (action == AMOTION_EVENT_ACTION_MOVE) { return TouchAction::Move; } return TouchAction::Unknown; } void on_content_rect_changed(GameActivity *activity, const ARect *rect) { LOGI("ContentRectChanged: {:p}\n", static_cast(activity)); struct android_app *app = reinterpret_cast(activity->instance); auto cmd = APP_CMD_CONTENT_RECT_CHANGED; app->contentRect = *rect; if (write(app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) { LOGE("Failure writing android_app cmd: {}\n", strerror(errno)); } } void on_app_cmd(android_app *app, int32_t cmd) { auto platform = reinterpret_cast(app->userData); assert(platform && "Platform is not valid"); switch (cmd) { case APP_CMD_INIT_WINDOW: { platform->resize(ANativeWindow_getWidth(app->window), ANativeWindow_getHeight(app->window)); platform->set_surface_ready(); break; } case APP_CMD_CONTENT_RECT_CHANGED: { // Get the new size auto width = app->contentRect.right - app->contentRect.left; auto height = app->contentRect.bottom - app->contentRect.top; platform->resize(width, height); break; } case APP_CMD_GAINED_FOCUS: { platform->set_focus(true); break; } case APP_CMD_LOST_FOCUS: { platform->set_focus(false); break; } } } bool key_event_filter(const GameActivityKeyEvent *event) { if (event->source == AINPUT_SOURCE_KEYBOARD) { return true; } return false; } bool motion_event_filter(const GameActivityMotionEvent *event) { if ((event->source == AINPUT_SOURCE_MOUSE) || (event->source == AINPUT_SOURCE_TOUCHSCREEN)) { return true; } return false; } } // namespace AndroidPlatform::AndroidPlatform(const PlatformContext &context) : Platform{context} { if (auto *android = dynamic_cast(&context)) { app = android->app; } } ExitCode AndroidPlatform::initialize(const std::vector &plugins) { for (auto plugin : plugins) { plugin->clear_platform(); } android_app_set_key_event_filter(app, key_event_filter); android_app_set_motion_event_filter(app, motion_event_filter); app->onAppCmd = on_app_cmd; app->activity->callbacks->onContentRectChanged = on_content_rect_changed; app->userData = this; auto code = Platform::initialize(plugins); if (code != ExitCode::Success) { return code; } // Wait until the android window is loaded before allowing the app to continue LOGI("Waiting on window surface to be ready"); do { if (!process_android_events(app)) { // Android requested for the app to close LOGI("Android app has been destroyed by the OS"); return ExitCode::Close; } } while (!surface_ready); return ExitCode::Success; } void AndroidPlatform::create_window(const Window::Properties &properties) { // Android window uses native window size // Required so that the vulkan sample can create a VkSurface window = std::make_unique(this, app->window, properties); } void AndroidPlatform::process_android_input_events(void) { auto input_buf = android_app_swap_input_buffers(app); if (!input_buf) { return; } if (input_buf->motionEventsCount) { for (int idx = 0; idx < input_buf->motionEventsCount; idx++) { auto event = &input_buf->motionEvents[idx]; assert((event->source == AINPUT_SOURCE_MOUSE || event->source == AINPUT_SOURCE_TOUCHSCREEN) && "Invalid motion event source"); std::int32_t action = event->action; float x = GameActivityPointerAxes_getX(&event->pointers[0]); float y = GameActivityPointerAxes_getY(&event->pointers[0]); if (event->source == AINPUT_SOURCE_MOUSE) { input_event(MouseButtonInputEvent{ translate_mouse_button(0), translate_mouse_action(action), x, y}); } else if (event->source == AINPUT_SOURCE_TOUCHSCREEN) { // Multiple pointers are not supported. size_t pointer_count = event->pointerCount; std::int32_t pointer_id = event->pointers[0].id; input_event(TouchInputEvent{ pointer_id, pointer_count, translate_touch_action(action), x, y}); } } android_app_clear_motion_events(input_buf); } if (input_buf->keyEventsCount) { for (int idx = 0; idx < input_buf->keyEventsCount; idx++) { auto event = &input_buf->keyEvents[idx]; assert((event->source == AINPUT_SOURCE_KEYBOARD) && "Invalid key event source"); input_event(KeyInputEvent{ translate_key_code(event->keyCode), translate_key_action(event->action)}); } android_app_clear_key_events(input_buf); } } void AndroidPlatform::terminate(ExitCode code) { switch (code) { case ExitCode::Success: case ExitCode::Close: log_output.clear(); break; case ExitCode::FatalError: { const std::string error_message = "Error! Could not launch selected sample:" + get_last_error(); send_notification(error_message); break; } default: break; } while (process_android_events(app)) { // Process events until app->destroyRequested is set } plugins.clear(); Platform::terminate(code); } void AndroidPlatform::send_notification(const std::string &message) { JNIEnv *env; app->activity->vm->AttachCurrentThread(&env, NULL); jclass cls = env->GetObjectClass(app->activity->javaGameActivity); jmethodID fatal_error = env->GetMethodID(cls, "fatalError", "(Ljava/lang/String;)V"); env->CallVoidMethod(app->activity->javaGameActivity, fatal_error, env->NewStringUTF(message.c_str())); app->activity->vm->DetachCurrentThread(); } void AndroidPlatform::set_surface_ready() { surface_ready = true; } GameActivity *AndroidPlatform::get_activity() { return app->activity; } android_app *AndroidPlatform::get_android_app() { return app; } std::vector AndroidPlatform::get_platform_sinks() { std::vector sinks; sinks.push_back(std::make_shared(PROJECT_NAME)); char timestamp[80]; std::time_t time = std::time(0); std::tm now = thread_safe_time(time); std::strftime(timestamp, 80, "%G-%m-%d_%H-%M-%S_log.txt", &now); log_output = vkb::fs::path::get(vkb::fs::path::Logs) + std::string(timestamp); sinks.push_back(std::make_shared(log_output, true)); return sinks; } } // namespace vkb