Skip to content

Commit

Permalink
[Android] Fixed event handling of pause/resume Android app.
Browse files Browse the repository at this point in the history
When an Android app is paused, its ANativeWindow is destroyed (APP_CMD_TERM_WINDOW) and the dependent EGLSurface must be destroyed as well.
When the app resumes, the ANativeWindow is re-initialized (APP_CMD_INIT_WINDOW) and the depednent EGLSurface must be re-created.
This is handled via a Canvas event listener for the AndroidGLSwapChainContext that responds to the new callbacks OnInit and OnDestroy.

Additional notes:
- Deprecated OnQuit and PostQuit functions of Canvas event handlers; This was only intended for desktop platforms when a window close button is pressed.
- Added OnInit and OnDestroy functions to Canvas::EventListener interface.
- Moved Android input event handling from AndroidCanvas.cpp file to new AndroidInputEventHandler.cpp/.h files.
  • Loading branch information
LukasBanana committed Aug 8, 2024
1 parent 03913a8 commit bc8dc02
Show file tree
Hide file tree
Showing 12 changed files with 419 additions and 230 deletions.
45 changes: 29 additions & 16 deletions include/LLGL/Canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,18 @@ class LLGL_EXPORT Canvas : public Surface

friend class Canvas;

//! \deprecated Since 0.04b; Use OnDestroy instead to detect when the canvas is about to be destroyed!
LLGL_DEPRECATED("Deprecated since 0.04b; Use OnDestroy instead!", "OnDestroy")
virtual void OnQuit(Canvas& sender, bool& veto);

//! Sent when the canvas is initialized or re-initialized.
virtual void OnInit(Canvas& sender);

/**
\brief Sent when the canvas is about to quit.
\param[in] sender Specifies the sender of this event.
\param[out] veto Specifies whether to cancel the quit event.
If set to true, the call to \c PostQuit does not change the state \c sender, only the event listeners get informed.
If no event listener sets this parameter to true, \c sender is set into 'Quit' state.
\todo Deprecate \c veto parameter; Mobile apps cannot veto to quit the app.
\brief Sent when the canvas' native object is about to be destroyed.
\remarks The Canvas instance itself may still remain active and receive a subsequent OnInit event to re-initialize the native object.
*/
virtual void OnQuit(Canvas& sender, bool& veto);
virtual void OnDestroy(Canvas& sender);

/**
\brief Sent when the canvas must redraw its content.
Expand Down Expand Up @@ -105,10 +108,8 @@ class LLGL_EXPORT Canvas : public Surface

public:

/**
\brief Returns true if this canvas is in the 'Quit' state.
\see PostQuit
*/
//! \deprecated Since 0.04b; Write a custom 'quit' state for your app instead!
LLGL_DEPRECATED("Deprecated since 0.04b; Use a custom state instead!")
virtual bool HasQuit() const;

//! This default implementation ignores the video mode descriptor completely and always return false.
Expand Down Expand Up @@ -137,13 +138,25 @@ class LLGL_EXPORT Canvas : public Surface
//! Removes the specified event listener from this canvas.
void RemoveEventListener(const EventListener* eventListener);

//! \deprecated Since 0.04b; Use PostDestroy instead.
LLGL_DEPRECATED("Deprecated since 0.04b; Use PostDestroy instead to signal the canvas is about to be destroyed.", "PostDestroy")
void PostQuit();

/**
\brief Posts a 'Quit' event to all event listeners.
\remarks If any of the event listener sets the \c veto flag to false within the \c OnQuit callback, the canvas will \e not be put into 'Quit' state.
\see EventListener::OnQuit
\see HasQuit
\brief Posts a signal that the canvas is initialized or re-initialized.
\remarks A canvas can not only be initialized when the app is launched, but also when the app is resumed, although this is platform dependent.
On Android, this will be signaled on the \c APP_CMD_INIT_WINDOW command.
\see EventListener::OnInit
*/
void PostQuit();
void PostInit();

/**
\brief Posts a signal that the canvas is about to be destroyed.
\remarks A canvas can not only be destroyed when the app is about to close, but also when the app is paused, although this is platform dependent.
On Android, this will be signaled on the \c APP_CMD_TERM_WINDOW command.
\see EventListener::OnDestroy
*/
void PostDestroy();

