refactor: Prune cached media using a worker (#529)
Previous code would prune any cached media every time `MainActivity` was created, causing extra IO just as the user is trying to use the app. Re-implement as a WorkManager worker so it can run when the device is idle, without interfering with the responsiveness of the device.
This commit is contained in:
parent
6f8a3f20a9
commit
d66d9d32a4
|
@ -114,7 +114,6 @@ import app.pachli.pager.MainPagerAdapter
|
|||
import app.pachli.updatecheck.UpdateCheck
|
||||
import app.pachli.usecase.DeveloperToolsUseCase
|
||||
import app.pachli.usecase.LogoutUsecase
|
||||
import app.pachli.util.deleteStaleCachedMedia
|
||||
import app.pachli.util.getDimension
|
||||
import app.pachli.util.unsafeLazy
|
||||
import app.pachli.util.updateShortcut
|
||||
|
@ -383,11 +382,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
|
|||
}
|
||||
}
|
||||
|
||||
externalScope.launch {
|
||||
// Flush old media that was cached for sharing
|
||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Pachli"))
|
||||
}
|
||||
|
||||
selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
|
||||
|
||||
onBackPressedDispatcher.addCallback(
|
||||
|
|
|
@ -37,6 +37,7 @@ import app.pachli.core.preferences.SharedPreferencesRepository
|
|||
import app.pachli.util.LocaleManager
|
||||
import app.pachli.util.setAppNightMode
|
||||
import app.pachli.worker.PruneCacheWorker
|
||||
import app.pachli.worker.PruneCachedMediaWorker
|
||||
import app.pachli.worker.PruneLogEntryEntityWorker
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
||||
|
@ -133,6 +134,16 @@ class PachliApplication : Application() {
|
|||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
pruneLogEntryEntityWorker,
|
||||
)
|
||||
|
||||
// Delete old cached media every ~ 12 hours when the device is idle
|
||||
val pruneCachedMediaWorker = PeriodicWorkRequestBuilder<PruneCachedMediaWorker>(12, TimeUnit.HOURS)
|
||||
.setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build())
|
||||
.build()
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
PruneCachedMediaWorker.PERIODIC_WORK_TAG,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
pruneCachedMediaWorker,
|
||||
)
|
||||
}
|
||||
|
||||
private fun upgradeSharedPreferences(oldVersion: Int, newVersion: Int) {
|
||||
|
|
|
@ -24,22 +24,18 @@ import android.graphics.Matrix
|
|||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Helper methods for obtaining and resizing media files
|
||||
*/
|
||||
|
||||
private const val MEDIA_TEMP_PREFIX = "Pachli_Share_Media"
|
||||
const val MEDIA_TEMP_PREFIX = "Pachli_Share_Media"
|
||||
const val MEDIA_SIZE_UNKNOWN = -1L
|
||||
|
||||
/**
|
||||
|
@ -167,35 +163,6 @@ fun getImageOrientation(uri: Uri, contentResolver: ContentResolver): Int {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun deleteStaleCachedMedia(mediaDirectory: File?) = withContext(Dispatchers.IO) {
|
||||
if (mediaDirectory == null || !mediaDirectory.exists()) {
|
||||
// Nothing to do
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val twentyfourHoursAgo = Calendar.getInstance()
|
||||
twentyfourHoursAgo.add(Calendar.HOUR, -24)
|
||||
val unixTime = twentyfourHoursAgo.timeInMillis
|
||||
|
||||
val files = mediaDirectory.listFiles { file ->
|
||||
unixTime > file.lastModified() && file.name.contains(
|
||||
MEDIA_TEMP_PREFIX,
|
||||
)
|
||||
}
|
||||
if (files == null || files.isEmpty()) {
|
||||
// Nothing to do
|
||||
return@withContext
|
||||
}
|
||||
|
||||
for (file in files) {
|
||||
try {
|
||||
file.delete()
|
||||
} catch (se: SecurityException) {
|
||||
Timber.e("Error removing stale cached media")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTemporaryMediaFilename(extension: String): String {
|
||||
return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.$extension"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2024 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.worker
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import app.pachli.R
|
||||
import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE
|
||||
import app.pachli.components.notifications.createWorkerNotification
|
||||
import app.pachli.util.MEDIA_TEMP_PREFIX
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import timber.log.Timber
|
||||
|
||||
/** Prune old media that was cached for sharing */
|
||||
@HiltWorker
|
||||
class PruneCachedMediaWorker @AssistedInject constructor(
|
||||
@Assisted val appContext: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
val notification: Notification = createWorkerNotification(applicationContext, R.string.notification_prune_cache)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val mediaDirectory = appContext.getExternalFilesDir("Pachli")
|
||||
if (mediaDirectory == null) {
|
||||
Timber.d("Skipping prune, shared storage is not available")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (!mediaDirectory.exists()) {
|
||||
Timber.d("Skipping prune, media directory does not exist: %s", mediaDirectory.absolutePath)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
val cutoffInMs = now.minusSeconds(OLDEST_ENTRY.inWholeSeconds).toEpochMilli()
|
||||
|
||||
mediaDirectory.listFiles { file ->
|
||||
file.lastModified() < cutoffInMs && file.name.startsWith(MEDIA_TEMP_PREFIX)
|
||||
}?.forEach { file ->
|
||||
try {
|
||||
file.delete()
|
||||
} catch (se: SecurityException) {
|
||||
Timber.e(se, "Error removing %s", file.name)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_PRUNE_CACHE, notification)
|
||||
|
||||
companion object {
|
||||
private val OLDEST_ENTRY = 24.hours
|
||||
const val PERIODIC_WORK_TAG = "PruneCachedMediaWorker"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue