Skip to content

Commit

Permalink
WIP: Support starting local server on Android
Browse files Browse the repository at this point in the history
Compile server as separate library.

Use foreground service to run native main function of server using JNI, avoiding dependency on SDL.

Server service must run in a different process because it needs to be terminated to restart the server.

Add detailed description why the service uses `android:foregroundServiceType="specialUse"` as this must convince the reviewer that the use case is valid. None of the other foreground service types cover hosting local game servers or anything related.

Support stopping server via notification action.
  • Loading branch information
Robyt3 committed Dec 14, 2024
1 parent 7e30a09 commit b258f22
Show file tree
Hide file tree
Showing 16 changed files with 649 additions and 108 deletions.
40 changes: 29 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -689,8 +689,8 @@ endif()
if(CLIENT AND NOT(SDL2_FOUND))
message(SEND_ERROR "You must install SDL2 to compile the ${CMAKE_PROJECT_NAME} client")
endif()
if(TARGET_OS STREQUAL "android" AND CLIENT AND NOT(CRYPTO_FOUND))
message(SEND_ERROR "You must install OpenSSL to compile the ${CMAKE_PROJECT_NAME} client")
if(TARGET_OS STREQUAL "android" AND NOT(CRYPTO_FOUND))
message(SEND_ERROR "You must install OpenSSL to compile ${CMAKE_PROJECT_NAME}")
endif()
if(NOT(GTEST_FOUND))
if(DOWNLOAD_GTEST)
Expand Down Expand Up @@ -741,7 +741,7 @@ elseif(TARGET_OS STREQUAL "android")
src/android/android_main.cpp
)
set(PLATFORM_LIBS ${TW_ANDROID_LIBS})
set(PLATFORM_CLIENT_LIBS ${PLATFORM_LIBS})
set(PLATFORM_CLIENT_LIBS ${TW_ANDROID_LIBS_CLIENT})
set(PLATFORM_CLIENT_INCLUDE_DIRS)
else()
find_package(Notify)
Expand Down Expand Up @@ -2782,14 +2782,25 @@ if(SERVER)
)

# Target
add_executable(game-server
${DEPS}
${SERVER_SRC}
${SERVER_ICON}
$<TARGET_OBJECTS:engine-shared>
$<TARGET_OBJECTS:game-shared>
$<TARGET_OBJECTS:rust-bridge-shared>
)
if(TARGET_OS STREQUAL "android")
add_library(game-server SHARED
${DEPS}
${SERVER_SRC}
${SERVER_ICON}
$<TARGET_OBJECTS:engine-shared>
$<TARGET_OBJECTS:game-shared>
$<TARGET_OBJECTS:rust-bridge-shared>
)
else()
add_executable(game-server
${DEPS}
${SERVER_SRC}
${SERVER_ICON}
$<TARGET_OBJECTS:engine-shared>
$<TARGET_OBJECTS:game-shared>
$<TARGET_OBJECTS:rust-bridge-shared>
)
endif()
set_property(TARGET game-server
PROPERTY OUTPUT_NAME ${SERVER_EXECUTABLE}
)
Expand Down Expand Up @@ -3600,6 +3611,13 @@ foreach(target ${TARGETS_OWN})
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
target_compile_definitions(${target} PRIVATE CONF_WEBASM)
endif()
if(TARGET_OS STREQUAL "android")
if(ANDROID_PACKAGE_NAME)
target_compile_definitions(${target} PRIVATE ANDROID_PACKAGE_NAME=${ANDROID_PACKAGE_NAME})
else()
message(FATAL_ERROR "ANDROID_PACKAGE_NAME must define the package name when compiling for Android (using underscores instead of dots, e.g. org_example_app)")
endif()
endif()
endforeach()

foreach(target ${TARGETS_DEP})
Expand Down
14 changes: 13 additions & 1 deletion cmake/FindAndroid.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,16 @@ FIND_LIBRARY(ANDROID_LIBRARY_OPENSLES
OpenSLES
)

