Update a couple of blobstore params to be configurable.

Instead of hardcoding values for IDLE_JOB_PERIOD_MS and
SESSION_EXPIRY_TIMEOUT_MS, make them configurable.

Bug: 157503601
Test: atest --test-mapping apex/blobstore
Change-Id: I4846a98144224873444d4d73b2bedde258992ac4
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
index b4a7cd4..9a711d3 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java
@@ -18,7 +18,6 @@
 import static android.provider.DeviceConfig.NAMESPACE_BLOBSTORE;
 import static android.text.format.Formatter.FLAG_IEC_UNITS;
 import static android.text.format.Formatter.formatFileSize;
-import static android.util.TimeUtils.formatDuration;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -58,18 +57,24 @@
      * Job Id for idle maintenance job ({@link BlobStoreIdleJobService}).
      */
     public static final int IDLE_JOB_ID = 0xB70B1D7; // 191934935L
-    /**
-     * Max time period (in millis) between each idle maintenance job run.
-     */
-    public static final long IDLE_JOB_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
-
-    /**
-     * Timeout in millis after which sessions with no updates will be deleted.
-     */
-    public static final long SESSION_EXPIRY_TIMEOUT_MILLIS = TimeUnit.DAYS.toMillis(7);
 
     public static class DeviceConfigProperties {
         /**
+         * Denotes the max time period (in millis) between each idle maintenance job run.
+         */
+        public static final String KEY_IDLE_JOB_PERIOD_MS = "idle_job_period_ms";
+        public static final long DEFAULT_IDLE_JOB_PERIOD_MS = TimeUnit.DAYS.toMillis(1);
+        public static long IDLE_JOB_PERIOD_MS = DEFAULT_IDLE_JOB_PERIOD_MS;
+
+        /**
+         * Denotes the timeout in millis after which sessions with no updates will be deleted.
+         */
+        public static final String KEY_SESSION_EXPIRY_TIMEOUT_MS =
+                "session_expiry_timeout_ms";
+        public static final long DEFAULT_SESSION_EXPIRY_TIMEOUT_MS = TimeUnit.DAYS.toMillis(7);
+        public static long SESSION_EXPIRY_TIMEOUT_MS = DEFAULT_SESSION_EXPIRY_TIMEOUT_MS;
+
+        /**
          * Denotes how low the limit for the amount of data, that an app will be allowed to acquire
          * a lease on, can be.
          */
@@ -119,6 +124,13 @@
             }
             properties.getKeyset().forEach(key -> {
                 switch (key) {
+                    case KEY_IDLE_JOB_PERIOD_MS:
+                        IDLE_JOB_PERIOD_MS = properties.getLong(key, DEFAULT_IDLE_JOB_PERIOD_MS);
+                        break;
+                    case KEY_SESSION_EXPIRY_TIMEOUT_MS:
+                        SESSION_EXPIRY_TIMEOUT_MS = properties.getLong(key,
+                                DEFAULT_SESSION_EXPIRY_TIMEOUT_MS);
+                        break;
                     case KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR:
                         TOTAL_BYTES_PER_APP_LIMIT_FLOOR = properties.getLong(key,
                                 DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR);
@@ -143,6 +155,12 @@
 
         static void dump(IndentingPrintWriter fout, Context context) {
             final String dumpFormat = "%s: [cur: %s, def: %s]";
+            fout.println(String.format(dumpFormat, KEY_IDLE_JOB_PERIOD_MS,
+                    TimeUtils.formatDuration(IDLE_JOB_PERIOD_MS),
+                    TimeUtils.formatDuration(DEFAULT_IDLE_JOB_PERIOD_MS)));
+            fout.println(String.format(dumpFormat, KEY_SESSION_EXPIRY_TIMEOUT_MS,
+                    TimeUtils.formatDuration(SESSION_EXPIRY_TIMEOUT_MS),
+                    TimeUtils.formatDuration(DEFAULT_SESSION_EXPIRY_TIMEOUT_MS)));
             fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
                     formatFileSize(context, TOTAL_BYTES_PER_APP_LIMIT_FLOOR, FLAG_IEC_UNITS),
                     formatFileSize(context, DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR,
@@ -167,6 +185,22 @@
     }
 
     /**
+     * Returns the max time period (in millis) between each idle maintenance job run.
+     */
+    public static long getIdleJobPeriodMs() {
+        return DeviceConfigProperties.IDLE_JOB_PERIOD_MS;
+    }
+
+    /**
+     * Returns whether a session is expired or not. A session is considered expired if the session
+     * has not been modified in a while (i.e. SESSION_EXPIRY_TIMEOUT_MS).
+     */
+    public static boolean hasSessionExpired(long sessionLastModifiedMs) {
+        return sessionLastModifiedMs
+                < System.currentTimeMillis() - DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
+    }
+
+    /**
      * Returns the maximum amount of data that an app can acquire a lease on.
      */
     public static long getAppDataBytesLimit() {
@@ -277,9 +311,6 @@
         fout.println("XML current version: " + XML_VERSION_CURRENT);
 
         fout.println("Idle job ID: " + IDLE_JOB_ID);
-        fout.println("Idle job period: " + formatDuration(IDLE_JOB_PERIOD_MILLIS));
-
-        fout.println("Session expiry timeout: " + formatDuration(SESSION_EXPIRY_TIMEOUT_MILLIS));
 
         fout.println("Total bytes per app limit: " + formatFileSize(context,
                 getAppDataBytesLimit(), FLAG_IEC_UNITS));
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
index 460e776..4b0f719 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreIdleJobService.java
@@ -16,7 +16,6 @@
 package com.android.server.blob;
 
 import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_ID;
-import static com.android.server.blob.BlobStoreConfig.IDLE_JOB_PERIOD_MILLIS;
 import static com.android.server.blob.BlobStoreConfig.LOGV;
 import static com.android.server.blob.BlobStoreConfig.TAG;
 
@@ -60,7 +59,7 @@
                 new ComponentName(context, BlobStoreIdleJobService.class))
                         .setRequiresDeviceIdle(true)
                         .setRequiresCharging(true)
-                        .setPeriodic(IDLE_JOB_PERIOD_MILLIS)
+                        .setPeriodic(BlobStoreConfig.getIdleJobPeriodMs())
                         .build();
         jobScheduler.schedule(job);
         if (LOGV) {
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
index 9376198..7a27b2c 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java
@@ -29,10 +29,10 @@
 import static android.os.UserHandle.USER_NULL;
 
 import static com.android.server.blob.BlobStoreConfig.LOGV;
-import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
 import static com.android.server.blob.BlobStoreConfig.TAG;
 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_CURRENT;
 import static com.android.server.blob.BlobStoreConfig.getAdjustedCommitTimeMs;
+import static com.android.server.blob.BlobStoreConfig.hasSessionExpired;
 import static com.android.server.blob.BlobStoreSession.STATE_ABANDONED;
 import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
 import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID;
@@ -986,9 +986,9 @@
             userSessions.removeIf((sessionId, blobStoreSession) -> {
                 boolean shouldRemove = false;
 
+                // TODO: handle the case where no content has been written to session yet.
                 // Cleanup sessions which haven't been modified in a while.
-                if (blobStoreSession.getSessionFile().lastModified()
-                        < System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS) {
+                if (hasSessionExpired(blobStoreSession.getSessionFile().lastModified())) {
                     shouldRemove = true;
                 }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
index d338b58..ade01dc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java
@@ -19,7 +19,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.server.blob.BlobStoreConfig.SESSION_EXPIRY_TIMEOUT_MILLIS;
+import static com.android.server.blob.BlobStoreConfig.DeviceConfigProperties.SESSION_EXPIRY_TIMEOUT_MS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -93,6 +93,7 @@
         doReturn(true).when(mBlobsDir).exists();
         doReturn(new File[0]).when(mBlobsDir).listFiles();
         doReturn(true).when(() -> BlobStoreConfig.hasLeaseWaitTimeElapsed(anyLong()));
+        doCallRealMethod().when(() -> BlobStoreConfig.hasSessionExpired(anyLong()));
 
         mContext = InstrumentationRegistry.getTargetContext();
         mHandler = new TestHandler(Looper.getMainLooper());
@@ -236,7 +237,7 @@
     public void testHandleIdleMaintenance_deleteStaleSessions() throws Exception {
         // Setup sessions
         final File sessionFile1 = mock(File.class);
-        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS + 1000)
+        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MS + 1000)
                 .when(sessionFile1).lastModified();
         final long sessionId1 = 342;
         final BlobHandle blobHandle1 = BlobHandle.createWithSha256("digest1".getBytes(),
@@ -256,7 +257,7 @@
         mUserSessions.append(sessionId2, session2);
 
         final File sessionFile3 = mock(File.class);
-        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MILLIS - 2000)
+        doReturn(System.currentTimeMillis() - SESSION_EXPIRY_TIMEOUT_MS - 2000)
                 .when(sessionFile3).lastModified();
         final long sessionId3 = 9484;
         final BlobHandle blobHandle3 = BlobHandle.createWithSha256("digest3".getBytes(),
diff --git a/tests/BlobStoreTestUtils/Android.bp b/tests/BlobStoreTestUtils/Android.bp
index 829c883..5c7c68b 100644
--- a/tests/BlobStoreTestUtils/Android.bp
+++ b/tests/BlobStoreTestUtils/Android.bp
@@ -15,6 +15,9 @@
 java_library {
   name: "BlobStoreTestUtils",
   srcs: ["src/**/*.java"],
-  static_libs: ["truth-prebuilt"],
+  static_libs: [
+    "truth-prebuilt",
+    "androidx.test.uiautomator_uiautomator",
+  ],
   sdk_version: "test_current",
 }
\ No newline at end of file
diff --git a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
index 6927e86..7cf58e1 100644
--- a/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
+++ b/tests/BlobStoreTestUtils/src/com/android/utils/blob/Utils.java
@@ -18,12 +18,16 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.app.Instrumentation;
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
 import android.app.blob.LeaseInfo;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import androidx.test.uiautomator.UiDevice;
 
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -32,6 +36,8 @@
 import java.io.OutputStream;
 
 public class Utils {
+    public static final String TAG = "BlobStoreTest";
+
     public static final int BUFFER_SIZE_BYTES = 16 * 1024;
 
     public static final long KB_IN_BYTES = 1000;
@@ -68,7 +74,8 @@
 
     public static void assertLeasedBlobs(BlobStoreManager blobStoreManager,
             BlobHandle... expectedBlobHandles) throws IOException {
-        assertThat(blobStoreManager.getLeasedBlobs()).containsExactly(expectedBlobHandles);
+        assertThat(blobStoreManager.getLeasedBlobs()).containsExactly(
+                (Object[]) expectedBlobHandles);
     }
 
     public static void assertNoLeasedBlobs(BlobStoreManager blobStoreManager)
@@ -141,4 +148,16 @@
         assertThat(leaseInfo.getDescriptionResId()).isEqualTo(descriptionResId);
         assertThat(leaseInfo.getDescription()).isEqualTo(description);
     }
+
+    public static void triggerIdleMaintenance(Instrumentation instrumentation) throws IOException {
+        runShellCmd(instrumentation, "cmd blob_store idle-maintenance");
+    }
+
+    private static String runShellCmd(Instrumentation instrumentation,
+            String cmd) throws IOException {
+        final UiDevice uiDevice = UiDevice.getInstance(instrumentation);
+        final String result = uiDevice.executeShellCommand(cmd);
+        Log.i(TAG, "Output of '" + cmd + "': '" + result + "'");
+        return result;
+    }
 }