Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 19 additions & 32 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
name: "Gradle build"
permissions: {}
on:
- push
- pull_request
- workflow_dispatch
push:
branches: [main, master]
pull_request:
workflow_dispatch:
inputs:
ref:
description: "Branch, tag, or commit SHA to build (defaults to the current branch)"
required: false
default: ""

jobs:
build:
name: "Gradle build ${{ matrix.target }}"
name: "Build Remote DroidGuard Server (debug)"
runs-on: ubuntu-latest
env:
GRADLE_MICROG_VERSION_WITHOUT_GIT: 1

strategy:
matrix:
target: [Debug, Release]

steps:
- name: "Free disk space"
run: |
# Deleting unneeded software packages
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo rm -rf /opt/hostedtoolcache/go

# Log available space
df -h
- name: "Checkout sources"
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: "Setup Java"
uses: actions/setup-java@v5
with:
Expand All @@ -41,24 +41,11 @@ jobs:
build-scan-publish: true
build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use"
build-scan-terms-of-use-agree: "yes"
- name: "Setup matchers"
run: |
# Setting up matchers...

matchers_dir='${{ github.workspace }}/.github/matchers'
matcher_list()
{
echo 'gradle-build-matcher.json'
echo 'gradle-build-kotlin-error-matcher.json'
}