set(TW_ANDROID_LIBS ${ANDROID_LIBRARY_EGL} ${ANDROID_LIBRARY_GLES1} ${ANDROID_LIBRARY_GLES2} ${ANDROID_LIBRARY_GLES3} ${ANDROID_LIBRARY_LOG} ${ANDROID_LIBRARY_ANDROID} ${ANDROID_LIBRARY_OPENSLES})
set(TW_ANDROID_LIBS
${ANDROID_LIBRARY_LOG}
${ANDROID_LIBRARY_ANDROID}
)

set(TW_ANDROID_LIBS_CLIENT
${TW_ANDROID_LIBS}
${ANDROID_LIBRARY_EGL}
${ANDROID_LIBRARY_GLES1}
${ANDROID_LIBRARY_GLES2}
${ANDROID_LIBRARY_GLES3}
${ANDROID_LIBRARY_OPENSLES}
)
6 changes: 4 additions & 2 deletions scripts/android/cmake_android.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,15 @@ function build_for_type() {
-DANDROID_NDK="$ANDROID_NDK_HOME" \
-DANDROID_ABI="${2}" \
-DANDROID_ARM_NEON=TRUE \
-DANDROID_PACKAGE_NAME="${PACKAGE_NAME//./_}" \
-DCMAKE_ANDROID_NDK="$ANDROID_NDK_HOME" \
-DCMAKE_SYSTEM_NAME=Android \
-DCMAKE_SYSTEM_VERSION="$ANDROID_API_LEVEL" \
-DCMAKE_ANDROID_ARCH_ABI="${2}" \
-DCARGO_NDK_TARGET="${3}" \
-DCARGO_NDK_API="$ANDROID_API_LEVEL" \
-B"${BUILD_FOLDER}/$ANDROID_SUB_BUILD_DIR/$1" \
-DSERVER=OFF \
-DSERVER=ON \
-DTOOLS=OFF \
-DDEV=TRUE \
-DCMAKE_CROSSCOMPILING=ON \
Expand All @@ -170,7 +171,7 @@ function build_for_type() {
cd "${BUILD_FOLDER}/$ANDROID_SUB_BUILD_DIR/$1" || exit 1
# We want word splitting
# shellcheck disable=SC2086
cmake --build . --target game-client $BUILD_FLAGS
cmake --build . --target game-client game-server $BUILD_FLAGS
)
}

Expand Down Expand Up @@ -228,6 +229,7 @@ log_info "Copying libraries..."
function copy_libs() {
mkdir -p "lib/$2"
cp "$ANDROID_SUB_BUILD_DIR/$1/libDDNet.so" "lib/$2" || exit 1
cp "$ANDROID_SUB_BUILD_DIR/$1/libDDNet-Server.so" "lib/$2" || exit 1
}

if [[ "${ANDROID_BUILD}" == "arm" || "${ANDROID_BUILD}" == "all" ]]; then
Expand Down
16 changes: 15 additions & 1 deletion scripts/android/files/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />

<!-- Local server runs in a foreground service with a notification to control it -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<!-- usesCleartextTraffic because unencrypted UDP packets -->
<application
android:usesCleartextTraffic="true"
Expand All @@ -53,7 +58,7 @@
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:hardwareAccelerated="true">
<activity
android:name="org.ddnet.client.NativeMain"
android:name="org.ddnet.client.ClientActivity"
android:alwaysRetainTaskState="true"
android:exported="true"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
Expand All @@ -75,5 +80,14 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<!-- Server service must run in a different process because it needs to be terminated to restart the server -->
<service
android:name="org.ddnet.client.ServerService"
android:exported="false"
android:process=":server_process"
android:foregroundServiceType="specialUse" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="This service is used for hosting a LAN server to play the game in a local area network as well as without internet. A foreground service is used because the server should also continue running while the client activity is not in the foreground as other players may be connected to the server." />
</application>
</manifest>
3 changes: 3 additions & 0 deletions scripts/android/files/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ android {
lintOptions {
abortOnError false
}
dependencies {
implementation 'androidx.core:core:1.13.1'
}
}

