Merge branch 'big-cache-update' into 'master'
Big cache update So I messed up the caching a bit with my update in the past, so this is a big update to fix lots of bugs (hopefully) and add a couple of nice cache clean up features. This should move us towards making F-Droid maintain itself more and more. More comments in the commits. See merge request !378
This commit is contained in:
		
						commit
						20a5f42359
					
				| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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()}. | ||||
|  * <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 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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
|  | ||||
| @ -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; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -26,12 +26,12 @@ | ||||
|     </string-array> | ||||
| 
 | ||||
|     <string-array name="keepCacheValues"> | ||||
|         <item>3600</item> | ||||
|         <item>86400</item> | ||||
|         <item>604800</item> | ||||
|         <item>2592000</item> | ||||
|         <item>31449600</item> | ||||
|         <item>2147483647</item> | ||||
|         <item>3600000</item> | ||||
|         <item>86400000</item> | ||||
|         <item>604800000</item> | ||||
|         <item>2592000000</item> | ||||
|         <item>31536000000</item> | ||||
|         <item>9223372036854775807</item> | ||||
|     </string-array> | ||||
| 
 | ||||
|     <string-array name="themeValues"> | ||||
|  | ||||
| @ -75,6 +75,7 @@ | ||||
|     <PreferenceCategory android:title="@string/other"> | ||||
|         <ListPreference android:title="@string/cache_downloaded" | ||||
|             android:key="keepCacheFor" | ||||
|             android:defaultValue="86400000" | ||||
|             android:entries="@array/keepCacheNames" | ||||
|             android:entryValues="@array/keepCacheValues" /> | ||||
|         <CheckBoxPreference android:title="@string/expert" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Hans-Christoph Steiner
						Hans-Christoph Steiner