Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4e2686f
Add XeniaCloud unsigned CI build workflow
Sep 7, 2026
88555e7
Merge pull request #1 from Krateos-BV/xenia-rebrand
xeniacloud Sep 7, 2026
4051869
XNT-54: rebrand app name, launcher icon, and splash color to Xenia Files
Sep 8, 2026
32a1e33
XNT-54: change applicationId to eu.xeniacloud.files and de-collide pr…
xeniacloud Sep 10, 2026
0307b94
Merge pull request #2 from Krateos-BV/xenia-rebrand
xeniacloud Sep 11, 2026
2daa451
XNT-54: remove upstream store links and rebrand root folder display name
v-giannakopoulos Sep 11, 2026
6fff887
Merge pull request #3 from Krateos-BV/xenia-rebrand-followups
xeniacloud Sep 11, 2026
88bbedb
XNT-87: fix xenia-ci trigger after xenia-rebrand branch deletion
v-giannakopoulos Sep 11, 2026
c7f2a45
fix(ecosystem): open the Xenia companion apps, not upstream's
xeniacloud Sep 13, 2026
45ec933
fix(export): confirm a finished export on screen
xeniacloud Sep 13, 2026
8aed6d2
Merge pull request #4 from Krateos-BV/xnt-91-xenia-ecosystem-manager
xeniacloud Sep 15, 2026
373d688
fix(branding): close the remaining Xenia Files rebrand gaps
xeniacloud Sep 13, 2026
b73d6e2
Merge pull request #7 from Krateos-BV/xnt-85-rebrand-gaps-on-master
xeniacloud Sep 15, 2026
eb224c6
fix(onboarding): drop provider signup and self-hosting calls to action
v-giannakopoulos Sep 15, 2026
e66c113
Merge pull request #8 from Krateos-BV/xnt-110-hide-provider-onboarding
xeniacloud Sep 15, 2026
983433c
fix(drawer): drop the capability parameter left unused by XNT-85
v-giannakopoulos Sep 15, 2026
3ba1e21
build: refresh the android-library pin to 8cbaa7e8
v-giannakopoulos Sep 15, 2026
58699b5
Merge pull request #9 from Krateos-BV/xnt-unused-capability-param
xeniacloud Sep 15, 2026
0e11b28
Merge pull request #10 from Krateos-BV/xnt-library-pin-refresh
xeniacloud Sep 15, 2026
9a1ceb1
Merge pull request #6 from Krateos-BV/xnt-89-export-confirmation
xeniacloud Sep 15, 2026
7a9e252
ci: drop android-library pin-freshness check
xeniacloud Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/xenia-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: 2026 XeniaCloud
# SPDX-License-Identifier: GPL-3.0-or-later

name: "XeniaCloud CI"

on:
workflow_dispatch:
push:
branches: [ master ]
pull_request:
branches: [ master ]

permissions: read-all

