From 6204a1602489bed32d4b702a6f05fc05e6b31eb2 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 15 Aug 2016 16:39:54 +0200 Subject: [PATCH 01/10] set default for "keep cached apps" There was no default set in the XML, so it just showed the default as blank closes #719 --- app/src/main/res/xml/preferences.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 31fb9aee9..454472b61 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -75,6 +75,7 @@ Date: Mon, 15 Aug 2016 17:14:17 +0200 Subject: [PATCH 02/10] use access time to remove old cache files >= android-21 In android-21, they exposed the formerly internal method for getting stat structs of files. From that, we can get the last access time, which is a much better way to determine which files to delete rather than last modified time. closes #644 --- .../org/fdroid/fdroid/installer/ApkCache.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java index ce627283b..587c9c87d 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java @@ -141,12 +141,15 @@ public class ApkCache { } /** - * Recursively delete files in {@code dir} that were last modified - * {@code secondsAgo} seconds ago, e.g. when it was downloaded. + * Recursively delete files in {@code dir} that were last used + * {@code secondsAgo} seconds 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 dir The directory to recurse in * @param secondsAgo The number of seconds old that marks a file for deletion. */ + @TargetApi(21) public static void clearOldFiles(File dir, long secondsAgo) { if (dir == null) { return; @@ -161,8 +164,19 @@ public class ApkCache { clearOldFiles(f, olderThan); f.delete(); } - if (FileUtils.isFileOlder(f, olderThan)) { - f.delete(); + if (Build.VERSION.SDK_INT < 21) { + if (FileUtils.isFileOlder(f, olderThan)) { + f.delete(); + } + } else { + try { + StructStat stat = Os.lstat(f.getAbsolutePath()); + if (stat.st_atime < olderThan) { + f.delete(); + } + } catch (ErrnoException e) { + e.printStackTrace(); + } } } } From fc98820c93a75703da9386d5f7dd016eb5913903 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 15 Aug 2016 21:36:33 +0200 Subject: [PATCH 03/10] move clearOldFiles() to CleanCacheService to be used on other files Now that there is the ability to remove files based on last access time, it makes sense to use this on all cached files, including icons, etc. --- .../org/fdroid/fdroid/CleanCacheService.java | 48 ++++++++++++++++- .../org/fdroid/fdroid/installer/ApkCache.java | 51 +------------------ ...heTest.java => CleanCacheServiceTest.java} | 9 ++-- 3 files changed, 54 insertions(+), 54 deletions(-) rename app/src/test/java/org/fdroid/fdroid/{installer/ApkCacheTest.java => CleanCacheServiceTest.java} (88%) diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index d51719917..b912992a9 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -1,12 +1,17 @@ package org.fdroid.fdroid; +import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; 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 org.apache.commons.io.FileUtils; import org.fdroid.fdroid.installer.ApkCache; @@ -52,7 +57,7 @@ public class CleanCacheService extends IntentService { return; } Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - ApkCache.clearApkCache(this); + clearOldFiles(ApkCache.getApkCacheDir(getBaseContext()), Preferences.get().getKeepCacheTime()); deleteStrayIndexFiles(); deleteOldInstallerFiles(); } @@ -111,4 +116,45 @@ public class CleanCacheService extends IntentService { } } } + + /** + * Recursively delete files in {@code dir} that were last used + * {@code secondsAgo} seconds 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 dir The directory to recurse in + * @param secondsAgo The number of seconds old that marks a file for deletion. + */ + @TargetApi(21) + public static void clearOldFiles(File dir, long secondsAgo) { + if (dir == null) { + return; + } + File[] files = dir.listFiles(); + if (files == null) { + return; + } + long olderThan = System.currentTimeMillis() - (secondsAgo * 1000L); + for (File f : files) { + if (f.isDirectory()) { + clearOldFiles(f, olderThan); + f.delete(); + } + if (Build.VERSION.SDK_INT < 21) { + if (FileUtils.isFileOlder(f, olderThan)) { + f.delete(); + } + } else { + try { + StructStat stat = Os.lstat(f.getAbsolutePath()); + if (stat.st_atime < olderThan) { + f.delete(); + } + } catch (ErrnoException e) { + e.printStackTrace(); + } + } + } + } } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java index 587c9c87d..47f6fb32e 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ApkCache.java @@ -26,7 +26,6 @@ import com.nostra13.universalimageloader.utils.StorageUtils; import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.Hasher; -import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.SanitizedFile; @@ -117,19 +116,14 @@ public class ApkCache { } } - public static void clearApkCache(Context context) { - clearOldFiles(getApkCacheDir(context), Preferences.get().getKeepCacheTime()); - } - - /** * This location is only for caching, do not install directly from this location * because if the file is on the External Storage, any other app could swap out * the APK while the install was in process, allowing malware to install things. - * Using {@link Installer#installPackage(Uri localApkUri, Uri downloadUri, String packageName)} + * Using {@link Installer#installPackage(Uri, Uri, Apk)} * is fine since that does the right thing. */ - private static File getApkCacheDir(Context context) { + public static File getApkCacheDir(Context context) { File apkCacheDir = new File(StorageUtils.getCacheDirectory(context, true), CACHE_DIR); if (apkCacheDir.isFile()) { apkCacheDir.delete(); @@ -139,45 +133,4 @@ public class ApkCache { } return apkCacheDir; } - - /** - * Recursively delete files in {@code dir} that were last used - * {@code secondsAgo} seconds 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 dir The directory to recurse in - * @param secondsAgo The number of seconds old that marks a file for deletion. - */ - @TargetApi(21) - public static void clearOldFiles(File dir, long secondsAgo) { - if (dir == null) { - return; - } - File[] files = dir.listFiles(); - if (files == null) { - return; - } - long olderThan = System.currentTimeMillis() - (secondsAgo * 1000L); - for (File f : files) { - if (f.isDirectory()) { - clearOldFiles(f, olderThan); - f.delete(); - } - if (Build.VERSION.SDK_INT < 21) { - if (FileUtils.isFileOlder(f, olderThan)) { - f.delete(); - } - } else { - try { - StructStat stat = Os.lstat(f.getAbsolutePath()); - if (stat.st_atime < olderThan) { - f.delete(); - } - } catch (ErrnoException e) { - e.printStackTrace(); - } - } - } - } } diff --git a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java b/app/src/test/java/org/fdroid/fdroid/CleanCacheServiceTest.java similarity index 88% rename from app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java rename to app/src/test/java/org/fdroid/fdroid/CleanCacheServiceTest.java index 7d9d26620..8494e6891 100644 --- a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java +++ b/app/src/test/java/org/fdroid/fdroid/CleanCacheServiceTest.java @@ -1,7 +1,8 @@ -package org.fdroid.fdroid.installer; +package org.fdroid.fdroid; import org.apache.commons.io.FileUtils; import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.CleanCacheService; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricGradleTestRunner; @@ -16,7 +17,7 @@ import static org.junit.Assert.assertTrue; // TODO: Use sdk=24 when Robolectric supports this @Config(constants = BuildConfig.class, sdk = 23) @RunWith(RobolectricGradleTestRunner.class) -public class ApkCacheTest { +public class CleanCacheServiceTest { @Test public void testClearOldFiles() throws IOException, InterruptedException { @@ -45,12 +46,12 @@ public class ApkCacheTest { assertTrue(second.createNewFile()); assertTrue(second.exists()); - ApkCache.clearOldFiles(dir, 3); + CleanCacheService.clearOldFiles(dir, 3); assertFalse(first.exists()); assertTrue(second.exists()); Thread.sleep(7000); - ApkCache.clearOldFiles(dir, 3); + CleanCacheService.clearOldFiles(dir, 3); assertFalse(first.exists()); assertFalse(second.exists()); } From c9e364371232461317710f0de3375340e5b7717f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 15 Aug 2016 22:47:45 +0200 Subject: [PATCH 04/10] switch "Keep Cache Time" units to milliseconds Both the Android task scheduler and the Java File operations want millis. For "Forever", Long.MAX_VALUE is used. --- .../org/fdroid/fdroid/CleanCacheService.java | 15 +++++----- .../java/org/fdroid/fdroid/Preferences.java | 28 ++++++++++++++----- app/src/main/res/values/donottranslate.xml | 12 ++++---- app/src/main/res/xml/preferences.xml | 2 +- .../fdroid/fdroid/CleanCacheServiceTest.java | 4 +-- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index b912992a9..a1f5d6c9b 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -17,6 +17,7 @@ import org.apache.commons.io.FileUtils; 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 @@ -33,9 +34,9 @@ public class CleanCacheService extends IntentService { */ public static void schedule(Context context) { long keepTime = Preferences.get().getKeepCacheTime(); - long interval = 604800000; // 1 day + long interval = TimeUnit.DAYS.toMillis(1); if (keepTime < interval) { - interval = keepTime * 1000; + interval = keepTime; } Intent intent = new Intent(context, CleanCacheService.class); @@ -119,15 +120,15 @@ public class CleanCacheService extends IntentService { /** * Recursively delete files in {@code dir} that were last used - * {@code secondsAgo} seconds ago. On {@code android-21} and newer, this + * {@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 dir The directory to recurse in - * @param secondsAgo The number of seconds old that marks a file for deletion. + * @param millisAgo The number of milliseconds old that marks a file for deletion. */ @TargetApi(21) - public static void clearOldFiles(File dir, long secondsAgo) { + public static void clearOldFiles(File dir, long millisAgo) { if (dir == null) { return; } @@ -135,7 +136,7 @@ public class CleanCacheService extends IntentService { if (files == null) { return; } - long olderThan = System.currentTimeMillis() - (secondsAgo * 1000L); + long olderThan = System.currentTimeMillis() - millisAgo; for (File f : files) { if (f.isDirectory()) { clearOldFiles(f, olderThan); @@ -148,7 +149,7 @@ public class CleanCacheService extends IntentService { } else { try { StructStat stat = Os.lstat(f.getAbsolutePath()); - if (stat.st_atime < olderThan) { + if ((stat.st_atime * 1000L) < olderThan) { f.delete(); } } catch (ErrnoException e) { diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 0a7a9faad..3216f4c87 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -16,6 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.concurrent.TimeUnit; import info.guardianproject.netcipher.NetCipher; @@ -30,11 +31,9 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh private static final String TAG = "Preferences"; - private final Context context; private final SharedPreferences preferences; private Preferences(Context context) { - this.context = context; preferences = PreferenceManager.getDefaultSharedPreferences(context); preferences.registerOnSharedPreferenceChangeListener(this); if (preferences.getString(PREF_LOCAL_REPO_NAME, null) == null) { @@ -72,7 +71,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh private static final int DEFAULT_UPD_HISTORY = 14; private static final boolean DEFAULT_PRIVILEGED_INSTALLER = false; //private static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true; - private static final long DEFAULT_KEEP_CACHE_SECONDS = 86400; // one day + private static final long DEFAULT_KEEP_CACHE_TIME = TimeUnit.DAYS.toMillis(1); // one day private static final boolean DEFAULT_UNSTABLE_UPDATES = false; //private static final boolean DEFAULT_LOCAL_REPO_HTTPS = false; private static final boolean DEFAULT_INCOMP_VER = false; @@ -136,14 +135,29 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh private static final String PREF_CACHE_APK = "cacheDownloaded"; /** - * Time in seconds to keep cached files. Anything that has been around longer will be deleted + * Time in millis to keep cached files. Anything that has been around longer will be deleted */ public long getKeepCacheTime() { - String value = preferences.getString(PREF_KEEP_CACHE_TIME, String.valueOf(DEFAULT_KEEP_CACHE_SECONDS)); + String value = preferences.getString(PREF_KEEP_CACHE_TIME, + String.valueOf(DEFAULT_KEEP_CACHE_TIME)); + + // the first time this was migrated, it was botched, so reset to default + switch (value) { + case "3600": + case "86400": + case "604800": + case "2592000": + case "31449600": + case "2147483647": + SharedPreferences.Editor editor = preferences.edit(); + editor.remove(PREF_KEEP_CACHE_TIME); + editor.apply(); + return Preferences.DEFAULT_KEEP_CACHE_TIME; + } if (preferences.contains(PREF_CACHE_APK)) { if (preferences.getBoolean(PREF_CACHE_APK, false)) { - value = context.getString(R.string.keep_forever); + value = String.valueOf(Long.MAX_VALUE); } SharedPreferences.Editor editor = preferences.edit(); editor.remove(PREF_CACHE_APK); @@ -154,7 +168,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh try { return Long.parseLong(value); } catch (NumberFormatException e) { - return DEFAULT_KEEP_CACHE_SECONDS; + return DEFAULT_KEEP_CACHE_TIME; } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 148db8f22..ed9644aef 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -26,12 +26,12 @@ - 3600 - 86400 - 604800 - 2592000 - 31449600 - 2147483647 + 3600000 + 86400000 + 604800000 + 2592000000 + 31536000000 + 9223372036854775807 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 454472b61..6f5b404f3 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -75,7 +75,7 @@ Date: Tue, 16 Aug 2016 11:19:42 +0200 Subject: [PATCH 05/10] clearOldFiles() test must run on the emulator It seems that Robolectric does not implement android.system.Os so the atime checking code cannot be tested there. Works fine on the emulator. --- .../fdroid/fdroid/CleanCacheServiceTest.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) rename app/src/{test => androidTest}/java/org/fdroid/fdroid/CleanCacheServiceTest.java (71%) diff --git a/app/src/test/java/org/fdroid/fdroid/CleanCacheServiceTest.java b/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java similarity index 71% rename from app/src/test/java/org/fdroid/fdroid/CleanCacheServiceTest.java rename to app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java index 0c32ef893..ea2e4eb5e 100644 --- a/app/src/test/java/org/fdroid/fdroid/CleanCacheServiceTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java @@ -1,12 +1,10 @@ package org.fdroid.fdroid; +import android.support.test.runner.AndroidJUnit4; + import org.apache.commons.io.FileUtils; -import org.fdroid.fdroid.BuildConfig; -import org.fdroid.fdroid.CleanCacheService; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricGradleTestRunner; -import org.robolectric.annotation.Config; import java.io.File; import java.io.IOException; @@ -14,11 +12,11 @@ import java.io.IOException; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -// TODO: Use sdk=24 when Robolectric supports this -@Config(constants = BuildConfig.class, sdk = 23) -@RunWith(RobolectricGradleTestRunner.class) +@RunWith(AndroidJUnit4.class) public class CleanCacheServiceTest { + public static final String TAG = "CleanCacheServiceTest"; + @Test public void testClearOldFiles() throws IOException, InterruptedException { File tempDir = new File(System.getProperty("java.io.tmpdir")); @@ -46,13 +44,18 @@ public class CleanCacheServiceTest { assertTrue(second.createNewFile()); assertTrue(second.exists()); - CleanCacheService.clearOldFiles(dir, 3000); + CleanCacheService.clearOldFiles(dir, 3000); // check all in dir assertFalse(first.exists()); assertTrue(second.exists()); Thread.sleep(7000); - CleanCacheService.clearOldFiles(dir, 3000); + CleanCacheService.clearOldFiles(second, 3000); // check just second file assertFalse(first.exists()); assertFalse(second.exists()); + + // make sure it doesn't freak out on a non-existant file + File nonexistant = new File(tempDir, "nonexistant"); + CleanCacheService.clearOldFiles(nonexistant, 1); + CleanCacheService.clearOldFiles(null, 1); } } From cbf591446071facafd8fc65efd76931fe5848b4f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 15 Aug 2016 22:49:49 +0200 Subject: [PATCH 06/10] if the "Keep Cache Time" pref changes, reschedule CleanCacheService Before, CleanCacheService was only scheduled at app start for once a day. If the user selects a time less than a day, then CleanCacheService should run more frequently. closes #719 --- .../fdroid/views/fragments/PreferencesFragment.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java index ff29171f6..4660ec344 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java @@ -15,6 +15,7 @@ import android.text.Html; import android.text.TextUtils; import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.CleanCacheService; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.PreferencesActivity; @@ -49,6 +50,7 @@ public class PreferencesFragment extends PreferenceFragment private static final int REQUEST_INSTALL_ORBOT = 0x1234; private CheckBoxPreference enableProxyCheckPref; private CheckBoxPreference useTorCheckPref; + private long currentKeepCacheTime; @Override public void onCreate(Bundle savedInstanceState) { @@ -144,6 +146,10 @@ public class PreferencesFragment extends PreferenceFragment case Preferences.PREF_KEEP_CACHE_TIME: entrySummary(key); + if (changing + && currentKeepCacheTime != Preferences.get().getKeepCacheTime()) { + CleanCacheService.schedule(getContext()); + } break; case Preferences.PREF_EXPERT: @@ -283,6 +289,8 @@ public class PreferencesFragment extends PreferenceFragment updateSummary(key, false); } + currentKeepCacheTime = Preferences.get().getKeepCacheTime(); + initPrivilegedInstallerPreference(); initManagePrivilegedAppPreference(); // this pref's default is dynamically set based on whether Orbot is installed From 09829515e8eae614baea2721044447da746e782b Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 15 Aug 2016 23:05:12 +0200 Subject: [PATCH 07/10] fix bug where files were never deleted from cache It was passing the wrong time value in the recursion, which made for a really old "olderThan" time. This also then flipped the logic on the next round through the recursion, causing files to be deleted even if "Keep Cache Time" was set to "Forever". closes #719 closes #736 --- app/src/main/java/org/fdroid/fdroid/CleanCacheService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index a1f5d6c9b..b5731304c 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -139,7 +139,7 @@ public class CleanCacheService extends IntentService { long olderThan = System.currentTimeMillis() - millisAgo; for (File f : files) { if (f.isDirectory()) { - clearOldFiles(f, olderThan); + clearOldFiles(f, millisAgo); f.delete(); } if (Build.VERSION.SDK_INT < 21) { From f6693ab1a1146358518a88896b846ee4cf4fa124 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 15 Aug 2016 23:47:19 +0200 Subject: [PATCH 08/10] avoid deleting cached files while they are being used If CleanCacheService runs while an APK is being installed, it should not delete the APK that is in the process of being installed. This does that by only deleting those files if they are older than an hour. Same goes for the index files. #738 --- .../org/fdroid/fdroid/CleanCacheService.java | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index b5731304c..bb1267cf7 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -80,7 +80,7 @@ public class CleanCacheService extends IntentService { for (File f : files) { if (f.getName().startsWith("install-")) { - FileUtils.deleteQuietly(f); + clearOldFiles(f, TimeUnit.HOURS.toMillis(1)); } } } @@ -110,52 +110,51 @@ public class CleanCacheService extends IntentService { for (File f : files) { if (f.getName().startsWith("index-")) { - FileUtils.deleteQuietly(f); + clearOldFiles(f, TimeUnit.HOURS.toMillis(1)); } if (f.getName().startsWith("dl-")) { - FileUtils.deleteQuietly(f); + clearOldFiles(f, TimeUnit.HOURS.toMillis(1)); } } } /** - * Recursively delete files in {@code dir} that were last used + * 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 dir The directory to recurse in + * @param f The file or directory to clean * @param millisAgo The number of milliseconds old that marks a file for deletion. */ @TargetApi(21) - public static void clearOldFiles(File dir, long millisAgo) { - if (dir == null) { - return; - } - File[] files = dir.listFiles(); - if (files == null) { + public static void clearOldFiles(File f, long millisAgo) { + if (f == null) { return; } long olderThan = System.currentTimeMillis() - millisAgo; - for (File f : files) { - if (f.isDirectory()) { - clearOldFiles(f, millisAgo); + if (f.isDirectory()) { + File[] files = f.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + clearOldFiles(file, millisAgo); + } + f.delete(); + } else if (Build.VERSION.SDK_INT < 21) { + if (FileUtils.isFileOlder(f, olderThan)) { f.delete(); } - if (Build.VERSION.SDK_INT < 21) { - if (FileUtils.isFileOlder(f, olderThan)) { + } else { + try { + StructStat stat = Os.lstat(f.getAbsolutePath()); + if ((stat.st_atime * 1000L) < olderThan) { f.delete(); } - } else { - try { - StructStat stat = Os.lstat(f.getAbsolutePath()); - if ((stat.st_atime * 1000L) < olderThan) { - f.delete(); - } - } catch (ErrnoException e) { - e.printStackTrace(); - } + } catch (ErrnoException e) { + e.printStackTrace(); } } } -} +} \ No newline at end of file From 0614213de05bc358cde8144c41a59a6a705795f7 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 16 Aug 2016 00:13:30 +0200 Subject: [PATCH 09/10] delete cached icons that have not been accessed in over a year The icon files are downloaded for each version of the app. Over time, old versions will pile up. This cleans out the ones that have not been used in over a year. On < android-21, this will delete icons that were downloaded over a year ago even if they are still in use because it is only possible to check mtime, not atime. --- .../main/java/org/fdroid/fdroid/CleanCacheService.java | 8 ++++++++ app/src/main/java/org/fdroid/fdroid/FDroidApp.java | 5 +---- app/src/main/java/org/fdroid/fdroid/Utils.java | 9 +++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index bb1267cf7..9089b12c6 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -61,6 +61,7 @@ public class CleanCacheService extends IntentService { clearOldFiles(ApkCache.getApkCacheDir(getBaseContext()), Preferences.get().getKeepCacheTime()); deleteStrayIndexFiles(); deleteOldInstallerFiles(); + deleteOldIcons(); } /** @@ -118,6 +119,13 @@ public class CleanCacheService extends IntentService { } } + /** + * Delete cached icons that have not been accessed in over a year. + */ + private void deleteOldIcons() { + clearOldFiles(Utils.getIconsCacheDir(this), TimeUnit.DAYS.toMillis(365)); + } + /** * Recursively delete files in {@code f} that were last used * {@code millisAgo} milliseconds ago. On {@code android-21} and newer, this diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 548e6582a..79fe4cdf3 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -45,7 +45,6 @@ import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiskCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; -import com.nostra13.universalimageloader.utils.StorageUtils; import org.acra.ACRA; import org.acra.ReportingInteractionMode; @@ -60,7 +59,6 @@ import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.net.IconDownloader; import org.fdroid.fdroid.net.WifiStateChangeService; -import java.io.File; import java.net.URL; import java.net.URLStreamHandler; import java.net.URLStreamHandlerFactory; @@ -262,8 +260,7 @@ public class FDroidApp extends Application { ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()) .imageDownloader(new IconDownloader(getApplicationContext())) .diskCache(new LimitedAgeDiskCache( - new File(StorageUtils.getCacheDirectory(getApplicationContext(), true), - "icons"), + Utils.getIconsCacheDir(this), null, new FileNameGenerator() { @Override diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 1d66b371a..c39b0ab1a 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -33,6 +33,7 @@ import android.util.Log; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer; +import com.nostra13.universalimageloader.utils.StorageUtils; import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.data.Repo; @@ -107,6 +108,14 @@ public final class Utils { return "/icons-120/"; } + /** + * @return the directory where cached icons are stored + */ + public static File getIconsCacheDir(Context context) { + File cacheDir = StorageUtils.getCacheDirectory(context.getApplicationContext(), true); + return new File(cacheDir, "icons"); + } + public static void copy(InputStream input, OutputStream output) throws IOException { byte[] buffer = new byte[BUFFER_SIZE]; while (true) { From 4dc141503517d6f629d49d4593cb1d9c1f19cce3 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 16 Aug 2016 16:50:04 +0200 Subject: [PATCH 10/10] document processes in CleanCacheService This breaks out each separate process into its own utility method, and adds javadoc to describe them. --- .../org/fdroid/fdroid/CleanCacheService.java | 21 +++++++++++++++++-- .../java/org/fdroid/fdroid/Preferences.java | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index 9089b12c6..9fca46e7d 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -23,7 +23,14 @@ 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()} + * {@link FDroidApp#onCreate()}. + *