/**
\brief Posts a draw event to all event listeners.
Expand Down
33 changes: 28 additions & 5 deletions sources/Platform/Android/AndroidApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

#include "AndroidApp.h"
#include "AndroidCanvas.h"
#include "AndroidInputEventHandler.h"
#include "../../Core/Assertion.h"
#include <thread>

Expand All @@ -25,7 +25,17 @@ struct AndroidAppInit
bool isContentReady;
};

static void AndroidAppCmdCallback(android_app* app, int32_t cmd)
static void AndroidAppLoopCmdCallback(android_app* app, int32_t cmd)
{
AndroidInputEventHandler::Get().BroadcastCommand(app, cmd);
}

static std::int32_t AndroidAppLoopInputEventCallback(android_app* app, AInputEvent* event)
{
return AndroidInputEventHandler::Get().BroadcastInputEvent(app, event);
}

static void AndroidAppInitCmdCallback(android_app* app, int32_t cmd)
{
AndroidAppInit* init = reinterpret_cast<AndroidAppInit*>(app->userData);

Expand Down Expand Up @@ -60,7 +70,7 @@ static void WaitUntilNativeWindowIsInitialized(android_app* app)

/* Poll all Android app events */
app->userData = reinterpret_cast<void*>(&init);
app->onAppCmd = AndroidAppCmdCallback;
app->onAppCmd = AndroidAppInitCmdCallback;

int ident = 0, events = 0;
android_poll_source* source = nullptr;
Expand Down Expand Up @@ -94,7 +104,7 @@ static void WaitUntilNativeWindowIsInitialized(android_app* app)
{
/* ... Otherwise, use LLGL specific callback to handle window resize/rotation */
app->userData = nullptr;
app->onAppCmd = AndroidCanvas::OnAndroidAppCommand;
app->onAppCmd = AndroidAppLoopCmdCallback;
}
}

Expand All @@ -116,8 +126,21 @@ void AndroidApp::Initialize(android_app* state)
if (state_->onInputEvent == nullptr)
{
/* Set default event handler */
state_->onInputEvent = AndroidCanvas::OnAndroidAppInputEvent;
state_->onInputEvent = AndroidAppLoopInputEventCallback;
}
}

Extent2D AndroidApp::GetContentRectSize(android_app* appState)
{
if (appState != nullptr)
{
return Extent2D
{
static_cast<std::uint32_t>(appState->contentRect.right - appState->contentRect.left),
static_cast<std::uint32_t>(appState->contentRect.bottom - appState->contentRect.top)
};
}
return Extent2D{};
}


Expand Down
4 changes: 4 additions & 0 deletions sources/Platform/Android/AndroidApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#define LLGL_ANDROID_APP_H


#include <LLGL/Types.h>
#include <android_native_app_glue.h>


Expand All @@ -27,6 +28,9 @@ class AndroidApp

static AndroidApp& Get();

// Returns the size of the content rect of the specified Android app state.
static Extent2D GetContentRectSize(android_app* appState);

// Initializes the Android app state. This should be called once when the device is created.
void Initialize(android_app* state);

Expand Down
184 changes: 4 additions & 180 deletions sources/Platform/Android/AndroidCanvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,186 +7,15 @@

#include "AndroidCanvas.h"
#include "AndroidApp.h"
#include "AndroidKeyCodes.h"
#include "AndroidInputEventHandler.h"
#include "../../Core/CoreUtils.h"
#include <LLGL/Platform/NativeHandle.h>
#include <mutex>
#include <float.h>


