Bug 1238785 - Add FileCleanupController and tests. r=ahunt
This controller is under-featured (e.g. it's not scheduling cleanups for future
dates and it doesn't cache files it already deleted) in favor of simplicity.
MozReview-Commit-ID: KJqKV0OH2ID
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java
@@ -0,0 +1,79 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.cleanup;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.support.annotation.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Encapsulates the code to run the {@link FileCleanupService}. Call
+ * {@link #startIfReady(Context, SharedPreferences, String)} to start the clean-up.
+ *
+ * Note: for simplicity, the current implementation does not cache which
+ * files have been cleaned up and will attempt to delete the same files
+ * each time it is run. If the file deletion list grows large, consider
+ * keeping a cache.
+ */
+public class FileCleanupController {
+
+ private static final long MILLIS_BETWEEN_CLEANUPS = TimeUnit.DAYS.toMillis(7);
+ @VisibleForTesting static final String PREF_LAST_CLEANUP_MILLIS = "cleanup.lastFileCleanupMillis";
+
+ // These will be prepended with the path of the profile we're cleaning up.
+ private static final String[] PROFILE_FILES_TO_CLEANUP = new String[] {
+ "health.db",
+ "health.db-journal",
+ };
+
+ /**
+ * Starts the clean-up if it's time to clean-up, otherwise returns. For simplicity,
+ * it does not schedule the cleanup for some point in the future - this method will
+ * have to be called again (i.e. polled) in order to run the clean-up service.
+ *
+ * @param context Context of the calling {@link android.app.Activity}
+ * @param sharedPrefs The {@link SharedPreferences} instance to store the controller state to
+ * @param profilePath The path to the profile the service should clean-up files from
+ */
+ public static void startIfReady(final Context context, final SharedPreferences sharedPrefs, final String profilePath) {
+ if (!isCleanupReady(sharedPrefs)) {
+ return;
+ }
+
+ recordCleanupScheduled(sharedPrefs);
+
+ final Intent fileCleanupIntent = new Intent(context, FileCleanupService.class);
+ fileCleanupIntent.setAction(FileCleanupService.ACTION_DELETE_FILES);
+ fileCleanupIntent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, getFilesToCleanup(profilePath + "/"));
+ context.startService(fileCleanupIntent);
+ }
+
+ private static boolean isCleanupReady(final SharedPreferences sharedPrefs) {
+ final long lastCleanupMillis = sharedPrefs.getLong(PREF_LAST_CLEANUP_MILLIS, -1);
+ return lastCleanupMillis + MILLIS_BETWEEN_CLEANUPS < System.currentTimeMillis();
+ }
+
+ private static void recordCleanupScheduled(final SharedPreferences sharedPrefs) {
+ final SharedPreferences.Editor editor = sharedPrefs.edit();
+ editor.putLong(PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()).apply();
+ }
+
+ @VisibleForTesting
+ static ArrayList<String> getFilesToCleanup(final String profilePath) {
+ final ArrayList<String> out = new ArrayList<>(PROFILE_FILES_TO_CLEANUP.length);
+ for (final String path : PROFILE_FILES_TO_CLEANUP) {
+ // Append a file separator, just in-case the caller didn't include one.
+ out.add(profilePath + File.separator + path);
+ }
+ return out;
+ }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -202,16 +202,17 @@ gbjar.sources += ['java/org/mozilla/geck
'animation/Rotate3DAnimation.java',
'animation/ViewHelper.java',
'ANRReporter.java',
'AppNotificationClient.java',
'BaseGeckoInterface.java',
'BootReceiver.java',
'BrowserApp.java',
'BrowserLocaleManager.java',
+ 'cleanup/FileCleanupController.java',
'cleanup/FileCleanupService.java',
'ContactService.java',
'ContextGetter.java',
'CrashHandler.java',
'CustomEditText.java',
'DataReportingNotification.java',
'db/AbstractPerProfileDatabaseProvider.java',
'db/AbstractTransactionalProvider.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java
@@ -0,0 +1,92 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.cleanup;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests functionality of the {@link FileCleanupController}.
+ */
+@RunWith(TestRunner.class)
+public class TestFileCleanupController {
+
+ @Test
+ public void testStartIfReadyEmptySharedPrefsRunsCleanup() {
+ final Context context = mock(Context.class);
+ FileCleanupController.startIfReady(context, getSharedPreferences(), "");
+ verify(context).startService(any(Intent.class));
+ }
+
+ @Test
+ public void testStartIfReadyLastRunNowDoesNotRun() {
+ final SharedPreferences sharedPrefs = getSharedPreferences();
+ sharedPrefs.edit()
+ .putLong(FileCleanupController.PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis())
+ .commit(); // synchronous to finish before test runs.
+
+ final Context context = mock(Context.class);
+ FileCleanupController.startIfReady(context, sharedPrefs, "");
+
+ verify(context, never()).startService((any(Intent.class)));
+ }
+
+ /**
+ * Depends on {@link #testStartIfReadyEmptySharedPrefsRunsCleanup()} success –
+ * i.e. we expect the cleanup to run with empty prefs.
+ */
+ @Test
+ public void testStartIfReadyDoesNotRunTwiceInSuccession() {
+ final Context context = mock(Context.class);
+ final SharedPreferences sharedPrefs = getSharedPreferences();
+
+ FileCleanupController.startIfReady(context, sharedPrefs, "");
+ verify(context).startService(any(Intent.class));
+
+ // Note: the Controller relies on SharedPrefs.apply, but
+ // robolectric made this a synchronous call. Yay!
+ FileCleanupController.startIfReady(context, sharedPrefs, "");
+ verify(context, atMost(1)).startService(any(Intent.class));
+ }
+
+ @Test
+ public void testGetFilesToCleanupContainsProfilePath() {
+ final String profilePath = "/a/profile/path";
+ final ArrayList<String> fileList = FileCleanupController.getFilesToCleanup(profilePath);
+ assertNotNull("Returned file list is non-null", fileList);
+
+ boolean atLeastOneStartsWithProfilePath = false;
+ final String pathToCheck = profilePath + "/"; // Ensure the calling code adds a slash to divide the path.
+ for (final String path : fileList) {
+ if (path.startsWith(pathToCheck)) {
+ // It'd be great if we could assert these individually so
+ // we could display the Strings in console output.
+ atLeastOneStartsWithProfilePath = true;
+ }
+ }
+ assertTrue("At least one returned String starts with a profile path", atLeastOneStartsWithProfilePath);
+ }
+
+ private SharedPreferences getSharedPreferences() {
+ return RuntimeEnvironment.application.getSharedPreferences("TestFileCleanupController", 0);
+ }
+}