diff --git a/.github/workflows/xenia-ci.yml b/.github/workflows/xenia-ci.yml
new file mode 100644
index 000000000000..eb801cbccfda
--- /dev/null
+++ b/.github/workflows/xenia-ci.yml
@@ -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
diff --git a/README.md b/README.md
index 1a23a124eb06..642d3bfa2c9e 100644
--- a/README.md
+++ b/README.md
@@ -6,16 +6,6 @@
[](https://api.reuse.software/info/github.com/nextcloud/android) [](https://drone.nextcloud.com/nextcloud/android) [](https://app.codacy.com/gh/nextcloud/android/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://github.com/nextcloud/android/releases/latest)
-[
](https://play.google.com/store/apps/details?id=com.nextcloud.client)
-[
](https://f-droid.org/packages/com.nextcloud.client/)
-[
](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)
diff --git a/REUSE.toml b/REUSE.toml
index 1fda7dfdbb65..2e07b9f220b2 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -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 "
SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks"
@@ -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"
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6235237224a3..33986f652062 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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
@@ -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"
diff --git a/app/src/main/ic_launcher-web-round.png b/app/src/main/ic_launcher-web-round.png
index dc58ff797add..233e0a6e1e67 100644
Binary files a/app/src/main/ic_launcher-web-round.png and b/app/src/main/ic_launcher-web-round.png differ
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
index 5ece49f5452a..da546b0df28f 100644
Binary files a/app/src/main/ic_launcher-web.png and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/com/nextcloud/client/ecosystem/XeniaEcosystemApp.kt b/app/src/main/java/com/nextcloud/client/ecosystem/XeniaEcosystemApp.kt
new file mode 100644
index 000000000000..c19c7d74d3bc
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/client/ecosystem/XeniaEcosystemApp.kt
@@ -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) {
+ NOTES(listOf("eu.xeniacloud.notes")),
+ TALK(listOf("eu.xeniacloud.talk"))
+}
diff --git a/app/src/main/java/com/nextcloud/client/ecosystem/XeniaEcosystemManager.kt b/app/src/main/java/com/nextcloud/client/ecosystem/XeniaEcosystemManager.kt
new file mode 100644
index 000000000000..9e6c31cb2bdb
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/client/ecosystem/XeniaEcosystemManager.kt
@@ -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(android.R.id.content) ?: return
+ Snackbar.make(rootContent, activity.getString(messageRes), Snackbar.LENGTH_LONG).show()
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
index daba20131e20..3bf185f90b5f 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
@@ -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
diff --git a/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt b/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
index de69651c67e0..f42c0170ec05 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
@@ -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
@@ -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
}
@@ -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)
@@ -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, storageManager: FileDataStorageManager): Pair =
withContext(Dispatchers.IO) {
diff --git a/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt b/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt
index b23b9331d2b2..810d5f90f7f1 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt
@@ -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 = emptyMap()
)
data class LogEntry(
diff --git a/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt b/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt
index 824e7b2d688c..aeee128da872 100644
--- a/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt
+++ b/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt
@@ -83,8 +83,8 @@ class FirstRunActivity :
registerActivityResult()
setupLoginButton()
- setupSignupButton(MDMConfig.showIntro(this))
- setupHostOwnServerTextView(MDMConfig.showIntro(this))
+ setupSignupButton(showProviderOptions())
+ setupHostOwnServerTextView(showProviderOptions())
deleteAccountAtFirstLaunch()
setupFeaturesViewAdapter()
handleOnBackPressed()
@@ -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
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
index 805b645a9095..7f1293476bef 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
@@ -49,8 +49,8 @@
import com.google.android.material.button.MaterialButton;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.progressindicator.LinearProgressIndicator;
-import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp;
-import com.nextcloud.android.common.core.utils.ecosystem.EcosystemManager;
+import com.nextcloud.client.ecosystem.XeniaEcosystemApp;
+import com.nextcloud.client.ecosystem.XeniaEcosystemManager;
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import com.nextcloud.client.account.User;
import com.nextcloud.client.di.Injectable;
@@ -87,7 +87,6 @@
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
-import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation;
import com.owncloud.android.operations.GetCapabilitiesOperation;
import com.owncloud.android.ui.events.AccountRemovedEvent;
@@ -229,7 +228,7 @@ protected int getMenuItemId() {
return R.id.nav_all_files;
}
- private EcosystemManager ecosystemManager;
+ private XeniaEcosystemManager ecosystemManager;
@Inject
AppPreferences preferences;
@@ -417,7 +416,7 @@ public void updateHeader() {
drawerHeader.setBackgroundColor(primaryColor);
if (!TextUtils.isEmpty(serverLogoURL) && URLUtil.isValidUrl(serverLogoURL)) {
- Target target = createSVGLogoTarget(primaryColor, capability);
+ Target target = createSVGLogoTarget(primaryColor);
GlideHelper.INSTANCE.loadIntoTarget(this,
accountManager.getCurrentOwnCloudAccount(),
serverLogoURL,
@@ -438,7 +437,7 @@ public void updateHeader() {
}
}
- private Target createSVGLogoTarget(int primaryColor, OCCapability capability) {
+ private Target createSVGLogoTarget(int primaryColor) {
return new CustomTarget<>() {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
@@ -475,8 +474,7 @@ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition s
};
LayerDrawable layerDrawable = new LayerDrawable(drawables);
- String name = capability.getServerName();
- setDrawerHeaderLogo(layerDrawable, name);
+ setDrawerHeaderLogo(layerDrawable);
}
@Override
@@ -497,11 +495,15 @@ private void showTopBanner(ConstraintLayout banner) {
final var optionalUser = getUser();
if (optionalUser.isPresent()) {
final var accountName = optionalUser.get().getAccountName();
- notesView.setOnClickListener(v -> ecosystemManager.openApp(EcosystemApp.NOTES, accountName));
- talkView.setOnClickListener(v -> ecosystemManager.openApp(EcosystemApp.TALK, accountName));
+ notesView.setOnClickListener(v -> ecosystemManager.openApp(XeniaEcosystemApp.NOTES, accountName));
+ talkView.setOnClickListener(v -> ecosystemManager.openApp(XeniaEcosystemApp.TALK, accountName));
}
- moreView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this));
+ // TODO(XNT-54): re-enable once XeniaCloud has real store listings. Upstream
+ // points this at a Play Store search for its own publisher; there is nothing
+ // to search for yet, so the tile is hidden rather than sending users to an
+ // empty result - or worse, to upstream's apps.
+ moreView.setVisibility(View.GONE);
assistantView.setOnClickListener(v -> startAssistantScreen());
final var optionalCapabilities = getCapabilities();
if (optionalCapabilities.isPresent()) {
@@ -542,18 +544,13 @@ private void showTopBanner(ConstraintLayout banner) {
banner.setVisibility(View.VISIBLE);
}
- private void setDrawerHeaderLogo(Drawable drawable, String serverName) {
+ // The server name is deliberately not drawn beside the logo. Upstream assumes a themed
+ // instance supplies an icon-only mark that the name completes; XeniaCloud's theming logo
+ // is a full wordmark, so drawing both renders the brand name twice.
+ private void setDrawerHeaderLogo(Drawable drawable) {
ImageView imageHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_logo);
imageHeader.setImageDrawable(drawable);
imageHeader.setAdjustViewBounds(true);
-
- if (!TextUtils.isEmpty(serverName)) {
- TextView serverNameView = mNavigationViewHeader.findViewById(R.id.drawer_header_server_name);
- serverNameView.setVisibility(View.VISIBLE);
- serverNameView.setText(serverName);
- serverNameView.setTextColor(themeColorUtils.unchangedFontColor(this));
- }
-
}
/**
@@ -797,7 +794,7 @@ private void launchActivityForSearch(SearchEvent searchEvent, int menuItemId) {
startActivity(intent);
}
- public EcosystemManager getEcosystemManager() {
+ public XeniaEcosystemManager getEcosystemManager() {
return ecosystemManager;
}
@@ -1170,7 +1167,7 @@ protected void onCreate(Bundle savedInstanceState) {
externalLinksProvider = new ExternalLinksProvider(getContentResolver());
arbitraryDataProvider = new ArbitraryDataProviderImpl(this);
- ecosystemManager = new EcosystemManager(this);
+ ecosystemManager = new XeniaEcosystemManager(this);
}
@Override
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
index 4aa7ae79761f..ed49379bf4a5 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
@@ -27,7 +27,7 @@
import com.elyeproj.loaderviewlibrary.LoaderImageView;
import com.google.android.material.chip.Chip;
-import com.nextcloud.android.common.core.utils.ecosystem.EcosystemApp;
+import com.nextcloud.client.ecosystem.XeniaEcosystemApp;
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import com.nextcloud.client.account.User;
import com.nextcloud.client.database.entity.OfflineOperationEntity;
@@ -471,7 +471,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi
if (activity instanceof DrawerActivity drawerActivity) {
final var ecosystemManager = drawerActivity.getEcosystemManager();
if (ecosystemManager != null) {
- listHeaderOpenInBinding.openInButton.setOnClickListener(v -> ecosystemManager.openApp(EcosystemApp.NOTES, user.getAccountName()));
+ listHeaderOpenInBinding.openInButton.setOnClickListener(v -> ecosystemManager.openApp(XeniaEcosystemApp.NOTES, user.getAccountName()));
}
}
}
diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
index 967d244f8bf9..bd9a994452f4 100755
--- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
+++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
@@ -15,12 +15,14 @@
import android.accounts.Account;
import android.app.Activity;
+import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
import android.net.Uri;
import android.os.Environment;
import android.os.StatFs;
@@ -29,9 +31,13 @@
import android.view.View;
import android.webkit.MimeTypeMap;
+import com.google.android.material.snackbar.Snackbar;
+
import com.nextcloud.client.account.CurrentAccountProvider;
import com.nextcloud.client.account.User;
import com.nextcloud.client.jobs.BackgroundJobManager;
+import com.nextcloud.client.jobs.FilesExportWork;
+import com.nextcloud.client.jobs.JobInfo;
import com.nextcloud.client.jobs.download.FileDownloadHelper;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.jobs.upload.FileUploadWorker;
@@ -101,6 +107,10 @@
import androidx.core.content.FileProvider;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.work.WorkInfo;
import kotlin.Unit;
/**
@@ -1128,7 +1138,69 @@ public void exportFiles(Collection files,
Context context,
View view,
BackgroundJobManager backgroundJobManager) {
- backgroundJobManager.startImmediateFilesExportJob(files);
+ LiveData job = backgroundJobManager.startImmediateFilesExportJob(files);
+
+ if (view == null || !(context instanceof LifecycleOwner lifecycleOwner)) {
+ return;
+ }
+
+ job.observe(lifecycleOwner, new Observer<>() {
+ @Override
+ public void onChanged(JobInfo jobInfo) {
+ if (jobInfo == null || !WorkInfo.State.SUCCEEDED.name().equals(jobInfo.getState())) {
+ return;
+ }
+ job.removeObserver(this);
+ showExportResult(view, jobInfo);
+ }
+ });
+ }
+
+ private void showExportResult(View view, JobInfo jobInfo) {
+ // The job outlives the screen that started it. The observer is scoped to the host
+ // activity, so by the time it fires the fragment's view may be detached and
+ // Snackbar.make would fail to find a parent. The notification still reports the
+ // outcome in that case.
+ if (!view.isAttachedToWindow()) {
+ return;
+ }
+
+ int exported = readCount(jobInfo, FilesExportWork.EXPORTED_COUNT);
+ int failed = readCount(jobInfo, FilesExportWork.FAILED_COUNT);
+
+ if (exported == 0 && failed == 0) {
+ return;
+ }
+
+ // Same wording as the summary notification, so the two never disagree.
+ Resources resources = view.getResources();
+ String message;
+ if (failed == 0) {
+ message = resources.getQuantityString(R.plurals.export_successful, exported, exported);
+ } else if (exported == 0) {
+ message = resources.getQuantityString(R.plurals.export_failed, failed, failed);
+ } else {
+ message = resources.getQuantityString(R.plurals.export_partially_failed, exported, exported);
+ }
+
+ Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
+ if (exported > 0) {
+ snackbar.setAction(R.string.locate_folder, v -> {
+ Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ view.getContext().startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log_OC.w(TAG, "No activity handles ACTION_VIEW_DOWNLOADS");
+ }
+ });
+ }
+ snackbar.show();
+ }
+
+ private int readCount(JobInfo jobInfo, String key) {
+ Object value = jobInfo.getOutput().get(key);
+ return value instanceof Integer ? (Integer) value : 0;
}
public long getOpIdWaitingFor() {
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 9d33a9f53b6a..d5e5587ce0b9 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -3,6 +3,7 @@
~
~ SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH
~ SPDX-FileCopyrightText: 2020-2024 Tobias Kaminsky
+ ~ SPDX-FileCopyrightText: 2026 XeniaCloud
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-->
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 0235efa7e6fb..2c457933111d 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,20 +1,22 @@
+ android:viewportWidth="480"
+ android:viewportHeight="480">
+ android:scaleX="1.041667"
+ android:scaleY="1.041667"
+ android:translateX="-35.70501"
+ android:translateY="13.034049">
+ android:pathData="m 188,296 c -36,0 -64,-28 -64,-62 0,-34 26,-61 60,-62 6,-40 38,-68 76,-68 38,0 70,28 76,68 34,1 60,28 60,62 0,34 -28,62 -64,62 z" />
diff --git a/app/src/main/res/drawable/logo.xml b/app/src/main/res/drawable/logo.xml
index 070fc5cfc667..2e12a8c5e653 100644
--- a/app/src/main/res/drawable/logo.xml
+++ b/app/src/main/res/drawable/logo.xml
@@ -1,16 +1,37 @@
+
-
+
+
+
+
+
diff --git a/app/src/main/res/drawable/nextcloud_logo.xml b/app/src/main/res/drawable/nextcloud_logo.xml
deleted file mode 100644
index ef6e0eb3ee03..000000000000
--- a/app/src/main/res/drawable/nextcloud_logo.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/nextcloud_splash_logo.xml b/app/src/main/res/drawable/nextcloud_splash_logo.xml
index d34ef1204da9..3b0b02e910f0 100644
--- a/app/src/main/res/drawable/nextcloud_splash_logo.xml
+++ b/app/src/main/res/drawable/nextcloud_splash_logo.xml
@@ -1,8 +1,7 @@
-
-
+
+
+
diff --git a/app/src/main/res/drawable/xenia_logo.xml b/app/src/main/res/drawable/xenia_logo.xml
new file mode 100644
index 000000000000..e119f9948a1e
--- /dev/null
+++ b/app/src/main/res/drawable/xenia_logo.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml
index 88f7ade88c4a..f01d39b9a7d5 100644
--- a/app/src/main/res/layout/drawer_header.xml
+++ b/app/src/main/res/layout/drawer_header.xml
@@ -31,22 +31,7 @@
android:contentDescription="@null"
android:maxWidth="96dp"
android:scaleType="fitCenter"
- android:src="@drawable/nextcloud_logo" />
-
-
+ android:src="@drawable/xenia_logo" />
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 2e264cebd31d..32a4a1a348f1 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index e34b6164db13..b36a63eab7ce 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index ddee9370af8e..ee8e7b4b9d69 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 702614fa7221..92353da317b8 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index d4dbd35854da..1cdd88a845af 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml
index 7df442ab878b..d3eefe60216e 100644
--- a/app/src/main/res/values/setup.xml
+++ b/app/src/main/res/values/setup.xml
@@ -3,6 +3,7 @@
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ ~ SPDX-FileCopyrightText: 2026 XeniaCloud
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-->
@@ -11,25 +12,37 @@
false
- Nextcloud
- nextcloud
- org.nextcloud
- com.nextcloud.android.providers.UsersAndGroupsSearchProvider
- com.nextcloud.android.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
- org.nextcloud.documents
- org.nextcloud.files
- org.nextcloud.imageCache.provider
+ Xenia Files
+ xeniacloud
+ eu.xeniacloud.files.provider
+ eu.xeniacloud.files.providers.UsersAndGroupsSearchProvider
+ eu.xeniacloud.files.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
+ eu.xeniacloud.files.documents
+ eu.xeniacloud.files.fileprovider
+ eu.xeniacloud.files.imageCache.provider
nextcloud.db
nextcloud
nextcloud
- Nextcloud
+ XeniaCloud
Mozilla/5.0 (Android) Nextcloud-android/%1$s%2$s
Mozilla/5.0 (Android %1$s) Mobile Nextcloud-android/%2$s
-
+
+ " (Xenia Files)"
- true
+ false
true
+
+ false
https://www.nextcloud.com/register
@@ -66,9 +79,9 @@
- #0082c9
- #006AA3
- #007cc2
+ #002266
+ #001233
+ #003399
#ffffff
#7fC0E3
@@ -89,14 +102,14 @@
true
https://nextcloud.com/privacy
false
- true
+ false
false
true
https://github.com/nextcloud/android
true
https://www.gnu.org/licenses/gpl-2.0.html
- "https://play.google.com/store/apps/details?id=com.nextcloud.client"
+
https://nextcloud.com/install
@@ -114,8 +127,8 @@
https://f-droid.org/repository/browse/?fdid=com.nextcloud.android.beta
https://download.nextcloud.com/android/dev/latest.apk
- https://play.google.com/apps/testing/com.nextcloud.client
- https://f-droid.org/repository/browse/?fdid=com.nextcloud.client
+
+
https://help.nextcloud.com/c/clients/android
https://app.transifex.com/nextcloud/nextcloud/android/
https://github.com/nextcloud/android/blob/master/CONTRIBUTING.md
@@ -128,7 +141,12 @@
nc
-
+
+ https://portal.xeniacloud.eu/index.php/login/v2
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c1589ad6f91d..fbf700acbac3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -171,7 +171,7 @@
No device credentials have been set up.
Show hidden files
Show app switcher
- Nextcloud app suggestions in navigation heading
+ Xenia app suggestions in navigation heading
Show media scan notifications
Notify about newly found media folders
Sync calendar and contacts
@@ -1468,12 +1468,15 @@
Flip vertically
Unable to edit image.
edited
- Nextcloud Notes
- Nextcloud Talk
- More Nextcloud Apps
+ Xenia Notes
+ Xenia Talk
+ More Xenia Apps
Notes
Talk
More
+ Account is missing. Cannot open the app.
+ Invalid account format. Must be an email.
+ App is not installed.
Details
%s mm
ƒ/%s
diff --git a/app/src/qa/res/values/setup.xml b/app/src/qa/res/values/setup.xml
index 6dd47627bcb4..658dfc255709 100644
--- a/app/src/qa/res/values/setup.xml
+++ b/app/src/qa/res/values/setup.xml
@@ -12,14 +12,14 @@
Nextcloud QA
- nextcloud.qa
- org.nextcloud.qa.provider
- org.nextcloud.qa.android.providers.UsersAndGroupsSearchProvider
- org.nextcloud.qa.android.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
- org.nextcloud.qa.documents
- org.nextcloud.qa.files
- org.nextcloud.qa.android.providers.imageCache
- Nextcloud QA
+ xeniacloud.qa
+ eu.xeniacloud.files.qa.provider
+ eu.xeniacloud.files.qa.providers.UsersAndGroupsSearchProvider
+ eu.xeniacloud.files.qa.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
+ eu.xeniacloud.files.qa.documents
+ eu.xeniacloud.files.qa.fileprovider
+ eu.xeniacloud.files.qa.imageCache.provider
+ XeniaCloud QA
true
https://github.com/nextcloud/android/commits/master
diff --git a/app/src/versionDev/res/values/setup.xml b/app/src/versionDev/res/values/setup.xml
index 4ae866e6728a..33ef70a98e91 100644
--- a/app/src/versionDev/res/values/setup.xml
+++ b/app/src/versionDev/res/values/setup.xml
@@ -12,14 +12,14 @@
Nextcloud Dev
- nextcloud.beta
- org.nextcloud.beta.provider
- org.nextcloud.beta.android.providers.UsersAndGroupsSearchProvider
- org.nextcloud.beta.android.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
- org.nextcloud.beta.documents
- org.nextcloud.beta.files
- org.nextcloud.beta.android.providers.imageCache
- Nextcloud Dev
+ xeniacloud.beta
+ eu.xeniacloud.files.beta.provider
+ eu.xeniacloud.files.beta.providers.UsersAndGroupsSearchProvider
+ eu.xeniacloud.files.beta.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
+ eu.xeniacloud.files.beta.documents
+ eu.xeniacloud.files.beta.fileprovider
+ eu.xeniacloud.files.beta.imageCache.provider
+ XeniaCloud Dev
true
https://github.com/nextcloud/android/commits/master
diff --git a/fastlane/Appfile b/fastlane/Appfile
index 64725e1fdeab..dd7ff6b807e8 100644
--- a/fastlane/Appfile
+++ b/fastlane/Appfile
@@ -1,2 +1,2 @@
json_key_file "~/.gradle/Google\ Play\ Android\ Developer-Fastlane.json"
-package_name "com.nextcloud.client"
+package_name "eu.xeniacloud.files"
diff --git a/fastlane/Screengrabfile b/fastlane/Screengrabfile
index 33be0a67f946..cc4d52b36a14 100644
--- a/fastlane/Screengrabfile
+++ b/fastlane/Screengrabfile
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-app_package_name 'com.nextcloud.client'
+app_package_name 'eu.xeniacloud.files'
use_tests_in_packages ['com.owncloud.android.screenshots']
test_instrumentation_runner 'androidx.test.runner.AndroidJUnitRunner'
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 56b5905afd71..050d2d5b8678 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,7 +5,7 @@
androidCommonLibraryVersion = "0.33.2"
androidGifDrawableVersion = "1.2.32"
androidImageCropperVersion = "4.7.0"
-androidLibraryVersion ="d22a01f4b8327b6593d6af75f6be3aa7fbc2c854"
+androidLibraryVersion ="8cbaa7e8a834f5c2fa6d1f24bd3b9923aa079f28"
androidOpensslVersion = "3.5.6"
androidPluginVersion = "9.3.2"
androidsvgVersion = "1.4"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 5a4972919d98..f124ea0267f4 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -21804,6 +21804,14 @@
+
+
+
+
+
+
+
+
diff --git a/scripts/analysis/detectWrongSettings.sh b/scripts/analysis/detectWrongSettings.sh
index d9ae88c323f3..31a8b45090fa 100755
--- a/scripts/analysis/detectWrongSettings.sh
+++ b/scripts/analysis/detectWrongSettings.sh
@@ -7,23 +7,6 @@
snapshotCount=$(./gradlew dependencies | grep SNAPSHOT -c)
betaCount=$(grep "true" app/src/main/res/values/setup.xml -c)
-# Read androidLibraryVersion from TOML
-libraryHash=$(grep 'androidLibraryVersion =' gradle/libs.versions.toml \
- | cut -d '=' -f2 \
- | tr -d ' "' )
-
-baseBranch="master"
-
-# Fetch last 10 commit SHAs from GitHub, without quotes
-lastHashes=$(curl -s "https://api.github.com/repos/nextcloud/android-library/commits?sha=$baseBranch" \
- | jq -r '.[].sha' | head -n 10)
-
-# Check if libraryHash is in lastHashes
-if ! echo "$lastHashes" | grep -q "^$libraryHash$"; then
- echo "Library commit not within last 10 hashes, please rebase!"
- exit 1
-fi
-
if [[ $snapshotCount -gt 0 ]] ; then
echo "Snapshot found in dependencies"
exit 1