Merge pull request #238 from Dvorinka/feat/play-release

feat(android): Play Store release identity + listing metadata
pull/3593/head
Tomáš Dvořák 2 weeks ago committed by GitHub
commit 4050b81c07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,5 +1,10 @@
name: Release name: Release
# One `v*` tag produces ONE GitHub release containing every platform:
# - server binaries + archives + checksums (GoReleaser) + GHCR docker images
# - desktop bundles: Linux .deb/.AppImage, Windows NSIS, macOS .dmg/.app.tar.gz
# - Android release APK + AAB (signed with ANDROID_KEYSTORE_* secrets when set)
# The desktop-* / android-v* tags still exist for platform-only releases.
on: on:
push: push:
tags: ['v*'] tags: ['v*']
@ -9,21 +14,17 @@ permissions:
packages: write packages: write
jobs: jobs:
goreleaser: server:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 22
cache: yarn cache: yarn
cache-dependency-path: frontend/yarn.lock
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version-file: go.mod
cache: true
- uses: docker/setup-qemu-action@v3 - uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3 - uses: docker/login-action@v3
@ -31,11 +32,120 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: goreleaser/goreleaser-action@v6 - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with: with:
distribution: goreleaser distribution: goreleaser
version: '~> v2' version: '~> v2'
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
desktop:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
cache-dependency-path: desktop/ui/yarn.lock
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: desktop
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev librsvg2-dev patchelf libfuse2 fuse3
- name: Install UI dependencies
working-directory: desktop/ui
run: yarn install --frozen-lockfile
- name: Install tauri-cli
run: cargo install tauri-cli --version "^2" --locked
- name: Build desktop bundles
working-directory: desktop
run: cargo tauri build
- name: Publish bundles to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
shell: bash
run: |
set -e
for _ in $(seq 1 90); do
gh release view "$TAG" >/dev/null 2>&1 && break
sleep 20
done
gh release view "$TAG" >/dev/null
FILES=$(find desktop/target/release/bundle -type f \
\( -name '*.deb' -o -name '*.AppImage' -o -name '*.msi' \
-o -name '*.exe' -o -name '*.dmg' -o -name '*.app.tar.gz' \))
[ -n "$FILES" ]
echo "$FILES" | while read -r f; do
name="cloudreve-desktop-$(basename "$f")"
cp "$f" "$name"
gh release upload "$TAG" "$name" --clobber
done
android:
runs-on: ubuntu-latest
defaults:
run:
working-directory: android
env:
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v4
- name: Decode release keystore
if: ${{ env.KEYSTORE_B64 != '' }}
run: |
echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/release.keystore"
{
echo "ANDROID_KEYSTORE_FILE=$RUNNER_TEMP/release.keystore"
echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"
echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}"
echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}"
} >> "$GITHUB_ENV"
- name: Build release APK + AAB
run: ./gradlew :app:assembleRelease :app:bundleRelease --console=plain
- name: Publish APK + AAB to GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
set -e
for _ in $(seq 1 90); do
gh release view "$TAG" >/dev/null 2>&1 && break
sleep 20
done
gh release view "$TAG" >/dev/null
APK=$(find app/build/outputs/apk/release -name 'app-release*.apk' | head -1)
AAB=$(find app/build/outputs/bundle/release -name 'app-release*.aab' | head -1)
cp "$APK" "cloudreve-android-${TAG}.apk"
cp "$AAB" "cloudreve-android-${TAG}.aab"
gh release upload "$TAG" \
"cloudreve-android-${TAG}.apk" \
"cloudreve-android-${TAG}.aab" --clobber

@ -0,0 +1,42 @@
# Privacy Policy — Cloudreve Mobile
**Last updated: 2026-09-20**
Cloudreve Mobile is an open-source client for self-hosted Cloudreve servers. It is designed so that your data stays between your device and the server you choose.
## What the app does
- Connects to a **Cloudreve server URL that you provide**. All file transfers, authentication, and API calls go directly between your device and that server.
- Stores your server address and session credentials **locally on your device** (Android DataStore / app-private storage).
- Uploads files, photos, or folders **only when you initiate it** (manual upload, share sheet, or a sync/camera-backup feature you explicitly enable).
- Optionally reads media files and folders you select for upload or sync features.
## What the app does not do
- No analytics, no telemetry, no crash reporting to us.
- No advertising, no tracking SDKs, no third-party data sharing.
- No data is sent to the developers of this app. There is no developer-operated backend — your configured server is the only remote endpoint.
## Data safety summary
| Category | Collected | Shared |
|---|---|---|
| Files and photos | Only files you choose to upload, sent to your own server | Never |
| Credentials | Stored on-device only | Never |
| Personal info | None | None |
## Encryption in transit
Traffic is encrypted when your server is configured with HTTPS. If you configure a plain-HTTP server address, traffic is not encrypted — this is under your control as the server operator.
## Data deletion
Uninstalling the app removes all locally stored credentials and settings. Data on your server is governed by your own server configuration.
## Source code
The full source is auditable: https://github.com/Dvorinka/cloudreve
## Contact
Open an issue at https://github.com/Dvorinka/cloudreve/issues

@ -4,3 +4,4 @@ build/
local.properties local.properties
*.iml *.iml
.cxx/ .cxx/
.kotlin/

