Skip to content

Commit

Permalink
Support running local server on Android
Browse files Browse the repository at this point in the history
Compile the server as a separate library for Android same as the client.

Use a foreground service to run the native `main` function of the server using Java Native Interface. Directly using JNI avoids the use of SDL as a wrapper for the server. The server service must run in a different process because it needs to be terminated to correctly restart the server. Otherwise it crashes because global variables are not initialized again when it is restarted. This also prevents simply running the server in another thread of the client process. A toast error message is shown on non-zero return values of server's `main` function.

A foreground service is used so the server can keep running while the client activity is in the background. Add detailed description why the service uses `android:foregroundServiceType="specialUse"` in the manifest as this should convince the reviewer that the use case is valid. None of the other foreground service types cover hosting local game servers or related use cases. The required permissions for using a foreground service with special use case are added as well as the permission to post notifications.

Showing a notification is necessary for foreground services to stay alive and it also allows showing the server status and controlling the server. In particular, an action to directly stop the server is shown. Another action to run commands based on user input in the notification is provided, which allows setting the initial rcon password.

The server will also be stopped automatically if the client is quit or if the server notification is deleted (which is possible on newer Android versions that do not allow ongoing notifications anymore).

Rename `NativeMain.java` to `ClientActivity.java` to differentiate the file better from the server class.
  • Loading branch information
Robyt3 committed Dec 15, 2024
1 parent 02b3a89 commit cef2c26
Show file tree
Hide file tree
Showing 17 changed files with 761 additions and 111 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
22 changes: 20 additions & 2 deletions scripts/android/files/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,27 @@
<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"
android:label="@string/app_name"
android:hasCode="true"
android:supportsRtl="true"
android:isGame="true"
android:appCategory="game"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
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:process=":client_process"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:screenOrientation="landscape"
Expand All @@ -75,5 +81,17 @@
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." />
<meta-data
android:name="android.app.lib_name"
android:value="DDNet-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
133 changes: 133 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,133 @@
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) {
if(serverServiceMessenger != null) {
return;
}
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 executeCommand(String command) {
synchronized(serverServiceMonitor) {
if(serverServiceMessenger == null) {
return;
}
try {
Message message = Message.obtain(null, ServerService.MESSAGE_CODE_EXECUTE_COMMAND, 0, 0);
message.getData().putString(ServerService.MESSAGE_EXTRA_COMMAND, command);
serverServiceMessenger.send(message);
} catch (RemoteException e) {
// Connection 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 cef2c26

Please sign in to comment.