Skip to content

Commit deb1e42

Browse files
committed
Merge branch 'feature/thumbnail-cache-fix' of https://github.com/zerox80/android into feature/thumbnail-cache-fix
2 parents 4f01521 + 3a467b0 commit deb1e42

File tree

20 files changed

+913
-43
lines changed

20 files changed

+913
-43
lines changed

opencloudApp/src/androidTest/java/eu/opencloud/android/settings/security/SettingsSecurityFragmentTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ class SettingsSecurityFragmentTest {
376376

377377
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
378378
assertTrue(prefLockAccessDocumentProvider.isChecked)
379+
io.mockk.verify { securityViewModel.setPrefLockAccessDocumentProvider(true) }
379380
}
380381

381382
@Test
@@ -385,6 +386,7 @@ class SettingsSecurityFragmentTest {
385386
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
386387
onView(withText(R.string.prefs_lock_access_from_document_provider)).perform(click())
387388
assertFalse(prefLockAccessDocumentProvider.isChecked)
389+
io.mockk.verify { securityViewModel.setPrefLockAccessDocumentProvider(false) }
388390
}
389391

390392
@Test

opencloudApp/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
API >= 23; the app needs to handle this
2424
-->
2525
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
26+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
2627
<!--
2728
Notifications are off by default since API 33;
2829
See note in https://developer.android.com/develop/ui/views/notifications/notification-permission

opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,12 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
114114
private var oidcSupported = false
115115

116116
private lateinit var binding: AccountSetupBinding
117+
private var pendingAuthorizationIntent: Intent? = null
117118

118119
// For handling AbstractAccountAuthenticator responses
119120
private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
120121
private var resultBundle: Bundle? = null
121-
private var pendingAuthorizationIntent: Intent? = null
122+
122123

123124
override fun onCreate(savedInstanceState: Bundle?) {
124125
super.onCreate(savedInstanceState)
@@ -139,6 +140,23 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
139140
}
140141
}
141142

143+
// Log OAuth redirect details for debugging (especially Firefox issues)
144+
Timber.d("onCreate called with intent data: ${intent.data}, isTaskRoot: $isTaskRoot")
145+
146+
if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
147+
Timber.d("OAuth redirect detected with code or error parameter")
148+
if (!isTaskRoot) {
149+
Timber.d("Not task root, forwarding OAuth redirect to existing LoginActivity instance")
150+
val newIntent = Intent(this, LoginActivity::class.java)
151+
newIntent.data = intent.data
152+
newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
153+
startActivity(newIntent)
154+
finish()
155+
return
156+
}
157+
}
158+
159+
142160
checkPasscodeEnforced(this)
143161

144162
// Protection against screen recording
@@ -245,7 +263,6 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
245263
pendingAuthorizationIntent = null
246264
}
247265

248-
249266
}
250267

251268
private fun handleDeepLink() {
@@ -634,6 +651,11 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
634651
}
635652

