clean up cached files in a low priority IntentService

This moves the cache file deletion to a dedicated IntentService that runs
at the lowest possible priority.  The cache cleanup does not need to happen
with any kind of priority, so it shouldn't delay the app start or take any
resources away from foreground processes.

This also changes the logic around the "Cache packages" preference. The
downloader always saves APKs, then if "Cache packages" is disabled, those
APKs are deleted when they are older than an hour.

This also simplifies Utils.deleteFiles() since the endswith arg is no
longer needed.
This commit is contained in:
Hans-Christoph Steiner 2016-04-14 18:59:24 -04:00
parent 6fa8477650
commit 83ee0c8f0b
5 changed files with 130 additions and 57 deletions

View File

@ -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());
}
}

View File

@ -445,6 +445,9 @@
<service
android:name=".net.DownloadCompleteService"
android:exported="false" />
<service
android:name=".CleanCacheService"
android:exported="false" />
<service android:name=".net.WifiStateChangeService" />
<service android:name=".localrepo.SwapService" />
</application>

View File

@ -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.
* <p/>
* 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);
}
}
}
}

View File

@ -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();

View File

@ -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);