/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/ui/window_android.h" #include #include #include #include #include #include #include "xenia/base/assert.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/ui/surface_android.h" #include "xenia/ui/windowed_app_context_android.h" namespace xe { namespace ui { std::unique_ptr Window::Create(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) { return std::make_unique( app_context, title, desired_logical_width, desired_logical_height); } AndroidWindow::~AndroidWindow() { EnterDestructor(); auto& android_app_context = static_cast(app_context()); if (android_app_context.GetActivityWindow() == this) { android_app_context.SetActivityWindow(nullptr); } } void AndroidWindow::OnActivitySurfaceLayoutChange() { auto& android_app_context = static_cast(app_context()); assert_true(android_app_context.GetActivityWindow() == this); uint32_t physical_width = uint32_t(android_app_context.window_surface_layout_right() - android_app_context.window_surface_layout_left()); uint32_t physical_height = uint32_t(android_app_context.window_surface_layout_bottom() - android_app_context.window_surface_layout_top()); OnDesiredLogicalSizeUpdate(SizeToLogical(physical_width), SizeToLogical(physical_height)); WindowDestructionReceiver destruction_receiver(this); OnActualSizeUpdate(physical_width, physical_height, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } bool AndroidWindow::OnActivitySurfaceMotionEvent(jobject event) { auto& android_app_context = static_cast(app_context()); JNIEnv* jni_env = android_app_context.ui_thread_jni_env(); const AndroidWindowedAppContext::JniIDs& jni_ids = android_app_context.jni_ids(); int32_t source = jni_env->CallIntMethod(event, jni_ids.motion_event_get_source); switch (source) { case AINPUT_SOURCE_TOUCHSCREEN: { // Returning true for all touch events regardless of whether they have // been handled to keep receiving touch events for the pointers in the // event (if returning false for a down event, no more events will be sent // for the pointers in it), and also because there are multiple pointers // in a single event, and different handler invocations may result in // different is_handled. WindowDestructionReceiver destruction_receiver(this); int32_t action_and_pointer_index = jni_env->CallIntMethod(event, jni_ids.motion_event_get_action); int32_t action = action_and_pointer_index & AMOTION_EVENT_ACTION_MASK; // For pointer ACTION_POINTER_DOWN and ACTION_POINTER_UP. int32_t pointer_index = (action_and_pointer_index & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; if (action == AMOTION_EVENT_ACTION_POINTER_DOWN || action == AMOTION_EVENT_ACTION_POINTER_UP) { int32_t touch_pointer_index = (action_and_pointer_index & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; TouchEvent e( this, jni_env->CallIntMethod(event, jni_ids.motion_event_get_pointer_id, touch_pointer_index), (action == AMOTION_EVENT_ACTION_POINTER_DOWN) ? TouchEvent::Action::kDown : TouchEvent::Action::kUp, jni_env->CallFloatMethod(event, jni_ids.motion_event_get_x, pointer_index), jni_env->CallFloatMethod(event, jni_ids.motion_event_get_y, pointer_index)); OnTouchEvent(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } else { TouchEvent::Action touch_event_action; switch (action) { case AMOTION_EVENT_ACTION_DOWN: touch_event_action = TouchEvent::Action::kDown; break; case AMOTION_EVENT_ACTION_UP: touch_event_action = TouchEvent::Action::kUp; break; case AMOTION_EVENT_ACTION_MOVE: touch_event_action = TouchEvent::Action::kMove; break; case AMOTION_EVENT_ACTION_CANCEL: touch_event_action = TouchEvent::Action::kCancel; break; default: return true; } int32_t touch_pointer_count = jni_env->CallIntMethod( event, jni_ids.motion_event_get_pointer_count); for (int32_t i = 0; i < touch_pointer_count; ++i) { TouchEvent e( this, jni_env->CallIntMethod(event, jni_ids.motion_event_get_pointer_id, i), touch_event_action, jni_env->CallFloatMethod(event, jni_ids.motion_event_get_x, i), jni_env->CallFloatMethod(event, jni_ids.motion_event_get_y, i)); OnTouchEvent(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } } return true; } break; case AINPUT_SOURCE_MOUSE: { WindowDestructionReceiver destruction_receiver(this); // X and Y can be outside the View (have negative coordinates, or beyond // the size of the element), and not only for ACTION_HOVER_EXIT (it's // predeced by ACTION_HOVER_MOVE, at least on Android API level 30, also // with out-of-bounds coordinates), when moving the mouse outside the // View, or when starting moving the mouse when the pointer was previously // outside the View in some cases. int32_t mouse_x = int32_t( std::min(float(GetActualPhysicalWidth()), std::max(0.0f, jni_env->CallFloatMethod( event, jni_ids.motion_event_get_x, 0))) + 0.5f); int32_t mouse_y = int32_t( std::min(float(GetActualPhysicalHeight()), std::max(0.0f, jni_env->CallFloatMethod( event, jni_ids.motion_event_get_y, 0))) + 0.5f); static const MouseEvent::Button kMouseEventButtons[] = { MouseEvent::Button::kLeft, MouseEvent::Button::kRight, MouseEvent::Button::kMiddle, MouseEvent::Button::kX1, MouseEvent::Button::kX2, }; static constexpr uint32_t kUsedMouseButtonMask = (UINT32_C(1) << xe::countof(kMouseEventButtons)) - 1; uint32_t new_mouse_button_state = uint32_t( jni_env->CallIntMethod(event, jni_ids.motion_event_get_button_state)); // OnMouseUp. uint32_t mouse_buttons_remaining = mouse_button_state_ & ~new_mouse_button_state & kUsedMouseButtonMask; uint32_t mouse_button_index; while ( xe::bit_scan_forward(mouse_buttons_remaining, &mouse_button_index)) { mouse_buttons_remaining &= ~(UINT32_C(1) << mouse_button_index); MouseEvent e(this, kMouseEventButtons[mouse_button_index], mouse_x, mouse_y); OnMouseUp(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } // Generic OnMouseMove regardless of the action since any event can // provide new coordinates. { MouseEvent e(this, MouseEvent::Button::kNone, mouse_x, mouse_y); OnMouseMove(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } // OnMouseWheel. // The axis value may be outside -1...1 if multiple scrolls have occurred // quickly. int32_t scroll_x = int32_t( jni_env->CallFloatMethod(event, jni_ids.motion_event_get_axis_value, AMOTION_EVENT_AXIS_HSCROLL, 0) * float(MouseEvent::kScrollPerDetent)); int32_t scroll_y = int32_t( jni_env->CallFloatMethod(event, jni_ids.motion_event_get_axis_value, AMOTION_EVENT_AXIS_VSCROLL, 0) * float(MouseEvent::kScrollPerDetent)); if (scroll_x || scroll_y) { MouseEvent e(this, MouseEvent::Button::kNone, mouse_x, mouse_y, scroll_x, scroll_y); OnMouseWheel(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } // OnMouseDown. mouse_buttons_remaining = new_mouse_button_state & ~mouse_button_state_ & kUsedMouseButtonMask; while ( xe::bit_scan_forward(mouse_buttons_remaining, &mouse_button_index)) { mouse_buttons_remaining &= ~(UINT32_C(1) << mouse_button_index); MouseEvent e(this, kMouseEventButtons[mouse_button_index], mouse_x, mouse_y); OnMouseDown(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } // Update the button state for state differences. mouse_button_state_ = new_mouse_button_state; return true; } break; } return false; } uint32_t AndroidWindow::GetLatestDpiImpl() const { auto& android_app_context = static_cast(app_context()); return android_app_context.GetPixelDensity(); } bool AndroidWindow::OpenImpl() { // Reset the input. mouse_button_state_ = 0; // The window is a proxy between the main activity and Xenia, so there can be // only one open window for an activity. auto& android_app_context = static_cast(app_context()); AndroidWindow* previous_activity_window = android_app_context.GetActivityWindow(); assert_null(previous_activity_window); if (previous_activity_window) { // Don't detach the old window as it's assuming it's still attached while // it's in an open phase. return false; } android_app_context.SetActivityWindow(this); // Report the initial layout. OnActivitySurfaceLayoutChange(); return true; } void AndroidWindow::RequestCloseImpl() { // Finishing the Activity would cause the entire WindowedAppContext to quit, // which is not the same behavior as on other platforms - the // WindowedAppContext quit is done explicitly by the listeners during // OnClosing if needed (the main Window is potentially being changed to a // different one, for instance). Therefore, only detach the window from the // app context. WindowDestructionReceiver destruction_receiver(this); OnBeforeClose(destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } OnAfterClose(); auto& android_app_context = static_cast(app_context()); if (android_app_context.GetActivityWindow() == this) { android_app_context.SetActivityWindow(nullptr); } } std::unique_ptr AndroidWindow::CreateSurfaceImpl( Surface::TypeFlags allowed_types) { if (allowed_types & Surface::kTypeFlag_AndroidNativeWindow) { auto& android_app_context = static_cast(app_context()); assert_true(android_app_context.GetActivityWindow() == this); ANativeWindow* activity_window_surface = android_app_context.GetWindowSurface(); if (activity_window_surface) { return std::make_unique( activity_window_surface); } } return nullptr; } void AndroidWindow::RequestPaintImpl() { auto& android_app_context = static_cast(app_context()); assert_true(android_app_context.GetActivityWindow() == this); android_app_context.PostInvalidateWindowSurface(); } std::unique_ptr MenuItem::Create(Type type, const std::string& text, const std::string& hotkey, std::function callback) { return std::make_unique(type, text, hotkey, callback); } } // namespace ui } // namespace xe