Skip to content

Commit 7ef9846

Browse files
committed
feat: complete download synchronization and conflict handling
1 parent a0c27e8 commit 7ef9846

18 files changed

Lines changed: 1026 additions & 118 deletions

File tree

opencloudApp/src/main/java/eu/opencloud/android/presentation/documentsprovider/DocumentsStorageProvider.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase
6666
import eu.opencloud.android.usecases.transfers.downloads.DownloadFileUseCase
6767
import eu.opencloud.android.usecases.transfers.uploads.UploadFilesFromSystemUseCase
6868
import eu.opencloud.android.utils.FileStorageUtils
69-
import eu.opencloud.android.utils.NotificationUtils
69+
7070
import kotlinx.coroutines.CoroutineScope
7171
import kotlinx.coroutines.Dispatchers
7272
import kotlinx.coroutines.launch
@@ -158,13 +158,9 @@ class DocumentsStorageProvider : DocumentsProvider() {
158158
)
159159
)
160160
Timber.d("Synced ${ocFile.remotePath} from ${ocFile.owner} with result: $result")
161-
if (result.getDataOrNull() is SynchronizeFileUseCase.SyncType.ConflictDetected) {
162-
context?.let {
163-
NotificationUtils.notifyConflict(
164-
fileInConflict = ocFile,
165-
context = it
166-
)
167-
}
161+
if (result.getDataOrNull() is SynchronizeFileUseCase.SyncType.ConflictResolvedWithCopy) {
162+
val conflictResult = result.getDataOrNull() as SynchronizeFileUseCase.SyncType.ConflictResolvedWithCopy
163+
Timber.i("File sync conflict auto-resolved. Conflicted copy at: ${conflictResult.conflictedCopyPath}")
168164
}
169165
}.start()
170166
}

