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:
Nik Clayton 2024-03-14 13:31:18 +01:00 committed by GitHub
parent 6f8a3f20a9
commit d66d9d32a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 40 deletions

View File

@ -114,7 +114,6 @@ import app.pachli.pager.MainPagerAdapter
import app.pachli.updatecheck.UpdateCheck import app.pachli.updatecheck.UpdateCheck
import app.pachli.usecase.DeveloperToolsUseCase import app.pachli.usecase.DeveloperToolsUseCase
import app.pachli.usecase.LogoutUsecase import app.pachli.usecase.LogoutUsecase
import app.pachli.util.deleteStaleCachedMedia
import app.pachli.util.getDimension import app.pachli.util.getDimension
import app.pachli.util.unsafeLazy import app.pachli.util.unsafeLazy
import app.pachli.util.updateShortcut 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, "") selectedEmojiPack = sharedPreferencesRepository.getString(EMOJI_PREFERENCE, "")
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(

View File

@ -37,6 +37,7 @@ import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.util.LocaleManager import app.pachli.util.LocaleManager
import app.pachli.util.setAppNightMode import app.pachli.util.setAppNightMode
import app.pachli.worker.PruneCacheWorker import app.pachli.worker.PruneCacheWorker
import app.pachli.worker.PruneCachedMediaWorker
import app.pachli.worker.PruneLogEntryEntityWorker import app.pachli.worker.PruneLogEntryEntityWorker
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
@ -133,6 +134,16 @@ class PachliApplication : Application() {
ExistingPeriodicWorkPolicy.KEEP, ExistingPeriodicWorkPolicy.KEEP,
pruneLogEntryEntityWorker, 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) { private fun upgradeSharedPreferences(oldVersion: Int, newVersion: Int) {

View File

@ -24,22 +24,18 @@ import android.graphics.Matrix
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
/** /**
* Helper methods for obtaining and resizing media files * 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 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 { fun getTemporaryMediaFilename(extension: String): String {
return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.$extension" return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())}.$extension"
} }

View File

@ -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"
}
}