diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e1ebc4a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,173 @@
+# AGENTS.md
+
+Guidance for agentic coding tools working in `moasdawiki-app`.
+
+## Project Snapshot
+
+- Android app module: `:app`
+- Language: Java 17 (no Kotlin sources currently)
+- Build system: Gradle wrapper (`./gradlew`) + Android Gradle Plugin
+- Main package: `net.moasdawiki.app`
+- App depends on MoasdaWiki server module artifact: `net.moasdawiki:moasdawiki-server`
+
+## Environment And Baseline
+
+- Use the wrapper, not a globally installed Gradle: `./gradlew ...`
+- Run commands from repository root.
+- Keep changes scoped; do not refactor unrelated files.
+- Preserve license headers in Java files.
+- Prefer additive, minimal-risk edits.
+
+## Build / Lint / Test Commands
+
+### Common Lifecycle Commands
+
+- Clean: `./gradlew clean`
+- Build everything (assemble + checks): `./gradlew :app:build`
+- Assemble debug APK: `./gradlew :app:assembleDebug`
+- Assemble release APK: `./gradlew :app:assembleRelease`
+- Install debug on connected device: `./gradlew :app:installDebug`
+
+### Lint Commands
+
+- Run lint (default variant): `./gradlew :app:lint`
+- Run debug lint only: `./gradlew :app:lintDebug`
+- Run release lint only: `./gradlew :app:lintRelease`
+- Apply safe lint fixes: `./gradlew :app:lintFix`
+
+### Unit Test Commands
+
+- Run all unit tests: `./gradlew :app:test`
+- Run debug unit tests: `./gradlew :app:testDebugUnitTest`
+- Run release unit tests: `./gradlew :app:testReleaseUnitTest`
+
+### Run A Single Unit Test (Important)
+
+- Single test class:
+ `./gradlew :app:testDebugUnitTest --tests "net.moasdawiki.app.YourTestClass"`
+- Single test method:
+ `./gradlew :app:testDebugUnitTest --tests "net.moasdawiki.app.YourTestClass.yourTestMethod"`
+- Wildcard match:
+ `./gradlew :app:testDebugUnitTest --tests "*YourTestClass*"`
+
+Notes:
+
+- Use fully-qualified class names for reliable filtering.
+- If the filter reports no tests found, verify package + method names.
+- Keep `Debug` test task unless you specifically need release behavior.
+
+### Instrumentation Test Commands
+
+- Run all connected instrumentation tests:
+ `./gradlew :app:connectedDebugAndroidTest`
+- Run one instrumentation class:
+ `./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=net.moasdawiki.app.YourAndroidTestClass`
+- Run one instrumentation method:
+ `./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=net.moasdawiki.app.YourAndroidTestClass#yourTestMethod`
+
+## High-Value Working Agreement
+
+- Before editing, inspect nearby code and follow existing patterns.
+- After editing, run the narrowest meaningful verification first.
+- For behavior changes, prefer adding/updating tests when test infra exists.
+- Do not introduce new build tools or formatting frameworks unless asked.
+
+## Source Layout (Current)
+
+- App code: `app/src/main/java/net/moasdawiki/app/`
+- Resources: `app/src/main/res/`
+- Manifest: `app/src/main/AndroidManifest.xml`
+- Gradle module config: `app/build.gradle`
+- Root build config: `build.gradle`, `settings.gradle`, `gradle.properties`
+
+There are currently no `app/src/test` or `app/src/androidTest` files in this repository snapshot.
+
+## Java Style Conventions To Follow
+
+### Formatting
+
+- 4-space indentation, no tabs.
+- Opening brace on same line for classes/methods/control blocks.
+- Keep methods reasonably focused; avoid large unrelated rewrites.
+- Prefer one statement per line for readability.
+- Preserve existing block comments and Javadoc style.
+
+### Imports
+
+- Use explicit imports; avoid wildcard imports.
+- Group imports with blank lines by domain:
+ 1) `android.*`
+ 2) `androidx.*`
+ 3) `net.moasdawiki.*`
+ 4) `java.*`
+- Keep import order stable and consistent within each group.
+
+### Types And Nullability
+
+- Use concrete types unless abstraction improves clarity.
+- Follow existing nullability annotations (`@NonNull`, `@Nullable`).
+- Annotate parameters/returns where null-safety is non-obvious.
+- Prefer immutable locals/fields (`final`) where practical.
+- Use boxed types only when null is a real state.
+
+### Naming
+
+- Classes: `PascalCase` (`MainActivity`, `SynchronizeWikiClient`).
+- Methods/fields/local variables: `camelCase`.
+- Constants: `UPPER_SNAKE_CASE`.
+- Android log tags: short static constant named `TAG`.
+- Preference keys/constants belong in `Constants`-style central locations.
+
+### Control Flow And Readability
+
+- Prefer early returns for invalid preconditions.
+- Keep nesting shallow when possible.
+- Extract helper methods for repeated logic.
+- Use descriptive method names that reflect side effects.
+
+### Error Handling And Logging
+
+- Catch specific exceptions whenever practical.
+- Log with Android `Log` at appropriate level (`d`, `i`, `w`, `e`).
+- Include exception object when logging failures.
+- Convert low-level failures into domain-friendly outcomes for callers.
+- For user-visible failures, pair logs with UI feedback (`Toast`/status text).
+- Avoid swallowing exceptions silently.
+
+### Threading / Android Behavior
+
+- Keep network and disk work off the UI thread.
+- UI updates must run on UI thread (`runOnUiThread(...)` pattern).
+- Continue using executor-based async style already present.
+- Preserve lifecycle-safe behavior in `Activity`/`Fragment` methods.
+
+## Testing Expectations For Agents
+
+- If you change pure logic, add or update a unit test.
+- If you change Android component behavior, prefer instrumentation tests when feasible.
+- For low-risk refactors, run at least targeted lint + targeted tests.
+- For broader edits, run `:app:lint` and `:app:testDebugUnitTest`.
+
+## Dependency And Build File Edits
+
+- Keep dependency changes minimal and justified.
+- Match existing dependency declaration style in touched file.
+- Do not upgrade AGP/Gradle/SDK versions unless explicitly requested.
+- Call out deprecation warnings you encounter during Gradle runs.
+
+## Commit And PR Hygiene (For Agents)
+
+- Make small, reviewable commits with clear intent.
+- In commit messages, explain why the change is needed.
+- Include verification commands run and their outcome in PR notes.
+- Do not include unrelated formatting churn.
+
+## Cursor/Copilot Rule Ingestion
+
+Checked in this repository:
+
+- `.cursorrules`: not present
+- `.cursor/rules/`: not present
+- `.github/copilot-instructions.md`: not present
+
+If these files are added later, agents should treat them as higher-priority repository instructions and merge them into this guidance.
diff --git a/app/build.gradle b/app/build.gradle
index b0f7e12..49a931e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,8 +4,8 @@ android {
namespace = "net.moasdawiki.app"
compileSdk = 36 // 36 = Android 16 Baklava
defaultConfig {
- applicationId "net.moasdawiki.app"
- minSdk = 33 // 33 = Android 13 Tiramisu
+ applicationId "space.hackenslacker.moasdawiki.app"
+ minSdk = 29 // 29 = Android 10 Q
targetSdk = 36 // should be same as compileSdk
versionCode = 50
versionName = "3.9.7.0"
@@ -34,4 +34,6 @@ dependencies {
implementation 'androidx.preference:preference:1.2.1'
// Fix duplicate class error
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.0"
+
+ testImplementation 'junit:junit:4.13.2'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1566600..baa0fb1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,7 +40,7 @@
diff --git a/app/src/main/java/net/moasdawiki/app/CalendarSyncAdapter.java b/app/src/main/java/net/moasdawiki/app/CalendarSyncAdapter.java
index 6da2fa4..5900bb1 100644
--- a/app/src/main/java/net/moasdawiki/app/CalendarSyncAdapter.java
+++ b/app/src/main/java/net/moasdawiki/app/CalendarSyncAdapter.java
@@ -66,7 +66,7 @@ public class CalendarSyncAdapter extends AbstractThreadedSyncAdapter {
static final String ACCOUNT_NAME = "MoasdaWiki";
static final String ACCOUNT_TYPE = "net.moasdawiki";
- static final String PROVIDER_NAME = "net.moasdawiki.app.provider";
+ static final String PROVIDER_NAME = "space.hackenslacker.moasdawiki.app.provider";
private static final String CALENDAR_NAME = "MoasdaWiki Events";
private static final Uri CALENDAR_URI = CalendarContract.Calendars.CONTENT_URI;
diff --git a/app/src/main/java/net/moasdawiki/app/MainActivity.java b/app/src/main/java/net/moasdawiki/app/MainActivity.java
index e0b9c39..191cb0e 100644
--- a/app/src/main/java/net/moasdawiki/app/MainActivity.java
+++ b/app/src/main/java/net/moasdawiki/app/MainActivity.java
@@ -44,13 +44,12 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
-import android.window.OnBackInvokedCallback;
-import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.MenuBuilder;
+import androidx.activity.OnBackPressedCallback;
import androidx.preference.PreferenceManager;
import net.moasdawiki.base.ServiceException;
@@ -76,7 +75,7 @@ import java.util.concurrent.Executors;
/**
* Displays the main window with the embedded wiki browser.
*/
-public class MainActivity extends AppCompatActivity implements OnBackInvokedCallback {
+public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final String SERVER_BASE_URL = "http://localhost:1/";
@@ -116,7 +115,12 @@ public class MainActivity extends AppCompatActivity implements OnBackInvokedCall
}
});
- getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this);
+ getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ MainActivity.this.onBackInvoked();
+ }
+ });
}
/**
diff --git a/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java b/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java
index 458c0e6..6709c76 100644
--- a/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java
+++ b/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java
@@ -50,8 +50,10 @@ import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
+import java.util.List;
/**
* Connects to the configured MoasdaWiki server and downloads the wiki files.
@@ -287,7 +289,8 @@ public class SynchronizeWikiClient {
// Antwort auswerten
ListModifiedFilesResponseXml response = parseXml(responseXml, ListModifiedFilesResponseXml.class);
- int fileCount = response.fileList.size();
+ List filePathsToDownload = determineFilesToDownload(response.fileList);
+ int fileCount = filePathsToDownload.size();
Log.d(TAG, "Downloading " + fileCount + " files from server");
if (fileCount == 0) {
// no files to download, cancel process
@@ -296,9 +299,9 @@ public class SynchronizeWikiClient {
for (int i = 0; i < fileCount; i++) {
feedback.progress(i, fileCount);
- SingleFileXml serverFile = response.fileList.get(i);
+ String filePath = filePathsToDownload.get(i);
try {
- downloadFileFromServer(serverHostPort, serverSessionId, serverFile.filePath);
+ downloadFileFromServer(serverHostPort, serverSessionId, filePath);
}
catch (ServiceException e) {
Log.w(TAG, "Error reading file from server, ignoring it", e);
@@ -321,6 +324,24 @@ public class SynchronizeWikiClient {
return new SyncResult(true, true, false, fileCount);
}
+ @NonNull
+ static List determineFilesToDownload(@NonNull List modifiedFiles) {
+ List filePathsToDownload = new ArrayList<>();
+ for (SingleFileXml modifiedFile : modifiedFiles) {
+ String filePath = modifiedFile.filePath;
+ if (filePath != null && !filePathsToDownload.contains(filePath)) {
+ filePathsToDownload.add(filePath);
+ }
+ }
+
+ String appConfigFilePath = Settings.getConfigFileApp();
+ if (!filePathsToDownload.contains(appConfigFilePath)) {
+ filePathsToDownload.add(appConfigFilePath);
+ }
+
+ return filePathsToDownload;
+ }
+
public static class SyncResult {
private final boolean sessionValid;
private final boolean sessionAuthorized;
diff --git a/app/src/main/res/xml/syncadapter.xml b/app/src/main/res/xml/syncadapter.xml
index b151c08..d79a684 100644
--- a/app/src/main/res/xml/syncadapter.xml
+++ b/app/src/main/res/xml/syncadapter.xml
@@ -1,6 +1,6 @@
.
+ */
+
+package net.moasdawiki.app;
+
+import net.moasdawiki.service.sync.SingleFileXml;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SynchronizeWikiClientTest {
+
+ @Test
+ public void determineFilesToDownloadAddsConfigFileAppWhenMissing() {
+ List modifiedFiles = new ArrayList<>();
+ modifiedFiles.add(file("/wiki/Home-App.txt"));
+
+ List result = SynchronizeWikiClient.determineFilesToDownload(modifiedFiles);
+
+ Assert.assertTrue(result.contains("/wiki/Home-App.txt"));
+ Assert.assertTrue(result.contains("/config-app.txt"));
+ }
+
+ @Test
+ public void determineFilesToDownloadDoesNotDuplicateConfigFileApp() {
+ List modifiedFiles = new ArrayList<>();
+ modifiedFiles.add(file("/config-app.txt"));
+
+ List result = SynchronizeWikiClient.determineFilesToDownload(modifiedFiles);
+
+ int count = 0;
+ for (String filePath : result) {
+ if ("/config-app.txt".equals(filePath)) {
+ count++;
+ }
+ }
+ Assert.assertEquals(1, count);
+ }
+
+ @Test
+ public void determineFilesToDownloadRemovesDuplicatesAndNullValues() {
+ List modifiedFiles = new ArrayList<>();
+ modifiedFiles.add(file("/wiki/PageA.txt"));
+ modifiedFiles.add(file("/wiki/PageA.txt"));
+ modifiedFiles.add(file(null));
+
+ List result = SynchronizeWikiClient.determineFilesToDownload(modifiedFiles);
+
+ int pageACount = 0;
+ for (String filePath : result) {
+ if ("/wiki/PageA.txt".equals(filePath)) {
+ pageACount++;
+ }
+ Assert.assertNotNull(filePath);
+ }
+ Assert.assertEquals(1, pageACount);
+ }
+
+ private static SingleFileXml file(String filePath) {
+ SingleFileXml xml = new SingleFileXml();
+ xml.filePath = filePath;
+ return xml;
+ }
+}