Compare commits

..

2 Commits

8 changed files with 298 additions and 14 deletions
+173
View File
@@ -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.
+4 -2
View File
@@ -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'
}
+1 -1
View File
@@ -40,7 +40,7 @@
<provider
android:name="net.moasdawiki.app.CalendarContentProvider"
android:authorities="net.moasdawiki.app.provider"
android:authorities="space.hackenslacker.moasdawiki.app.provider"
android:exported="false"
android:syncable="true"/>
@@ -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;
@@ -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;
@@ -60,7 +59,6 @@ import net.moasdawiki.http.HttpRequest;
import net.moasdawiki.server.RequestDispatcher;
import net.moasdawiki.service.HttpResponse;
import net.moasdawiki.service.repository.RepositoryService;
import net.moasdawiki.util.EscapeUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -76,7 +74,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 +114,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();
}
});
}
/**
@@ -392,7 +395,11 @@ public class MainActivity extends AppCompatActivity implements OnBackInvokedCall
@NonNull
private String getWikiserverSearchUrl(@NonNull String query) {
return SERVER_BASE_URL + "search/?text=" + EscapeUtils.encodeUrlParameter(query);
return Uri.parse(SERVER_BASE_URL + "search/")
.buildUpon()
.appendQueryParameter("text", query)
.build()
.toString();
}
@NonNull
@@ -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<String> 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<String> determineFilesToDownload(@NonNull List<SingleFileXml> modifiedFiles) {
List<String> 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;
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="net.moasdawiki.app.provider"
android:contentAuthority="space.hackenslacker.moasdawiki.app.provider"
android:accountType="net.moasdawiki"
android:userVisible="false"
android:supportsUploading="false"
@@ -0,0 +1,81 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2026 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation (GPL-3.0-only).
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
*/
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<SingleFileXml> modifiedFiles = new ArrayList<>();
modifiedFiles.add(file("/wiki/Home-App.txt"));
List<String> result = SynchronizeWikiClient.determineFilesToDownload(modifiedFiles);
Assert.assertTrue(result.contains("/wiki/Home-App.txt"));
Assert.assertTrue(result.contains("/config-app.txt"));
}
@Test
public void determineFilesToDownloadDoesNotDuplicateConfigFileApp() {
List<SingleFileXml> modifiedFiles = new ArrayList<>();
modifiedFiles.add(file("/config-app.txt"));
List<String> 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<SingleFileXml> modifiedFiles = new ArrayList<>();
modifiedFiles.add(file("/wiki/PageA.txt"));
modifiedFiles.add(file("/wiki/PageA.txt"));
modifiedFiles.add(file(null));
List<String> 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;
}
}