opencloudApp/src/main/java/eu/opencloud/android/presentation/files/details/FileDetailsFragment.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import eu.opencloud.android.presentation.authentication.EXTRA_ACCOUNT
6161
import eu.opencloud.android.presentation.authentication.EXTRA_ACTION
6262
import eu.opencloud.android.presentation.authentication.LoginActivity
6363
import eu.opencloud.android.presentation.common.UIResult
64-
import eu.opencloud.android.presentation.conflicts.ConflictsResolveActivity
64+
6565
import eu.opencloud.android.presentation.files.details.FileDetailsViewModel.ActionsInDetailsView.NONE
6666
import eu.opencloud.android.presentation.files.details.FileDetailsViewModel.ActionsInDetailsView.SYNC
6767
import eu.opencloud.android.presentation.files.details.FileDetailsViewModel.ActionsInDetailsView.SYNC_AND_OPEN
@@ -190,10 +190,8 @@ class FileDetailsFragment : FileFragment() {
190190
SynchronizeFileUseCase.SyncType.AlreadySynchronized -> {
191191
showMessageInSnackbar(getString(R.string.sync_file_nothing_to_do_msg))
192192
}
193-
is SynchronizeFileUseCase.SyncType.ConflictDetected -> {
194-
val showConflictActivityIntent = Intent(requireActivity(), ConflictsResolveActivity::class.java)
195-
showConflictActivityIntent.putExtra(ConflictsResolveActivity.EXTRA_FILE, file)
196-
startActivity(showConflictActivityIntent)
193+
is SynchronizeFileUseCase.SyncType.ConflictResolvedWithCopy -> {
194+
showMessageInSnackbar(getString(R.string.sync_conflict_resolved_with_copy))
197195
}
198196

199197
is SynchronizeFileUseCase.SyncType.DownloadEnqueued -> {

opencloudApp/src/main/java/eu/opencloud/android/presentation/settings/security/SettingsSecurityFragment.kt

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ import eu.opencloud.android.presentation.security.biometric.BiometricManager
4242
import eu.opencloud.android.presentation.security.passcode.PassCodeActivity
4343
import eu.opencloud.android.presentation.security.pattern.PatternActivity
4444
import eu.opencloud.android.presentation.settings.SettingsFragment.Companion.removePreferenceFromScreen
45+
import eu.opencloud.android.providers.WorkManagerProvider
46+
import org.koin.android.ext.android.inject
4547
import org.koin.androidx.viewmodel.ext.android.viewModel
4648

4749
class SettingsSecurityFragment : PreferenceFragmentCompat() {
4850

4951
// ViewModel
5052
private val securityViewModel by viewModel<SettingsSecurityViewModel>()
53+
private val workManagerProvider: WorkManagerProvider by inject()
5154

5255
private var screenSecurity: PreferenceScreen? = null
5356
private var prefPasscode: CheckBoxPreference? = null
@@ -56,6 +59,9 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
5659
private var prefLockApplication: ListPreference? = null
5760
private var prefLockAccessDocumentProvider: CheckBoxPreference? = null
5861
private var prefTouchesWithOtherVisibleWindows: CheckBoxPreference? = null
62+
private var prefDownloadEverything: CheckBoxPreference? = null
63+
private var prefAutoSync: CheckBoxPreference? = null
64+
private var prefPreferLocalOnConflict: CheckBoxPreference? = null
5965

6066
private val enablePasscodeLauncher =
6167
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -111,6 +117,16 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
111117

112118
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
113119
setPreferencesFromResource(R.xml.settings_security, rootKey)
120+
initializePreferences(rootKey)
121+
configureLockPreferences()
122+
configureBiometricPreference()
123+
configureSecurityPreferences()
124+
configureDownloadAndSyncPreferences()
125+
}
126+
127+
128+
@Suppress("UnusedParameter")
129+
private fun initializePreferences(rootKey: String?) {
114130
screenSecurity = findPreference(SCREEN_SECURITY)
115131
prefPasscode = findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE)
116132
prefPattern = findPreference(PatternActivity.PREFERENCE_SET_PATTERN)
@@ -132,10 +148,15 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
132148
}
133149
prefLockAccessDocumentProvider = findPreference(PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER)
134150
prefTouchesWithOtherVisibleWindows = findPreference(PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS)
151+
prefDownloadEverything = findPreference(PREFERENCE_DOWNLOAD_EVERYTHING)
152+
prefAutoSync = findPreference(PREFERENCE_AUTO_SYNC)
153+
prefPreferLocalOnConflict = findPreference(PREFERENCE_PREFER_LOCAL_ON_CONFLICT)
135154

136155
prefPasscode?.isVisible = !securityViewModel.isSecurityEnforcedEnabled()
137156
prefPattern?.isVisible = !securityViewModel.isSecurityEnforcedEnabled()
157+
}
138158

159+
private fun configureLockPreferences() {
139160
// Passcode lock
140161
prefPasscode?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
141162
if (securityViewModel.isPatternSet()) {
@@ -169,8 +190,9 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
169190
}
170191
false
171192
}
193+
}
172194

173-
// Biometric lock
195+
private fun configureBiometricPreference() {
174196
if (prefBiometric != null) {
175197
if (!BiometricManager.isHardwareDetected()) { // Biometric not supported
176198
screenSecurity?.removePreferenceFromScreen(prefBiometric)
@@ -192,8 +214,12 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
192214
}
193215

194216
// Lock application
195-
if (prefPasscode?.isChecked == false && prefPattern?.isChecked == false) { prefLockApplication?.isEnabled = false }
217+
if (prefPasscode?.isChecked == false && prefPattern?.isChecked == false) {
218+
prefLockApplication?.isEnabled = false
219+
}
220+
}
196221

222+
private fun configureSecurityPreferences() {
197223
// Lock access from document provider
198224
prefLockAccessDocumentProvider?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
199225
securityViewModel.setPrefLockAccessDocumentProvider(true)
@@ -224,6 +250,62 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
224250
}
225251
}
226252