concurrency:
group: xenia-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: 21
- name: Build Generic debug (unsigned)
run: |
echo "org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g" >> gradle.properties
echo "org.gradle.caching=true" >> gradle.properties
echo "org.gradle.parallel=true" >> gradle.properties
./gradlew assembleGenericDebug
- name: Upload unsigned APK
uses: actions/upload-artifact@v4
with:
name: app-generic-debug
path: app/build/outputs/apk/generic/debug/*.apk
retention-days: 90
10 changes: 0 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@

[![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/android)](https://api.reuse.software/info/github.com/nextcloud/android) [![Build Status](https://drone.nextcloud.com/api/badges/nextcloud/android/status.svg)](https://drone.nextcloud.com/nextcloud/android) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/fb4cf26336774ee3a5c9adfe829c41aa)](https://app.codacy.com/gh/nextcloud/android/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Releases](https://img.shields.io/github/release/nextcloud/android.svg)](https://github.com/nextcloud/android/releases/latest)

[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Download from Google Play"
height="80">](https://play.google.com/store/apps/details?id=com.nextcloud.client)
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/com.nextcloud.client/)
[<img src="https://github.com/user-attachments/assets/713d71c5-3dec-4ec4-a3f2-8d28d025a9c6"
alt="Get it with Obtainium"
height="80">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.nextcloud.client%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fnextcloud%2Fandroid%22%2C%22author%22%3A%22nextcloud%22%2C%22name%22%3A%22Nextcloud%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5Enextcloud.*%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22Nextcloud%20ist%20eine%20Cloudanwendung%2C%20die%20selbst%20gehostet%20werden%20kann.%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Atrue%7D%22%2C%22overrideSource%22%3Anull%7D)

Signing certificate fingerprint to [verify](https://developer.android.com/studio/command-line/apksigner#usage-verify) the APK using the official Android documentation.
- APK with "gplay" name, found [here](https://github.com/nextcloud/android/releases) or distributed via Google Play Store
- APK with "nextcloud", found [here](https://github.com/nextcloud/android/releases)
Expand Down
8 changes: 7 additions & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"

[[annotations]]
path = ["app/src/**/res/mipmap-**dpi/ic_launcher.png", "app/src/**/ic_launcher-web.png", "src/**/fastlane/metadata/en-US/images/*.png", "src/generic/fastlane/metadata/android/en-US/images/icon.png", "src/versionDev/fastlane/metadata/android/en-US/images/icon.png", "app/src/main/ic_launcher-web-round.png"]
path = ["app/src/qa/res/mipmap-**dpi/ic_launcher.png", "app/src/versionDev/res/mipmap-**dpi/ic_launcher.png", "app/src/qa/ic_launcher-web.png", "app/src/versionDev/ic_launcher-web.png", "src/**/fastlane/metadata/en-US/images/*.png", "src/generic/fastlane/metadata/android/en-US/images/icon.png", "src/versionDev/fastlane/metadata/android/en-US/images/icon.png"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2017-2025 Nextcloud GmbH <https://nextcloud.com/trademarks/>"
SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks"
Expand All @@ -46,3 +46,9 @@ path = [".idea/**", "app/schemas/com.nextcloud.client.database.NextcloudDatabase
precedence = "aggregate"
SPDX-FileCopyrightText = "2016-2025 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"

[[annotations]]
path = ["app/src/main/res/mipmap-mdpi/ic_launcher.png", "app/src/main/res/mipmap-hdpi/ic_launcher.png", "app/src/main/res/mipmap-xhdpi/ic_launcher.png", "app/src/main/res/mipmap-xxhdpi/ic_launcher.png", "app/src/main/res/mipmap-xxxhdpi/ic_launcher.png", "app/src/main/ic_launcher-web.png", "app/src/main/ic_launcher-web-round.png"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 XeniaCloud"
SPDX-License-Identifier = "AGPL-3.0-or-later"
12 changes: 6 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ android {
"TEST_SERVER_PASSWORD" to ncTestServerPassword.toString(),
"disableAnalytics" to "true"
)
applicationId = "com.nextcloud.client"
applicationId = "eu.xeniacloud.files"
minSdk = 28
targetSdk = 36
compileSdk = 37
Expand Down Expand Up @@ -155,29 +155,29 @@ android {
productFlavors {
// used for f-droid
register("generic") {
applicationId = "com.nextcloud.client"
applicationId = "eu.xeniacloud.files"
dimension = "default"
}

register("gplay") {
applicationId = "com.nextcloud.client"
applicationId = "eu.xeniacloud.files"
dimension = "default"
}

register("huawei") {
applicationId = "com.nextcloud.client"
applicationId = "eu.xeniacloud.files"
dimension = "default"
}

register("versionDev") {
applicationId = "com.nextcloud.android.beta"
applicationId = "eu.xeniacloud.files.beta"
dimension = "default"
versionCode = 20220322
versionName = "20220322"
}

register("qa") {
applicationId = "com.nextcloud.android.qa"
applicationId = "eu.xeniacloud.files.qa"
dimension = "default"
versionCode = 1
versionName = "1"
Expand Down
Binary file modified app/src/main/ic_launcher-web-round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/main/ic_launcher-web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 XeniaCloud
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.ecosystem

/**
* The XeniaCloud companion apps this client can hand an account to.
*
* Replaces android-common's `EcosystemApp`, whose package names are hardcoded to
* upstream's clients. There is no FILES entry: this *is* the Files client.
*
* These are the production applicationIds. The dev and QA flavours of those apps carry
* `.dev`/`.qa` suffixes and are deliberately not listed: the drawer should find the app a
* user actually installs, not a debug build. Add them here if that ever changes.
*/
enum class XeniaEcosystemApp(val packageNames: List<String>) {
NOTES(listOf("eu.xeniacloud.notes")),
TALK(listOf("eu.xeniacloud.talk"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 XeniaCloud
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.ecosystem

import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.util.Log
import android.view.View
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.core.utils.ecosystem.AccountReceiverCallback
import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager
import com.owncloud.android.R
import java.util.regex.Pattern

/**
* Opens the XeniaCloud companion apps from the drawer's ecosystem tiles and from the
* file-list "open in" button.
*
* This exists because android-common's [EcosystemManager] hardcodes upstream's package
* names in its `EcosystemApp` enum, which is a plain enum with no string-resource seam
* and no open member to subclass. There is nothing to override, so the sending half of
* the logic is reimplemented here against [XeniaEcosystemApp] instead.
*
* Only the sending half. [receiveAccount] delegates straight to the library, which
* carries no package names or branding on that path - it only reads the account name out
* of an incoming intent.
*
* The wire contract is deliberately unchanged: the intent action is still
* `com.nextcloud.intent.OPEN_ECOSYSTEM_APP` and the extra key is still `KEY_ACCOUNT`.
* Both are opaque identifiers that Files, Talk and Notes have to agree on, not branding,
* and renaming either would force a synchronised three-app release for no user benefit.
*/
class XeniaEcosystemManager(private val activity: Activity) {
companion object {
private const val TAG = "XeniaEcosystemManager"

// Must match android-common's EcosystemManager, and the intent-filter in this
// app's manifest. See the class doc.
private const val ECOSYSTEM_INTENT_ACTION = "com.nextcloud.intent.OPEN_ECOSYSTEM_APP"
private const val EXTRA_KEY_ACCOUNT = "KEY_ACCOUNT"
}

private val accountNamePattern = Pattern.compile(EcosystemManager.ACCOUNT_NAME_PATTERN_REGEX)

// Only ever touched by receiveAccount; built lazily so the send path does not
// pay for a second copy of the account-name Pattern.
private val delegate by lazy { EcosystemManager(activity) }

/**
* Opens a XeniaCloud companion app, handing it the account to switch to.
*
* Unlike the library version, a missing app does not redirect to the Play Store:
* XeniaCloud has no store listings yet, so that redirect would land the user on a
* "not found" page. The user gets a snackbar and nothing else happens.
*
* @param app the companion app to open
* @param accountName the account to hand over, e.g. "user@cloud.example.com"
*/
fun openApp(app: XeniaEcosystemApp, accountName: String?) {
if (accountName.isNullOrBlank()) {
Log.w(TAG, "no account name given for ${app.name}")
showSnackbar(R.string.xenia_ecosystem_account_missing)
return
}

if (!accountNamePattern.matcher(accountName).matches()) {
Log.w(TAG, "account name is not in the expected form")
showSnackbar(R.string.xenia_ecosystem_account_invalid)
return
}

val installedPackage = app.packageNames.firstOrNull { packageName ->
activity.packageManager.getLaunchIntentForPackage(packageName) != null
}

if (installedPackage == null) {
Log.w(TAG, "none of ${app.packageNames} is installed")
showSnackbar(R.string.xenia_ecosystem_app_not_installed)
return
}

val launchIntent = Intent(ECOSYSTEM_INTENT_ACTION).apply {
setPackage(installedPackage)
putExtra(EXTRA_KEY_ACCOUNT, accountName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}

try {
activity.startActivity(launchIntent)
} catch (e: ActivityNotFoundException) {
// The package is installed but does not handle the ecosystem action - an
// older build, or one of the companion apps shipped without the filter.
Log.e(TAG, "$installedPackage does not handle $ECOSYSTEM_INTENT_ACTION", e)
showSnackbar(R.string.xenia_ecosystem_app_not_installed)
}
}

/**
* Handles an incoming ecosystem intent. Delegates to android-common unchanged.
*/
fun receiveAccount(intent: Intent?, callback: AccountReceiverCallback) {
delegate.receiveAccount(intent, callback)
}

private fun showSnackbar(messageRes: Int) {
val rootContent = activity.findViewById<View>(android.R.id.content) ?: return
Snackbar.make(rootContent, activity.getString(messageRes), Snackbar.LENGTH_LONG).show()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ internal class BackgroundJobManagerImpl(
user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE,
started = timestamp,
progress = info.progress.getInt("progress", -1),
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE
workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE,
output = info.outputData.keyValueMap
)
} else {
null
Expand Down
15 changes: 12 additions & 3 deletions app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.nextcloud.client.account.User
import com.nextcloud.client.jobs.worker.WorkerFilesPayload
import com.owncloud.android.R
Expand Down Expand Up @@ -46,6 +47,12 @@ class FilesExportWork(
companion object {
private const val NOTIFICATION_ID = 179
const val FILES_TO_DOWNLOAD = "files_to_download"

// Read back from JobInfo.output so the caller can confirm the export on screen
// with the same counts the summary notification reports.
const val EXPORTED_COUNT = "exported_count"
const val FAILED_COUNT = "failed_count"

private val TAG = FilesExportWork::class.simpleName
}

Expand All @@ -58,7 +65,7 @@ class FilesExportWork(
if (fileIds.isEmpty()) {
Log_OC.w(TAG, "file export was started without any file")
WorkerFilesPayload.cleanup(path)
return Result.success()
return Result.success(exportCounts(0, 0))
}

val storageManager = FileDataStorageManager(user, contentResolver)
Expand All @@ -67,13 +74,15 @@ class FilesExportWork(
val (succeeded, failed) = exportFiles(fileIds, storageManager)
notificationManager.cancel(NOTIFICATION_ID)
showSummaryNotification(succeeded, failed)
return Result.success(exportCounts(succeeded, failed))
} finally {
WorkerFilesPayload.cleanup(path)
}

return Result.success()
}

private fun exportCounts(succeeded: Int, failed: Int) =
workDataOf(EXPORTED_COUNT to succeeded, FAILED_COUNT to failed)

@Suppress("DEPRECATION")
private suspend fun exportFiles(fileIDs: List<Long>, storageManager: FileDataStorageManager): Pair<Int, Int> =
withContext(Dispatchers.IO) {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ data class JobInfo(
val user: String = "",
val workerClass: String = "",
val started: Date = Date(0),
val progress: Int = 0
val progress: Int = 0,
val output: Map<String, Any?> = emptyMap()
)

data class LogEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ class FirstRunActivity :

registerActivityResult()
setupLoginButton()
setupSignupButton(MDMConfig.showIntro(this))
setupHostOwnServerTextView(MDMConfig.showIntro(this))
setupSignupButton(showProviderOptions())
setupHostOwnServerTextView(showProviderOptions())
deleteAccountAtFirstLaunch()
setupFeaturesViewAdapter()
handleOnBackPressed()
Expand Down Expand Up @@ -130,6 +130,16 @@ class FirstRunActivity :
}
}

/**
* Whether onboarding offers provider signup and self-hosting.
*
* Gated separately from [MDMConfig.showIntro], which also decides whether onboarding
* launches at all, so that a single-tenant build can drop these two calls to action
* while keeping the onboarding screens.
*/
private fun showProviderOptions(): Boolean =
MDMConfig.showIntro(this) && resources.getBoolean(R.bool.show_onboarding_provider_options)

private fun setupSignupButton(isProviderOrOwnInstallationVisible: Boolean) {
defaultViewThemeUtils?.material?.colorMaterialButtonOutlinedOnPrimary(binding.signup)
binding.signup.visibility = if (isProviderOrOwnInstallationVisible) View.VISIBLE else View.GONE
Expand Down
Loading