diff --git a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java b/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java similarity index 67% rename from app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java rename to app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java index 7d9d26620..ea2e4eb5e 100644 --- a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java @@ -1,11 +1,10 @@ -package org.fdroid.fdroid.installer; +package org.fdroid.fdroid; + +import android.support.test.runner.AndroidJUnit4; import org.apache.commons.io.FileUtils; -import org.fdroid.fdroid.BuildConfig; 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; @@ -13,10 +12,10 @@ 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) -public class ApkCacheTest { +@RunWith(AndroidJUnit4.class) +public class CleanCacheServiceTest { + + public static final String TAG = "CleanCacheServiceTest"; @Test public void testClearOldFiles() throws IOException, InterruptedException { @@ -45,13 +44,18 @@ public class ApkCacheTest { assertTrue(second.createNewFile()); assertTrue(second.exists()); - ApkCache.clearOldFiles(dir, 3); + CleanCacheService.clearOldFiles(dir, 3000); // check all in dir assertFalse(first.exists()); assertTrue(second.exists()); Thread.sleep(7000); - ApkCache.clearOldFiles(dir, 3); + 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); } } diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java index d51719917..9fca46e7d 100644 --- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java +++ b/app/src/main/java/org/fdroid/fdroid/CleanCacheService.java @@ -1,23 +1,36 @@ 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; 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()} + * {@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 {
@@ -28,9 +41,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);
@@ -52,9 +65,20 @@ public class CleanCacheService extends IntentService {
return;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
- ApkCache.clearApkCache(this);
+ 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());
}
/**
@@ -74,7 +98,7 @@ public class CleanCacheService extends IntentService {
for (File f : files) {
if (f.getName().startsWith("install-")) {
- FileUtils.deleteQuietly(f);
+ clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
}
}
}
@@ -104,11 +128,58 @@ 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));
}
}
}
-}
+
+ /**
+ * 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
+ * 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 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 f, long millisAgo) {
+ if (f == null) {
+ return;
+ }
+ long olderThan = System.currentTimeMillis() - 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();
+ }
+ } else {
+ try {
+ StructStat stat = Os.lstat(f.getAbsolutePath());
+ if ((stat.st_atime * 1000L) < olderThan) {
+ f.delete();
+ }
+ } catch (ErrnoException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
\ No newline at end of file
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/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java
index 0a7a9faad..626b44182 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);
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/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) {
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..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,31 +133,4 @@ public class ApkCache {
}
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) {
- 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 (FileUtils.isFileOlder(f, olderThan)) {
- f.delete();
- }
- }
- }
}
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
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 @@