636653
private fun handleGetAuthorizationCodeResponse(intent: Intent) {
654+
if (!::binding.isInitialized) {
655+
pendingAuthorizationIntent = intent
656+
return
657+
}
658+
637659
val authorizationCode = intent.data?.getQueryParameter("code")
638660
val state = intent.data?.getQueryParameter("state")
639661

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,8 @@ class FileDetailsFragment : FileFragment() {
192192
SynchronizeFileUseCase.SyncType.AlreadySynchronized -> {
193193
showMessageInSnackbar(getString(R.string.sync_file_nothing_to_do_msg))
194194
}
195-
is SynchronizeFileUseCase.SyncType.ConflictDetected -> {
196-
val showConflictActivityIntent = Intent(requireActivity(), ConflictsResolveActivity::class.java)
197-
showConflictActivityIntent.putExtra(ConflictsResolveActivity.EXTRA_FILE, file)
198-
startActivity(showConflictActivityIntent)
195+
is SynchronizeFileUseCase.SyncType.ConflictResolvedWithCopy -> {
196+
showMessageInSnackbar(getString(R.string.sync_conflict_resolved_with_copy))
199197
}
200198

201199
is SynchronizeFileUseCase.SyncType.DownloadEnqueued -> {

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

Lines changed: 72 additions & 1 deletion
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,12 @@ 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+
<<<<<<< HEAD
65+
private var prefPreferLocalOnConflict: CheckBoxPreference? = null
66+
=======
67+
>>>>>>> aa6841e77 (Sync Option + Download all files)
5968

6069
private val enablePasscodeLauncher =
6170
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@@ -132,6 +141,12 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
132141
}
133142
prefLockAccessDocumentProvider = findPreference(PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER)
134143
prefTouchesWithOtherVisibleWindows = findPreference(PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS)
144+
prefDownloadEverything = findPreference(PREFERENCE_DOWNLOAD_EVERYTHING)
145+
prefAutoSync = findPreference(PREFERENCE_AUTO_SYNC)
146+
<<<<<<< HEAD
147+
prefPreferLocalOnConflict = findPreference(PREFERENCE_PREFER_LOCAL_ON_CONFLICT)
148+
=======
149+
>>>>>>> aa6841e77 (Sync Option + Download all files)
135150

136151
prefPasscode?.isVisible = !securityViewModel.isSecurityEnforcedEnabled()
137152
prefPattern?.isVisible = !securityViewModel.isSecurityEnforcedEnabled()
@@ -196,7 +211,7 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
196211

197212
// Lock access from document provider
198213
prefLockAccessDocumentProvider?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
199-
securityViewModel.setPrefLockAccessDocumentProvider(true)
214+
securityViewModel.setPrefLockAccessDocumentProvider(newValue as Boolean)
200215
notifyDocumentsProviderRoots(requireContext())
201216
true
202217
}
@@ -222,6 +237,59 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
222237
}
223238
true
224239
}
240+
241+
// Download Everything Feature
242+
prefDownloadEverything?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
243+
if (newValue as Boolean) {
244+
activity?.let {
245+
AlertDialog.Builder(it)
246+
.setTitle(getString(R.string.download_everything_warning_title))
247+
.setMessage(getString(R.string.download_everything_warning_message))
248+
.setNegativeButton(getString(R.string.common_no), null)
249+
.setPositiveButton(getString(R.string.common_yes)) { _, _ ->
250+
securityViewModel.setDownloadEverything(true)
251+
prefDownloadEverything?.isChecked = true
252+
workManagerProvider.enqueueDownloadEverythingWorker()
253+
}
254+
.show()
255+
.avoidScreenshotsIfNeeded()
256+
}
257+
return@setOnPreferenceChangeListener false
258+
} else {
259+
securityViewModel.setDownloadEverything(false)
260+
workManagerProvider.cancelDownloadEverythingWorker()
261+
true
262+
}
263+
}
264+
265+
// Auto-Sync Feature
266+
prefAutoSync?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
267+
if (newValue as Boolean) {
268+
activity?.let {
269+
AlertDialog.Builder(it)
270+
.setTitle(getString(R.string.auto_sync_warning_title))
271+
.setMessage(getString(R.string.auto_sync_warning_message))
272+
.setNegativeButton(getString(R.string.common_no), null)
273+
.setPositiveButton(getString(R.string.common_yes)) { _, _ ->
274+
securityViewModel.setAutoSync(true)
275+
prefAutoSync?.isChecked = true
276+
workManagerProvider.enqueueLocalFileSyncWorker()
277+
}
278+
.show()
279+
.avoidScreenshotsIfNeeded()
280+
}
281+
return@setOnPreferenceChangeListener false
282+
} else {
283+
securityViewModel.setAutoSync(false)
284+
workManagerProvider.cancelLocalFileSyncWorker()
285+
true
286+
}
287+
}
288+
// Conflict Resolution Strategy
289+
prefPreferLocalOnConflict?.setOnPreferenceChangeListener { _: Preference?, newValue: Any ->
290+
securityViewModel.setPreferLocalOnConflict(newValue as Boolean)
291+
true
292+
}
225293
}
226294