matcher_list | while IFS='' read -r NAME; do
if test -f "${matchers_dir:?}/${NAME:?}"; then
echo "::add-matcher::${matchers_dir:?}/${NAME:?}"
echo "Matcher configured: ${NAME:?}"
fi
done
- name: "Execute Gradle assemble"
run: "./gradlew assemble${{ matrix.target }}"
- name: "Execute Gradle lint"
run: "./gradlew lint${{ matrix.target }}"
- name: "Assemble debug APK"
run: "./gradlew :remote-droidguard-server:assembleDebug"
- name: "Upload debug APK"
uses: actions/upload-artifact@v4
with:
name: RemoteDroidGuard-Server-debug.apk
path: remote-droidguard-server/build/outputs/apk/debug/*.apk
if-no-files-found: error
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,54 @@ private const val TAG = "RemoteGuardImpl"
class RemoteHandleImpl(private val context: Context, private val packageName: String) : IDroidGuardHandle.Stub() {
private var flow: String? = null
private var request: DroidGuardResultsRequest? = null
private var sessionId: String? = null
private val url: String
get() = DroidGuardPreferences.getNetworkServerUrl(context) ?: throw IllegalStateException("Network URL required")

override fun init(flow: String?) {
Log.d(TAG, "init($flow)")
this.flow = flow
// Try to initialize multi-step session
try {
sessionId = callInitEndpoint(flow, null)
Log.d(TAG, "Multi-step session initialized: $sessionId")
} catch (e: Exception) {
Log.w(TAG, "Failed to initialize multi-step session, will fallback to single-shot", e)
sessionId = null
}
}

override fun snapshot(map: Map<Any?, Any?>?): ByteArray {
Log.d(TAG, "snapshot($map)")

// Multi-step mode: use /v2/snapshot with sessionId
val currentSessionId = sessionId
if (currentSessionId != null) {
return snapshotMultiStep(currentSessionId, map)
}

// Single-shot mode: use / endpoint (backwards compatibility)
return snapshotSingleShot(map)
}

private fun snapshotMultiStep(sessionId: String, map: Map<Any?, Any?>?): ByteArray {
Log.d(TAG, "Using multi-step snapshot with sessionId: $sessionId")
val endpoint = "$url/v2/snapshot?sessionId=${Uri.encode(sessionId)}"
val payload = map.orEmpty().map { Uri.encode(it.key as String) + "=" + Uri.encode(it.value as String) }.joinToString("&")

val connection = URL(endpoint).openConnection() as HttpURLConnection
Log.d(TAG, "POST $endpoint: $payload")
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
connection.requestMethod = "POST"
connection.doInput = true
connection.doOutput = true
connection.outputStream.use { it.write(payload.encodeToByteArray()) }
val bytes = connection.inputStream.use { it.readBytes() }.decodeToString()
return Base64.decode(bytes, Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING)
}

private fun snapshotSingleShot(map: Map<Any?, Any?>?): ByteArray {
Log.d(TAG, "Using single-shot snapshot (backwards compatibility)")
val paramsMap = mutableMapOf("flow" to flow, "source" to packageName)
for (key in request?.bundle?.keySet().orEmpty()) {
request?.bundle?.getString(key)?.let { paramsMap["x-request-$key"] = it }
Expand All @@ -49,14 +87,76 @@ class RemoteHandleImpl(private val context: Context, private val packageName: St

override fun close() {
Log.d(TAG, "close()")

// Close multi-step session on server if one exists
val currentSessionId = sessionId
if (currentSessionId != null) {
try {
closeSession(currentSessionId)
} catch (e: Exception) {
Log.w(TAG, "Failed to close session on server", e)
}
}

// Clean up local state
this.sessionId = null
this.request = null
this.flow = null
}

private fun closeSession(sessionId: String) {
Log.d(TAG, "Closing session on server: $sessionId")
val endpoint = "$url/v2/close?sessionId=${Uri.encode(sessionId)}"
val connection = URL(endpoint).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doInput = true
connection.doOutput = true
connection.outputStream.use { it.write(ByteArray(0)) }
val responseCode = connection.responseCode
Log.d(TAG, "Close session response: $responseCode")
connection.disconnect()
}

override fun initWithRequest(flow: String?, request: DroidGuardResultsRequest?): DroidGuardInitReply? {
Log.d(TAG, "initWithRequest($flow, $request)")
this.flow = flow
this.request = request
// Try to initialize multi-step session
try {
sessionId = callInitEndpoint(flow, request)
Log.d(TAG, "Multi-step session initialized: $sessionId")
} catch (e: Exception) {
Log.w(TAG, "Failed to initialize multi-step session, will fallback to single-shot", e)
sessionId = null
}
return null
}

private fun callInitEndpoint(flow: String?, request: DroidGuardResultsRequest?): String? {
val paramsMap = mutableMapOf("flow" to flow, "source" to packageName)
for (key in request?.bundle?.keySet().orEmpty()) {
request?.bundle?.getString(key)?.let { paramsMap["x-request-$key"] = it }
}
val params = paramsMap.map { Uri.encode(it.key) + "=" + Uri.encode(it.value) }.joinToString("&")
val endpoint = "$url/v2/init?$params"

Log.d(TAG, "POST $endpoint")
val connection = URL(endpoint).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doInput = true
connection.doOutput = true
connection.outputStream.use { it.write(ByteArray(0)) }

val responseCode = connection.responseCode
if (responseCode != 200) {
throw RuntimeException("Init endpoint returned $responseCode")
}

val sessionId = connection.inputStream.use { it.readBytes() }.decodeToString().trim()
if (sessionId.isEmpty()) {
throw RuntimeException("Init endpoint returned empty sessionId")
}

return sessionId
}
}
66 changes: 66 additions & 0 deletions remote-droidguard-server/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
namespace "org.microg.gms.droidguard.server"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"

defaultConfig {
applicationId "org.microg.gms.droidguard.remote_server"
versionName "1.0.0"
versionCode 1
minSdkVersion 21
targetSdkVersion androidTargetSdk

multiDexEnabled true
}

buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled false
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}

buildFeatures {
buildConfig = true
}

lintOptions {
disable 'MissingTranslation'
}
}

dependencies {
implementation project(':play-services-droidguard')
implementation project(':play-services-tasks-ktx')

implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "com.google.android.material:material:$materialVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
implementation "androidx.multidex:multidex:$multidexVersion"
}

if (file('user.gradle').exists()) {
apply from: 'user.gradle'
}
49 changes: 49 additions & 0 deletions remote-droidguard-server/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application
android:name=".RemoteDroidGuardApp"
android:allowBackup="true"
android:icon="@drawable/ic_launcher_dg"
android:roundIcon="@drawable/ic_launcher_dg"
android:label="@string/app_name"
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".DroidGuardServerService"
android:exported="false"
android:foregroundServiceType="dataSync" />

<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.droidguard.server

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log

/**
* Starts [DroidGuardServerService] automatically when the device boots,
* if the user has enabled "Start Server on Boot" in SharedPreferences.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_BOOT_COMPLETED &&
intent.action != "android.intent.action.QUICKBOOT_POWERON"
) return

// Check if boot start is enabled in SharedPreferences
val prefs = context.getSharedPreferences("droidguard_server_prefs", Context.MODE_PRIVATE)
val bootStartEnabled = prefs.getBoolean(MainActivity.PREF_BOOT_START, true)

if (!bootStartEnabled) {
Log.i(TAG, "Boot start is disabled — not starting DroidGuardServerService")
return
}

Log.i(TAG, "Boot completed — starting DroidGuardServerService")
val serviceIntent = Intent(context, DroidGuardServerService::class.java)
.setAction(DroidGuardServerService.ACTION_START)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}

companion object {
private const val TAG = "DroidGuardBootReceiver"
}
}
Loading
Loading