Use WorkManager to clean the cache.

This commit is contained in:
Isira Seneviratne 2020-12-26 11:03:19 +05:30
parent df66d127c2
commit 7c81b1ad15
9 changed files with 151 additions and 161 deletions

View File

@ -1,11 +1,13 @@
package org.fdroid.fdroid;
import android.app.Instrumentation;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.compat.FileCompatTest;
import org.fdroid.fdroid.work.CleanCacheWorker;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -16,9 +18,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class CleanCacheServiceTest {
public static final String TAG = "CleanCacheServiceTest";
public class CleanCacheWorkerTest {
public static final String TAG = "CleanCacheWorkerTest";
@Test
public void testClearOldFiles() throws IOException, InterruptedException {
@ -48,18 +49,18 @@ public class CleanCacheServiceTest {
assertTrue(second.createNewFile());
assertTrue(second.exists());
CleanCacheService.clearOldFiles(dir, 3000); // check all in dir
CleanCacheWorker.clearOldFiles(dir, 3000); // check all in dir
assertFalse(first.exists());
assertTrue(second.exists());
Thread.sleep(7000);
CleanCacheService.clearOldFiles(second, 3000); // check just second file
CleanCacheWorker.clearOldFiles(second, 3000); // check just second file
assertFalse(first.exists());
assertFalse(second.exists());
// make sure it doesn't freak out on a non-existent file
File nonexistent = new File(tempDir, "nonexistent");
CleanCacheService.clearOldFiles(nonexistent, 1);
CleanCacheService.clearOldFiles(null, 1);
CleanCacheWorker.clearOldFiles(nonexistent, 1);
CleanCacheWorker.clearOldFiles(null, 1);
}
}

View File

@ -241,14 +241,6 @@
android:name=".installer.InstallerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".CleanCacheService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".CleanCacheJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<service
android:name=".DeleteCacheService"
android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -1,22 +0,0 @@
package org.fdroid.fdroid;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.app.job.JobService;
/**
* Shim to run {@link CleanCacheService} with {@link android.app.job.JobScheduler}
*/
@TargetApi(21)
public class CleanCacheJobService extends JobService {
@Override
public boolean onStartJob(JobParameters jobParameters) {
CleanCacheService.start(this);
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return true;
}
}

View File

@ -1,30 +0,0 @@
package org.fdroid.fdroid;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import androidx.annotation.RequiresApi;
import java.io.File;
/**
* Helper class to prevent {@link VerifyError}s from occurring in {@link CleanCacheService#clearOldFiles(File, long)}
* due to the fact that {@link Os} was only introduced in API 21.
*/
@RequiresApi(21)
class CleanCacheService21 {
static void deleteIfOld(File file, long olderThan) {
if (file == null || !file.exists()) {
return;
}
try {
StructStat stat = Os.lstat(file.getAbsolutePath());
if ((stat.st_atime * 1000L) < olderThan) {
file.delete();
}
} catch (ErrnoException e) {
e.printStackTrace();
}
}
}

View File

@ -40,16 +40,17 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.StrictMode;
import androidx.annotation.Nullable;
import androidx.collection.LongSparseArray;
import androidx.core.content.ContextCompat;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.collection.LongSparseArray;
import androidx.core.content.ContextCompat;
import com.nostra13.universalimageloader.cache.disc.DiskCache;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiskCache;
import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache;
@ -57,8 +58,7 @@ import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import info.guardianproject.netcipher.NetCipher;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import org.acra.ACRA;
import org.acra.ReportField;
import org.acra.ReportingInteractionMode;
@ -73,20 +73,26 @@ import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.installer.ApkFileProvider;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.nearby.SDCardScannerService;
import org.fdroid.fdroid.nearby.WifiStateChangeService;
import org.fdroid.fdroid.net.ConnectivityMonitorService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.HttpDownloader;
import org.fdroid.fdroid.net.ImageLoaderForUIL;
import org.fdroid.fdroid.nearby.WifiStateChangeService;
import org.fdroid.fdroid.panic.HidingManager;
import org.fdroid.fdroid.work.CleanCacheWorker;
import org.fdroid.fdroid.work.WorkUtils;
import javax.microedition.khronos.opengles.GL10;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.Security;
import java.util.List;
import java.util.UUID;
import javax.microedition.khronos.opengles.GL10;
import info.guardianproject.netcipher.NetCipher;
import info.guardianproject.netcipher.proxy.OrbotHelper;
@ReportsCrashes(mailTo = BuildConfig.ACRA_REPORT_EMAIL,
mode = ReportingInteractionMode.DIALOG,
reportDialogClass = org.fdroid.fdroid.acra.CrashReportActivity.class,
@ -421,7 +427,7 @@ public class FDroidApp extends Application {
}
});
CleanCacheService.schedule(this);
WorkUtils.scheduleCleanCache(this);
notificationHelper = new NotificationHelper(getApplicationContext());
@ -551,7 +557,7 @@ public class FDroidApp extends Application {
* problems that arise from executing the code twice. This happens due to the `android:process`
* statement in AndroidManifest.xml causes another process to be created to run
* {@link org.fdroid.fdroid.acra.CrashReportActivity}. This was causing lots of things to be
* started/run twice including {@link CleanCacheService} and {@link WifiStateChangeService}.
* started/run twice including {@link CleanCacheWorker} and {@link WifiStateChangeService}.
* <p>
* Note that it is not perfect, because some devices seem to not provide a list of running app
* processes when asked. In such situations, F-Droid may regress to the behaviour where some

View File

@ -3,9 +3,10 @@ package org.fdroid.fdroid.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.fdroid.fdroid.CleanCacheService;
import org.fdroid.fdroid.DeleteCacheService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.work.WorkUtils;
public class DeviceStorageReceiver extends BroadcastReceiver {
@Override
@ -18,7 +19,7 @@ public class DeviceStorageReceiver extends BroadcastReceiver {
int percentageFree = Utils.getPercent(Utils.getImageCacheDirAvailableMemory(context),
Utils.getImageCacheDirTotalMemory(context));
if (percentageFree > 2) {
CleanCacheService.start(context);
WorkUtils.scheduleCleanCache(context);
} else {
DeleteCacheService.deleteAll(context);
}

View File

@ -48,7 +48,6 @@ import androidx.preference.SwitchPreference;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import org.fdroid.fdroid.CleanCacheService;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Languages;
import org.fdroid.fdroid.Preferences;
@ -58,6 +57,7 @@ import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
import org.fdroid.fdroid.work.WorkUtils;
import info.guardianproject.netcipher.proxy.OrbotHelper;
@ -304,7 +304,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
entrySummary(key);
if (changing
&& currentKeepCacheTime != Preferences.get().getKeepCacheTime()) {
CleanCacheService.schedule(getActivity());
WorkUtils.scheduleCleanCache(requireContext());
}
break;

View File

@ -1,90 +1,45 @@
package org.fdroid.fdroid;
package org.fdroid.fdroid.work;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Process;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.core.content.ContextCompat;
import androidx.annotation.RequiresApi;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.installer.ApkCache;
import java.io.File;
import java.util.concurrent.TimeUnit;
/**
* Handles cleaning up caches files that are not going to be used, and do not
* block the operation of the app itself. For things that must happen before
* F-Droid starts normal operation, that should go into
* {@link FDroidApp#onCreate()}.
* <p>
* These files should only be deleted when they are at least an hour-ish old,
* in case they are actively in use while {@code CleanCacheService} is running.
* {@link #clearOldFiles(File, long)} checks the file age using access time from
* {@link android.system.StructStat#st_atime} on {@link android.os.Build.VERSION_CODES#LOLLIPOP}
* and newer. On older Android, last modified time from {@link File#lastModified()}
* is used.
*/
public class CleanCacheService extends JobIntentService {
public static final String TAG = "CleanCacheService";
public class CleanCacheWorker extends Worker {
private static final String TAG = CleanCacheWorker.class.getSimpleName();
private static final int JOB_ID = 0x982374;
/**
* Schedule or cancel this service to update the app index, according to the
* current preferences. Should be called a) at boot, b) if the preference
* is changed, or c) on startup, in case we get upgraded.
*/
public static void schedule(Context context) {
long keepTime = Preferences.get().getKeepCacheTime();
long interval = TimeUnit.DAYS.toMillis(1);
if (keepTime < interval) {
interval = keepTime;
}
if (Build.VERSION.SDK_INT < 21) {
Intent intent = new Intent(context, CleanCacheService.class);
PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pending);
alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 5000, interval, pending);
} else {
Utils.debugLog(TAG, "Using android-21 JobScheduler for updates");
JobScheduler jobScheduler = ContextCompat.getSystemService(context, JobScheduler.class);
ComponentName componentName = new ComponentName(context, CleanCacheJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, componentName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
.setPeriodic(interval);
if (Build.VERSION.SDK_INT >= 26) {
builder.setRequiresBatteryNotLow(true);
}
jobScheduler.schedule(builder.build());
}
}
public static void start(Context context) {
enqueueWork(context, CleanCacheService.class, JOB_ID, new Intent(context, CleanCacheService.class));
public CleanCacheWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
protected void onHandleWork(@NonNull Intent intent) {
public Result doWork() {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
deleteExpiredApksFromCache();
deleteStrayIndexFiles();
deleteOldInstallerFiles();
deleteOldIcons();
try {
deleteExpiredApksFromCache();
deleteStrayIndexFiles();
deleteOldInstallerFiles();
deleteOldIcons();
return Result.success();
} catch (Exception e) {
return Result.failure();
}
}
/**
@ -93,7 +48,7 @@ public class CleanCacheService extends JobIntentService {
* any APK in the cache that is older than that preference specifies.
*/
private void deleteExpiredApksFromCache() {
File cacheDir = ApkCache.getApkCacheDir(getBaseContext());
File cacheDir = ApkCache.getApkCacheDir(getApplicationContext());
clearOldFiles(cacheDir, Preferences.get().getKeepCacheTime());
}
@ -102,13 +57,15 @@ public class CleanCacheService extends JobIntentService {
* a safe place before installing. It doesn't clean up them reliably yet.
*/
private void deleteOldInstallerFiles() {
File filesDir = getFilesDir();
File filesDir = getApplicationContext().getFilesDir();
if (filesDir == null) {
Utils.debugLog(TAG, "The files directory doesn't exist.");
return;
}
final File[] files = filesDir.listFiles();
if (files == null) {
Utils.debugLog(TAG, "The files directory doesn't have any files.");
return;
}
@ -132,13 +89,15 @@ public class CleanCacheService extends JobIntentService {
* {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*"
*/
private void deleteStrayIndexFiles() {
File cacheDir = getCacheDir();
File cacheDir = getApplicationContext().getCacheDir();
if (cacheDir == null) {
Utils.debugLog(TAG, "The cache directory doesn't exist.");
return;
}
final File[] files = cacheDir.listFiles();
if (files == null) {
Utils.debugLog(TAG, "The cache directory doesn't have files.");
return;
}
@ -156,7 +115,7 @@ public class CleanCacheService extends JobIntentService {
* Delete cached icons that have not been accessed in over a year.
*/
private void deleteOldIcons() {
clearOldFiles(Utils.getImageCacheDir(this), TimeUnit.DAYS.toMillis(365));
clearOldFiles(Utils.getImageCacheDir(getApplicationContext()), TimeUnit.DAYS.toMillis(365));
}
/**
@ -170,24 +129,58 @@ public class CleanCacheService extends JobIntentService {
*/
public static void clearOldFiles(File f, long millisAgo) {
if (f == null) {
Utils.debugLog(TAG, "No files to be cleared.");
return;
}
long olderThan = System.currentTimeMillis() - millisAgo;
if (f.isDirectory()) {
File[] files = f.listFiles();
if (files == null) {
Utils.debugLog(TAG, "No more files to be cleared.");
return;
}
for (File file : files) {
clearOldFiles(file, millisAgo);
}
f.delete();
} else if (Build.VERSION.SDK_INT < 21) {
deleteFileAndLog(f);
} else if (Build.VERSION.SDK_INT <= 21) {
if (FileUtils.isFileOlder(f, olderThan)) {
f.delete();
deleteFileAndLog(f);
}
} else {
CleanCacheService21.deleteIfOld(f, olderThan);
Impl21.deleteIfOld(f, olderThan);
}
}
}
private static void deleteFileAndLog(final File file) {
file.delete();
Utils.debugLog(TAG, "Deleted file: " + file);
}
@RequiresApi(api = 21)
private static class Impl21 {
/**
* Recursively delete files in {@code f} that were last used
* {@code millisAgo} milliseconds ago. On {@code android-21} and newer, this
* is based on the last access of the file, on older Android versions, it is
* based on the last time the file was modified, e.g. downloaded.
*
* @param file The file or directory to clean
* @param olderThan The number of milliseconds old that marks a file for deletion.
*/
public static void deleteIfOld(File file, long olderThan) {
if (file == null || !file.exists()) {
Utils.debugLog(TAG, "No files to be cleared.");
return;
}
try {
StructStat stat = Os.lstat(file.getAbsolutePath());
if ((stat.st_atime * 1000L) < olderThan) {
deleteFileAndLog(file);
}
} catch (ErrnoException e) {
Utils.debugLog(TAG, "An exception occurred while deleting: ", e);
}
}
}
}

View File

@ -0,0 +1,49 @@
package org.fdroid.fdroid.work;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import java.util.concurrent.TimeUnit;
public class WorkUtils {
private static final String TAG = WorkUtils.class.getSimpleName();
private WorkUtils() { }
/**
* Schedule or cancel a work request to update the app index, according to the
* current preferences. Should be called a) at boot, b) if the preference
* is changed, or c) on startup, in case we get upgraded.
*/
public static void scheduleCleanCache(@NonNull final Context context) {
final WorkManager workManager = WorkManager.getInstance(context);
final long keepTime = Preferences.get().getKeepCacheTime();
long interval = TimeUnit.DAYS.toMillis(1);
if (keepTime < interval) {
interval = keepTime;
}
final Constraints.Builder constraintsBuilder = new Constraints.Builder()
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true);
if (Build.VERSION.SDK_INT >= 23) {
constraintsBuilder.setRequiresDeviceIdle(true);
}
final PeriodicWorkRequest cleanCache =
new PeriodicWorkRequest.Builder(CleanCacheWorker.class, interval, TimeUnit.MILLISECONDS)
.setConstraints(constraintsBuilder.build())
.build();
workManager.enqueueUniquePeriodicWork("clean_cache",
ExistingPeriodicWorkPolicy.REPLACE, cleanCache);
Utils.debugLog(TAG, "Scheduled periodic work for cleaning the cache.");
}
}