namespace LLGL
{


static Extent2D GetAndroidContentSize()
{
if (android_app* app = AndroidApp::Get().GetState())
{
return Extent2D
{
static_cast<std::uint32_t>(app->contentRect.right - app->contentRect.left),
static_cast<std::uint32_t>(app->contentRect.bottom - app->contentRect.top)
};
}
return Extent2D{};
}


/*
* AndroidInputEventHandler
*/

class AndroidInputEventHandler
{

public:

AndroidInputEventHandler(const AndroidInputEventHandler&) = delete;
AndroidInputEventHandler& operator = (const AndroidInputEventHandler&) = delete;

static AndroidInputEventHandler& Get();

void RegisterCanvas(Canvas* canvas);
void UnregisterCanvas(Canvas* canvas);

void BroadcastCommand(android_app* app, int32_t cmd);
std::int32_t BroadcastInputEvent(android_app* app, AInputEvent* event);

private:

AndroidInputEventHandler() = default;

private:

std::mutex lock_;
std::vector<Canvas*> canvases_;
float prevMotionPos_[2] = { -FLT_MAX, -FLT_MAX };

};


AndroidInputEventHandler& AndroidInputEventHandler::Get()
{
static AndroidInputEventHandler instance;
return instance;
}

void AndroidInputEventHandler::RegisterCanvas(Canvas* canvas)
{
std::lock_guard<std::mutex> guard{ lock_ };
canvases_.push_back(canvas);
}

void AndroidInputEventHandler::UnregisterCanvas(Canvas* canvas)
{
std::lock_guard<std::mutex> guard{ lock_ };
RemoveFromList(canvases_, canvas);
}

#define FOREACH_CANVAS_CALL(FUNC) \
do for (Canvas* canvas : canvases_) { canvas->FUNC; } while (false)

void AndroidInputEventHandler::BroadcastCommand(android_app* app, int32_t cmd)
{
switch (cmd)
{
case APP_CMD_TERM_WINDOW:
{
FOREACH_CANVAS_CALL(PostQuit());
}
break;

case APP_CMD_WINDOW_REDRAW_NEEDED:
{
FOREACH_CANVAS_CALL(PostDraw());
}
break;

case APP_CMD_WINDOW_RESIZED:
{
const Extent2D contentSize = GetAndroidContentSize();
FOREACH_CANVAS_CALL(PostResize(contentSize));
}
break;
}
}

std::int32_t AndroidInputEventHandler::BroadcastInputEvent(android_app* app, AInputEvent* event)
{
std::lock_guard<std::mutex> guard{ lock_ };

switch (AInputEvent_getType(event))
{
case AINPUT_EVENT_TYPE_KEY:
{
const std::int32_t keyCode = AKeyEvent_getKeyCode(event);
const Key key = MapAndroidKeyCode(keyCode);

switch (AKeyEvent_getAction(event))
{
case AKEY_EVENT_ACTION_DOWN:
{
FOREACH_CANVAS_CALL(PostKeyDown(key));
}
break;

case AKEY_EVENT_ACTION_UP:
{
FOREACH_CANVAS_CALL(PostKeyUp(key));
}
break;
}
}
return 1;

case AINPUT_EVENT_TYPE_MOTION:
{
const float posX = AMotionEvent_getX(event, 0);
const float posY = AMotionEvent_getY(event, 0);

const LLGL::Offset2D position{ static_cast<std::int32_t>(posX), static_cast<std::int32_t>(posY) };
const std::uint32_t numTouches = static_cast<std::uint32_t>(AMotionEvent_getPointerCount(event));

switch (AMotionEvent_getAction(event))
{
case AMOTION_EVENT_ACTION_DOWN:
{
/* Initialize previous position with current position when first touch down is detected */
prevMotionPos_[0] = posX;
prevMotionPos_[1] = posY;
FOREACH_CANVAS_CALL(PostPanGesture(position, numTouches, 0.0f, 0.0f, EventAction::Began));
}
break;

case AMOTION_EVENT_ACTION_MOVE:
{
/* Determine motion delta by difference between current and previous position */
const float posXPrec = AMotionEvent_getXPrecision(event);
const float posYPrec = AMotionEvent_getYPrecision(event);
const float dx = (posX - prevMotionPos_[0]) / posXPrec;
const float dy = (posY - prevMotionPos_[1]) / posYPrec;
prevMotionPos_[0] = posX;
prevMotionPos_[1] = posY;
FOREACH_CANVAS_CALL(PostPanGesture(position, numTouches, dx, dy, EventAction::Changed));
}
break;

case AMOTION_EVENT_ACTION_UP:
{
FOREACH_CANVAS_CALL(PostPanGesture(position, numTouches, 0.0f, 0.0f, EventAction::Ended));
}
break;
}
}
return 1;
}

return 0;
}

#undef FOREACH_CANVAS_CALL


/*
* Surface class
*/
Expand Down Expand Up @@ -260,7 +89,7 @@ bool AndroidCanvas::GetNativeHandle(void* nativeHandle, std::size_t nativeHandle

Extent2D AndroidCanvas::GetContentSize() const
{
return GetAndroidContentSize();
return AndroidApp::GetContentRectSize(AndroidApp::Get().GetState());
}

void AndroidCanvas::SetTitle(const UTF8String& title)
Expand All @@ -273,14 +102,9 @@ UTF8String AndroidCanvas::GetTitle() const
return {}; //todo...
}

void AndroidCanvas::OnAndroidAppCommand(android_app* app, int32_t cmd)
{
AndroidInputEventHandler::Get().BroadcastCommand(app, cmd);
}

std::int32_t AndroidCanvas::OnAndroidAppInputEvent(android_app* app, AInputEvent* event)
void AndroidCanvas::UpdateNativeWindow(android_app* app)
{
return AndroidInputEventHandler::Get().BroadcastInputEvent(app, event);
window_ = (app != nullptr ? app->window : nullptr);
}


Expand Down
7 changes: 5 additions & 2 deletions sources/Platform/Android/AndroidCanvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ class AndroidCanvas : public Canvas

public:

static void OnAndroidAppCommand(android_app* app, int32_t cmd);
static std::int32_t OnAndroidAppInputEvent(android_app* app, AInputEvent* event);
/*
Updates the pointer to ANativeWindow from the specified app state.
If the input is null, the window will be reset to null.
*/
void UpdateNativeWindow(android_app* app);

private:

Expand Down
Loading

0 comments on commit bc8dc02

Please sign in to comment.