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; + } +}