253+
private fun configureDownloadAndSyncPreferences() {
254+
// Download Everything Feature
255+
prefDownloadEverything?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
256+
if (newValue as Boolean) {
257+
activity?.let {
258+
AlertDialog.Builder(it)
259+
.setTitle(getString(R.string.download_everything_warning_title))
260+
.setMessage(getString(R.string.download_everything_warning_message))
261+
.setNegativeButton(getString(R.string.common_no), null)
262+
.setPositiveButton(getString(R.string.common_yes)) { _, _ ->
263+
securityViewModel.setDownloadEverything(true)
264+
prefDownloadEverything?.isChecked = true
265+
workManagerProvider.enqueueDownloadEverythingWorker()
266+
}
267+
.show()
268+
.avoidScreenshotsIfNeeded()
269+
}
270+
return@setOnPreferenceChangeListener false
271+
} else {
272+
securityViewModel.setDownloadEverything(false)
273+
workManagerProvider.cancelDownloadEverythingWorker()
274+
true
275+
}
276+
}
277+
278+
// Auto-Sync Feature
279+
prefAutoSync?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
280+
if (newValue as Boolean) {
281+
activity?.let {
282+
AlertDialog.Builder(it)
283+
.setTitle(getString(R.string.auto_sync_warning_title))
284+
.setMessage(getString(R.string.auto_sync_warning_message))
285+
.setNegativeButton(getString(R.string.common_no), null)
286+
.setPositiveButton(getString(R.string.common_yes)) { _, _ ->
287+
securityViewModel.setAutoSync(true)
288+
prefAutoSync?.isChecked = true
289+
workManagerProvider.enqueueLocalFileSyncWorker()
290+
}
291+
.show()
292+
.avoidScreenshotsIfNeeded()
293+
}
294+
return@setOnPreferenceChangeListener false
295+
} else {
296+
securityViewModel.setAutoSync(false)
297+
workManagerProvider.cancelLocalFileSyncWorker()
298+
true
299+
}
300+
}
301+
302+
// Conflict Resolution Strategy
303+
prefPreferLocalOnConflict?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
304+
securityViewModel.setPreferLocalOnConflict(newValue as Boolean)
305+
true
306+
}
307+
}
308+
227309
private fun enableBiometricAndLockApplication() {
228310
prefBiometric?.apply {
229311
isEnabled = true
@@ -246,5 +328,8 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
246328
const val PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS = "touches_with_other_visible_windows"
247329
const val EXTRAS_LOCK_ENFORCED = "EXTRAS_LOCK_ENFORCED"
248330
const val PREFERENCE_LOCK_ATTEMPTS = "PrefLockAttempts"
331+
const val PREFERENCE_DOWNLOAD_EVERYTHING = "download_everything"
332+
const val PREFERENCE_AUTO_SYNC = "auto_sync_local_changes"
333+
const val PREFERENCE_PREFER_LOCAL_ON_CONFLICT = "prefer_local_on_conflict"
249334
}
250335
}

opencloudApp/src/main/java/eu/opencloud/android/presentation/settings/security/SettingsSecurityViewModel.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,25 @@ class SettingsSecurityViewModel(
6363
integerKey = R.integer.lock_delay_enforced
6464
)
6565
) != LockTimeout.DISABLED
66+
67+
// Download Everything Feature
68+
fun isDownloadEverythingEnabled(): Boolean =
69+
preferencesProvider.getBoolean(SettingsSecurityFragment.PREFERENCE_DOWNLOAD_EVERYTHING, false)
70+
71+
fun setDownloadEverything(enabled: Boolean) =
72+
preferencesProvider.putBoolean(SettingsSecurityFragment.PREFERENCE_DOWNLOAD_EVERYTHING, enabled)
73+
74+
// Auto-Sync Feature
75+
fun isAutoSyncEnabled(): Boolean =
76+
preferencesProvider.getBoolean(SettingsSecurityFragment.PREFERENCE_AUTO_SYNC, false)
77+
78+
fun setAutoSync(enabled: Boolean) =
79+
preferencesProvider.putBoolean(SettingsSecurityFragment.PREFERENCE_AUTO_SYNC, enabled)
80+
81+
// Conflict Resolution Strategy
82+
fun isPreferLocalOnConflictEnabled(): Boolean =
83+
preferencesProvider.getBoolean(SettingsSecurityFragment.PREFERENCE_PREFER_LOCAL_ON_CONFLICT, false)
84+
85+
fun setPreferLocalOnConflict(enabled: Boolean) =
86+
preferencesProvider.putBoolean(SettingsSecurityFragment.PREFERENCE_PREFER_LOCAL_ON_CONFLICT, enabled)
6687
}

