Skip to content

Commit cc52053

Browse files
committed
Picture Upload: Make UX more instant
..by running uploads 10sec after app comes to foreground For #78
1 parent f6280c0 commit cc52053

3 files changed

Lines changed: 61 additions & 4 deletions

File tree

opencloudApp/src/main/java/eu/opencloud/android/MainApp.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import eu.opencloud.android.presentation.settings.logging.SettingsLogsFragment.C
6767
import eu.opencloud.android.providers.CoroutinesDispatcherProvider
6868
import eu.opencloud.android.providers.LogsProvider
6969
import eu.opencloud.android.providers.MdmProvider
70+
import eu.opencloud.android.providers.WorkManagerProvider
7071
import eu.opencloud.android.ui.activity.FileDisplayActivity
7172
import eu.opencloud.android.ui.activity.FileDisplayActivity.Companion.PREFERENCE_CLEAR_DATA_ALREADY_TRIGGERED
7273
import eu.opencloud.android.ui.activity.WhatsNewActivity
@@ -78,6 +79,8 @@ import eu.opencloud.android.utils.FILE_SYNC_NOTIFICATION_CHANNEL_ID
7879
import eu.opencloud.android.utils.MEDIA_SERVICE_NOTIFICATION_CHANNEL_ID
7980
import eu.opencloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID
8081
import kotlinx.coroutines.CoroutineScope
82+
import kotlinx.coroutines.Dispatchers
83+
import kotlinx.coroutines.launch
8184
import kotlinx.coroutines.runBlocking
8285
import kotlinx.coroutines.withContext
8386
import org.koin.android.ext.android.inject
@@ -117,10 +120,11 @@ class MainApp : Application() {
117120

118121
SingleSessionManager.setUserAgent(userAgent)
119122

120-
121-
122123
initDependencyInjection()
123124

125+
val workManagerProvider: WorkManagerProvider by inject()
126+
var startedActivities = 0
127+
124128
// register global protection with pass code, pattern lock and biometric lock
125129
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
126130
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
@@ -219,6 +223,16 @@ class MainApp : Application() {
219223

220224
override fun onActivityStarted(activity: Activity) {
221225
Timber.v("${activity.javaClass.simpleName} onStart() starting")
226+
if (startedActivities == 0) {
227+
// App entered foreground — ensure the periodic worker is registered
228+
// (recovers if the chain was dropped) and trigger an immediate scan
229+
// so the user doesn't have to wait up to 15 min.
230+
CoroutineScope(Dispatchers.IO).launch {
231+
workManagerProvider.enqueueAutomaticUploadsWorker()
232+
workManagerProvider.enqueueImmediateAutomaticUploadsWorker()
233+
}
234+
}
235+
startedActivities++
222236
PassCodeManager.onActivityStarted(activity)
223237
PatternManager.onActivityStarted(activity)
224238
BiometricManager.onActivityStarted(activity)
@@ -233,6 +247,7 @@ class MainApp : Application() {
233247
}
234248

235249
override fun onActivityStopped(activity: Activity) {
250+
startedActivities--
236251
Timber.v("${activity.javaClass.simpleName} onStop() ending")
237252
PassCodeManager.onActivityStopped(activity)
238253
PatternManager.onActivityStopped(activity)

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import android.content.Context
2525
import androidx.lifecycle.LiveData
2626
import androidx.work.Constraints
2727
import androidx.work.ExistingPeriodicWorkPolicy
28+
import androidx.work.ExistingWorkPolicy
2829
import androidx.work.NetworkType
2930
import androidx.work.OneTimeWorkRequestBuilder
3031
import androidx.work.PeriodicWorkRequestBuilder
@@ -40,6 +41,7 @@ import eu.opencloud.android.workers.OldLogsCollectorWorker
4041
import eu.opencloud.android.workers.RemoveLocallyFilesWithLastUsageOlderThanGivenTimeWorker
4142
import eu.opencloud.android.workers.UploadFileFromContentUriWorker
4243
import eu.opencloud.android.workers.UploadFileFromFileSystemWorker
44+
import timber.log.Timber
4345

4446
class WorkManagerProvider(
4547
val context: Context
@@ -55,6 +57,36 @@ class WorkManagerProvider(
5557
.enqueueUniquePeriodicWork(AutomaticUploadsWorker.AUTOMATIC_UPLOADS_WORKER, ExistingPeriodicWorkPolicy.KEEP, automaticUploadsWorker)
5658
}
5759

60+
/**
61+
* Trigger an immediate one-time upload scan, e.g. when the app enters foreground.
62+
* Skips if either the periodic or immediate worker is already running to avoid
63+
* concurrent scans or redundant enqueues on rapid foreground/background switches.
64+
*/
65+
fun enqueueImmediateAutomaticUploadsWorker() {
66+
val wm = WorkManager.getInstance(context)
67+
68+
val periodicRunning = wm.getWorkInfosForUniqueWork(AutomaticUploadsWorker.AUTOMATIC_UPLOADS_WORKER)
69+
.get().any { it.state == WorkInfo.State.RUNNING }
70+
val immediateRunning = wm.getWorkInfosForUniqueWork(AutomaticUploadsWorker.IMMEDIATE_UPLOADS_WORKER)
71+
.get().any { it.state == WorkInfo.State.RUNNING }
72+
73+
if (periodicRunning || immediateRunning) {
74+
Timber.d("Automatic uploads worker already running, skipping immediate run")
75+
return
76+
}
77+
78+
val immediateWorker = OneTimeWorkRequestBuilder<AutomaticUploadsWorker>()
79+
.addTag(AutomaticUploadsWorker.IMMEDIATE_UPLOADS_WORKER)
80+
.setInitialDelay(AutomaticUploadsWorker.WRITE_SAFETY_BUFFER_MS, java.util.concurrent.TimeUnit.MILLISECONDS)
81+
.build()
82+
83+
wm.enqueueUniqueWork(
84+
AutomaticUploadsWorker.IMMEDIATE_UPLOADS_WORKER,
85+
ExistingWorkPolicy.KEEP,
86+
immediateWorker
87+
)
88+
}
89+
5890
fun enqueueOldLogsCollectorWorker() {
5991
val constraintsRequired = Constraints.Builder().setRequiredNetworkType(NetworkType.NOT_REQUIRED).build()
6092

opencloudApp/src/main/java/eu/opencloud/android/workers/AutomaticUploadsWorker.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ class AutomaticUploadsWorker(
174174
chargingOnly = folderBackUpConfiguration.chargingOnly
175175
)
176176
}
177-
updateTimestamp(folderBackUpConfiguration, syncType, currentTimestamp)
177+
// Save safeTimestamp (not currentTimestamp) so that files skipped by the
178+
// write-safety buffer are re-evaluated on the next run instead of being lost.
179+
val safeTimestamp = currentTimestamp - WRITE_SAFETY_BUFFER_MS
180+
updateTimestamp(folderBackUpConfiguration, syncType, safeTimestamp)
178181
}
179182

180183
private fun showNotification(
@@ -255,10 +258,15 @@ class AutomaticUploadsWorker(
255258
val documentTree = DocumentFile.fromTreeUri(applicationContext, sourceUri)
256259
val arrayOfLocalFiles = documentTree?.listFiles() ?: arrayOf()
257260

261+
// Exclude files modified within the last few seconds. Camera apps may still be
262+
// writing the file (not all apps use atomic rename), so picking it up too early
263+
// can result in uploading a truncated or 0-byte JPEG.
264+
val safeTimestamp = currentTimestamp - WRITE_SAFETY_BUFFER_MS
265+
258266
val filteredList: List<DocumentFile> = arrayOfLocalFiles
259267
.sortedBy { it.lastModified() }
260268
.filter { it.lastModified() >= lastSyncTimestamp }
261-
.filter { it.lastModified() < currentTimestamp }
269+
.filter { it.lastModified() < safeTimestamp }
262270
.filter { MimetypeIconUtil.getBestMimeTypeByFilename(it.name).startsWith(syncType.prefixForType) }
263271

264272
Timber.i("Last sync ${syncType.name}: ${Date(lastSyncTimestamp)}")
@@ -321,9 +329,11 @@ class AutomaticUploadsWorker(
321329

322330
companion object {
323331
const val AUTOMATIC_UPLOADS_WORKER = "AUTOMATIC_UPLOADS_WORKER"
332+
const val IMMEDIATE_UPLOADS_WORKER = "IMMEDIATE_AUTOMATIC_UPLOADS_WORKER"
324333
const val repeatInterval: Long = 15L
325334
val repeatIntervalTimeUnit: TimeUnit = TimeUnit.MINUTES
326335
private const val pictureUploadsNotificationId = 101
327336
private const val videoUploadsNotificationId = 102
337+
const val WRITE_SAFETY_BUFFER_MS = 10_000L
328338
}
329339
}

0 commit comments

Comments
 (0)