+ * 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 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 IntentService { @@ -58,12 +65,22 @@ public class CleanCacheService extends IntentService { return; } Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); - clearOldFiles(ApkCache.getApkCacheDir(getBaseContext()), Preferences.get().getKeepCacheTime()); + deleteExpiredApksFromCache(); deleteStrayIndexFiles(); deleteOldInstallerFiles(); deleteOldIcons(); } + /** + * All downloaded APKs will be cached for a certain amount of time, which is + * specified by the user in the "Keep Cache Time" preference. This removes + * any APK in the cache that is older than that preference specifies. + */ + private void deleteExpiredApksFromCache() { + File cacheDir = ApkCache.getApkCacheDir(getBaseContext()); + clearOldFiles(cacheDir, Preferences.get().getKeepCacheTime()); + } + /** * {@link org.fdroid.fdroid.installer.Installer} instances copy the APK into * a safe place before installing. It doesn't clean up them reliably yet. diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 3216f4c87..626b44182 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -71,7 +71,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh private static final int DEFAULT_UPD_HISTORY = 14; private static final boolean DEFAULT_PRIVILEGED_INSTALLER = false; //private static final boolean DEFAULT_LOCAL_REPO_BONJOUR = true; - private static final long DEFAULT_KEEP_CACHE_TIME = TimeUnit.DAYS.toMillis(1); // one day + private static final long DEFAULT_KEEP_CACHE_TIME = TimeUnit.DAYS.toMillis(1); private static final boolean DEFAULT_UNSTABLE_UPDATES = false; //private static final boolean DEFAULT_LOCAL_REPO_HTTPS = false; private static final boolean DEFAULT_INCOMP_VER = false;