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 @@ [![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) -[](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 transition) { @@ -475,8 +474,7 @@ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition 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