allprojects {
Expand Down
3 changes: 2 additions & 1 deletion scripts/android/files/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ if [ "${APK_PACKAGE_FOLDER}" != "org/ddnet/client" ]; then
mv src/main/java/org/ddnet/client src/main/java/"${APK_PACKAGE_FOLDER}"
fi

sed -i "s/org.ddnet.client/${APK_PACKAGE_NAME}/g" src/main/java/"${APK_PACKAGE_FOLDER}"/NativeMain.java
sed -i "s/org.ddnet.client/${APK_PACKAGE_NAME}/g" src/main/java/"${APK_PACKAGE_FOLDER}"/ClientActivity.java
sed -i "s/org.ddnet.client/${APK_PACKAGE_NAME}/g" src/main/java/"${APK_PACKAGE_FOLDER}"/ServerService.java
sed -i "s/org.ddnet.client/${APK_PACKAGE_NAME}/g" proguard-rules.pro

# disable hid manager for now
Expand Down
127 changes: 127 additions & 0 deletions scripts/android/files/java/org/ddnet/client/ClientActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.ddnet.client;

import android.app.NativeActivity;
import android.content.*;
import android.content.pm.ActivityInfo;
import android.os.*;

import androidx.core.content.ContextCompat;

import org.libsdl.app.SDLActivity;

public class ClientActivity extends SDLActivity {

private static final int COMMAND_RESTART_APP = SDLActivity.COMMAND_USER + 1;

private String[] launchArguments = new String[0];

private final Object serverServiceMonitor = new Object();
private Messenger serverServiceMessenger = null;
private final ServiceConnection serverServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized(serverServiceMonitor) {
serverServiceMessenger = new Messenger(service);
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
synchronized(serverServiceMonitor) {
serverServiceMessenger = null;
}
}
};

@Override
protected String[] getLibraries() {
return new String[] {
"DDNet",
};
}

@Override
public void onCreate(Bundle savedInstanceState) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

Intent intent = getIntent();
if(intent != null) {
String gfxBackend = intent.getStringExtra("gfx-backend");
if(gfxBackend != null) {
if(gfxBackend.equals("Vulkan")) {
launchArguments = new String[] {"gfx_backend Vulkan"};
} else if(gfxBackend.equals("GLES")) {
launchArguments = new String[] {"gfx_backend GLES"};
}
}
}

super.onCreate(savedInstanceState);
}

@Override
protected void onDestroy() {
super.onDestroy();
synchronized(serverServiceMonitor) {
if(serverServiceMessenger != null) {
unbindService(serverServiceConnection);
}
}
}

@Override
protected String[] getArguments() {
return launchArguments;
}

@Override
protected boolean onUnhandledMessage(int command, Object param) {
switch(command) {
case COMMAND_RESTART_APP:
restartApp();
return true;
}
return false;
}

private void restartApp() {
Intent restartIntent =
Intent.makeRestartActivityTask(
getPackageManager().getLaunchIntentForPackage(
getPackageName()
).getComponent()
);
restartIntent.setPackage(getPackageName());
startActivity(restartIntent);
}

// Called from native code, see android_main.cpp
public void startServer() {
synchronized(serverServiceMonitor) {
Intent startIntent = new Intent(this, ServerService.class);
ContextCompat.startForegroundService(this, startIntent);
bindService(startIntent, serverServiceConnection, 0);
}
}

// Called from native code, see android_main.cpp
public void stopServer() {
synchronized(serverServiceMonitor) {
if(serverServiceMessenger != null) {
try {
serverServiceMessenger.send(Message.obtain(null, ServerService.MESSAGE_STOP, 0, 0));
} catch (RemoteException e) {
// Server already stopped or connection otherwise broken
unbindService(serverServiceConnection);
}
}
}
}

// Called from native code, see android_main.cpp
public boolean isServerRunning() {
synchronized(serverServiceMonitor) {
return serverServiceMessenger != null;
}
}
}
65 changes: 0 additions & 65 deletions scripts/android/files/java/org/ddnet/client/NativeMain.java

This file was deleted.

Loading

0 comments on commit b258f22

Please sign in to comment.