diff --git a/app/src/androidTest/java/org/fdroid/fdroid/UtilsTest.java b/app/src/androidTest/java/org/fdroid/fdroid/UtilsTest.java index da038dd10..96f701328 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/UtilsTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/UtilsTest.java @@ -1,13 +1,18 @@ package org.fdroid.fdroid; +import android.app.Instrumentation; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import org.apache.commons.io.FileUtils; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.File; +import java.io.IOException; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -137,4 +142,34 @@ public class UtilsTest { } // TODO write tests that work with a Certificate + + @Test + public void testClearOldFiles() throws IOException, InterruptedException { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + File dir = new File(TestUtils.getWriteableDir(instrumentation), "clearOldFiles"); + FileUtils.deleteQuietly(dir); + dir.mkdirs(); + assertTrue(dir.isDirectory()); + + File first = new File(dir, "first"); + File second = new File(dir, "second"); + assertFalse(first.exists()); + assertFalse(second.exists()); + + first.createNewFile(); + assertTrue(first.exists()); + + Thread.sleep(7000); + second.createNewFile(); + assertTrue(second.exists()); + + Utils.clearOldFiles(dir, 3); + assertFalse(first.exists()); + assertTrue(second.exists()); + + Thread.sleep(7000); + Utils.clearOldFiles(dir, 3); + assertFalse(first.exists()); + assertFalse(second.exists()); + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c499b50d7..b9b3419c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -445,6 +445,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java new file mode 100644 index 000000000..48d5910c3 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -0,0 +1,70 @@ +package org.fdroid.fdroid; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.os.Process; + +import org.apache.commons.io.FileUtils; + +import java.io.File; + +/** + * 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()} + */ +public class CleanCacheService extends IntentService { + public static final String TAG = "CleanCacheService"; + + public static void start(Context context) { + Intent intent = new Intent(context, CleanCacheService.class); + context.startService(intent); + } + + public CleanCacheService() { + super("CleanCacheService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); + + int cachetime; + if (Preferences.get().shouldCacheApks()) { + cachetime = Integer.MAX_VALUE; + } else { + cachetime = 3600; // keep for 1 hour to allow resumable downloads + } + Utils.clearOldFiles(Utils.getApkCacheDir(this), cachetime); + deleteStrayIndexFiles(); + } + + /** + * Delete index files which were downloaded, but not removed (e.g. due to F-Droid being + * force closed during processing of the file, before getting a chance to delete). This + * may include both "index-*-downloaded" and "index-*-extracted.xml" files. + *

+ * Note that if the SD card is not ready, then the cache directory will probably not be + * available. In this situation no files will be deleted (and thus they may still exist + * after the SD card becomes available). + */ + private void deleteStrayIndexFiles() { + File cacheDir = getCacheDir(); + if (cacheDir == null) { + return; + } + + final File[] files = cacheDir.listFiles(); + if (files == null) { + return; + } + + for (File f : files) { + if (f.getName().startsWith("index-")) { + FileUtils.deleteQuietly(f); + } + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 6ec2355b8..4213dce38 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -221,29 +221,7 @@ public class FDroidApp extends Application { } }); - // Clear cached apk files. We used to just remove them after they'd - // been installed, but this causes problems for proprietary gapps - // users since the introduction of verification (on pre-4.2 Android), - // because the install intent says it's finished when it hasn't. - if (!Preferences.get().shouldCacheApks()) { - Utils.deleteFiles(Utils.getApkCacheDir(this), null, ".apk"); - } - - // Index files which downloaded, but were not removed (e.g. due to F-Droid being force - // closed during processing of the file, before getting a chance to delete). This may - // include both "index-*-downloaded" and "index-*-extracted.xml" files. The first is from - // either signed or unsigned repos, and the later is from signed repos. - Utils.deleteFiles(getCacheDir(), "index-", null); - - // As above, but for legacy F-Droid clients that downloaded under a different name, and - // extracted to the files directory rather than the cache directory. - // TODO: This can be removed in a a few months or a year (e.g. 2016) because people will - // have upgraded their clients, this code will have executed, and they will not have any - // left over files any more. Even if they do hold off upgrading until this code is removed, - // the only side effect is that they will have a few more MiB of storage taken up on their - // device until they uninstall and re-install F-Droid. - Utils.deleteFiles(getCacheDir(), "dl-", null); - Utils.deleteFiles(getFilesDir(), "index-", null); + CleanCacheService.start(this); UpdateService.schedule(getApplicationContext()); bluetoothAdapter = getBluetoothAdapter(); diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index c538274e4..fa6148ece 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -37,6 +37,7 @@ import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; import com.nostra13.universalimageloader.utils.StorageUtils; +import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Repo; @@ -336,6 +337,26 @@ public final class Utils { return apkCacheDir; } + /** + * Recursively delete files in {@code dir} that were last modified + * {@code secondsAgo} seconds ago, e.g. when it was downloaded. + * + * @param dir The directory to recurse in + * @param secondsAgo The number of seconds old that marks a file for deletion. + */ + public static void clearOldFiles(File dir, long secondsAgo) { + long olderThan = System.currentTimeMillis() - (secondsAgo * 1000L); + for (File f : dir.listFiles()) { + if (f.isDirectory()) { + clearOldFiles(f, olderThan); + f.delete(); + } + if (FileUtils.isFileOlder(f, olderThan)) { + f.delete(); + } + } + } + public static String calcFingerprint(String keyHexString) { if (TextUtils.isEmpty(keyHexString) || keyHexString.matches(".*[^a-fA-F0-9].*")) { @@ -650,40 +671,6 @@ public final class Utils { } } - /** - * Remove all files from the {@param directory} either beginning with {@param startsWith} - * or ending with {@param endsWith}. Note that if the SD card is not ready, then the - * cache directory will probably not be available. In this situation no files will be - * deleted (and thus they may still exist after the SD card becomes available). - */ - public static void deleteFiles(@Nullable File directory, @Nullable String startsWith, @Nullable String endsWith) { - - if (directory == null) { - return; - } - - final File[] files = directory.listFiles(); - if (files == null) { - return; - } - - if (startsWith != null) { - debugLog(TAG, "Cleaning up files in " + directory + " that start with \"" + startsWith + "\""); - } - - if (endsWith != null) { - debugLog(TAG, "Cleaning up files in " + directory + " that end with \"" + endsWith + "\""); - } - - for (File f : files) { - if (((startsWith != null && f.getName().startsWith(startsWith)) - || (endsWith != null && f.getName().endsWith(endsWith))) - && !f.delete()) { - Log.w(TAG, "Couldn't delete cache file " + f); - } - } - } - public static void debugLog(String tag, String msg) { if (BuildConfig.DEBUG) { Log.d(tag, msg);