diff --git a/libs/android/app/AndroidManifest.xml.in b/libs/android/app/AndroidManifest.xml.in
new file mode 100644
index 00000000..d5c80b40
--- /dev/null
+++ b/libs/android/app/AndroidManifest.xml.in
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/android/app/psemek.keystore b/libs/android/app/psemek.keystore
new file mode 100644
index 00000000..5f7f7d59
Binary files /dev/null and b/libs/android/app/psemek.keystore differ
diff --git a/libs/android/app/src/psemek/app/MainActivity.java.in b/libs/android/app/src/psemek/app/MainActivity.java.in
new file mode 100644
index 00000000..62670b10
--- /dev/null
+++ b/libs/android/app/src/psemek/app/MainActivity.java.in
@@ -0,0 +1,94 @@
+package psemek.app;
+
+import android.app.Activity;
+import android.app.ActionBar;
+import android.content.Context;
+import android.opengl.GLSurfaceView;
+import android.opengl.GLSurfaceView.Renderer;
+import android.view.WindowInsetsController;
+import android.view.WindowInsets.Type;
+import android.view.MotionEvent;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import android.opengl.GLES30;
+import android.os.Bundle;
+import java.lang.System;
+
+public class MainActivity extends Activity {
+
+ class RendererImpl implements Renderer {
+
+ @Override
+ public void onSurfaceCreated(GL10 gl10, EGLConfig config) {
+ MainActivity.this.nativeApp.init();
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl10, int width, int height) {
+ MainActivity.this.nativeApp.resize(width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl10) {
+ MainActivity.this.nativeApp.drawFrame();
+ }
+
+ }
+
+ class ViewImpl extends GLSurfaceView {
+
+ private final RendererImpl renderer;
+
+ public ViewImpl(Context context) {
+ super(context);
+ setEGLContextClientVersion(3);
+ setEGLConfigChooser(8, 8, 8, 8, 24, 8);
+ renderer = new RendererImpl();
+ setRenderer(renderer);
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ if (e.getAction() == MotionEvent.ACTION_DOWN) {
+ MainActivity.this.nativeApp.touch((int)e.getX(), (int)e.getY());
+ }
+ return true;
+ }
+
+ }
+
+ private PsemekApplication nativeApp;
+ private ViewImpl view;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ System.loadLibrary("boost_random");
+ System.loadLibrary("TARGET_NAME");
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+
+ nativeApp = new PsemekApplication(this.getAssets());
+
+ view = new ViewImpl(this);
+ setContentView(view);
+
+ WindowInsetsController windowInsetsController = view.getWindowInsetsController();
+ if (windowInsetsController != null) {
+ windowInsetsController.hide(Type.systemBars());
+ windowInsetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ nativeApp.destroy();
+ super.onDestroy();
+ }
+
+}
diff --git a/libs/android/app/src/psemek/app/PsemekApplication.java b/libs/android/app/src/psemek/app/PsemekApplication.java
new file mode 100644
index 00000000..3a089736
--- /dev/null
+++ b/libs/android/app/src/psemek/app/PsemekApplication.java
@@ -0,0 +1,136 @@
+package psemek.app;
+import android.util.Log;
+import android.content.res.AssetManager;
+import android.media.AudioTrack;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.Arrays;
+import java.lang.Thread;
+
+public class PsemekApplication {
+
+ private AudioTrack audioTrack;
+ private Thread audioThread;
+ private long nativeApp;
+
+ private static native void setupLogging();
+ private static native void setAssetManager(AssetManager assetManager);
+
+ private static native long createNativeApp();
+ private static native void destroyNativeApp(long ptr);
+ private static native void resizeNative(long ptr, int width, int height);
+ private static native void touchNative(long ptr, int x, int y);
+ private static native void drawFrameNative(long ptr);
+
+ private static native int audioFrequencyNative();
+ private static native int audioGetSamples(float buffer[], int sampleOffset, int sampleCount);
+
+ private class StreamEventCallbackImpl extends AudioTrack.StreamEventCallback {
+
+ private float buffer[];
+
+ public StreamEventCallbackImpl() {
+ super();
+ buffer = new float[1024];
+ }
+
+ @Override
+ public void onDataRequest(AudioTrack track, int sizeInFrames) {
+ Log.e("psemek", "Requested audio " + sizeInFrames);
+ int sizeInSamples = sizeInFrames * 2;
+ if (buffer.length < sizeInSamples) {
+ buffer = Arrays.copyOf(buffer, sizeInSamples);
+ }
+
+ int samples = PsemekApplication.audioGetSamples(buffer, 0, buffer.length);
+ track.write(buffer, 0, samples, AudioTrack.WRITE_BLOCKING);
+ }
+
+ }
+
+ private class AudioThreadImpl extends Thread {
+
+ private float buffer[];
+
+ public AudioThreadImpl(int bufferSizeInFrames) {
+ super("audio");
+
+ buffer = new float[bufferSizeInFrames * 2];
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ int samples = PsemekApplication.audioGetSamples(buffer, 0, buffer.length);
+ PsemekApplication.this.audioTrack.write(buffer, 0, samples, AudioTrack.WRITE_BLOCKING);
+ try {
+ Thread.sleep(1);
+ }
+ catch (InterruptedException e) {}
+ }
+ }
+ }
+
+ public PsemekApplication(AssetManager assetManager) {
+ setupLogging();
+ setAssetManager(assetManager);
+
+ AudioAttributes audioAttributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_GAME)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build();
+
+ AudioFormat audioFormat = new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
+ .setSampleRate(audioFrequencyNative())
+ .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+ .build();
+
+ int bufferSize = AudioTrack.getMinBufferSize(audioFrequencyNative(), AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT);
+
+ audioTrack = new AudioTrack.Builder()
+ .setAudioAttributes(audioAttributes)
+ .setAudioFormat(audioFormat)
+ .setBufferSizeInBytes(bufferSize)
+ .setTransferMode(AudioTrack.MODE_STREAM)
+ .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
+ .build();
+
+ audioThread = new AudioThreadImpl(audioTrack.getBufferCapacityInFrames());
+ audioThread.start();
+
+ audioTrack.play();
+ }
+
+ public void init() {
+ nativeApp = createNativeApp();
+ }
+
+ public void resize(int width, int height) {
+ resizeNative(nativeApp, width, height);
+ }
+
+ public void touch(int x, int y)
+ {
+ touchNative(nativeApp, x, y);
+ }
+
+ public void drawFrame() {
+ if (nativeApp != 0) {
+ drawFrameNative(nativeApp);
+ }
+ }
+
+ public void destroy() {
+ audioThread.stop();
+ if (nativeApp != 0)
+ destroyNativeApp(nativeApp);
+ nativeApp = 0;
+ }
+
+}
diff --git a/libs/android/source/resource.cpp b/libs/android/source/resource.cpp
index 7d554a0a..90694505 100644
--- a/libs/android/source/resource.cpp
+++ b/libs/android/source/resource.cpp
@@ -41,7 +41,7 @@ namespace psemek::app
std::unique_ptr open_resource(std::filesystem::path const & relative_path)
{
- log::error() << "Opening resource " << relative_path;
+ log::info() << "Opening resource " << relative_path;
auto asset = AAssetManager_open(assetManager, relative_path.c_str(), AASSET_MODE_STREAMING);
if (!asset)
diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt
index 32ec3027..ac851340 100644
--- a/package/CMakeLists.txt
+++ b/package/CMakeLists.txt
@@ -30,7 +30,13 @@ function(psemek_package_output_path target outvar)
message(FATAL "psemek_package_output_path must only be called during packaging")
endif()
- set(${outvar} "${CMAKE_CURRENT_LIST_DIR}/${PSEMEK_PACKAGE_OUTPUT_PATH}/${target}${PSEMEK_PACKAGE_VERSION_SUFFIX}-${PSEMEK_PACKAGE_SUFFIX}.zip" PARENT_SCOPE)
+ if(ANDROID)
+ set(_PACKAGE_EXTENSION apk)
+ else()
+ set(_PACKAGE_EXTENSION zip)
+ endif()
+
+ set(${outvar} "${CMAKE_CURRENT_LIST_DIR}/${PSEMEK_PACKAGE_OUTPUT_PATH}/${target}${PSEMEK_PACKAGE_VERSION_SUFFIX}-${PSEMEK_PACKAGE_SUFFIX}.${_PACKAGE_EXTENSION}" PARENT_SCOPE)
endfunction()
function(psemek_add_executable_impl target is_application)
@@ -71,12 +77,17 @@ function(psemek_add_executable_impl target is_application)
)
endif()
- if(NOT ANDROID)
- psemek_package_output_path(${target} _OUTPUT_PATH)
+ psemek_package_output_path(${target} _OUTPUT_PATH)
+ if(NOT ANDROID)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND echo Packaging target ${target} into ${_OUTPUT_PATH}
COMMAND zip -v "${_OUTPUT_PATH}" -j $ ${PSEMEK_PACKAGE_COPY_FILES}
+ COMMAND echo Packaged target ${target} into ${_OUTPUT_PATH}
+ )
+ else()
+ add_custom_command(TARGET ${target} POST_BUILD
+ COMMAND ${PSEMEK_PACKAGE_HELPER} "${target}" "${PSEMEK_APPLICATION_NAME}" "${_OUTPUT_PATH}"
)
endif()
endif()
@@ -151,10 +162,11 @@ function(psemek_package_files target)
if(PSEMEK_PACKAGE_MODE)
if(PSEMEK_PACKAGE_TARGET)
if(ANDROID)
-
+ add_custom_command(TARGET ${target} POST_BUILD
+ WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
+ COMMAND ${PSEMEK_COPY_FILES} ${ARGN}
+ )
else()
- psemek_package_output_path(${target} _OUTPUT_PATH)
-
add_custom_command(TARGET ${target} POST_BUILD
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
COMMAND zip -v "${_OUTPUT_PATH}" -r ${ARGN}
diff --git a/package/android/Dockerfile b/package/android/Dockerfile
new file mode 100644
index 00000000..b32c61e7
--- /dev/null
+++ b/package/android/Dockerfile
@@ -0,0 +1,62 @@
+FROM ubuntu:22.04
+
+# Install tools
+RUN apt-get update && apt-get upgrade -y
+RUN apt-get install -y \
+ build-essential cmake git default-jre openjdk-19-jdk \
+ libxext-dev libgl-dev \
+ wget zip zstd \
+ libpng-dev libboost-all-dev \
+ libxi-dev libxrender-dev
+
+# Set user
+RUN useradd -u 1000 -U -d /home -M worker
+RUN chown -R worker:worker /home /usr/local
+USER worker
+
+# Install android sdkmanager
+RUN mkdir -v /home/sdk
+WORKDIR /home/sdk
+RUN wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip && unzip commandlinetools-linux-9477386_latest.zip && rm commandlinetools-linux-9477386_latest.zip
+RUN mv -v cmdline-tools latest
+RUN mkdir -v cmdline-tools
+RUN mv -v latest cmdline-tools
+
+# Install sdk
+RUN yes | cmdline-tools/latest/bin/sdkmanager "build-tools;34.0.0" "platforms;android-34"
+
+# Install ndk separately
+RUN wget https://dl.google.com/android/repository/android-ndk-r26-beta1-linux.zip && unzip android-ndk-r26-beta1-linux.zip && rm android-ndk-r26-beta1-linux.zip
+RUN mkdir -v ndk
+RUN mv -v android-ndk-r26-beta1 ndk/26.0.10404224-beta1
+
+# Env variables
+ENV SDK_ROOT=/home/sdk
+ENV BUILD_TOOLS_ROOT=${SDK_ROOT}/build-tools/34.0.0
+ENV NDK_ROOT=${SDK_ROOT}/ndk/26.0.10404224-beta1
+ENV PLATFORM_ROOT=${SDK_ROOT}/platforms/android-34
+ENV PNG_ROOT=/home/png/install
+ENV BOOST_ROOT=/home/boost/install
+
+# Build libpng
+RUN mkdir -v /home/png
+WORKDIR /home/png
+RUN git clone https://github.com/glennrp/libpng.git -b libpng16 --depth 1 source
+RUN mkdir -v build install
+RUN cmake -S source -B build -DCMAKE_INSTALL_PREFIX=install/arm64-v8a -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" \
+ -DANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON -DANDROID_PLATFORM=34 -DANDROID_STL=c++_shared -DANDROID_CPP_FEATURES="rtti exceptions" -DANDROID_ABI=arm64-v8a
+RUN cmake --build build -t install -j
+
+# Build boost
+RUN mkdir -v /home/boost
+WORKDIR /home/boost
+RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.82.0/source/boost_1_82_0.tar.gz && tar xvf boost_1_82_0.tar.gz && rm boost_1_82_0.tar.gz
+RUN mv -v boost_1_82_0 source
+RUN cd source && ./bootstrap.sh --with-libraries=random --prefix=../install/arm64-v8a
+RUN mkdir -p build install
+RUN echo "using clang : android : ${NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++ --target=aarch64-none-linux-android34 --sysroot=${NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/sysroot ;" > user-config.jam
+RUN cd source && ./b2 toolset=clang-android target-os=android architecture=arm variant=release link=shared threading=single cxxflags=-fPIC --user-config=../user-config.jam --build-dir=../build install
+
+# Finalize
+WORKDIR /home
+COPY package.sh package-helper.sh copy-files.sh ./
\ No newline at end of file
diff --git a/package/android/copy-files.sh b/package/android/copy-files.sh
new file mode 100755
index 00000000..62af28ef
--- /dev/null
+++ b/package/android/copy-files.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -e
+
+cp -rv "$@" /home/app/assets/
\ No newline at end of file
diff --git a/package/android/package-helper.sh b/package/android/package-helper.sh
new file mode 100755
index 00000000..72829c63
--- /dev/null
+++ b/package/android/package-helper.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+set -e
+
+if [ "$#" -ne 3 ]; then
+ echo "Usage: package-helper "
+ exit -1
+fi
+
+echo "$1" > /home/target-name
+echo "$2" > /home/application-name
+echo "$3" > /home/apk-name
\ No newline at end of file
diff --git a/package/android/package.sh b/package/android/package.sh
new file mode 100755
index 00000000..fdbf3b45
--- /dev/null
+++ b/package/android/package.sh
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+
+set -e
+
+mkdir build-host tools
+cmake -S source -B build-host -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=tools -DPSEMEK_PACKAGE_MODE=ON -DPSEMEK_PACKAGE_HOST=ON -DPSEMEK_BACKEND=OFF
+cmake --build build-host -t install -j
+
+cp -r source/psemek/libs/android/app .
+
+mkdir -p app/assets
+
+mkdir -p build-target/arm64-v8a
+cmake -S source -B build-target/arm64-v8a/build \
+ -DCMAKE_BUILD_TYPE=Release -DANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON -DANDROID_PLATFORM=34 -DANDROID_STL=c++_shared -DANDROID_CPP_FEATURES="rtti exceptions" \
+ -DANDROID_ABI=arm64-v8a -DCMAKE_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" \
+ -DCMAKE_FIND_ROOT_PATH="${BOOST_ROOT}/arm64-v8a;${PNG_ROOT}/arm64-v8a;$(pwd)/tools" -DCMAKE_INSTALL_PREFIX="build-target/arm64-v8a/install" \
+ -DPSEMEK_BACKEND=ANDROID -DPSEMEK_PACKAGE_MODE=ON -DPSEMEK_PACKAGE_TARGET=ON -DPSEMEK_PACKAGE_TOOLS_PATH="" -DPSEMEK_PACKAGE_HELPER=$(pwd)/package-helper.sh -DPSEMEK_COPY_FILES=$(pwd)/copy-files.sh
+cmake --build build-target/arm64-v8a/build -t install -j
+
+mkdir -p app/lib/arm64-v8a
+cp -v build-target/arm64-v8a/install/lib/*.so app/lib/arm64-v8a/
+cp -v ${NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so app/lib/arm64-v8a/
+cp -v ${BOOST_ROOT}/arm64-v8a/lib/*.so app/lib/arm64-v8a/
+cp -v ${PNG_ROOT}/arm64-v8a/lib/*.so app/lib/arm64-v8a/
+
+cd app
+
+cp ./AndroidManifest.xml.in ./AndroidManifest.xml
+sed -i -e "s/APPLICATION_NAME/$(cat ../application-name)/g" ./AndroidManifest.xml
+
+export TARGET_NAME=$(cat ../target-name)
+export APK_NAME=$(cat ../apk-name)
+
+cp src/psemek/app/MainActivity.java.in src/psemek/app/MainActivity.java
+sed -i -e "s/TARGET_NAME/${TARGET_NAME}/g" src/psemek/app/MainActivity.java
+
+mkdir -p dex bin
+
+${BUILD_TOOLS_ROOT}/aapt package -f -m -J ./src -M ./AndroidManifest.xml -I ${PLATFORM_ROOT}/android.jar
+
+javac -d obj -classpath src -classpath ${PLATFORM_ROOT}/android.jar src/psemek/app/*.java
+
+${BUILD_TOOLS_ROOT}/d8 --output ./dex obj/psemek/app/*.class
+
+${BUILD_TOOLS_ROOT}/aapt package -f -m -F ./bin/${TARGET_NAME}.unaligned.apk -M ./AndroidManifest.xml -I ${PLATFORM_ROOT}/android.jar
+zip -r ./bin/${TARGET_NAME}.unaligned.apk assets
+${BUILD_TOOLS_ROOT}/aapt add ./bin/${TARGET_NAME}.unaligned.apk lib/arm64-v8a/*
+cd dex
+${BUILD_TOOLS_ROOT}/aapt add ../bin/${TARGET_NAME}.unaligned.apk classes.dex
+cd ..
+
+${BUILD_TOOLS_ROOT}/zipalign -f 4 ./bin/${TARGET_NAME}.unaligned.apk "${APK_NAME}"
+
+${BUILD_TOOLS_ROOT}/apksigner sign --ks psemek.keystore --ks-pass "pass:${PSEMEK_KEYSTORE_PASSWORD}" "${APK_NAME}"
+
+echo "Packaged target ${TARGET_NAME} into ${APK_NAME}"
\ No newline at end of file
diff --git a/package/bin/psemek-package b/package/bin/psemek-package
new file mode 100755
index 00000000..296993a9
--- /dev/null
+++ b/package/bin/psemek-package
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+set -e
+
+if [ "$#" -ne 2 ]; then
+ echo "Usage: psemek-package "
+ echo "Supported platforms:"
+ echo " linux"
+ echo " windows"
+ echo " android"
+ exit 0
+fi
+
+case ${1} in
+ linux) ;;
+ windows) ;;
+ android) ;;
+ *)
+ echo "Unknown platform: ${1}"
+ exit -1
+esac
+
+PROJECT_DIR=`realpath "${2}"`
+
+docker run -u 1000 -v "${PROJECT_DIR}":/home/source -e PSEMEK_KEYSTORE_PASSWORD=${PSEMEK_KEYSTORE_PASSWORD} lisyarus/psemek:package-${1} /home/package.sh
+
+echo Packaging finished
diff --git a/package/bin/psemek-package-linux b/package/bin/psemek-package-linux
deleted file mode 100755
index 648fbba5..00000000
--- a/package/bin/psemek-package-linux
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$#" -ne 1 ]; then
- echo "Usage: psemek-package-linux "
- exit 0
-fi
-
-PROJECT_DIR=`realpath "${1}"`
-
-docker run -u 1000 -v "${PROJECT_DIR}":/home/source lisyarus/psemek:package-linux /home/package.sh
-
-echo Packaging finished
diff --git a/package/bin/psemek-package-win b/package/bin/psemek-package-win
deleted file mode 100755
index e3531c3e..00000000
--- a/package/bin/psemek-package-win
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$#" -ne 1 ]; then
- echo "Usage: psemek-package-win "
- exit 0
-fi
-
-PROJECT_DIR=`realpath "${1}"`
-
-docker run -u 1000 -v "${PROJECT_DIR}":/home/source lisyarus/psemek:package-win /home/package.sh
-
-echo Packaging finished