package org.fdroid.fdroid;
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 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()}.
* <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 android.system.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 {
/**
* Schedule or cancel this service to update the app index, according to the
* current preferences. Should be called a) at boot, b) if the preference
* is changed, or c) on startup, in case we get upgraded.
*/
public static void schedule(Context context) {
long keepTime = Preferences.get().getKeepCacheTime();
long interval = TimeUnit.DAYS.toMillis(1);
if (keepTime < interval) {
interval = keepTime;
}
Intent intent = new Intent(context, CleanCacheService.class);
PendingIntent pending = PendingIntent.getService(context, 0, intent, 0);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarm.cancel(pending);
alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 5000, interval, pending);
}
public CleanCacheService() {
super("CleanCacheService");
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
return;
}
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
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.
*/
private void deleteOldInstallerFiles() {
File filesDir = getFilesDir();
if (filesDir == null) {
return;
}
final File[] files = filesDir.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.getName().startsWith("install-")) {
clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
}
}
}
/**
* 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).
* <p>
* This also deletes temp files that are created by
* {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*"
*/
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-")) {
clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
}
if (f.getName().startsWith("dl-")) {
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.
*/
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 {
CleanCacheService21.deleteIfOld(f, olderThan);
}
}
}