opencloudApp/src/main/java/eu/opencloud/android/providers/WorkManagerProvider.kt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import eu.opencloud.android.workers.AccountDiscoveryWorker
3636
import eu.opencloud.android.workers.AvailableOfflinePeriodicWorker
3737
import eu.opencloud.android.workers.AvailableOfflinePeriodicWorker.Companion.AVAILABLE_OFFLINE_PERIODIC_WORKER
3838
import eu.opencloud.android.workers.AutomaticUploadsWorker
39+
import eu.opencloud.android.workers.DownloadEverythingWorker
40+
import eu.opencloud.android.workers.LocalFileSyncWorker
3941
import eu.opencloud.android.workers.OldLogsCollectorWorker
4042
import eu.opencloud.android.workers.RemoveLocallyFilesWithLastUsageOlderThanGivenTimeWorker
4143
import eu.opencloud.android.workers.UploadFileFromContentUriWorker
@@ -129,4 +131,60 @@ class WorkManagerProvider(
129131

130132
fun cancelAllWorkByTag(tag: String) = WorkManager.getInstance(context).cancelAllWorkByTag(tag)
131133

134+
// Download Everything Feature
135+
fun enqueueDownloadEverythingWorker() {
136+
val constraintsRequired = Constraints.Builder()
137+
.setRequiredNetworkType(NetworkType.CONNECTED)
138+
.setRequiresBatteryNotLow(true)
139+
.setRequiresStorageNotLow(true)
140+
.build()
141+
142+
val downloadEverythingWorker = PeriodicWorkRequestBuilder<DownloadEverythingWorker>(
143+
repeatInterval = DownloadEverythingWorker.repeatInterval,
144+
repeatIntervalTimeUnit = DownloadEverythingWorker.repeatIntervalTimeUnit
145+
)
146+
.addTag(DownloadEverythingWorker.DOWNLOAD_EVERYTHING_WORKER)
147+
.setConstraints(constraintsRequired)
148+
.build()
149+
150+
WorkManager.getInstance(context)
151+
.enqueueUniquePeriodicWork(
152+
DownloadEverythingWorker.DOWNLOAD_EVERYTHING_WORKER,
153+
ExistingPeriodicWorkPolicy.KEEP,
154+
downloadEverythingWorker
155+
)
156+
}
157+
158+
fun cancelDownloadEverythingWorker() {
159+
WorkManager.getInstance(context)
160+
.cancelUniqueWork(DownloadEverythingWorker.DOWNLOAD_EVERYTHING_WORKER)
161+
}
162+
163+
// Local File Sync (Auto-Sync) Feature
164+
fun enqueueLocalFileSyncWorker() {
165+
val constraintsRequired = Constraints.Builder()
166+
.setRequiredNetworkType(NetworkType.CONNECTED)
167+
.build()
168+
169+
val localFileSyncWorker = PeriodicWorkRequestBuilder<LocalFileSyncWorker>(
170+
repeatInterval = LocalFileSyncWorker.repeatInterval,
171+
repeatIntervalTimeUnit = LocalFileSyncWorker.repeatIntervalTimeUnit
172+
)
173+
.addTag(LocalFileSyncWorker.LOCAL_FILE_SYNC_WORKER)
174+
.setConstraints(constraintsRequired)
175+
.build()
176+
177+
WorkManager.getInstance(context)
178+
.enqueueUniquePeriodicWork(
179+
LocalFileSyncWorker.LOCAL_FILE_SYNC_WORKER,
180+
ExistingPeriodicWorkPolicy.KEEP,
181+
localFileSyncWorker
182+
)
183+
}
184+
185+
fun cancelLocalFileSyncWorker() {
186+
WorkManager.getInstance(context)
187+
.cancelUniqueWork(LocalFileSyncWorker.LOCAL_FILE_SYNC_WORKER)
188+
}
189+
132190
}

0 commit comments

Comments
 (0)