diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 4eee9aeeb4bb..1a8d0df4d294 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -297,7 +297,8 @@ class BackgroundJobFactory @Inject constructor( params, accountManager, powerManagementService, - connectivityService + connectivityService, + preferences ) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 84754e5ee6c4..fbcf9c0d98fc 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -172,5 +172,6 @@ interface BackgroundJobManager { fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean fun startOfflineOperations() fun startPeriodicallyOfflineOperation() - fun scheduleInternal2WaySync() + fun scheduleInternal2WaySync(intervalMinutes: Long) + fun cancelInternal2WaySyncJob() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 9a33bedda28b..aea2fc68ba17 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -702,12 +702,17 @@ internal class BackgroundJobManagerImpl( ) } - override fun scheduleInternal2WaySync() { + override fun scheduleInternal2WaySync(intervalMinutes: Long) { val request = periodicRequestBuilder( jobClass = InternalTwoWaySyncWork::class, - jobName = JOB_INTERNAL_TWO_WAY_SYNC + jobName = JOB_INTERNAL_TWO_WAY_SYNC, + intervalMins = intervalMinutes ).build() - workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.KEEP, request) + workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request) + } + + override fun cancelInternal2WaySyncJob() { + workManager.cancelJob(JOB_INTERNAL_TWO_WAY_SYNC) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt index 1f273d6cd703..fda84ce36159 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt @@ -13,6 +13,7 @@ import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.MainApp import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -21,13 +22,14 @@ import com.owncloud.android.operations.SynchronizeFolderOperation import com.owncloud.android.utils.FileStorageUtils import java.io.File -@Suppress("Detekt.NestedBlockDepth", "ReturnCount") +@Suppress("Detekt.NestedBlockDepth", "ReturnCount", "LongParameterList") class InternalTwoWaySyncWork( private val context: Context, params: WorkerParameters, private val userAccountManager: UserAccountManager, private val powerManagementService: PowerManagementService, - private val connectivityService: ConnectivityService + private val connectivityService: ConnectivityService, + private val appPreferences: AppPreferences ) : Worker(context, params) { private var shouldRun = true @@ -36,7 +38,9 @@ class InternalTwoWaySyncWork( var result = true - if (powerManagementService.isPowerSavingEnabled || + @Suppress("ComplexCondition") + if (!appPreferences.isTwoWaySyncEnabled || + powerManagementService.isPowerSavingEnabled || !connectivityService.isConnected || connectivityService.isInternetWalled || !connectivityService.connectivity.isWifi @@ -61,13 +65,6 @@ class InternalTwoWaySyncWork( return checkFreeSpaceResult } - // do not attempt to sync root folder - if (folder.remotePath == OCFile.ROOT_PATH) { - folder.internalFolderSyncTimestamp = -1L - fileDataStorageManager.saveFile(folder) - continue - } - Log_OC.d(TAG, "Folder ${folder.remotePath}: started!") val operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager) .execute(context) diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index b2517b5fa579..68c49eb4bedd 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -391,4 +391,10 @@ default void onDarkThemeModeChanged(DarkMode mode) { @NonNull String getLastSelectedMediaFolder(); + + void setTwoWaySyncStatus(boolean value); + boolean isTwoWaySyncEnabled(); + + void setTwoWaySyncInterval(Long value); + Long getTwoWaySyncInterval(); } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 0801d9102745..50ee37e3f6c0 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -102,6 +102,9 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__STORAGE_PERMISSION_REQUESTED = "storage_permission_requested"; private static final String PREF__IN_APP_REVIEW_DATA = "in_app_review_data"; + private static final String PREF__TWO_WAY_STATUS = "two_way_sync_status"; + private static final String PREF__TWO_WAY_SYNC_INTERVAL = "two_way_sync_interval"; + private static final String LOG_ENTRY = "log_entry"; private final Context context; @@ -789,4 +792,24 @@ public void setLastSelectedMediaFolder(@NonNull String path) { public String getLastSelectedMediaFolder() { return preferences.getString(PREF__MEDIA_FOLDER_LAST_PATH, OCFile.ROOT_PATH); } + + @Override + public void setTwoWaySyncStatus(boolean value) { + preferences.edit().putBoolean(PREF__TWO_WAY_STATUS, value).apply(); + } + + @Override + public boolean isTwoWaySyncEnabled() { + return preferences.getBoolean(PREF__TWO_WAY_STATUS, true); + } + + @Override + public void setTwoWaySyncInterval(Long value) { + preferences.edit().putLong(PREF__TWO_WAY_SYNC_INTERVAL, value).apply(); + } + + @Override + public Long getTwoWaySyncInterval() { + return preferences.getLong(PREF__TWO_WAY_SYNC_INTERVAL, 15L); + } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt index f3e4d89abcc2..0cfc83439ed5 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt @@ -17,8 +17,13 @@ import android.os.Handler import android.os.Looper import android.widget.Toast import com.google.common.io.Resources +import com.owncloud.android.R import com.owncloud.android.datamodel.ReceiverFlag +fun Context.hourPlural(hour: Int): String = resources.getQuantityString(R.plurals.hours, hour, hour) + +fun Context.minPlural(min: Int): String = resources.getQuantityString(R.plurals.minutes, min, min) + @SuppressLint("UnspecifiedRegisterReceiverFlag") fun Context.registerBroadcastReceiver(receiver: BroadcastReceiver?, filter: IntentFilter, flag: ReceiverFlag): Intent? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index e38bb9b72548..b0a44d1e44bd 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -375,7 +375,11 @@ public void onCreate() { backgroundJobManager.scheduleMediaFoldersDetectionJob(); backgroundJobManager.startMediaFoldersDetectionJob(); backgroundJobManager.schedulePeriodicHealthStatus(); - backgroundJobManager.scheduleInternal2WaySync(); + + if (preferences.isTwoWaySyncEnabled()) { + backgroundJobManager.scheduleInternal2WaySync(preferences.getTwoWaySyncInterval()); + } + backgroundJobManager.startPeriodicallyOfflineOperation(); } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 33c81700688b..1dbc31ded241 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2643,7 +2643,10 @@ public List getInternalTwoWaySyncFolders(User user) { List files = new ArrayList<>(fileEntities.size()); for (FileEntity fileEntity : fileEntities) { - files.add(createFileInstance(fileEntity)); + OCFile file = createFileInstance(fileEntity); + if (file.isFolder() && !file.isRootDirectory()) { + files.add(file); + } } return files; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt index fbd0dcc26a87..f441cf96a08d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt @@ -10,44 +10,58 @@ package com.owncloud.android.ui.activity import android.annotation.SuppressLint import android.os.Bundle import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View -import androidx.core.view.MenuProvider +import android.widget.ArrayAdapter import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.download.FileDownloadWorker +import com.nextcloud.utils.extensions.hourPlural +import com.nextcloud.utils.extensions.minPlural +import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.InternalTwoWaySyncLayoutBinding +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.adapter.InternalTwoWaySyncAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +class InternalTwoWaySyncActivity : + DrawerActivity(), + Injectable, + InternalTwoWaySyncAdapter.InternalTwoWaySyncAdapterOnUpdate { + private val tag = "InternalTwoWaySyncActivity" -class InternalTwoWaySyncActivity : DrawerActivity(), Injectable { @Inject lateinit var backgroundJobManager: BackgroundJobManager lateinit var binding: InternalTwoWaySyncLayoutBinding private lateinit var internalTwoWaySyncAdapter: InternalTwoWaySyncAdapter + private var disableForAllFoldersMenuButton: MenuItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - internalTwoWaySyncAdapter = InternalTwoWaySyncAdapter(fileDataStorageManager, user.get(), this) + internalTwoWaySyncAdapter = InternalTwoWaySyncAdapter(fileDataStorageManager, user.get(), this, this) binding = InternalTwoWaySyncLayoutBinding.inflate(layoutInflater) setContentView(binding.root) setupToolbar() setupActionBar() - setupMenuProvider() setupTwoWaySyncAdapter() setupEmptyList() + setupTwoWaySyncToggle() + setupTwoWaySyncInterval() + checkLayoutVisibilities(preferences.isTwoWaySyncEnabled) } private fun setupActionBar() { @@ -57,12 +71,14 @@ class InternalTwoWaySyncActivity : DrawerActivity(), Injectable { @SuppressLint("NotifyDataSetChanged") private fun setupTwoWaySyncAdapter() { - binding.run { - list.run { - setEmptyView(emptyList.emptyListView) - adapter = internalTwoWaySyncAdapter - layoutManager = LinearLayoutManager(this@InternalTwoWaySyncActivity) - adapter?.notifyDataSetChanged() + if (preferences.isTwoWaySyncEnabled) { + binding.run { + list.run { + setEmptyView(emptyList.emptyListView) + adapter = internalTwoWaySyncAdapter + layoutManager = LinearLayoutManager(this@InternalTwoWaySyncActivity) + adapter?.notifyDataSetChanged() + } } } } @@ -92,46 +108,129 @@ class InternalTwoWaySyncActivity : DrawerActivity(), Injectable { } } + @Suppress("TooGenericExceptionCaught") private fun disableTwoWaySyncAndWorkers() { lifecycleScope.launch(Dispatchers.IO) { - backgroundJobManager.cancelTwoWaySyncJob() + try { + backgroundJobManager.cancelTwoWaySyncJob() + + val currentUser = user.get() - val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(user.get()) - folders.forEach { folder -> - FileDownloadWorker.cancelOperation(user.get().accountName, folder.fileId) - backgroundJobManager.cancelFilesDownloadJob(user.get(), folder.fileId) + val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(currentUser) + folders.forEach { folder -> + FileDownloadWorker.cancelOperation(currentUser.accountName, folder.fileId) + backgroundJobManager.cancelFilesDownloadJob(currentUser, folder.fileId) + + folder.internalFolderSyncTimestamp = -1L + fileDataStorageManager.saveFile(folder) + } - folder.internalFolderSyncTimestamp = -1L - fileDataStorageManager.saveFile(folder) + withContext(Dispatchers.Main) { + internalTwoWaySyncAdapter.update() + } + } catch (e: Exception) { + Log_OC.d(tag, "Error caught at disableTwoWaySyncAndWorkers: $e") } + } + } + + @Suppress("MagicNumber") + private fun setupTwoWaySyncInterval() { + val durations = listOf( + 15.minutes to minPlural(15), + 30.minutes to minPlural(30), + 45.minutes to minPlural(45), + 1.hours to hourPlural(1), + 2.hours to hourPlural(2), + 4.hours to hourPlural(4), + 6.hours to hourPlural(6), + 8.hours to hourPlural(8), + 12.hours to hourPlural(12), + 24.hours to hourPlural(24) + ) + val selectedDuration = durations.find { it.first.inWholeMinutes == preferences.twoWaySyncInterval } - launch(Dispatchers.Main) { - internalTwoWaySyncAdapter.update() + val adapter = ArrayAdapter( + this, + android.R.layout.simple_dropdown_item_1line, + durations.map { it.second } + ) + + binding.twoWaySyncInterval.run { + setAdapter(adapter) + setText(selectedDuration?.second ?: minPlural(15), false) + setOnItemClickListener { _, _, position, _ -> + handleDurationSelected(durations[position].first.inWholeMinutes) } } } - private fun setupMenuProvider() { - addMenuProvider( - object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.activity_internal_two_way_sync, menu) - } + private fun handleDurationSelected(duration: Long) { + preferences.twoWaySyncInterval = duration + backgroundJobManager.scheduleInternal2WaySync(duration) + } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - return when (menuItem.itemId) { - android.R.id.home -> { - onBackPressed() - true - } - R.id.action_dismiss_two_way_sync -> { - disableTwoWaySyncAndWorkers() - true - } - else -> false - } - } + private fun setupTwoWaySyncToggle() { + binding.twoWaySyncToggle.isChecked = preferences.isTwoWaySyncEnabled + binding.twoWaySyncToggle.setOnCheckedChangeListener { _, isChecked -> + preferences.setTwoWaySyncStatus(isChecked) + setupTwoWaySyncAdapter() + checkLayoutVisibilities(isChecked) + checkDisableForAllFoldersMenuButtonVisibility() + + if (isChecked) { + backgroundJobManager.scheduleInternal2WaySync(preferences.twoWaySyncInterval) + } else { + backgroundJobManager.cancelInternal2WaySyncJob() } - ) + } + } + + private fun checkLayoutVisibilities(condition: Boolean) { + binding.listFrameLayout.setVisibleIf(condition) + binding.twoWaySyncIntervalLayout.setVisibleIf(condition) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.activity_internal_two_way_sync, menu) + disableForAllFoldersMenuButton = menu?.findItem(R.id.action_dismiss_two_way_sync) + checkDisableForAllFoldersMenuButtonVisibility() + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + } + R.id.action_dismiss_two_way_sync -> { + disableTwoWaySyncAndWorkers() + } + } + + return super.onOptionsItemSelected(item) + } + + private fun checkDisableForAllFoldersMenuButtonVisibility() { + lifecycleScope.launch { + val folderSize = withContext(Dispatchers.IO) { + fileDataStorageManager.getInternalTwoWaySyncFolders(user.get()).size + } + + checkDisableForAllFoldersMenuButtonVisibility(preferences.isTwoWaySyncEnabled, folderSize) + } + } + + private fun checkDisableForAllFoldersMenuButtonVisibility(isTwoWaySyncEnabled: Boolean, folderSize: Int) { + val showDisableButton = isTwoWaySyncEnabled && folderSize > 0 + + disableForAllFoldersMenuButton?.let { + it.setVisible(showDisableButton) + it.setEnabled(showDisableButton) + } + } + + override fun onUpdate(folderSize: Int) { + checkDisableForAllFoldersMenuButtonVisibility(preferences.isTwoWaySyncEnabled, folderSize) } } 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 54eb81f2a795..b17f814b57ab 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 @@ -328,7 +328,7 @@ private void setupSyncCategory() { viewThemeUtils.files.themePreferenceCategory(preferenceCategorySync); setupAutoUploadPreference(preferenceCategorySync); - setupInternalTwoWaySyncPreference(preferenceCategorySync); + setupInternalTwoWaySyncPreference(); } private void setupMoreCategory() { @@ -567,7 +567,7 @@ private void setupAutoUploadPreference(PreferenceCategory preferenceCategoryMore } } - private void setupInternalTwoWaySyncPreference(PreferenceCategory preferenceCategorySync) { + private void setupInternalTwoWaySyncPreference() { Preference twoWaySync = findPreference("internal_two_way_sync"); twoWaySync.setOnPreferenceClickListener(preference -> { @@ -665,8 +665,7 @@ private void setupHiddenFilesPreference(PreferenceCategory preferenceCategoryDet } } - private void setupShowEcosystemAppsPreference(PreferenceCategory preferenceCategoryDetails, - boolean fShowEcosystemAppsEnabled) { + private void setupShowEcosystemAppsPreference(PreferenceCategory preferenceCategoryDetails, boolean fShowEcosystemAppsEnabled) { showEcosystemApps = (ThemeableSwitchPreference) findPreference("show_ecosystem_apps"); if (fShowEcosystemAppsEnabled) { showEcosystemApps.setOnPreferenceClickListener(preference -> { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt index b766a3d67953..f04ad691eac5 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt @@ -20,8 +20,14 @@ import com.owncloud.android.datamodel.OCFile class InternalTwoWaySyncAdapter( private val dataStorageManager: FileDataStorageManager, private val user: User, - val context: Context + val context: Context, + private val onUpdateListener: InternalTwoWaySyncAdapterOnUpdate ) : RecyclerView.Adapter() { + + interface InternalTwoWaySyncAdapterOnUpdate { + fun onUpdate(folderSize: Int) + } + var folders: List = dataStorageManager.getInternalTwoWaySyncFolders(user) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InternalTwoWaySyncViewHolder { @@ -46,5 +52,6 @@ class InternalTwoWaySyncAdapter( fun update() { folders = dataStorageManager.getInternalTwoWaySyncFolders(user) notifyDataSetChanged() + onUpdateListener.onUpdate(folders.size) } } diff --git a/app/src/main/res/layout/internal_two_way_sync_layout.xml b/app/src/main/res/layout/internal_two_way_sync_layout.xml index f7ca5f32cd67..43476a42db1c 100644 --- a/app/src/main/res/layout/internal_two_way_sync_layout.xml +++ b/app/src/main/res/layout/internal_two_way_sync_layout.xml @@ -5,7 +5,8 @@ ~ SPDX-FileCopyrightText: 2024 Tobias Kaminsky ~ SPDX-License-Identifier: AGPL-3.0-or-later --> - - + android:layout_height="wrap_content" /> - + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 062fdb0e244d..1568e68aabcc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,6 +115,25 @@ Dark Follow system Theme + Enable two way sync + Interval + + + %d hours + %d hour + %d hours + %d hours + %d hours + %d hours + + + %d minutes + %d minute + %d minutes + %d minutes + %d minutes + %d minutes + Try %1$s on your device! I want to invite you to use %1$s on your device.\nDownload here: %2$s