From d66d9d32a40e99d89f16332427aa7f23ae3f5e1f Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 14 Mar 2024 13:31:18 +0100 Subject: [PATCH] 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. --- app/src/main/java/app/pachli/MainActivity.kt | 6 -- .../main/java/app/pachli/PachliApplication.kt | 11 +++ .../main/java/app/pachli/util/MediaUtils.kt | 35 +-------- .../pachli/worker/PruneCachedMediaWorker.kt | 78 +++++++++++++++++++ 4 files changed, 90 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/app/pachli/worker/PruneCachedMediaWorker.kt diff --git a/app/src/main/java/app/pachli/MainActivity.kt b/app/src/main/java/app/pachli/MainActivity.kt index 9fb452551..a20c94a13 100644 --- a/app/src/main/java/app/pachli/MainActivity.kt +++ b/app/src/main/java/app/pachli/MainActivity.kt @@ -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( diff --git a/app/src/main/java/app/pachli/PachliApplication.kt b/app/src/main/java/app/pachli/PachliApplication.kt index 884cbedf9..5172541ed 100644 --- a/app/src/main/java/app/pachli/PachliApplication.kt +++ b/app/src/main/java/app/pachli/PachliApplication.kt @@ -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(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) { diff --git a/app/src/main/java/app/pachli/util/MediaUtils.kt b/app/src/main/java/app/pachli/util/MediaUtils.kt index 808e44fb4..928ac34e2 100644 --- a/app/src/main/java/app/pachli/util/MediaUtils.kt +++ b/app/src/main/java/app/pachli/util/MediaUtils.kt @@ -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" } diff --git a/app/src/main/java/app/pachli/worker/PruneCachedMediaWorker.kt b/app/src/main/java/app/pachli/worker/PruneCachedMediaWorker.kt new file mode 100644 index 000000000..66182375c --- /dev/null +++ b/app/src/main/java/app/pachli/worker/PruneCachedMediaWorker.kt @@ -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 . + */ + +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" + } +}