@ -10,11 +10,11 @@ android {
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "org.cloudreve.android" applicationId = "com.tdvorak.cloudreve"
minSdk = 28 minSdk = 28
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.1.0" versionName = "1.0.0"
} }
signingConfigs { signingConfigs {

@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-sdk
android:minSdkVersion="28"
android:targetSdkVersion="35" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@ -16,6 +20,7 @@
<application <application
android:name=".CloudreveApp" android:name=".CloudreveApp"
android:allowBackup="true" android:allowBackup="true"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"

@ -149,6 +149,7 @@ data class UploadSessionResponse(
val uri: String = "", val uri: String = "",
@SerialName("storage_policy") val storagePolicy: StoragePolicy? = null, @SerialName("storage_policy") val storagePolicy: StoragePolicy? = null,
@SerialName("callback_secret") val callbackSecret: String = "", @SerialName("callback_secret") val callbackSecret: String = "",
@SerialName("rapid_uploaded") val rapidUploaded: Boolean = false,
) )
@Serializable @Serializable

@ -109,22 +109,28 @@ class FileRepository(private val api: ApiClient) {
mimeType = mime, mimeType = mime,
) )
val urls = session.uploadUrls if (session.rapidUploaded) return session
if (urls.isNullOrEmpty()) {
// Zero-chunk policies (e.g. rapid-upload hit) need nothing further.
return session
}
val svc = api.service() val urls = session.uploadUrls
// Local/slave policies return no presigned URLs: chunks go to the
// conventional POST /api/v4/file/upload/{session}/{index} endpoint and
// the server completes the session once every chunk has landed.
val conventional = urls.isNullOrEmpty()
val chunkSize = if (session.chunkSize > 0) session.chunkSize else localFile.length().coerceAtLeast(1) val chunkSize = if (session.chunkSize > 0) session.chunkSize else localFile.length().coerceAtLeast(1)
val totalChunks = ((localFile.length() + chunkSize - 1) / chunkSize).toInt().coerceAtLeast(1)
val svc = api.service()
var sent = 0L var sent = 0L
localFile.inputStream().buffered().use { input -> localFile.inputStream().buffered().use { input ->
val buffer = ByteArray(chunkSize.coerceAtMost(64L * 1024 * 1024).toInt()) val buffer = ByteArray(chunkSize.coerceAtMost(64L * 1024 * 1024).toInt())
var index = 0 for (index in 0 until totalChunks) {
while (true) {
val read = input.readNBytes(buffer, 0, buffer.size) val read = input.readNBytes(buffer, 0, buffer.size)
if (read <= 0 || index >= urls.size) break if (index == 0 && read == 0 && localFile.length() > 0) break
val target = resolveUrl(urls[index]) val target = if (conventional) {
session_serverBase() + "/api/v4/file/upload/${session.sessionId}/$index"
} else {
if (index >= urls!!.size) break
resolveUrl(urls[index])
}
val body = buffer.copyOf(read) val body = buffer.copyOf(read)
.toRequestBody(mime.toMediaTypeOrNull()) .toRequestBody(mime.toMediaTypeOrNull())
val resp = if (target.contains("/file/upload/")) { val resp = if (target.contains("/file/upload/")) {
@ -138,8 +144,8 @@ class FileRepository(private val api: ApiClient) {
} }
resp.body()?.close() resp.body()?.close()
sent += read sent += read
index++
onProgress(sent, localFile.length()) onProgress(sent, localFile.length())
if (read <= 0) break
} }
} }

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- stylized cloud -->
<path
android:fillColor="#FFFFFF"
android:pathData="M79,62c-1.1,0 -2.2,0.1 -3.3,0.3C72,52 62.4,44 51,44c-10,0 -18.5,6.4 -21.7,15.4C22.9,60.6 19,64.7 19,69.7 19,76.6 25.2,82 32.5,82L79,82c6.6,0 12,-4.9 12,-11S85.6,62 79,62z" />
</vector>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#4A6FA5</color> <color name="ic_launcher_background">#FFFFFF</color>
</resources> </resources>

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Cloudreve</string> <string name="app_name">Cloudreve Mobile</string>
</resources> </resources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -0,0 +1,21 @@
Cloudreve Mobile is the open-source Android client for Cloudreve — the self-hosted cloud storage platform. Connect to your own Cloudreve server and take your files anywhere.
YOUR FILES, YOUR SERVER
No third-party cloud. Your data lives on infrastructure you control — the app talks directly to the server you configure, nothing else.
FEATURES
• Browse, upload, and download files and folders
• Create and manage share links
• Full-text search across your drive
• Automatic camera backup with Wi-Fi-only option
• Keep files offline with one tap
• Local sync folder — mirror a device folder to your server
• Android Files app integration (DocumentsProvider)
• Share-sheet target — send files from any app
• Quick Settings tile for one-tap camera backup
• Notifications for uploads, downloads, and background tasks
OPEN SOURCE
Fully open source — audit the code, self-host the server, own your data.
Requires a self-hosted Cloudreve server (v4+). Source code and documentation: https://github.com/Dvorinka/cloudreve

@ -0,0 +1 @@
Your self-hosted cloud — sync, share, and manage files on your terms

@ -0,0 +1,21 @@
Cloudreve Mobile is the open-source Android client for Cloudreve — the self-hosted cloud storage platform. Connect to your own Cloudreve server and take your files anywhere.
YOUR FILES, YOUR SERVER
No third-party cloud. Your data lives on infrastructure you control — the app talks directly to the server you configure, nothing else.
FEATURES
• Browse, upload, and download files and folders
• Create and manage share links
• Full-text search across your drive
• Automatic camera backup with Wi-Fi-only option
• Keep files offline with one tap
• Local sync folder — mirror a device folder to your server
• Android Files app integration (DocumentsProvider)
• Share-sheet target — send files from any app
• Quick Settings tile for one-tap camera backup
• Notifications for uploads, downloads, and background tasks
OPEN SOURCE
Fully open source — audit the code, self-host the server, own your data.
Requires a self-hosted Cloudreve server (v4+). Source code and documentation: https://github.com/Dvorinka/cloudreve

@ -0,0 +1 @@
Your self-hosted cloud — sync, share, and manage files on your terms
Loading…
Cancel
Save