227295
private fun enableBiometricAndLockApplication() {
@@ -246,5 +314,8 @@ class SettingsSecurityFragment : PreferenceFragmentCompat() {
246314
const val PREFERENCE_TOUCHES_WITH_OTHER_VISIBLE_WINDOWS = "touches_with_other_visible_windows"
247315
const val EXTRAS_LOCK_ENFORCED = "EXTRAS_LOCK_ENFORCED"
248316
const val PREFERENCE_LOCK_ATTEMPTS = "PrefLockAttempts"
317+
const val PREFERENCE_DOWNLOAD_EVERYTHING = "download_everything"
318+
const val PREFERENCE_AUTO_SYNC = "auto_sync_local_changes"
319+
const val PREFERENCE_PREFER_LOCAL_ON_CONFLICT = "prefer_local_on_conflict"
249320
}
250321
}

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
}

opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ import eu.opencloud.android.presentation.spaces.SpacesListFragment.Companion.BUN
110110
import eu.opencloud.android.presentation.spaces.SpacesListFragment.Companion.REQUEST_KEY_CLICK_SPACE
111111
import eu.opencloud.android.presentation.spaces.SpacesListViewModel
112112
import eu.opencloud.android.presentation.transfers.TransfersViewModel
113+
import eu.opencloud.android.presentation.settings.security.SettingsSecurityFragment
113114
import eu.opencloud.android.providers.WorkManagerProvider
114115
import eu.opencloud.android.syncadapter.FileSyncAdapter
115116
import eu.opencloud.android.ui.dialog.FileAlreadyExistsDialog
@@ -284,9 +285,28 @@ class FileDisplayActivity : FileActivity(),
284285

285286

286287
checkNotificationPermission()
288+
checkManageExternalStoragePermission()
287289
Timber.v("onCreate() end")
288290
}
289291

292+
private fun checkManageExternalStoragePermission() {
293+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
294+
if (!android.os.Environment.isExternalStorageManager()) {
295+
val builder = AlertDialog.Builder(this)
296+
builder.setTitle(getString(R.string.app_name))
297+
builder.setMessage("To save offline files, the app needs access to all files.")
298+
builder.setPositiveButton("Settings") { _, _ ->
299+
val intent = Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
300+
intent.addCategory("android.intent.category.DEFAULT")
301+
intent.data = Uri.parse("package:$packageName")
302+
startActivity(intent)
303+
}
304+
builder.setNegativeButton("Cancel", null)
305+
builder.show()
306+
}
307+
}
308+
}
309+
290310
private fun checkNotificationPermission() {
291311
// Ask for permission only in case it's api >= 33 and notifications are not granted.
292312
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
@@ -376,6 +396,16 @@ class FileDisplayActivity : FileActivity(),
376396
syncProfileOperation.syncUserProfile()
377397
val workManagerProvider = WorkManagerProvider(context = baseContext)
378398
workManagerProvider.enqueueAvailableOfflinePeriodicWorker()
399+
400+
// Enqueue Download Everything worker if enabled
401+
if (sharedPreferences.getBoolean(SettingsSecurityFragment.PREFERENCE_DOWNLOAD_EVERYTHING, false)) {
402+
workManagerProvider.enqueueDownloadEverythingWorker()
403+
}
404+
405+
// Enqueue Local File Sync worker if enabled
406+
if (sharedPreferences.getBoolean(SettingsSecurityFragment.PREFERENCE_AUTO_SYNC, false)) {
407+
workManagerProvider.enqueueLocalFileSyncWorker()
408+
}
379409
} else {
380410
file?.isFolder?.let { isFolder ->
381411
updateFragmentsVisibility(!isFolder)
@@ -1354,10 +1384,8 @@ class FileDisplayActivity : FileActivity(),
13541384
}
13551385
}
13561386

1357-
is SynchronizeFileUseCase.SyncType.ConflictDetected -> {
1358-
val showConflictActivityIntent = Intent(this, ConflictsResolveActivity::class.java)
1359-
showConflictActivityIntent.putExtra(ConflictsResolveActivity.EXTRA_FILE, file)
1360-
startActivity(showConflictActivityIntent)
1387+
is SynchronizeFileUseCase.SyncType.ConflictResolvedWithCopy -> {
1388+
showSnackMessage(getString(R.string.sync_conflict_resolved_with_copy))
13611389
}
13621390

13631391
is SynchronizeFileUseCase.SyncType.DownloadEnqueued -> {

0 commit comments

Comments
 (0)