diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer_dark_blue.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer_dark_blue.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer_dark_white.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer_dark_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer_light_white.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_openDrawer_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_dark_black.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_dark_black.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_dark_blue.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_dark_blue.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_dark_white.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_dark_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_light_black.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_light_black.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_light_white.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_open_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_showPowerCheckDialog.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_showPowerCheckDialog.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_showPowerCheckDialog.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_showPowerCheckDialog.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_dark_black.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_dark_black.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_dark_blue.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_dark_blue.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_dark_white.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_dark_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_light_black.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_light_black.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_light_white.png similarity index 100% rename from app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AutoUploadFragmentIT_testSyncedFolderDialog_light_white.png diff --git a/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt similarity index 57% rename from app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.kt rename to app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt index 6ae0157b6706..ccfc8243d98d 100644 --- a/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt @@ -8,37 +8,51 @@ */ package com.nextcloud.client -import android.content.Intent import android.os.Looper +import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.test.core.app.launchActivity +import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.client.preferences.SubFolderRule import com.owncloud.android.AbstractIT +import com.owncloud.android.R import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderDisplayItem -import com.owncloud.android.ui.activity.SyncedFoldersActivity import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.Companion.newInstance +import com.owncloud.android.ui.fragment.autoUpload.AutoUploadFragment +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.utils.ScreenshotTest import org.junit.Test -class SyncedFoldersActivityIT : AbstractIT() { - private val testClassName = "com.nextcloud.client.SyncedFoldersActivityIT" +class AutoUploadFragmentIT : AbstractIT() { + private val testClassName = "com.nextcloud.client.AutoUploadFragmentIT" + + private fun findFragment(sut: NavigatorActivity): AutoUploadFragment = + sut.supportFragmentManager.fragments.filterIsInstance().firstOrNull() + ?: throw IllegalStateException("AutoUploadFragment not found in NavigatorActivity!") + + private fun launch(block: (ActivityScenario) -> Unit) { + val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.AutoUpload) + ActivityScenario.launch(intent).use(block) + } @Test @ScreenshotTest fun open() { - launchActivity().use { scenario -> + launch { scenario -> val screenShotName = createName(testClassName + "_" + "open", "") onView(isRoot()).check(matches(isDisplayed())) scenario.onActivity { sut -> - sut.adapter.clear() - screenshotViaName(sut.binding.emptyList.emptyListView, screenShotName) + val fragment = findFragment(sut) + fragment.adapter.clear() + val emptyListView = fragment.requireView().findViewById(R.id.empty_list_view) + screenshotViaName(emptyListView, screenShotName) } } } @@ -67,15 +81,15 @@ class SyncedFoldersActivityIT : AbstractIT() { SyncedFolder.NOT_SCANNED_YET ) - val intent = Intent(targetContext, SyncedFoldersActivity::class.java) - launchActivity(intent).use { scenario -> + launch { scenario -> val screenShotName = createName(testClassName + "_" + "testSyncedFolderDialog", "") onView(isRoot()).check(matches(isDisplayed())) scenario.onActivity { sut -> - val fragment = newInstance(item, 0) - fragment!!.show(sut.supportFragmentManager, "") - screenshot(fragment.requireDialog().window?.decorView, screenShotName) + val dialog = newInstance(item, 0) ?: throw IllegalStateException("Dialog could not be created") + dialog.show(findFragment(sut).childFragmentManager, "") + findFragment(sut).childFragmentManager.executePendingTransactions() + screenshot(dialog.requireDialog().window?.decorView, screenShotName) } } } @@ -87,18 +101,17 @@ class SyncedFoldersActivityIT : AbstractIT() { Looper.prepare() } - val intent = Intent(targetContext, SyncedFoldersActivity::class.java) - - launchActivity(intent).use { scenario -> + launch { scenario -> var dialog: AlertDialog? = null scenario.onActivity { sut -> - dialog = sut.buildPowerCheckDialog() - sut.showPowerCheckDialog() + val fragment = findFragment(sut) + dialog = fragment.buildPowerCheckDialog() + fragment.showPowerCheckDialog() } val screenShotName = createName(testClassName + "_" + "showPowerCheckDialog", "") onView(isRoot()).check(matches(isDisplayed())) - screenshot(dialog!!.window?.decorView, screenShotName) + screenshot(dialog?.window?.decorView, screenShotName) } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt b/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt index 9bec54c4cdfa..666b6e7dd383 100644 --- a/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt @@ -7,6 +7,7 @@ package com.owncloud.android import android.Manifest +import androidx.test.core.app.ActivityScenario import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView @@ -22,7 +23,8 @@ import androidx.test.rule.GrantPermissionRule import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.SettingsActivity -import com.owncloud.android.ui.activity.SyncedFoldersActivity +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.utils.ScreenshotTest import org.junit.Assert.assertTrue import org.junit.BeforeClass @@ -103,7 +105,8 @@ class ScreenshotsIT : AbstractIT() { @Test @ScreenshotTest fun autoUploadScreenshot() { - launchActivity().use { + val intent = NavigatorActivity.intent(targetContext, NavigatorScreen.AutoUpload) + ActivityScenario.launch(intent).use { onView(isRoot()).check(matches(isDisplayed())) Screengrab.screenshot("05_autoUpload") assertTrue(true) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c9a86717fd3b..f20c2687a55f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -279,9 +279,6 @@ - diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt index 93fbfcea3144..3ed6816db592 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.kt @@ -72,7 +72,6 @@ import com.owncloud.android.ui.activity.RichDocumentsEditorWebView import com.owncloud.android.ui.activity.SettingsActivity import com.owncloud.android.ui.activity.ShareActivity import com.owncloud.android.ui.activity.SsoGrantPermissionActivity -import com.owncloud.android.ui.activity.SyncedFoldersActivity import com.owncloud.android.ui.activity.TextEditorWebView import com.owncloud.android.ui.activity.ToolbarActivity import com.owncloud.android.ui.activity.UploadFilesActivity @@ -125,6 +124,7 @@ import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment import com.owncloud.android.ui.fragment.localfilelist.LocalFileListFragment import com.owncloud.android.ui.fragment.notifications.NotificationsFragment +import com.owncloud.android.ui.fragment.autoUpload.AutoUploadFragment import com.owncloud.android.ui.fragment.uploadList.UploadListFragment import com.owncloud.android.ui.navigation.NavigatorActivity import com.owncloud.android.ui.preview.media.PreviewMediaDownloadFragment @@ -152,6 +152,9 @@ internal interface ComponentsModule { @ContributesAndroidInjector fun uploadListFragment(): UploadListFragment + @ContributesAndroidInjector + fun autoUploadFragment(): AutoUploadFragment + @ContributesAndroidInjector fun activitiesFragment(): ActivitiesFragment @@ -230,9 +233,6 @@ internal interface ComponentsModule { @ContributesAndroidInjector fun ssoGrantPermissionActivity(): SsoGrantPermissionActivity - @ContributesAndroidInjector - fun syncedFoldersActivity(): SyncedFoldersActivity - @ContributesAndroidInjector fun trashbinFileActionsBottomSheet(): TrashbinFileActionsBottomSheet diff --git a/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt b/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt index 198d37f18f12..cad947c6700f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt @@ -41,7 +41,8 @@ import com.owncloud.android.datamodel.MediaProvider import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.activity.ManageAccountsActivity -import com.owncloud.android.ui.activity.SyncedFoldersActivity +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.SyncedFolderUtils import com.owncloud.android.utils.theme.ViewThemeUtils @@ -195,7 +196,7 @@ class MediaFoldersDetectionWork( private fun sendNotification(contentTitle: String, subtitle: String, user: User, path: String, type: Int) { val notificationId = randomIdGenerator.nextInt() - val intent = Intent(context, SyncedFoldersActivity::class.java).apply { + val intent = NavigatorActivity.intent(context, NavigatorScreen.AutoUpload).apply { putExtra(NOTIFICATION_ID, notificationId) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) putExtra(NotificationWork.KEY_NOTIFICATION_ACCOUNT, user.accountName) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensions.kt new file mode 100644 index 000000000000..af1a235e7c2f --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensions.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils.extensions + +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import java.util.Locale + +private val prioritizedFolders = listOf("Camera", "Screenshots") + +fun List.sortAutoUploadFolderItems(): List = + sortedWith(::compareItems) + +private fun compareItems(f1: SyncedFolderDisplayItem?, f2: SyncedFolderDisplayItem?): Int = when { + f1 == null && f2 == null -> 0 + f1 == null -> -1 + f2 == null -> 1 + f1.isEnabled && f2.isEnabled -> compareEnabledItems(f1.folderName, f2.folderName) + else -> compareMixedItems(f1, f2) +} + +private fun compareEnabledItems(name1: String?, name2: String?): Int = when { + name1 == null -> -1 + name2 == null -> 1 + else -> compareNames(name1, name2) +} + +private fun compareMixedItems(f1: SyncedFolderDisplayItem, f2: SyncedFolderDisplayItem): Int { + val name1 = f1.folderName + val name2 = f2.folderName + return when { + name1 == null && name2 == null -> 0 + f1.isEnabled -> -1 + f2.isEnabled -> 1 + name1 == null -> -1 + name2 == null -> 1 + else -> comparePrioritizedNames(name1, name2) + } +} + +private fun comparePrioritizedNames(name1: String, name2: String): Int { + val prioritizedFolder = prioritizedFolders.firstOrNull { it == name1 || it == name2 } + return when { + prioritizedFolder == null -> compareNames(name1, name2) + prioritizedFolder == name1 && prioritizedFolder == name2 -> 0 + prioritizedFolder == name1 -> -1 + else -> 1 + } +} + +private fun compareNames(name1: String, name2: String): Int = + name1.lowercase(Locale.getDefault()).compareTo(name2.lowercase(Locale.getDefault())) diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 0225393f37d5..8aaaa67714e5 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -81,7 +81,8 @@ import com.owncloud.android.lib.resources.status.NextcloudVersion; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.lib.resources.status.OwnCloudVersion; -import com.owncloud.android.ui.activity.SyncedFoldersActivity; +import com.owncloud.android.ui.navigation.NavigatorActivity; +import com.owncloud.android.ui.navigation.NavigatorScreen; import com.owncloud.android.ui.notifications.NotificationUtils; import com.owncloud.android.utils.FilesSyncHelper; import com.owncloud.android.utils.PermissionUtil; @@ -864,7 +865,7 @@ private static void showAutoUploadAlertDialog(Context context) { .setTitle(R.string.drawer_synced_folders) .setMessage(R.string.synced_folders_new_info) .setPositiveButton(R.string.drawer_open, (dialog, which) -> { - Intent folderSyncIntent = new Intent(context, SyncedFoldersActivity.class); + Intent folderSyncIntent = NavigatorActivity.Companion.intent(context, NavigatorScreen.AutoUpload.INSTANCE); dialog.dismiss(); context.startActivity(folderSyncIntent); }) 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 c652079543de..20c0a8933a3e 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 @@ -1464,7 +1464,7 @@ protected void handleDeepLink(@NonNull Uri uri) { switch (deepLinkType) { case OPEN_AUTO_UPLOAD: - startActivity(new Intent(this, SyncedFoldersActivity.class)); + pushFragment(NavigatorScreen.AutoUpload.INSTANCE); break; case OPEN_EXTERNAL_URL: Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri.getQueryParameter("url"))); @@ -1499,9 +1499,7 @@ public BottomNavigationView getBottomNavigationView() { } private void checkStoragePermissionWarningBannerVisibility() { - if (this instanceof SyncedFoldersActivity syncedFoldersActivity) { - syncedFoldersActivity.setupStoragePermissionWarningBanner(); - } else if (this instanceof UploadFilesActivity uploadFilesActivity) { + if (this instanceof UploadFilesActivity uploadFilesActivity) { uploadFilesActivity.setupStoragePermissionWarningBanner(); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 8a8acc98942d..6e30f8cf4706 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -484,9 +484,8 @@ class FileDisplayActivity : R.string.drawer_open ) { dialog: DialogInterface?, which: Int -> // show instant upload - val syncedFoldersIntent = Intent(applicationContext, SyncedFoldersActivity::class.java) dialog?.dismiss() - startActivity(syncedFoldersIntent) + pushFragment(NavigatorScreen.AutoUpload) }.setNegativeButton( R.string.drawer_close ) { dialog: DialogInterface?, which: Int -> dialog?.dismiss() } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 39e78f17c6b5..0faac11a5ee6 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -76,6 +76,8 @@ import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment; import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.ui.model.ExtendedSettingsActivityDialog; +import com.owncloud.android.ui.navigation.NavigatorActivity; +import com.owncloud.android.ui.navigation.NavigatorScreen; import com.owncloud.android.utils.ClipboardUtil; import com.owncloud.android.utils.DeviceCredentialUtils; import com.owncloud.android.utils.EncryptionUtils; @@ -681,8 +683,7 @@ private void setupAutoUploadPreference(PreferenceCategory preferenceCategoryMore preferenceCategoryMore.removePreference(autoUpload); } else { autoUpload.setOnPreferenceClickListener(preference -> { - Intent intent = new Intent(this, SyncedFoldersActivity.class); - startActivity(intent); + startActivity(NavigatorActivity.Companion.intent(this, NavigatorScreen.AutoUpload.INSTANCE)); return true; }); } @@ -896,8 +897,7 @@ private void setupAutoUploadCategory(PreferenceScreen preferenceScreen) { if (pSyncedFolder != null) { if (getResources().getBoolean(R.bool.syncedFolder_light)) { pSyncedFolder.setOnPreferenceClickListener(preference -> { - Intent intent = new Intent(this, SyncedFoldersActivity.class); - startActivity(intent); + startActivity(NavigatorActivity.Companion.intent(this, NavigatorScreen.AutoUpload.INSTANCE)); return true; }); } else { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt deleted file mode 100644 index ddb2b64c03c2..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ /dev/null @@ -1,973 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-FileCopyrightText: 2016 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.activity - -import android.annotation.SuppressLint -import android.app.NotificationManager -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Bundle -import android.os.Looper -import android.text.TextUtils -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AlertDialog -import androidx.drawerlayout.widget.DrawerLayout -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.nextcloud.client.appinfo.AppInfo -import com.nextcloud.client.core.Clock -import com.nextcloud.client.device.PowerManagementService -import com.nextcloud.client.di.Injectable -import com.nextcloud.client.jobs.MediaFoldersDetectionWork -import com.nextcloud.client.jobs.NotificationWork -import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.preferences.SubFolderRule -import com.nextcloud.ui.component.UploadWarningCard -import com.nextcloud.utils.BatteryOptimizationHelper -import com.nextcloud.utils.extensions.getParcelableArgument -import com.nextcloud.utils.extensions.isDialogFragmentReady -import com.nextcloud.utils.extensions.setVisibleIf -import com.owncloud.android.MainApp -import com.owncloud.android.R -import com.owncloud.android.databinding.StoragePermissionWarningBannerBinding -import com.owncloud.android.databinding.SyncedFoldersLayoutBinding -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl -import com.owncloud.android.datamodel.MediaFolder -import com.owncloud.android.datamodel.MediaFolderType -import com.owncloud.android.datamodel.MediaProvider -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.datamodel.SyncedFolder -import com.owncloud.android.datamodel.SyncedFolderDisplayItem -import com.owncloud.android.datamodel.SyncedFolderProvider -import com.owncloud.android.files.services.NameCollisionPolicy -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.ui.adapter.SyncedFolderAdapter -import com.owncloud.android.ui.adapter.storagePermissionBanner.setup -import com.owncloud.android.ui.decoration.MediaGridItemDecoration -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment -import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment -import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener -import com.owncloud.android.ui.dialog.extensions.themeButtons -import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable -import com.owncloud.android.utils.FilesSyncHelper -import com.owncloud.android.utils.PermissionUtil -import com.owncloud.android.utils.SyncedFolderUtils -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.util.Locale -import javax.inject.Inject - -/** - * Activity displaying all auto-synced folders and/or instant upload media folders. - */ -@Suppress("TooManyFunctions", "LargeClass") -class SyncedFoldersActivity : - FileActivity(), - SyncedFolderAdapter.ClickListener, - OnSyncedFolderPreferenceListener, - Injectable { - - companion object { - private const val SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG" - private const val SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG" - private const val SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG" - private const val SUB_FOLDER_WARNING_DIALOG_TAG = "SUB_FOLDER_WARNING_DIALOG_TAG" - - // yes, there is a typo in this value - private const val KEY_SYNCED_FOLDER_INITIATED_PREFIX = "syncedFolderIntitiated_" - private val PRIORITIZED_FOLDERS = arrayOf("Camera", "Screenshots") - private val TAG = SyncedFoldersActivity::class.java.simpleName - - /** - * Sorts list of [SyncedFolderDisplayItem]s. - * - * @param syncFolderItemList list of items to be sorted - * @return sorted list of items - */ - @JvmStatic - @Suppress("ComplexMethod") - fun sortSyncedFolderItems(syncFolderItemList: List): List { - return syncFolderItemList.sortedWith { f1, f2 -> - if (f1 == null && f2 == null) { - 0 - } else if (f1 == null) { - -1 - } else if (f2 == null) { - 1 - } else if (f1.isEnabled && f2.isEnabled) { - when { - f1.folderName == null -> -1 - - f2.folderName == null -> 1 - - else -> f1.folderName.lowercase(Locale.getDefault()).compareTo( - f2.folderName.lowercase(Locale.getDefault()) - ) - } - } else if (f1.folderName == null && f2.folderName == null) { - 0 - } else if (f1.isEnabled) { - -1 - } else if (f2.isEnabled) { - 1 - } else if (f1.folderName == null) { - -1 - } else if (f2.folderName == null) { - 1 - } else { - for (folder in PRIORITIZED_FOLDERS) { - if (folder == f1.folderName && folder == f2.folderName) { - return@sortedWith 0 - } else if (folder == f1.folderName) { - return@sortedWith -1 - } else if (folder == f2.folderName) { - return@sortedWith 1 - } - } - f1.folderName.lowercase(Locale.getDefault()).compareTo( - f2.folderName.lowercase(Locale.getDefault()) - ) - } - } - } - } - - @Inject - lateinit var powerManagementService: PowerManagementService - - @Inject - lateinit var clock: Clock - - @Inject - lateinit var syncedFolderProvider: SyncedFolderProvider - - @Inject - lateinit var appInfo: AppInfo - - private var uploadWarningCard: UploadWarningCard? = null - - lateinit var binding: SyncedFoldersLayoutBinding - lateinit var adapter: SyncedFolderAdapter - - private var dialogFragment: SyncedFolderPreferencesDialogFragment? = null - private var path: String? = null - private var type = 0 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = SyncedFoldersLayoutBinding.inflate(layoutInflater) - setContentView(binding.root) - uploadWarningCard = UploadWarningCard( - this, - powerManagementService, - syncedFolderProvider, - backgroundJobManager, - lifecycleScope, - viewThemeUtils - ) - if (intent != null && intent.extras != null) { - val accountName = intent.extras!!.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) - val optionalUser = user - if (optionalUser.isPresent && accountName != null) { - val user = optionalUser.get() - if (!accountName.equals(user.accountName, ignoreCase = true)) { - accountManager.setCurrentOwnCloudAccount(accountName) - setUser(userAccountManager.user) - } - } - path = intent.getStringExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_PATH) - type = intent.getIntExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_TYPE, -1) - - // Cancel notification - val notificationId = intent.getIntExtra(MediaFoldersDetectionWork.NOTIFICATION_ID, 0) - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(notificationId) - } - - // setup toolbar - setupToolbar() - updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_synced_folders)) - setupDrawer(menuItemId) - setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - if (supportActionBar != null) { - supportActionBar!!.setDisplayHomeAsUpEnabled(true) - } - if (mDrawerToggle != null) { - mDrawerToggle.isDrawerIndicatorEnabled = false - } - - PermissionUtil.requestStoragePermissionIfNeeded(this) - setupContent() - if (themeUtils.themingEnabled(this)) { - setTheme(R.style.FallbackThemingTheme) - } - binding.emptyList.emptyListViewAction.setOnClickListener { showHiddenItems() } - } - - override fun getMenuItemId(): Int = R.id.nav_settings - - override fun onResume() { - super.onResume() - highlightNavigationViewItem(menuItemId) - - setupStoragePermissionWarningBanner() - PermissionUtil.dismissStoragePermissionDialogFragment(this) - if (PermissionUtil.checkStoragePermission(this)) { - load(getItemsDisplayedPerFolder(), false) - } - uploadWarningCard?.bind(binding.autoUploadBatterySaverWarningCard) - } - - fun setupStoragePermissionWarningBanner() { - val storagePermissionWarningBanner = binding.storagePermissionWarningBanner.root - StoragePermissionWarningBannerBinding.bind(storagePermissionWarningBanner).apply { - setup(this@SyncedFoldersActivity, R.string.storage_permission_banner_auto_upload_text) - } - storagePermissionWarningBanner.setVisibleIf(!PermissionUtil.checkStoragePermission(this)) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.activity_synced_folders, menu) - return true - } - - fun buildPowerCheckDialog(): AlertDialog { - val builder = MaterialAlertDialogBuilder(this) - .setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() } - .setTitle(R.string.autoupload_disable_power_save_check) - .setMessage(getString(R.string.power_save_check_dialog_message)) - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, builder) - - return builder.create() - } - - @VisibleForTesting - fun showPowerCheckDialog() { - buildPowerCheckDialog().show() - } - - /** - * sets up the UI elements and loads all media/synced folders. - */ - private fun setupContent() { - val gridWidth = resources.getInteger(R.integer.media_grid_width) - val lightVersion = resources.getBoolean(R.bool.syncedFolder_light) - adapter = SyncedFolderAdapter( - lifecycleScope, - this, - clock, - gridWidth, - this, - lightVersion, - viewThemeUtils, - powerManagementService, - connectivityService - ) - uploadWarningCard?.register(this, binding.autoUploadBatterySaverWarningCard) - - binding.emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders) - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.emptyList.emptyListViewAction) - val lm = GridLayoutManager(this, gridWidth) - adapter.setLayoutManager(lm) - val spacing = resources.getDimensionPixelSize(R.dimen.media_grid_spacing) - binding.list.addItemDecoration(MediaGridItemDecoration(spacing)) - binding.list.layoutManager = lm - binding.list.adapter = adapter - } - - private fun showHiddenItems() { - if (adapter.sectionCount == 0 && adapter.unfilteredSectionCount > adapter.sectionCount) { - adapter.toggleHiddenItemsVisibility() - binding.emptyList.emptyListView.visibility = View.GONE - binding.list.visibility = View.VISIBLE - } - } - - override fun onDestroy() { - super.onDestroy() - uploadWarningCard?.unregister(this) - } - - /** - * loads all media/synced folders, adds them to the recycler view adapter and shows the list. - * - * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder - */ - @SuppressLint("NotifyDataSetChanged") - private fun load(perFolderMediaItemLimit: Int, force: Boolean) { - if (adapter.itemCount > 0 && !force) { - return - } - - showLoadingContent() - lifecycleScope.launch(Dispatchers.IO) { - val mediaFolders = MediaProvider.getImageFolders( - contentResolver, - perFolderMediaItemLimit, - this@SyncedFoldersActivity, - false - ) - mediaFolders.addAll( - MediaProvider.getVideoFolders( - contentResolver, - perFolderMediaItemLimit, - this@SyncedFoldersActivity, - false - ) - ) - - val syncedFolderArrayList = syncedFolderProvider.syncedFolders - val currentAccountSyncedFoldersList: MutableList = ArrayList() - val user = userAccountManager.user - for (syncedFolder in syncedFolderArrayList) { - if (syncedFolder.account == user.accountName) { - val folder = File(syncedFolder.localPath) - - // delete non-existing & disabled synced folders - if (!folder.exists() && !syncedFolder.isEnabled) { - syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) - } else { - currentAccountSyncedFoldersList.add(syncedFolder) - } - } - } - - val syncFolderItems = sortSyncedFolderItems( - mergeFolderData(currentAccountSyncedFoldersList, mediaFolders) - ).filterNotNull() - - withContext(Dispatchers.Main) { - adapter.setSyncFolderItems(syncFolderItems) - adapter.notifyDataSetChanged() - showList() - - if (!TextUtils.isEmpty(path)) { - val section = adapter.getSectionByLocalPathAndType(path, type) - if (section >= 0) { - adapter.get(section)?.let { - onSyncFolderSettingsClick(section, it) - } - } - } - } - } - } - - /** - * merges two lists of [SyncedFolder] and [MediaFolder] items into one of SyncedFolderItems. - * - * @param syncedFolders the synced folders - * @param mediaFolders the media folders - * @return the merged list of SyncedFolderItems - */ - @Suppress("NestedBlockDepth") // legacy code - private fun mergeFolderData( - syncedFolders: List, - mediaFolders: List - ): List { - val syncedFoldersMap = createSyncedFoldersMap(syncedFolders) - val result: MutableList = ArrayList() - for (mediaFolder in mediaFolders) { - if (syncedFoldersMap.containsKey(mediaFolder.absolutePath + "-" + mediaFolder.type)) { - val syncedFolder = syncedFoldersMap[mediaFolder.absolutePath + "-" + mediaFolder.type] - syncedFoldersMap.remove(mediaFolder.absolutePath + "-" + mediaFolder.type) - if (syncedFolder != null && SyncedFolderUtils.isQualifyingMediaFolder(syncedFolder)) { - if (MediaFolderType.CUSTOM == syncedFolder.type) { - result.add(createSyncedFolderWithoutMediaFolder(syncedFolder)) - } else { - result.add(createSyncedFolder(syncedFolder, mediaFolder)) - } - } - } else { - if (SyncedFolderUtils.isQualifyingMediaFolder(mediaFolder)) { - result.add(createSyncedFolderFromMediaFolder(mediaFolder)) - } - } - } - for (syncedFolder in syncedFoldersMap.values) { - result.add(createSyncedFolderWithoutMediaFolder(syncedFolder)) - } - return result - } - - private fun createSyncedFolderWithoutMediaFolder(syncedFolder: SyncedFolder): SyncedFolderDisplayItem { - val localFolder = File(syncedFolder.localPath) - val files = SyncedFolderUtils.getFileList(localFolder) - val filePaths = getDisplayFilePathList(files) - return SyncedFolderDisplayItem( - syncedFolder.id, - syncedFolder.localPath, - syncedFolder.remotePath, - syncedFolder.isWifiOnly, - syncedFolder.isChargingOnly, - syncedFolder.alsoUploadExistingFiles(), - syncedFolder.isSubfolderByDate, - syncedFolder.account, - syncedFolder.uploadAction, - syncedFolder.nameCollisionPolicyInt, - syncedFolder.isEnabled, - clock.currentTime, - filePaths, - localFolder.name, - files.size.toLong(), - syncedFolder.type, - syncedFolder.isHidden, - syncedFolder.subfolderRule, - syncedFolder.isExcludeHidden, - syncedFolder.lastScanTimestampMs - ) - } - - /** - * creates a SyncedFolderDisplayItem merging a [SyncedFolder] and a [MediaFolder] object instance. - * - * @param syncedFolder the synced folder object - * @param mediaFolder the media folder object - * @return the created SyncedFolderDisplayItem - */ - private fun createSyncedFolder(syncedFolder: SyncedFolder, mediaFolder: MediaFolder): SyncedFolderDisplayItem = - SyncedFolderDisplayItem( - syncedFolder.id, - syncedFolder.localPath, - syncedFolder.remotePath, - syncedFolder.isWifiOnly, - syncedFolder.isChargingOnly, - syncedFolder.alsoUploadExistingFiles(), - syncedFolder.isSubfolderByDate, - syncedFolder.account, - syncedFolder.uploadAction, - syncedFolder.nameCollisionPolicyInt, - syncedFolder.isEnabled, - clock.currentTime, - mediaFolder.filePaths, - mediaFolder.folderName, - mediaFolder.numberOfFiles, - mediaFolder.type, - syncedFolder.isHidden, - syncedFolder.subfolderRule, - syncedFolder.isExcludeHidden, - syncedFolder.lastScanTimestampMs - ) - - /** - * creates a [SyncedFolderDisplayItem] based on a [MediaFolder] object instance. - * - * @param mediaFolder the media folder object - * @return the created SyncedFolderDisplayItem - */ - private fun createSyncedFolderFromMediaFolder(mediaFolder: MediaFolder): SyncedFolderDisplayItem = - SyncedFolderDisplayItem( - SyncedFolder.UNPERSISTED_ID, - mediaFolder.absolutePath, - getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName, - true, - false, - true, - false, - account.name, - FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, - NameCollisionPolicy.ASK_USER.serialize(), - false, - clock.currentTime, - mediaFolder.filePaths, - mediaFolder.folderName, - mediaFolder.numberOfFiles, - mediaFolder.type, - false, - SubFolderRule.YEAR_MONTH, - false, - SyncedFolder.NOT_SCANNED_YET - ) - - private fun getItemsDisplayedPerFolder(): Int = resources.getInteger(R.integer.media_grid_width) * 2 - - private fun getDisplayFilePathList(files: List?): List? { - if (!files.isNullOrEmpty()) { - return files.take(getItemsDisplayedPerFolder()) - .map { it.absolutePath } - } - return null - } - - /** - * creates a lookup map for a list of given [SyncedFolder]s with their local path as the key. - * - * @param syncFolders list of [SyncedFolder]s - * @return the lookup map for [SyncedFolder]s - */ - private fun createSyncedFoldersMap(syncFolders: List?): MutableMap { - val result: MutableMap = HashMap() - if (syncFolders != null) { - for (syncFolder in syncFolders) { - result[syncFolder.localPath + "-" + syncFolder.type] = syncFolder - } - } - return result - } - - /** - * show recycler view list or the empty message info (in case list is empty). - */ - private fun showList() { - binding.list.visibility = View.VISIBLE - binding.loadingContent.visibility = View.GONE - checkAndShowEmptyListContent() - } - - private fun checkAndShowEmptyListContent() { - if (adapter.sectionCount == 0 && adapter.unfilteredSectionCount > adapter.sectionCount) { - binding.emptyList.emptyListView.visibility = View.VISIBLE - val hiddenFoldersCount = adapter.hiddenFolderCount - showEmptyContent( - getString(R.string.drawer_synced_folders), - resources.getQuantityString( - R.plurals.synced_folders_show_hidden_folders, - hiddenFoldersCount, - hiddenFoldersCount - ), - resources.getQuantityString( - R.plurals.synced_folders_show_hidden_folders, - hiddenFoldersCount, - hiddenFoldersCount - ) - ) - } else if (adapter.sectionCount == 0 && adapter.unfilteredSectionCount == 0) { - binding.emptyList.emptyListView.visibility = View.VISIBLE - showEmptyContent( - getString(R.string.drawer_synced_folders), - getString(R.string.synced_folders_no_results) - ) - } else { - binding.emptyList.emptyListView.visibility = View.GONE - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - var result = true - when (item.itemId) { - android.R.id.home -> finish() - - R.id.action_create_custom_folder -> { - Log_OC.d(TAG, "Show custom folder dialog") - if (PermissionUtil.checkStoragePermission(this)) { - val emptyCustomFolder = SyncedFolderDisplayItem( - SyncedFolder.UNPERSISTED_ID, - null, - null, - true, - false, - true, - false, - account.name, - FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, - NameCollisionPolicy.ASK_USER.serialize(), - false, - clock.currentTime, - null, - MediaFolderType.CUSTOM, - false, - SubFolderRule.YEAR_MONTH, - false, - SyncedFolder.NOT_SCANNED_YET - ) - onSyncFolderSettingsClick(0, emptyCustomFolder) - } else { - PermissionUtil.requestStoragePermissionIfNeeded(this) - } - result = super.onOptionsItemSelected(item) - } - - R.id.action_auto_upload_all_folders_delete_uploaded -> onAllSyncFolderDeleteUploadedClick() - - else -> result = super.onOptionsItemSelected(item) - } - return result - } - - override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { - check(Looper.getMainLooper().isCurrentThread) { "This must be called on the main thread!" } - - dialogFragment = SyncedFolderPreferencesDialogFragment.newInstance( - syncedFolderDisplayItem, - section - ) - - showDialog(dialogFragment!!, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) - } - - private fun onAllSyncFolderDeleteUploadedClick() { - val dialog = ConfirmationDialogFragment.newInstance( - R.string.autoupload_delete_uploaded_all_dialog_description, - null, - R.string.autoupload_delete_uploaded_all_dialog_title, - R.drawable.selector_trashbin, - R.string.autoupload_delete_uploaded_all_dialog_button_all_users, - R.string.autoupload_delete_uploaded_all_dialog_button_current_user, - R.string.common_cancel - ) - dialog.isCancelable = false - dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { - override fun onConfirmation(callerTag: String?) { - FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) - } - - override fun onCancel(callerTag: String?) { - FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders( - syncedFolderProvider, - backgroundJobManager, - user.get() - ) - } - - override fun onNeutral(callerTag: String?) = Unit - }) - - showDialog(dialog, SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG) - } - override fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { - syncedFolderDisplayItem ?: return - - val dialog = ConfirmationDialogFragment.newInstance( - R.string.autoupload_delete_uploaded_dialog_description, - null, - R.string.autoupload_delete_uploaded_dialog_title, - R.drawable.selector_trashbin, - R.string.common_ok, - R.string.common_cancel, - -1 - ) - dialog.isCancelable = false - dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { - override fun onConfirmation(callerTag: String?) { - FilesSyncHelper.startLocalDeletionForSyncedFolder(syncedFolderDisplayItem, backgroundJobManager) - } - - override fun onNeutral(callerTag: String?) = Unit - - override fun onCancel(callerTag: String?) = Unit - }) - showDialog(dialog, SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG) - } - - private fun showDialog(dialog: DialogFragment, tag: String) { - if (isDialogFragmentReady(dialog) && - lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) - ) { - val fragmentTransaction = supportFragmentManager - .beginTransaction() - .addToBackStack(null) - - dialog.show(fragmentTransaction, tag) - } else { - Log_OC.d(TAG, "SyncFolderDeleteUploaded dialog not ready") - } - } - - override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { - if (syncedFolder == null) return - - syncedFolder.isHidden = !syncedFolder.isHidden - saveOrUpdateSyncedFolder(syncedFolder) - adapter.setSyncFolderItem(section, syncedFolder) - checkAndShowEmptyListContent() - } - - private fun showEmptyContent(headline: String, message: String, action: String) { - showEmptyContent(headline, message) - binding.emptyList.emptyListViewAction.text = action - binding.emptyList.emptyListViewAction.visibility = View.VISIBLE - binding.emptyList.emptyListViewText.visibility = View.GONE - } - - private fun showLoadingContent() { - binding.loadingContent.visibility = View.VISIBLE - binding.emptyList.emptyListViewAction.visibility = View.GONE - } - - private fun showEmptyContent(headline: String, message: String) { - binding.emptyList.emptyListViewAction.visibility = View.GONE - binding.emptyList.emptyListView.visibility = View.VISIBLE - binding.list.visibility = View.GONE - binding.loadingContent.visibility = View.GONE - binding.emptyList.emptyListViewHeadline.text = headline - binding.emptyList.emptyListViewText.text = message - binding.emptyList.emptyListViewText.visibility = View.VISIBLE - binding.emptyList.emptyListIcon.visibility = View.VISIBLE - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER && - resultCode == RESULT_OK && - dialogFragment != null - ) { - val chosenFolder: OCFile? = FolderPickerActivity.EXTRA_FOLDER?.let { - data?.getParcelableArgument(it, OCFile::class.java) - } - dialogFragment?.setRemoteFolderSummary(chosenFolder?.remotePath) - } else if ( - requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER && - resultCode == RESULT_OK && - dialogFragment != null - ) { - val localPath = data!!.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES) - dialogFragment!!.setLocalFolderSummary(localPath) - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { - if (syncedFolder == null) { - return - } - - // custom folders newly created aren't in the list already, - // so triggering a refresh - if (MediaFolderType.CUSTOM == syncedFolder.type && syncedFolder.id == SyncedFolder.UNPERSISTED_ID) { - val newCustomFolder = SyncedFolderDisplayItem( - SyncedFolder.UNPERSISTED_ID, - syncedFolder.localPath, - syncedFolder.remotePath, - syncedFolder.isWifiOnly, - syncedFolder.isChargingOnly, - syncedFolder.isExisting, - syncedFolder.isSubfolderByDate, - syncedFolder.account, - syncedFolder.uploadAction, - syncedFolder.nameCollisionPolicy.serialize(), - syncedFolder.isEnabled, - clock.currentTime, - File(syncedFolder.localPath).name, - syncedFolder.type, - syncedFolder.isHidden, - syncedFolder.subFolderRule, - syncedFolder.isExcludeHidden, - SyncedFolder.NOT_SCANNED_YET - ) - saveOrUpdateSyncedFolder(newCustomFolder) - adapter.addSyncFolderItem(newCustomFolder) - } else { - val item = adapter.get(syncedFolder.section) ?: return - updateSyncedFolderItem( - item, - syncedFolder.id, - syncedFolder.localPath, - syncedFolder.remotePath, - syncedFolder.isWifiOnly, - syncedFolder.isChargingOnly, - syncedFolder.isExisting, - syncedFolder.isSubfolderByDate, - syncedFolder.uploadAction, - syncedFolder.nameCollisionPolicy.serialize(), - syncedFolder.isEnabled, - syncedFolder.subFolderRule, - syncedFolder.isExcludeHidden - ) - saveOrUpdateSyncedFolder(item) - - adapter.notifyItemChanged(adapter.getSectionHeaderIndex(syncedFolder.section)) - } - dialogFragment = null - if (syncedFolder.isEnabled) { - showBatteryOptimizationDialogIfNeeded() - } - } - - override fun showSubFolderWarningDialog() { - val dialog = ConfirmationDialogFragment.newInstance( - messageResId = R.string.auto_upload_sub_folder_warning, - messageArguments = null, - titleResId = R.string.sync_duplication, - titleIconId = R.drawable.ic_info, - positiveButtonTextId = R.string.dialog_close, - negativeButtonTextId = -1, - neutralButtonTextId = -1 - ) - - if (isDialogFragmentReady(dialog)) { - dialog.show(supportFragmentManager, SUB_FOLDER_WARNING_DIALOG_TAG) - } - } - - private fun saveOrUpdateSyncedFolder(item: SyncedFolderDisplayItem) { - if (item.id == SyncedFolder.UNPERSISTED_ID) { - // newly set up folder sync config - storeSyncedFolder(item) - } else { - // existing synced folder setup to be updated - syncedFolderProvider.updateSyncFolder(item) - if (item.isEnabled) { - backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) - } else { - val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id - val arbitraryDataProvider = - ArbitraryDataProviderImpl(MainApp.getAppContext()) - arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey) - } - } - } - - private fun storeSyncedFolder(item: SyncedFolderDisplayItem) { - val arbitraryDataProvider = - ArbitraryDataProviderImpl(MainApp.getAppContext()) - val storedId = syncedFolderProvider.storeSyncedFolder(item) - if (storedId != -1L) { - item.id = storedId - if (item.isEnabled) { - backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) - } else { - val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id - arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey) - } - } - } - - override fun onCancelSyncedFolderPreference() { - dialogFragment = null - } - - override fun onSyncStatusToggleClick(section: Int, item: SyncedFolderDisplayItem?) { - item ?: return - - // Ensure the item is persisted - if (item.id <= SyncedFolder.UNPERSISTED_ID) { - syncedFolderProvider.storeSyncedFolder(item) - .takeIf { it != -1L } - ?.let { item.id = it } - } else { - syncedFolderProvider.updateSyncedFolderEnabled(item.id, item.isEnabled) - } - - if (item.isEnabled) { - Log_OC.d(TAG, "auto-upload configuration sync status is enabled: " + item.remotePath) - backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) - showBatteryOptimizationDialogIfNeeded() - return - } - - Log_OC.d(TAG, "auto-upload configuration sync status is disabled: " + item.remotePath) - - lifecycleScope.launch(Dispatchers.IO) { - fileUploadHelper.removeEntityFromUploadEntities(item.id) - } - } - - override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { - if (syncedFolder == null) { - return - } - - Log_OC.d(TAG, "deleting auto upload configuration: " + syncedFolder.remotePath) - - lifecycleScope.launch(Dispatchers.IO) { - fileUploadHelper.removeEntityFromUploadEntities(syncedFolder.id) - syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) - withContext(Dispatchers.Main) { - adapter.removeItem(syncedFolder.section) - } - } - } - - /** - * update given synced folder with the given values. - * - * @param item the synced folder to be updated - * @param localPath the local path - * @param remotePath the remote path - * @param wifiOnly upload on wifi only - * @param chargingOnly upload on charging only - * @param existing also upload existing - * @param subfolderByDate created sub folders - * @param uploadAction upload action - * @param nameCollisionPolicy what to do on name collision - * @param enabled is sync enabled - * @param excludeHidden exclude hidden file or folder, for {@link MediaFolderType#CUSTOM} only - */ - @Suppress("LongParameterList") - private fun updateSyncedFolderItem( - item: SyncedFolderDisplayItem, - id: Long, - localPath: String, - remotePath: String, - wifiOnly: Boolean, - chargingOnly: Boolean, - existing: Boolean, - subfolderByDate: Boolean, - uploadAction: Int, - nameCollisionPolicy: Int, - enabled: Boolean, - subFolderRule: SubFolderRule, - excludeHidden: Boolean - ) { - item.id = id - item.localPath = localPath - item.remotePath = remotePath - item.isWifiOnly = wifiOnly - item.isChargingOnly = chargingOnly - item.setExisting(existing) - item.isSubfolderByDate = subfolderByDate - item.uploadAction = uploadAction - item.setNameCollisionPolicy(nameCollisionPolicy) - item.setEnabled(enabled, clock.currentTime) - item.setSubFolderRule(subFolderRule) - item.setExcludeHidden(excludeHidden) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - when (requestCode) { - PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> { - // If request is cancelled, result arrays are empty. - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - load(getItemsDisplayedPerFolder(), true) - } - } - - else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - } - - private fun showBatteryOptimizationDialogIfNeeded() { - if (!BatteryOptimizationHelper.isBatteryOptimizationEnabled(this)) { - Log_OC.d(TAG, "battery optimization is disabled") - return - } - - showBatteryOptimizationDialog() - } - - private fun showBatteryOptimizationDialog() { - if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - Log_OC.w(TAG, "Activity not resumed, skipping battery dialog") - return - } - - val dialog = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) - .setTitle(R.string.battery_optimization_title) - .setMessage(R.string.battery_optimization_message) - .setPositiveButton(R.string.battery_optimization_disable) { _, _ -> - BatteryOptimizationHelper.openBatteryOptimizationSettings(this) - } - .setNeutralButton(R.string.battery_optimization_close, null) - .setIcon(R.drawable.ic_battery_alert) - - val alertDialog = dialog.show() - alertDialog.themeButtons(viewThemeUtils) - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt index c27a6573b7b2..e88b2622fb62 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt @@ -7,10 +7,9 @@ */ package com.owncloud.android.ui.dialog -import android.app.Activity import android.app.Dialog +import android.content.Context import android.content.DialogInterface -import android.content.Intent import android.graphics.Typeface import android.os.Bundle import android.text.TextUtils @@ -32,8 +31,6 @@ import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.ui.activity.FolderPickerActivity -import com.owncloud.android.ui.activity.UploadFilesActivity import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.FileUtil @@ -66,14 +63,15 @@ class SyncedFolderPreferencesDialogFragment : private var binding: SyncedFoldersSettingsLayoutBinding? = null private var isNeutralButtonActive = true - @Deprecated("Deprecated in Java") - override fun onAttach(activity: Activity) { - super.onAttach(activity) - require(activity is OnSyncedFolderPreferenceListener) { - ( - "The host activity must implement " + - OnSyncedFolderPreferenceListener::class.java.canonicalName - ) + private val listener: OnSyncedFolderPreferenceListener? + get() = parentFragment as? OnSyncedFolderPreferenceListener + ?: activity as? OnSyncedFolderPreferenceListener + + override fun onAttach(context: Context) { + super.onAttach(context) + requireNotNull(listener) { + "The parent fragment or host activity must implement " + + OnSyncedFolderPreferenceListener::class.java.canonicalName } } @@ -239,26 +237,12 @@ class SyncedFolderPreferencesDialogFragment : setupViews(binding, enabled) } - /** - * set (new) remote path on activity result of the folder picker activity. The result gets originally propagated to - * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment - * directly. - * - * @param path the remote path to be set - */ fun setRemoteFolderSummary(path: String?) { syncedFolder?.remotePath = path binding?.remoteFolderSummary?.text = path checkAndUpdateSaveButtonState() } - /** - * set (new) local path on activity result of the folder picker activity. The result gets originally propagated to - * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment - * directly. - * - * @param path the local path to be set - */ fun setLocalFolderSummary(path: String?) { syncedFolder?.localPath = path binding?.localFolderSummary?.text = FileStorageUtils.pathToUserFriendlyDisplay(path, activity, resources) @@ -400,18 +384,8 @@ class SyncedFolderPreferencesDialogFragment : binding.syncEnabled.setOnClickListener { setEnabled(!syncedFolder.isEnabled) } } - binding.remoteFolderContainer.setOnClickListener { - val action = Intent(activity, FolderPickerActivity::class.java).apply { - putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION) - } - requireActivity().startActivityForResult(action, REQUEST_CODE__SELECT_REMOTE_FOLDER) - } - binding.localFolderContainer.setOnClickListener { - val action = Intent(activity, UploadFilesActivity::class.java) - action.putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true) - action.putExtra(UploadFilesActivity.REQUEST_CODE_KEY, REQUEST_CODE__SELECT_LOCAL_FOLDER) - requireActivity().startActivityForResult(action, REQUEST_CODE__SELECT_LOCAL_FOLDER) - } + binding.remoteFolderContainer.setOnClickListener { listener?.onRemoteFolderPickerRequested() } + binding.localFolderContainer.setOnClickListener { listener?.onLocalFolderPickerRequested() } binding.settingInstantBehaviourContainer.setOnClickListener { showBehaviourDialog() } binding.settingInstantNameCollisionPolicyContainer.setOnClickListener { showNameCollisionPolicyDialog() } @@ -495,26 +469,28 @@ class SyncedFolderPreferencesDialogFragment : fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) fun onCancelSyncedFolderPreference() fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) + fun onRemoteFolderPickerRequested() + fun onLocalFolderPickerRequested() } private inner class OnSyncedFolderSaveClickListener : View.OnClickListener { override fun onClick(v: View) { dismiss() - (activity as OnSyncedFolderPreferenceListener?)?.onSaveSyncedFolderPreference(syncedFolder) + listener?.onSaveSyncedFolderPreference(syncedFolder) } } private inner class OnSyncedFolderCancelClickListener : View.OnClickListener { override fun onClick(v: View) { dismiss() - (activity as OnSyncedFolderPreferenceListener?)?.onCancelSyncedFolderPreference() + listener?.onCancelSyncedFolderPreference() } } private inner class OnSyncedFolderDeleteClickListener : View.OnClickListener { override fun onClick(v: View) { dismiss() - (activity as OnSyncedFolderPreferenceListener?)?.onDeleteSyncedFolderPreference(syncedFolder) + listener?.onDeleteSyncedFolderPreference(syncedFolder) } } @@ -531,7 +507,6 @@ class SyncedFolderPreferencesDialogFragment : companion object { const val SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable" - const val REQUEST_CODE__SELECT_REMOTE_FOLDER = 0 const val REQUEST_CODE__SELECT_LOCAL_FOLDER = 1 private val TAG = SyncedFolderPreferencesDialogFragment::class.java.simpleName private const val BEHAVIOUR_DIALOG_STATE = "BEHAVIOUR_DIALOG_STATE" diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/AutoUploadFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/AutoUploadFragment.kt new file mode 100644 index 000000000000..495109a6a8f0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/AutoUploadFragment.kt @@ -0,0 +1,291 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.core.Clock +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.ui.component.UploadWarningCard +import com.nextcloud.utils.extensions.getTypedActivity +import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentAutoUploadBinding +import com.owncloud.android.databinding.StoragePermissionWarningBannerBinding +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.ui.adapter.SyncedFolderAdapter +import com.owncloud.android.ui.adapter.storagePermissionBanner.setup +import com.owncloud.android.ui.decoration.MediaGridItemDecoration +import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment +import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener +import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import com.owncloud.android.ui.fragment.autoUpload.data.AutoUploadDisplayItemFactory +import com.owncloud.android.ui.fragment.autoUpload.data.AutoUploadItemsLoader +import com.owncloud.android.ui.fragment.autoUpload.data.AutoUploadPersistence +import com.owncloud.android.ui.fragment.autoUpload.handler.AutoUploadClickHandler +import com.owncloud.android.ui.fragment.autoUpload.handler.AutoUploadPickers +import com.owncloud.android.ui.fragment.autoUpload.handler.AutoUploadIntentHandler +import com.owncloud.android.ui.fragment.autoUpload.ui.AutoUploadDialogs +import com.owncloud.android.ui.fragment.autoUpload.ui.AutoUploadEmptyContent +import com.owncloud.android.ui.fragment.autoUpload.ui.AutoUploadMenuProvider +import com.owncloud.android.ui.navigation.NavigatorActivity +import com.owncloud.android.utils.PermissionUtil +import com.owncloud.android.utils.theme.ThemeUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Suppress("TooManyFunctions") +class AutoUploadFragment : + Fragment(), + OnSyncedFolderPreferenceListener { + + @Inject lateinit var powerManagementService: PowerManagementService + + @Inject lateinit var clock: Clock + + @Inject lateinit var syncedFolderProvider: SyncedFolderProvider + + @Inject lateinit var userAccountManager: UserAccountManager + + @Inject lateinit var backgroundJobManager: BackgroundJobManager + + @Inject lateinit var connectivityService: ConnectivityService + + @Inject lateinit var viewThemeUtils: ViewThemeUtils + + @Inject lateinit var themeUtils: ThemeUtils + + lateinit var adapter: SyncedFolderAdapter + + private var binding: FragmentAutoUploadBinding? = null + private var uploadWarningCard: UploadWarningCard? = null + private var emptyContent: AutoUploadEmptyContent? = null + private lateinit var itemsLoader: AutoUploadItemsLoader + private lateinit var dialogs: AutoUploadDialogs + private lateinit var clickHandler: AutoUploadClickHandler + + private val intentHandler = AutoUploadIntentHandler() + + private val folderPickers = AutoUploadPickers(this) { + childFragmentManager.findFragmentByTag(AutoUploadClickHandler.SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + as? SyncedFolderPreferencesDialogFragment + } + + private val navigatorActivity: NavigatorActivity? + get() = getTypedActivity(NavigatorActivity::class.java) + + // region Lifecycle + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + intentHandler.readMediaFolder(activity?.intent) + val activity = navigatorActivity ?: return + if (savedInstanceState == null) { + intentHandler.handleNotification(activity, userAccountManager) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val binding = FragmentAutoUploadBinding.inflate(inflater, container, false) + this.binding = binding + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val activity = navigatorActivity ?: return + val binding = binding ?: return + + setupHelpers(activity) + PermissionUtil.requestStoragePermissionIfNeeded(activity) + setupList(activity, binding) + if (themeUtils.themingEnabled(activity)) { + activity.theme.applyStyle(R.style.FallbackThemingTheme, true) + } + addMenuProvider(activity) + } + + override fun onResume() { + super.onResume() + val activity = navigatorActivity ?: return + val binding = binding ?: return + + setupStoragePermissionWarningBanner(activity, binding) + PermissionUtil.dismissStoragePermissionDialogFragment(activity) + if (PermissionUtil.checkStoragePermission(activity)) { + load(activity) + } + uploadWarningCard?.bind(binding.autoUploadBatterySaverWarningCard) + } + + override fun onDestroyView() { + uploadWarningCard?.unregister(requireContext()) + uploadWarningCard = null + emptyContent = null + super.onDestroyView() + binding = null + } + // endregion + + // region OnSyncedFolderPreferenceListener + override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { + clickHandler.onSaveAutoUploadFolderPreference(syncedFolder) + } + + override fun onCancelSyncedFolderPreference() = Unit + + override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { + clickHandler.onDeleteAutoUploadFolderPreference(syncedFolder) + } + + override fun onRemoteFolderPickerRequested() { + folderPickers.pickRemoteFolder() + } + + override fun onLocalFolderPickerRequested() { + folderPickers.pickLocalFolder() + } + // endregion + + @VisibleForTesting + fun buildPowerCheckDialog(): AlertDialog = dialogs.buildPowerCheckDialog() + + @VisibleForTesting + fun showPowerCheckDialog() { + buildPowerCheckDialog().show() + } + + // region Setup + private fun setupHelpers(activity: NavigatorActivity) { + val itemFactory = AutoUploadDisplayItemFactory( + clock, + getItemsDisplayedPerFolder(), + getString(R.string.instant_upload_path) + ) + val persistence = AutoUploadPersistence(syncedFolderProvider, backgroundJobManager) + itemsLoader = AutoUploadItemsLoader(syncedFolderProvider, itemFactory) + dialogs = AutoUploadDialogs(this, viewThemeUtils, syncedFolderProvider, backgroundJobManager) + clickHandler = AutoUploadClickHandler( + this, + itemFactory, + persistence, + dialogs, + backgroundJobManager, + getAdapter = { adapter }, + onItemVisibilityChanged = { emptyContent?.checkAndShowEmptyListContent() } + ) + uploadWarningCard = UploadWarningCard( + activity, + powerManagementService, + syncedFolderProvider, + backgroundJobManager, + lifecycleScope, + viewThemeUtils + ) + } + + private fun setupList(activity: NavigatorActivity, binding: FragmentAutoUploadBinding) { + val gridWidth = resources.getInteger(R.integer.media_grid_width) + adapter = SyncedFolderAdapter( + lifecycleScope, + activity, + clock, + gridWidth, + clickHandler, + resources.getBoolean(R.bool.syncedFolder_light), + viewThemeUtils, + powerManagementService, + connectivityService + ) + uploadWarningCard?.register(activity, binding.autoUploadBatterySaverWarningCard) + emptyContent = AutoUploadEmptyContent(binding, adapter, viewThemeUtils) + + val layoutManager = GridLayoutManager(activity, gridWidth) + adapter.setLayoutManager(layoutManager) + binding.list.run { + addItemDecoration(MediaGridItemDecoration(resources.getDimensionPixelSize(R.dimen.media_grid_spacing))) + this.layoutManager = layoutManager + adapter = this@AutoUploadFragment.adapter + } + } + + private fun addMenuProvider(activity: NavigatorActivity) { + val menuProvider = AutoUploadMenuProvider( + onCreateCustomFolder = { onCreateCustomFolderClick() }, + onDeleteAllUploaded = { onDeleteAllUploadedClick() } + ) + activity.addMenuProvider(menuProvider, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + + private fun setupStoragePermissionWarningBanner(activity: NavigatorActivity, binding: FragmentAutoUploadBinding) { + val banner = binding.storagePermissionWarningBanner.root + StoragePermissionWarningBannerBinding.bind(banner).setup( + activity, + R.string.storage_permission_banner_auto_upload_text + ) + banner.setVisibleIf(!PermissionUtil.checkStoragePermission(activity)) + } + // endregion + + // region Menu Actions + private fun onCreateCustomFolderClick() { + val activity = navigatorActivity ?: return + clickHandler.onCreateCustomFolderClick(activity, activity.account.name) + } + + private fun onDeleteAllUploadedClick() { + val user = navigatorActivity?.user?.orElse(null) ?: return + clickHandler.onDeleteAllUploadedClick(user) + } + // endregion + + // region Loading + private fun getItemsDisplayedPerFolder(): Int = resources.getInteger(R.integer.media_grid_width) * 2 + + @SuppressLint("NotifyDataSetChanged") + private fun load(activity: NavigatorActivity) { + if (adapter.itemCount > 0) { + return + } + + val perFolderMediaItemLimit = getItemsDisplayedPerFolder() + val newItemAccountName = activity.account.name + emptyContent?.showLoadingContent() + + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + val currentAccountName = userAccountManager.user.accountName + val items = itemsLoader.load(activity, perFolderMediaItemLimit, currentAccountName, newItemAccountName) + + withContext(Dispatchers.Main) { + adapter.setSyncFolderItems(items) + adapter.notifyDataSetChanged() + emptyContent?.showList() + openMediaFolderSettingsFromIntent() + } + } + } + + private fun openMediaFolderSettingsFromIntent() { + val section = intentHandler.findMediaFolderSection(adapter) ?: return + adapter.get(section)?.let { clickHandler.onSyncFolderSettingsClick(section, it) } + } + // endregion +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt new file mode 100644 index 000000000000..647a15dcdbfa --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt @@ -0,0 +1,163 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.data + +import com.nextcloud.client.core.Clock +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.datamodel.MediaFolder +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import com.owncloud.android.utils.SyncedFolderUtils +import java.io.File + +class AutoUploadDisplayItemFactory( + private val clock: Clock, + private val itemsDisplayedPerFolder: Int, + private val instantUploadPath: String +) { + + fun createAutoUploadFolderWithoutMediaFolder(syncedFolder: SyncedFolder): SyncedFolderDisplayItem { + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + return createFromAutoUploadFolder( + syncedFolder, + filePaths = getDisplayFilePaths(files), + folderName = localFolder.name, + numberOfFiles = files.size.toLong(), + type = syncedFolder.type + ) + } + + fun createAutoUploadFolder(syncedFolder: SyncedFolder, mediaFolder: MediaFolder): SyncedFolderDisplayItem = + createFromAutoUploadFolder( + syncedFolder, + filePaths = mediaFolder.filePaths, + folderName = mediaFolder.folderName, + numberOfFiles = mediaFolder.numberOfFiles, + type = mediaFolder.type + ) + + fun createAutoUploadFolderFromMediaFolder(mediaFolder: MediaFolder, accountName: String): SyncedFolderDisplayItem = + SyncedFolderDisplayItem( + SyncedFolder.UNPERSISTED_ID, + mediaFolder.absolutePath, + "$instantUploadPath/${mediaFolder.folderName}", + true, + false, + true, + false, + accountName, + FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, + NameCollisionPolicy.ASK_USER.serialize(), + false, + clock.currentTime, + mediaFolder.filePaths, + mediaFolder.folderName, + mediaFolder.numberOfFiles, + mediaFolder.type, + false, + SubFolderRule.YEAR_MONTH, + false, + SyncedFolder.NOT_SCANNED_YET + ) + + fun createEmptyCustomFolder(accountName: String): SyncedFolderDisplayItem = SyncedFolderDisplayItem( + SyncedFolder.UNPERSISTED_ID, + null, + null, + true, + false, + true, + false, + accountName, + FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, + NameCollisionPolicy.ASK_USER.serialize(), + false, + clock.currentTime, + null, + MediaFolderType.CUSTOM, + false, + SubFolderRule.YEAR_MONTH, + false, + SyncedFolder.NOT_SCANNED_YET + ) + + fun createNewCustomFolder(syncedFolder: SyncedFolderParcelable): SyncedFolderDisplayItem = SyncedFolderDisplayItem( + SyncedFolder.UNPERSISTED_ID, + syncedFolder.localPath, + syncedFolder.remotePath, + syncedFolder.isWifiOnly, + syncedFolder.isChargingOnly, + syncedFolder.isExisting, + syncedFolder.isSubfolderByDate, + syncedFolder.account, + syncedFolder.uploadAction, + syncedFolder.nameCollisionPolicy.serialize(), + syncedFolder.isEnabled, + clock.currentTime, + File(syncedFolder.localPath).name, + syncedFolder.type, + syncedFolder.isHidden, + syncedFolder.subFolderRule, + syncedFolder.isExcludeHidden, + SyncedFolder.NOT_SCANNED_YET + ) + + fun updateAutoUploadFolderItem(item: SyncedFolderDisplayItem, syncedFolder: SyncedFolderParcelable) { + item.apply { + id = syncedFolder.id + localPath = syncedFolder.localPath + remotePath = syncedFolder.remotePath + isWifiOnly = syncedFolder.isWifiOnly + isChargingOnly = syncedFolder.isChargingOnly + setExisting(syncedFolder.isExisting) + isSubfolderByDate = syncedFolder.isSubfolderByDate + uploadAction = syncedFolder.uploadAction + setNameCollisionPolicy(syncedFolder.nameCollisionPolicy.serialize()) + setEnabled(syncedFolder.isEnabled, clock.currentTime) + setSubFolderRule(syncedFolder.subFolderRule) + setExcludeHidden(syncedFolder.isExcludeHidden) + } + } + + @Suppress("LongParameterList") + private fun createFromAutoUploadFolder( + syncedFolder: SyncedFolder, + filePaths: List?, + folderName: String?, + numberOfFiles: Long, + type: MediaFolderType? + ): SyncedFolderDisplayItem = SyncedFolderDisplayItem( + syncedFolder.id, + syncedFolder.localPath, + syncedFolder.remotePath, + syncedFolder.isWifiOnly, + syncedFolder.isChargingOnly, + syncedFolder.alsoUploadExistingFiles(), + syncedFolder.isSubfolderByDate, + syncedFolder.account, + syncedFolder.uploadAction, + syncedFolder.nameCollisionPolicyInt, + syncedFolder.isEnabled, + clock.currentTime, + filePaths, + folderName, + numberOfFiles, + type, + syncedFolder.isHidden, + syncedFolder.subfolderRule, + syncedFolder.isExcludeHidden, + syncedFolder.lastScanTimestampMs + ) + + private fun getDisplayFilePaths(files: List): List? = + files.take(itemsDisplayedPerFolder).map { it.absolutePath }.ifEmpty { null } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadItemsLoader.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadItemsLoader.kt new file mode 100644 index 000000000000..a04519d34035 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadItemsLoader.kt @@ -0,0 +1,95 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.data + +import androidx.appcompat.app.AppCompatActivity +import com.nextcloud.utils.extensions.sortAutoUploadFolderItems +import com.owncloud.android.datamodel.MediaFolder +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.MediaProvider +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.utils.SyncedFolderUtils +import java.io.File + +class AutoUploadItemsLoader( + private val syncedFolderProvider: SyncedFolderProvider, + private val itemFactory: AutoUploadDisplayItemFactory +) { + + fun load( + activity: AppCompatActivity, + perFolderMediaItemLimit: Int, + currentAccountName: String, + newItemAccountName: String + ): List { + val mediaFolders = getMediaFolders(activity, perFolderMediaItemLimit) + val syncedFolders = getAutoUploadFoldersAndDeleteObsolete(currentAccountName) + val items = mergeFolderData(syncedFolders, mediaFolders, newItemAccountName) + return items.sortAutoUploadFolderItems().filterNotNull() + } + + private fun getMediaFolders(activity: AppCompatActivity, itemLimit: Int): List { + val contentResolver = activity.contentResolver + return MediaProvider.getImageFolders(contentResolver, itemLimit, activity, false) + + MediaProvider.getVideoFolders(contentResolver, itemLimit, activity, false) + } + + private fun getAutoUploadFoldersAndDeleteObsolete(accountName: String): List { + val (obsolete, active) = syncedFolderProvider.syncedFolders + .filter { it.account == accountName } + .partition { !File(it.localPath).exists() && !it.isEnabled } + + obsolete.forEach { syncedFolderProvider.deleteSyncedFolder(it.id) } + return active + } + + private fun mergeFolderData( + syncedFolders: List, + mediaFolders: List, + accountName: String + ): List { + val syncedFoldersByKey = syncedFolders.associateByTo(HashMap()) { folderKey(it.localPath, it.type) } + + val mediaItems = mediaFolders.mapNotNull { mediaFolder -> + val syncedFolder = syncedFoldersByKey.remove(folderKey(mediaFolder.absolutePath, mediaFolder.type)) + createDisplayItem(syncedFolder, mediaFolder, accountName) + } + val remainingItems = syncedFoldersByKey.values.map { itemFactory.createAutoUploadFolderWithoutMediaFolder(it) } + + return mediaItems + remainingItems + } + + private fun createDisplayItem( + syncedFolder: SyncedFolder?, + mediaFolder: MediaFolder, + accountName: String + ): SyncedFolderDisplayItem? = when { + syncedFolder == null -> createQualifyingMediaFolderItem(mediaFolder, accountName) + + !SyncedFolderUtils.isQualifyingMediaFolder(syncedFolder) -> null + + syncedFolder.type == MediaFolderType.CUSTOM -> itemFactory.createAutoUploadFolderWithoutMediaFolder( + syncedFolder + ) + + else -> itemFactory.createAutoUploadFolder(syncedFolder, mediaFolder) + } + + private fun createQualifyingMediaFolderItem( + mediaFolder: MediaFolder, + accountName: String + ): SyncedFolderDisplayItem? { + if (!SyncedFolderUtils.isQualifyingMediaFolder(mediaFolder)) { + return null + } + return itemFactory.createAutoUploadFolderFromMediaFolder(mediaFolder, accountName) + } + + private fun folderKey(path: String?, type: MediaFolderType?): String = "$path-$type" +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadPersistence.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadPersistence.kt new file mode 100644 index 000000000000..7259ac5027c2 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadPersistence.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.data + +import com.nextcloud.client.jobs.BackgroundJobManager +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.utils.FilesSyncHelper + +class AutoUploadPersistence( + private val syncedFolderProvider: SyncedFolderProvider, + private val backgroundJobManager: BackgroundJobManager +) { + + fun saveOrUpdateAutoUploadFolder(item: SyncedFolderDisplayItem) { + if (item.id == SyncedFolder.UNPERSISTED_ID) { + storeAutoUploadFolder(item) + return + } + + syncedFolderProvider.updateSyncFolder(item) + applySyncState(item) + } + + fun persistSyncStatus(item: SyncedFolderDisplayItem) { + if (item.id > SyncedFolder.UNPERSISTED_ID) { + syncedFolderProvider.updateSyncedFolderEnabled(item.id, item.isEnabled) + return + } + + val storedId = syncedFolderProvider.storeSyncedFolder(item) + if (storedId != INVALID_ID) { + item.id = storedId + } + } + + fun deleteAutoUploadFolder(id: Long) { + syncedFolderProvider.deleteSyncedFolder(id) + } + + private fun storeAutoUploadFolder(item: SyncedFolderDisplayItem) { + val storedId = syncedFolderProvider.storeSyncedFolder(item) + if (storedId == INVALID_ID) { + return + } + + item.id = storedId + applySyncState(item) + } + + private fun applySyncState(item: SyncedFolderDisplayItem) { + if (item.isEnabled) { + backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) + return + } + + ArbitraryDataProviderImpl(MainApp.getAppContext()) + .deleteKeyForAccount(FilesSyncHelper.GLOBAL, MISSPELLED_SYNCED_FOLDER_INITIATED_PREFIX + item.id) + } + + companion object { + private const val INVALID_ID = -1L + private const val MISSPELLED_SYNCED_FOLDER_INITIATED_PREFIX = "syncedFolderIntitiated_" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt new file mode 100644 index 000000000000..c29776feb642 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt @@ -0,0 +1,149 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.handler + +import android.os.Looper +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.account.User +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.adapter.SyncedFolderAdapter +import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment +import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import com.owncloud.android.ui.fragment.autoUpload.data.AutoUploadDisplayItemFactory +import com.owncloud.android.ui.fragment.autoUpload.data.AutoUploadPersistence +import com.owncloud.android.ui.fragment.autoUpload.ui.AutoUploadDialogs +import com.owncloud.android.utils.PermissionUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList", "TooManyFunctions") +class AutoUploadClickHandler( + private val fragment: Fragment, + private val itemFactory: AutoUploadDisplayItemFactory, + private val persistence: AutoUploadPersistence, + private val dialogs: AutoUploadDialogs, + private val backgroundJobManager: BackgroundJobManager, + private val getAdapter: () -> SyncedFolderAdapter, + private val onItemVisibilityChanged: () -> Unit +) : SyncedFolderAdapter.ClickListener { + + private val fileUploadHelper = FileUploadHelper.instance() + + fun onCreateCustomFolderClick(activity: AppCompatActivity, accountName: String) { + Log_OC.d(TAG, "Show custom folder dialog") + if (!PermissionUtil.checkStoragePermission(activity)) { + PermissionUtil.requestStoragePermissionIfNeeded(activity) + return + } + + onSyncFolderSettingsClick(0, itemFactory.createEmptyCustomFolder(accountName)) + } + + fun onDeleteAllUploadedClick(user: User) { + dialogs.showDeleteAllUploadedDialog(user) + } + + override fun onSyncStatusToggleClick(section: Int, item: SyncedFolderDisplayItem?) { + item ?: return + persistence.persistSyncStatus(item) + + if (item.isEnabled) { + Log_OC.d(TAG, "auto-upload configuration sync status is enabled: " + item.remotePath) + backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) + dialogs.showBatteryOptimizationDialogIfNeeded() + return + } + + Log_OC.d(TAG, "auto-upload configuration sync status is disabled: " + item.remotePath) + fragment.lifecycleScope.launch(Dispatchers.IO) { + fileUploadHelper.removeEntityFromUploadEntities(item.id) + } + } + + override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + check(Looper.getMainLooper().isCurrentThread) { "This must be called on the main thread!" } + + val dialog = SyncedFolderPreferencesDialogFragment.newInstance(syncedFolderDisplayItem, section) ?: return + dialogs.showDialog(dialog, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + } + + override fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + syncedFolderDisplayItem ?: return + dialogs.showDeleteUploadedDialog(syncedFolderDisplayItem) + } + + override fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?) { + item ?: return + item.isHidden = !item.isHidden + persistence.saveOrUpdateAutoUploadFolder(item) + getAdapter().setSyncFolderItem(section, item) + onItemVisibilityChanged() + } + + override fun showSubFolderWarningDialog() { + dialogs.showSubFolderWarningDialog() + } + + fun onSaveAutoUploadFolderPreference(syncedFolder: SyncedFolderParcelable?) { + syncedFolder ?: return + + val isSaved = if (isNewCustomFolder(syncedFolder)) { + addCustomFolder(syncedFolder) + } else { + updateExistingFolder(syncedFolder) + } + + if (isSaved && syncedFolder.isEnabled) { + dialogs.showBatteryOptimizationDialogIfNeeded() + } + } + + fun onDeleteAutoUploadFolderPreference(syncedFolder: SyncedFolderParcelable?) { + syncedFolder ?: return + Log_OC.d(TAG, "deleting auto upload configuration: " + syncedFolder.remotePath) + + fragment.lifecycleScope.launch(Dispatchers.IO) { + fileUploadHelper.removeEntityFromUploadEntities(syncedFolder.id) + persistence.deleteAutoUploadFolder(syncedFolder.id) + withContext(Dispatchers.Main) { + getAdapter().removeItem(syncedFolder.section) + } + } + } + + private fun isNewCustomFolder(syncedFolder: SyncedFolderParcelable): Boolean = + syncedFolder.type == MediaFolderType.CUSTOM && syncedFolder.id == SyncedFolder.UNPERSISTED_ID + + private fun addCustomFolder(syncedFolder: SyncedFolderParcelable): Boolean { + val newCustomFolder = itemFactory.createNewCustomFolder(syncedFolder) + persistence.saveOrUpdateAutoUploadFolder(newCustomFolder) + getAdapter().addSyncFolderItem(newCustomFolder) + return true + } + + private fun updateExistingFolder(syncedFolder: SyncedFolderParcelable): Boolean { + val adapter = getAdapter() + val item = adapter.get(syncedFolder.section) ?: return false + itemFactory.updateAutoUploadFolderItem(item, syncedFolder) + persistence.saveOrUpdateAutoUploadFolder(item) + adapter.notifyItemChanged(adapter.getSectionHeaderIndex(syncedFolder.section)) + return true + } + + companion object { + private val TAG = AutoUploadClickHandler::class.java.simpleName + const val SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadIntentHandler.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadIntentHandler.kt new file mode 100644 index 000000000000..a40aae6a9ad3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadIntentHandler.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.handler + +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.MediaFoldersDetectionWork +import com.nextcloud.client.jobs.NotificationWork +import com.owncloud.android.ui.activity.BaseActivity +import com.owncloud.android.ui.adapter.SyncedFolderAdapter + +class AutoUploadIntentHandler { + + private var mediaFolderPath: String? = null + private var mediaFolderType = 0 + + fun readMediaFolder(intent: Intent?) { + if (intent?.extras == null) { + return + } + + mediaFolderPath = intent.getStringExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_PATH) + mediaFolderType = intent.getIntExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_TYPE, -1) + } + + fun findMediaFolderSection(adapter: SyncedFolderAdapter): Int? { + val path = mediaFolderPath + if (path.isNullOrEmpty()) { + return null + } + + return adapter.getSectionByLocalPathAndType(path, mediaFolderType).takeIf { it >= 0 } + } + + fun handleNotification(activity: BaseActivity, userAccountManager: UserAccountManager) { + val intent = activity.intent?.takeIf { it.extras != null } ?: return + + val accountName = intent.getStringExtra(NotificationWork.KEY_NOTIFICATION_ACCOUNT) + switchToNotificationAccount(activity, userAccountManager, accountName) + + val notificationId = intent.getIntExtra(MediaFoldersDetectionWork.NOTIFICATION_ID, 0) + val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager + notificationManager?.cancel(notificationId) + } + + private fun switchToNotificationAccount( + activity: BaseActivity, + userAccountManager: UserAccountManager, + accountName: String? + ) { + val currentUser = activity.user.orElse(null) + if (accountName == null || + currentUser == null || + accountName.equals(currentUser.accountName, ignoreCase = true) + ) { + return + } + + userAccountManager.setCurrentOwnCloudAccount(accountName) + activity.setUser(userAccountManager.user) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadPickers.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadPickers.kt new file mode 100644 index 000000000000..4c8f5bff89dc --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadPickers.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.handler + +import android.app.Activity +import android.content.Intent +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import com.nextcloud.utils.extensions.getParcelableArgument +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FolderPickerActivity +import com.owncloud.android.ui.activity.UploadFilesActivity +import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment + +class AutoUploadPickers( + private val fragment: Fragment, + private val findPreferencesDialog: () -> SyncedFolderPreferencesDialogFragment? +) { + + private val remoteFolderLauncher = + fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + onRemoteFolderPicked(result) + } + + private val localFolderLauncher = + fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + onLocalFolderPicked(result) + } + + fun pickRemoteFolder() { + val intent = Intent(fragment.requireContext(), FolderPickerActivity::class.java).apply { + putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION) + } + remoteFolderLauncher.launch(intent) + } + + fun pickLocalFolder() { + val intent = Intent(fragment.requireContext(), UploadFilesActivity::class.java).apply { + putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true) + putExtra( + UploadFilesActivity.REQUEST_CODE_KEY, + SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER + ) + } + localFolderLauncher.launch(intent) + } + + private fun onRemoteFolderPicked(result: ActivityResult) { + if (result.resultCode != Activity.RESULT_OK) { + return + } + + val dialog = findPreferencesDialog() ?: return + val chosenFolder = result.data?.getParcelableArgument(FolderPickerActivity.EXTRA_FOLDER, OCFile::class.java) + dialog.setRemoteFolderSummary(chosenFolder?.remotePath) + } + + private fun onLocalFolderPicked(result: ActivityResult) { + if (result.resultCode != Activity.RESULT_OK) { + return + } + + val dialog = findPreferencesDialog() ?: return + dialog.setLocalFolderSummary(result.data?.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES)) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadDialogs.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadDialogs.kt new file mode 100644 index 000000000000..ac7c2ee966a7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadDialogs.kt @@ -0,0 +1,168 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.ui + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.account.User +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.utils.BatteryOptimizationHelper +import com.nextcloud.utils.extensions.isDialogFragmentReady +import com.owncloud.android.R +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.dialog.extensions.themeButtons +import com.owncloud.android.utils.FilesSyncHelper +import com.owncloud.android.utils.theme.ViewThemeUtils + +class AutoUploadDialogs( + private val fragment: Fragment, + private val viewThemeUtils: ViewThemeUtils, + private val syncedFolderProvider: SyncedFolderProvider, + private val backgroundJobManager: BackgroundJobManager +) { + + fun buildPowerCheckDialog(): AlertDialog { + val context = fragment.requireContext() + val builder = MaterialAlertDialogBuilder(context) + .setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() } + .setTitle(R.string.autoupload_disable_power_save_check) + .setMessage(context.getString(R.string.power_save_check_dialog_message)) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, builder) + + return builder.create() + } + + fun showDeleteAllUploadedDialog(user: User) { + val dialog = ConfirmationDialogFragment.newInstance( + R.string.autoupload_delete_uploaded_all_dialog_description, + null, + R.string.autoupload_delete_uploaded_all_dialog_title, + R.drawable.selector_trashbin, + R.string.autoupload_delete_uploaded_all_dialog_button_all_users, + R.string.autoupload_delete_uploaded_all_dialog_button_current_user, + R.string.common_cancel + ) + dialog.isCancelable = false + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) + } + + override fun onCancel(callerTag: String?) { + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders( + syncedFolderProvider, + backgroundJobManager, + user + ) + } + + override fun onNeutral(callerTag: String?) = Unit + }) + + showDialog(dialog, SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG) + } + + fun showDeleteUploadedDialog(syncedFolderDisplayItem: SyncedFolderDisplayItem) { + val dialog = ConfirmationDialogFragment.newInstance( + R.string.autoupload_delete_uploaded_dialog_description, + null, + R.string.autoupload_delete_uploaded_dialog_title, + R.drawable.selector_trashbin, + R.string.common_ok, + R.string.common_cancel, + -1 + ) + dialog.isCancelable = false + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { + FilesSyncHelper.startLocalDeletionForSyncedFolder(syncedFolderDisplayItem, backgroundJobManager) + } + + override fun onNeutral(callerTag: String?) = Unit + + override fun onCancel(callerTag: String?) = Unit + }) + showDialog(dialog, SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG) + } + + fun showSubFolderWarningDialog() { + val dialog = ConfirmationDialogFragment.newInstance( + messageResId = R.string.auto_upload_sub_folder_warning, + messageArguments = null, + titleResId = R.string.sync_duplication, + titleIconId = R.drawable.ic_info, + positiveButtonTextId = R.string.dialog_close, + negativeButtonTextId = -1, + neutralButtonTextId = -1 + ) + + if (isActivityReadyFor(dialog)) { + dialog.show(fragment.childFragmentManager, SUB_FOLDER_WARNING_DIALOG_TAG) + } + } + + fun showDialog(dialog: DialogFragment, tag: String) { + if (!isActivityReadyFor(dialog) || !isResumed()) { + Log_OC.d(TAG, "SyncFolderDeleteUploaded dialog not ready") + return + } + + val fragmentTransaction = fragment.childFragmentManager + .beginTransaction() + .addToBackStack(null) + dialog.show(fragmentTransaction, tag) + } + + fun showBatteryOptimizationDialogIfNeeded() { + if (!BatteryOptimizationHelper.isBatteryOptimizationEnabled(fragment.requireContext())) { + Log_OC.d(TAG, "battery optimization is disabled") + return + } + + showBatteryOptimizationDialog() + } + + private fun showBatteryOptimizationDialog() { + if (!isResumed()) { + Log_OC.w(TAG, "Fragment not resumed, skipping battery dialog") + return + } + + val context = fragment.requireContext() + val dialog = MaterialAlertDialogBuilder(context, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.battery_optimization_title) + .setMessage(R.string.battery_optimization_message) + .setPositiveButton(R.string.battery_optimization_disable) { _, _ -> + BatteryOptimizationHelper.openBatteryOptimizationSettings(context) + } + .setNeutralButton(R.string.battery_optimization_close, null) + .setIcon(R.drawable.ic_battery_alert) + + val alertDialog = dialog.show() + alertDialog.themeButtons(viewThemeUtils) + } + + private fun isActivityReadyFor(dialog: DialogFragment): Boolean = + (fragment.activity as? AppCompatActivity)?.isDialogFragmentReady(dialog) == true + + private fun isResumed(): Boolean = fragment.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + + companion object { + private val TAG = AutoUploadDialogs::class.java.simpleName + private const val SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG" + private const val SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG" + private const val SUB_FOLDER_WARNING_DIALOG_TAG = "SUB_FOLDER_WARNING_DIALOG_TAG" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadEmptyContent.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadEmptyContent.kt new file mode 100644 index 000000000000..814daf639a3a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadEmptyContent.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.ui + +import android.view.View +import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentAutoUploadBinding +import com.owncloud.android.ui.adapter.SyncedFolderAdapter +import com.owncloud.android.utils.theme.ViewThemeUtils + +class AutoUploadEmptyContent( + private val binding: FragmentAutoUploadBinding, + private val adapter: SyncedFolderAdapter, + viewThemeUtils: ViewThemeUtils +) { + + private val emptyList = binding.emptyList + private val resources = binding.root.resources + + private val hasOnlyHiddenItems: Boolean + get() = adapter.sectionCount == 0 && adapter.unfilteredSectionCount > 0 + + private val hasNoItems: Boolean + get() = adapter.sectionCount == 0 && adapter.unfilteredSectionCount == 0 + + init { + emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders) + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(emptyList.emptyListViewAction) + emptyList.emptyListViewAction.setOnClickListener { showHiddenItems() } + } + + fun showList() { + binding.list.visibility = View.VISIBLE + binding.loadingContent.visibility = View.GONE + checkAndShowEmptyListContent() + } + + fun checkAndShowEmptyListContent() { + when { + hasOnlyHiddenItems -> showHiddenFoldersAction() + hasNoItems -> showEmptyContent(resources.getString(R.string.synced_folders_no_results)) + else -> emptyList.emptyListView.visibility = View.GONE + } + } + + fun showLoadingContent() { + binding.loadingContent.visibility = View.VISIBLE + emptyList.emptyListViewAction.visibility = View.GONE + } + + private fun showHiddenItems() { + if (!hasOnlyHiddenItems) { + return + } + + adapter.toggleHiddenItemsVisibility() + emptyList.emptyListView.visibility = View.GONE + binding.list.visibility = View.VISIBLE + } + + private fun showHiddenFoldersAction() { + val hiddenFoldersCount = adapter.hiddenFolderCount + val hiddenFoldersText = resources.getQuantityString( + R.plurals.synced_folders_show_hidden_folders, + hiddenFoldersCount, + hiddenFoldersCount + ) + + showEmptyContent(hiddenFoldersText) + emptyList.emptyListViewAction.text = hiddenFoldersText + emptyList.emptyListViewAction.visibility = View.VISIBLE + emptyList.emptyListViewText.visibility = View.GONE + } + + private fun showEmptyContent(message: String) { + emptyList.emptyListViewAction.visibility = View.GONE + emptyList.emptyListView.visibility = View.VISIBLE + binding.list.visibility = View.GONE + binding.loadingContent.visibility = View.GONE + emptyList.emptyListViewHeadline.text = resources.getString(R.string.drawer_synced_folders) + emptyList.emptyListViewText.text = message + emptyList.emptyListViewText.visibility = View.VISIBLE + emptyList.emptyListIcon.visibility = View.VISIBLE + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadMenuProvider.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadMenuProvider.kt new file mode 100644 index 000000000000..31c87fd3b654 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/ui/AutoUploadMenuProvider.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.ui + +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.view.MenuProvider +import com.owncloud.android.R + +class AutoUploadMenuProvider( + private val onCreateCustomFolder: () -> Unit, + private val onDeleteAllUploaded: () -> Unit +) : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_synced_folders, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_create_custom_folder -> { + onCreateCustomFolder() + true + } + + R.id.action_auto_upload_all_folders_delete_uploaded -> { + onDeleteAllUploaded() + true + } + + else -> false + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt index 0930c5c71bd0..0bff64f17523 100644 --- a/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt +++ b/app/src/main/java/com/owncloud/android/ui/navigation/NavigatorScreen.kt @@ -13,6 +13,7 @@ import com.owncloud.android.R import com.owncloud.android.ui.fragment.ActivitiesFragment import com.owncloud.android.ui.fragment.community.CommunityFragment import com.owncloud.android.ui.fragment.notifications.NotificationsFragment +import com.owncloud.android.ui.fragment.autoUpload.AutoUploadFragment import com.owncloud.android.ui.fragment.uploadList.UploadListFragment import com.owncloud.android.ui.navigation.model.ActionBarStyle import com.owncloud.android.ui.trashbin.TrashbinFragment @@ -29,6 +30,9 @@ sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : P @Parcelize object Notifications : NavigatorScreen(NOTIFICATIONS_TAG, hasDrawer = false) + @Parcelize + object AutoUpload : NavigatorScreen(AUTO_UPLOAD_TAG, hasDrawer = false) + @Parcelize object Trashbin : NavigatorScreen(TRASHBIN_TAG) @@ -41,6 +45,7 @@ sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : P private const val COMMUNITY_TAG = "Community" private const val NOTIFICATIONS_TAG = "Notifications" private const val UPLOAD_LIST_TAG = "UploadList" + private const val AUTO_UPLOAD_TAG = "AutoUpload" fun fromTag(tag: String?): NavigatorScreen? = when (tag) { ACTIVITIES_TAG -> Activities @@ -48,6 +53,7 @@ sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : P NOTIFICATIONS_TAG -> Notifications TRASHBIN_TAG -> Trashbin UPLOAD_LIST_TAG -> UploadList + AUTO_UPLOAD_TAG -> AutoUpload else -> null } } @@ -58,6 +64,7 @@ sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : P Trashbin -> R.id.nav_trashbin UploadList -> R.id.nav_uploads Notifications -> -1 + AutoUpload -> R.id.nav_settings } fun actionBarStyle(): Pair = when (this) { @@ -66,6 +73,7 @@ sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : P Trashbin -> ActionBarStyle.Plain to R.string.drawer_item_trashbin UploadList -> ActionBarStyle.Plain to R.string.uploads_view_title Notifications -> ActionBarStyle.Plain to R.string.drawer_item_notifications + AutoUpload -> ActionBarStyle.Plain to R.string.drawer_synced_folders } fun toFragment(): Fragment = when (this) { @@ -74,5 +82,6 @@ sealed class NavigatorScreen(val tag: String, val hasDrawer: Boolean = true) : P Notifications -> NotificationsFragment() Trashbin -> TrashbinFragment() UploadList -> UploadListFragment() + AutoUpload -> AutoUploadFragment() } } diff --git a/app/src/main/res/layout/fragment_auto_upload.xml b/app/src/main/res/layout/fragment_auto_upload.xml new file mode 100644 index 000000000000..379ed1a766db --- /dev/null +++ b/app/src/main/res/layout/fragment_auto_upload.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/synced_folders_layout.xml b/app/src/main/res/layout/synced_folders_layout.xml deleted file mode 100644 index d0da597e6f7b..000000000000 --- a/app/src/main/res/layout/synced_folders_layout.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt b/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt new file mode 100644 index 000000000000..d8ce488776ac --- /dev/null +++ b/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt @@ -0,0 +1,116 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.utils.extensions + +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.files.services.NameCollisionPolicy +import org.junit.Assert.assertTrue +import org.junit.Test + +class SyncedFolderDisplayItemExtensionsTest { + + @Test + fun regular() { + assertSortedAfterShuffle( + create("Folder1", true), + create("Folder2", true) + ) + } + + @Test + fun withNull() { + assertSortedAfterShuffle( + null, + null, + create("Folder1", true), + create("Folder2", true) + ) + } + + @Test + fun withNullAndEnableStatus() { + assertSortedAfterShuffle( + null, + null, + create("Folder1", true), + create("Folder2", true), + create("Folder3", true), + create("Folder4", true), + create("Folder5", false), + create("Folder6", false), + create("Folder7", false), + create("Folder8", false) + ) + } + + @Test + fun withNullFolderName() { + assertSortedAfterShuffle( + null, + null, + create("Folder1", true), + create(null, false), + create("Folder2", false), + create("Folder3", false), + create("Folder4", false), + create("Folder5", false) + ) + } + + @Test + fun withNullFolderNameAllEnabled() { + assertSortedAfterShuffle( + null, + null, + create(null, true), + create("Folder1", true), + create("Folder2", true), + create("Folder3", true), + create("Folder4", true) + ) + } + + private fun assertSortedAfterShuffle(vararg expectedOrder: SyncedFolderDisplayItem?) { + val expected = expectedOrder.toList() + val actual = expected.shuffled().sortAutoUploadFolderItems() + + val isSameOrder = expected.size == actual.size && expected.zip(actual).all { (e, a) -> e === a } + assertTrue("expected:\n${expected.describe()}\nactual:\n${actual.describe()}", isSameOrder) + } + + private fun List.describe(): String = + joinToString("\n") { item -> item?.let { "${it.folderName} ${it.isEnabled}" } ?: "null" } + + private fun create(folderName: String?, enabled: Boolean): SyncedFolderDisplayItem = SyncedFolderDisplayItem( + 1, + "localPath", + "remotePath", + true, + true, + true, + true, + "test@nextcloud.com", + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE, + NameCollisionPolicy.ASK_USER.serialize(), + enabled, + System.currentTimeMillis(), + ArrayList(), + folderName, + 2, + MediaFolderType.IMAGE, + false, + SubFolderRule.YEAR_MONTH, + true, + SyncedFolder.NOT_SCANNED_YET + ) +} diff --git a/app/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java b/app/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java deleted file mode 100644 index 41e1d47e22cb..000000000000 --- a/app/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.activity; - -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.client.preferences.SubFolderRule; -import com.owncloud.android.datamodel.MediaFolderType; -import com.owncloud.android.datamodel.SyncedFolder; -import com.owncloud.android.datamodel.SyncedFolderDisplayItem; -import com.owncloud.android.files.services.NameCollisionPolicy; - -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertTrue; - -public class SyncedFoldersActivityTest { - - @Test - public void regular() { - SyncedFolderDisplayItem[] sortedArray = { - create("Folder1", true), - create("Folder2", true), - }; - - assertTrue(sortAndTest(Arrays.asList(sortedArray))); - } - - @Test - public void withNull() { - SyncedFolderDisplayItem[] sortedArray = { - null, - null, - create("Folder1", true), - create("Folder2", true), - }; - - assertTrue(sortAndTest(Arrays.asList(sortedArray))); - } - - @Test - public void withNullAndEnableStatus() { - SyncedFolderDisplayItem[] sortedArray = { - null, - null, - create("Folder1", true), - create("Folder2", true), - create("Folder3", true), - create("Folder4", true), - create("Folder5", false), - create("Folder6", false), - create("Folder7", false), - create("Folder8", false), - }; - - assertTrue(sortAndTest(Arrays.asList(sortedArray))); - } - - @Test - public void withNullFolderName() { - SyncedFolderDisplayItem[] sortedArray = { - null, - null, - create("Folder1", true), - create(null, false), - create("Folder2", false), - create("Folder3", false), - create("Folder4", false), - create("Folder5", false), - }; - - assertTrue(sortAndTest(Arrays.asList(sortedArray))); - } - - @Test - public void withNullFolderNameAllEnabled() { - SyncedFolderDisplayItem[] sortedArray = { - null, - null, - create(null, true), - create("Folder1", true), - create("Folder2", true), - create("Folder3", true), - create("Folder4", true), - }; - - assertTrue(sortAndTest(Arrays.asList(sortedArray))); - } - - private List shuffle(List list) { - List shuffled = new ArrayList<>(list); - - Collections.shuffle(shuffled); - - return shuffled; - } - - private boolean sortAndTest(List sortedList) { - List unsortedList = shuffle(sortedList); - - return test(sortedList, SyncedFoldersActivity.sortSyncedFolderItems(unsortedList)); - } - - private boolean test(List target, List actual) { - - for (int i = 0; i < target.size(); i++) { - boolean compare; - - compare = target.get(i) == (actual.get(i)); - - if (!compare) { - - System.out.println("target:"); - - for (SyncedFolderDisplayItem item : target) { - if (item == null) { - System.out.println("null"); - } else { - System.out.println(item.getFolderName() + " " + item.isEnabled()); - } - } - - System.out.println(); - System.out.println("actual:"); - for (SyncedFolderDisplayItem item : actual) { - if (item == null) { - System.out.println("null"); - } else { - System.out.println(item.getFolderName() + " " + item.isEnabled()); - } - } - - return false; - } - } - - return true; - } - - private SyncedFolderDisplayItem create(String folderName, boolean enabled) { - return new SyncedFolderDisplayItem(1, - "localPath", - "remotePath", - true, - true, - true, - true, - "test@nextcloud.com", - FileUploadWorker.LOCAL_BEHAVIOUR_MOVE, - NameCollisionPolicy.ASK_USER.serialize(), - enabled, - System.currentTimeMillis(), - new ArrayList<>(), - folderName, - 2, - MediaFolderType.IMAGE, - false, - SubFolderRule.YEAR_MONTH, - true, - SyncedFolder.NOT_SCANNED_YET); - } -}