Bug 1238785 - Add FileCleanupController and tests. r=ahunt draft
authorMichael Comella <michael.l.comella@gmail.com>
Tue, 12 Apr 2016 16:34:10 -0700
changeset 350220 2ab0e0f3e618f63b81dda92f83d4e5b669acd900
parent 350219 169d81de3d4a014c3145269328b296c20cbefbd5
child 350221 45940791001c81c24cb493dab7a5feddc3c8e7ff
push id15272
push usermichael.l.comella@gmail.com
push dateWed, 13 Apr 2016 00:15:45 +0000
reviewersahunt
bugs1238785
milestone48.0a1
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
mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java
mobile/android/base/moz.build
mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java
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);
+    }
+}