QOwnNotes Mobile is an Android-first, offline-capable Markdown notes application. It preserves key QOwnNotes behavior while synchronizing through the Nextcloud Notes API.
- Import one or more accounts from the Nextcloud Files Android app through Single Sign-On.
- Read, search, create, rename, edit, and delete notes while keeping Room as the offline source of truth.
- Synchronize with Nextcloud Notes API 1.2 or newer using incremental pulls, ETags, conflict-safe updates, and durable pending changes.
- Long-press notes to select several and move them to the Nextcloud trash bin together.
- Browse and restore server note versions and remotely trashed notes when the Nextcloud QOwnNotesAPI app is installed.
- Favorite notes with a star and keep them above other notes, including while offline.
- Create a note from text shared by another Android application.
- Render CommonMark and GitHub Flavored Markdown, QOwnNotes task states, wiki links, legacy
note://links, tables, fenced code, and safe remote images. - Edit highlighted Markdown source with formatting actions, undo and redo, cursor preservation, and local draft persistence.
- Find text inside an open note, select and copy rendered text, and adjust the note text size.
- Detect read-only and QOwnNotes-encrypted notes and fail closed for unsafe HTML, links, images, and filesystem access.
- Use light and dark themes on Android 9 and newer.
The production application is named QOwnNotes and uses org.qownnotes.mobile. Development
builds are named QOwnNotes Dev and use org.qownnotes.mobile.dev, so both can be installed on
the same device. See CHANGELOG.md for release details.
| Note list | Note view | Note edit |
|---|---|---|
![]() |
![]() |
![]() |
Install the Nextcloud Files Android app, sign in to the desired server there, then choose Add Nextcloud account in QOwnNotes Mobile. The app requires the Nextcloud Notes server app with Notes API 1.2 or newer. Downloaded notes and account metadata remain available offline; credentials stay in Nextcloud's SSO integration and are not copied into the QOwnNotes database.
Use Remove in an account's note list to delete that account reference, its synchronization history, and its cached notes from QOwnNotes Mobile. This does not remove the account from Nextcloud Files or delete notes from the server.
Use New to create a QOwnNotes-compatible note or Edit while viewing a writable note. Drafts are stored locally first and synchronized through the Nextcloud Notes API; updates use the last known ETag to avoid blindly overwriting a concurrent server edit.
Editing does not wait for Nextcloud. Changes are cached immediately while the app is running and written to Room after a 500 ms pause, when editing finishes, or when the screen stops. A persisted edit remains visible after restarting the app and stays queued if synchronization cannot reach the server. A later edit, manual refresh, or return to the note list tries synchronization again.
Updates use the last known ETag. If the server copy changed in the meantime, the app keeps the local text and marks the note as conflicted instead of overwriting the server. Durable background retry and conflict resolution are not implemented yet. Because the live draft cache is in memory, an abrupt process kill during the 500 ms save window can lose only the newest, not-yet-persisted characters.
devenv shellThe first invocation downloads JDK 17, the Android SDK, build tools, platform
List the available recipes:
justjust build-devjust build is an alias for just build-dev. Configure local development signing as described
below before running either recipe.
The APK is written to:
app/build/outputs/apk/debug/app-debug.apk
Create the project-local emulator once:
just create-avdStart it in one devenv shell:
just start-emulatorIn another devenv shell, install and launch the development app. The recipe waits until
Android's package manager is ready:
just deploy-devjust run is an alias for just deploy-dev.
Enable ADB access in the NixOS configuration and rebuild the system:
{
programs.adb.enable = true;
users.users.<username>.extraGroups = [ "adbusers" ];
}Enable USB debugging on the device, connect it, accept the authorization prompt, and verify the connection:
adb devicesInstall and launch the development app:
just deploy-devAfter configuring the release signing variables described below, build and deploy the production application over USB with:
just build-release
just deploy-releasejust testjust format
just format-checkjust license-checkWith an emulator or physical device running:
just device-testCI runs the same instrumentation suite on an API 36 emulator. Real Nextcloud SSO interoperability remains a manual test because it requires an installed and authenticated Nextcloud Files app.
just checkLocal development and release builds retrieve signing files from a private Vaultwarden instance. Create one item for each signing identity with these attachments:
Development item:
qownnotes-development.jks
qownnotes-development.env
Release item:
qownnotes-release.jks
qownnotes-release.env
The development dotenv attachment must contain:
ANDROID_DEV_KEYSTORE_PASSWORD=...
ANDROID_DEV_KEY_ALIAS=...
ANDROID_DEV_KEY_PASSWORD=...The release dotenv attachment must contain:
ANDROID_KEYSTORE_PASSWORD=...
ANDROID_KEY_ALIAS=...
ANDROID_KEY_PASSWORD=...Configure the Bitwarden CLI for Vaultwarden and log in once:
bw logout
bw config server https://vaultwarden.example.com
bw login
just build-dev
just deploy-dev
just build-release
just deploy-release
just releaseWhen the vault is locked, a signing recipe runs bw unlock --raw and prompts for the master
password itself. The resulting BW_SESSION exists only inside the wrapper, so it does not need to
be exported manually or stored in shell configuration. An already exported BW_SESSION is still
accepted for automation.
The public devenv.nix configuration sets VAULTWARDEN_DEV_SIGNING_ITEM and
VAULTWARDEN_SIGNING_ITEM to the item names above. The variables may instead contain item UUIDs and
may name the same item when all four attachments are stored together. Attachment names can be
overridden locally:
VAULTWARDEN_DEV_KEYSTORE_ATTACHMENT
VAULTWARDEN_DEV_SIGNING_ENV_ATTACHMENT
VAULTWARDEN_KEYSTORE_ATTACHMENT
VAULTWARDEN_SIGNING_ENV_ATTACHMENT
The signing wrapper downloads the selected attachments into a private temporary directory, uses the matching SecretSpec profile to validate and inject the dotenv values, sets the corresponding keystore path, and removes both files when Gradle exits. It never sources the downloaded dotenv file as shell code, and Gradle does not inherit the Vaultwarden session or item configuration.
The reproducible devenv shell provides bw, jq, and secretspec. Outside that shell, install
the Bitwarden Password Manager CLI, jq, and SecretSpec separately. GitHub Actions continues using
its existing ANDROID_DEV_* and ANDROID_* repository secrets. When either complete signing
environment is already present, the wrapper skips Vaultwarden and SecretSpec.
The signed outputs are written to:
app/build/outputs/apk/debug/app-debug.apk
app/build/outputs/apk/release/app-release.apk
app/build/outputs/bundle/release/app-release.aab
CI runs checks and uploads a debug APK for every pushed branch except release, and for pull
requests. A push to the release branch instead runs the dedicated release workflow, which reads
the committed version, runs the checks, builds signed packages, extracts that version's section
from CHANGELOG.md, and publishes an immutable GitHub release tagged v<version>. Before pushing
to the release branch, increment both values in version.properties and add the matching changelog
section. Configure the following GitHub Actions repository secrets:
ANDROID_KEYSTORE_BASE64
ANDROID_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS
ANDROID_KEY_PASSWORD
If QOwnNotes Mobile has already been distributed, use its existing release or upload key. Android will not accept updates signed by a replacement key. For a first release, create and configure a key from the repository root with:
mkdir -p .signing
keytool -genkeypair \
-keystore .signing/qownnotes-release.jks \
-storetype PKCS12 \
-alias qownnotes-release \
-keyalg RSA \
-keysize 4096 \
-validity 10000
base64 -w 0 .signing/qownnotes-release.jks | gh secret set ANDROID_KEYSTORE_BASE64
printf '%s' 'qownnotes-release' | gh secret set ANDROID_KEY_ALIAS
gh secret set ANDROID_KEYSTORE_PASSWORD
gh secret set ANDROID_KEY_PASSWORDThe final two commands prompt without putting the passwords in shell history. PKCS12 normally uses
the same password for the keystore and key. Store the .jks file and password in a durable password
manager backup; losing them can prevent future application updates.
Pushes to main also replace the GitHub prerelease tagged continuous with a signed QOwnNotes
Dev APK and its SHA-256 checksum. Use a separate development key so publishing continuous builds
does not expose the stable release key to the main workflow. Configure these additional secrets:
ANDROID_DEV_KEYSTORE_BASE64
ANDROID_DEV_KEYSTORE_PASSWORD
ANDROID_DEV_KEY_ALIAS
ANDROID_DEV_KEY_PASSWORD
Create and configure a development key from the repository root with keytool and the GitHub CLI:
mkdir -p .signing
keytool -genkeypair \
-keystore .signing/qownnotes-development.jks \
-storetype PKCS12 \
-alias qownnotes-development \
-keyalg RSA \
-keysize 4096 \
-validity 10000
base64 -w 0 .signing/qownnotes-development.jks | gh secret set ANDROID_DEV_KEYSTORE_BASE64
printf '%s' 'qownnotes-development' | gh secret set ANDROID_DEV_KEY_ALIAS
gh secret set ANDROID_DEV_KEYSTORE_PASSWORD
gh secret set ANDROID_DEV_KEY_PASSWORDThe final two commands prompt for the passwords without putting them in shell history. PKCS12
normally uses the same password for the keystore and key, so enter the password chosen by keytool
for both secrets. Keep the .jks file and its password in a secure backup: every continuous APK
must use the same key for Android to accept it as an update. The .signing/ directory is ignored by
Git.
The Base64 variables contain the binary keystores in a text form GitHub Actions can store. The
keystore-password, alias, and key-password variables select and unlock the private key inside each
keystore. ANDROID_KEYSTORE_PATH and ANDROID_DEV_KEYSTORE_PATH are local or temporary file paths,
not repository secrets. ANDROID_VERSION_CODE is also not a secret: continuous CI sets it to the
current Unix timestamp so every development build has a higher Android version code.
just clean

