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.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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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