Merge "Remove metered vs unmetered network checks"
diff --git a/Android.bp b/Android.bp
index 43abc24..f67340a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -26,6 +26,9 @@
         "android.system.keystore2-V1-java",
         "framework-annotations-lib",
     ],
+    optimize: {
+        proguard_flags_files: ["proguard.flags"]
+    },
     static_libs: [
         "android.hardware.security.keymint-V1-java",
         "android.security.remoteprovisioning-java",
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..0182963
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1 @@
+-keep class com.android.remoteprovisioner.SettingsManager { *; }
diff --git a/src/com/android/remoteprovisioner/PeriodicProvisioner.java b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
index d9fcc79..1291376 100644
--- a/src/com/android/remoteprovisioner/PeriodicProvisioner.java
+++ b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
@@ -19,7 +19,6 @@
 import static java.lang.Math.min;
 
 import android.content.Context;
-import android.net.ConnectivityManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.security.remoteprovisioning.AttestationPoolStatus;
@@ -46,9 +45,6 @@
     // How long to wait in between key pair generations to avoid flooding keystore with requests.
     private static final Duration KEY_GENERATION_PAUSE = Duration.ofMillis(1000);
 
-    // If the connection is metered when the job service is started, try to avoid provisioning.
-    private static final long METERED_CONNECTION_EXPIRATION_CHECK = Duration.ofDays(1).toMillis();
-
     private static final String SERVICE = "android.security.remoteprovisioning";
     private static final String TAG = "RemoteProvisioningService";
     private Context mContext;
@@ -71,21 +67,6 @@
                 Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
                 return Result.failure();
             }
-
-            ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
-                    Context.CONNECTIVITY_SERVICE);
-            boolean isMetered = cm.isActiveNetworkMetered();
-            Log.i(TAG, "Connection is metered: " + isMetered);
-            long expiringBy;
-            if (isMetered) {
-                // Check a shortened duration to attempt to avoid metered connection
-                // provisioning.
-                expiringBy = System.currentTimeMillis() + METERED_CONNECTION_EXPIRATION_CHECK;
-            } else {
-                expiringBy = SettingsManager.getExpiringBy(mContext)
-                                                  .plusMillis(System.currentTimeMillis())
-                                                  .toMillis();
-            }
             ImplInfo[] implInfos = binder.getImplementationInfo();
             if (implInfos == null) {
                 Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
@@ -93,35 +74,27 @@
                 return Result.failure();
             }
             int[] keysNeededForSecLevel = new int[implInfos.length];
-            boolean provisioningNeeded =
-                    isProvisioningNeeded(binder, expiringBy, implInfos, keysNeededForSecLevel);
             GeekResponse resp = null;
-            if (!provisioningNeeded) {
-                if (!isMetered) {
-                    // So long as the connection is unmetered, go ahead and grab an updated
-                    // device configuration file.
-                    resp = ServerInterface.fetchGeek(mContext);
-                    SettingsManager.setDeviceConfig(mContext,
-                            resp.numExtraAttestationKeys,
-                            resp.timeToRefresh,
-                            resp.provisioningUrl);
-                    if (resp.numExtraAttestationKeys == 0) {
-                        binder.deleteAllKeys();
-                    }
+            if (SettingsManager.getExtraSignedKeysAvailable(mContext) == 0) {
+                // Provisioning has been purposefully disabled in the past. Go ahead and grab
+                // an EEK just to see if provisioning should resume.
+                resp = fetchGeekAndUpdate(binder);
+                if (resp.numExtraAttestationKeys == 0) {
+                    return Result.success();
                 }
+            }
+            boolean provisioningNeeded =
+                    isProvisioningNeeded(binder,
+                                         SettingsManager.getExpirationTime(mContext).toEpochMilli(),
+                                         implInfos, keysNeededForSecLevel);
+            if (!provisioningNeeded) {
                 return Result.success();
             }
-            resp = ServerInterface.fetchGeek(mContext);
-            SettingsManager.setDeviceConfig(mContext,
-                        resp.numExtraAttestationKeys,
-                        resp.timeToRefresh,
-                        resp.provisioningUrl);
-
+            // Resp may already be populated in the extremely rare case that this job is executing
+            // to resume provisioning for the first time after a server-induced RKP shutdown. Grab
+            // a fresh response anyways to refresh the challenge.
+            resp = fetchGeekAndUpdate(binder);
             if (resp.numExtraAttestationKeys == 0) {
-                // Provisioning is disabled. Check with the server if it's time to turn it back
-                // on. If not, quit. Avoid checking if the connection is metered. Opt instead
-                // to just continue using the fallback factory provisioned key.
-                binder.deleteAllKeys();
                 return Result.success();
             }
             for (int i = 0; i < implInfos.length; i++) {
@@ -142,12 +115,32 @@
             Log.e(TAG, "Encountered RemoteProvisioningException", e);
             if (SettingsManager.getFailureCounter(mContext) > FAILURE_MAXIMUM) {
                 Log.e(TAG, "Too many failures, resetting defaults.");
-                SettingsManager.clearPreferences(mContext);
+                SettingsManager.resetDefaultConfig(mContext);
             }
             return Result.failure();
         }
     }
 
+    /**
+     * Fetch a GEEK from the server and update SettingsManager appropriately with the return
+     * values. This will also delete all keys in the attestation key pool if the server has
+     * indicated that RKP should be turned off.
+     */
+    private GeekResponse fetchGeekAndUpdate(IRemoteProvisioning binder)
+            throws RemoteException, RemoteProvisioningException {
+        GeekResponse resp = ServerInterface.fetchGeek(mContext);
+        SettingsManager.setDeviceConfig(mContext,
+                    resp.numExtraAttestationKeys,
+                    resp.timeToRefresh,
+                    resp.provisioningUrl);
+
+        if (resp.numExtraAttestationKeys == 0) {
+            // The server has indicated that provisioning is disabled.
+            binder.deleteAllKeys();
+        }
+        return resp;
+    }
+
     public static void batchProvision(IRemoteProvisioning binder, Context context,
                                int keysToProvision, int secLevel,
                                byte[] geekChain, byte[] challenge)
diff --git a/src/com/android/remoteprovisioner/ServerInterface.java b/src/com/android/remoteprovisioner/ServerInterface.java
index 697d8dd..571b559 100644
--- a/src/com/android/remoteprovisioner/ServerInterface.java
+++ b/src/com/android/remoteprovisioner/ServerInterface.java
@@ -60,6 +60,8 @@
     public static List<byte[]> requestSignedCertificates(Context context, byte[] csr,
                                                          byte[] challenge) throws
             RemoteProvisioningException {
+        checkDataBudget(context);
+        int bytesTransacted = 0;
         try {
             URL url = new URL(SettingsManager.getUrl(context) + CERTIFICATE_SIGNING_URL
                               + Base64.encodeToString(challenge, Base64.URL_SAFE));
@@ -73,12 +75,14 @@
             // the output stream being automatically closed.
             try (OutputStream os = con.getOutputStream()) {
                 os.write(csr, 0, csr.length);
+                bytesTransacted += csr.length;
             }
 
             if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
                 int failures = SettingsManager.incrementFailureCounter(context);
                 Log.e(TAG, "Server connection for signing failed, response code: "
                         + con.getResponseCode() + "\nRepeated failure count: " + failures);
+                SettingsManager.consumeErrDataBudget(context, bytesTransacted);
                 throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
             }
             SettingsManager.clearFailureCounter(context);
@@ -88,17 +92,17 @@
             int read = 0;
             while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
                 cborBytes.write(buffer, 0, read);
+                bytesTransacted += read;
             }
             return CborUtils.parseSignedCertificates(cborBytes.toByteArray());
         } catch (SocketTimeoutException e) {
-            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Server timed out", e);
-            throw makeNetworkError(context, "Server timed out");
         } catch (IOException e) {
-            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Failed to request signed certificates from the server", e);
-            throw makeNetworkError(context, e.getMessage());
         }
+        SettingsManager.incrementFailureCounter(context);
+        SettingsManager.consumeErrDataBudget(context, bytesTransacted);
+        throw makeNetworkError(context, "Error getting CSR signed.");
     }
 
     /**
@@ -114,6 +118,8 @@
      * @return A GeekResponse object which optionally contains configuration data.
      */
     public static GeekResponse fetchGeek(Context context) throws RemoteProvisioningException {
+        checkDataBudget(context);
+        int bytesTransacted = 0;
         try {
             URL url = new URL(SettingsManager.getUrl(context) + GEEK_URL);
             HttpURLConnection con = (HttpURLConnection) url.openConnection();
@@ -125,12 +131,14 @@
             byte[] config = CborUtils.buildProvisioningInfo(context);
             try (OutputStream os = con.getOutputStream()) {
                 os.write(config, 0, config.length);
+                bytesTransacted += config.length;
             }
 
             if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
                 int failures = SettingsManager.incrementFailureCounter(context);
                 Log.e(TAG, "Server connection for GEEK failed, response code: "
                         + con.getResponseCode() + "\nRepeated failure count: " + failures);
+                SettingsManager.consumeErrDataBudget(context, bytesTransacted);
                 throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
             }
             SettingsManager.clearFailureCounter(context);
@@ -141,20 +149,36 @@
             int read = 0;
             while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
                 cborBytes.write(buffer, 0, read);
+                bytesTransacted += read;
             }
             inputStream.close();
-            return CborUtils.parseGeekResponse(cborBytes.toByteArray());
+            GeekResponse resp = CborUtils.parseGeekResponse(cborBytes.toByteArray());
+            if (resp == null) {
+                throw new RemoteProvisioningException(
+                        IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR,
+                        "Response failed to parse.");
+            }
+            return resp;
         } catch (SocketTimeoutException e) {
-            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Server timed out", e);
         } catch (IOException e) {
             // This exception will trigger on a completely malformed URL.
-            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Failed to fetch GEEK from the servers.", e);
         }
+        SettingsManager.incrementFailureCounter(context);
+        SettingsManager.consumeErrDataBudget(context, bytesTransacted);
         throw makeNetworkError(context, "Error fetching GEEK");
     }
 
+    private static void checkDataBudget(Context context) throws RemoteProvisioningException {
+        if (!SettingsManager.hasErrDataBudget(context, null /* curTime */)) {
+            int bytesConsumed = SettingsManager.getErrDataBudgetConsumed(context);
+            throw makeNetworkError(context,
+                    "Out of data budget due to repeated errors. Consumed "
+                    + bytesConsumed + " bytes.");
+        }
+    }
+
     private static RemoteProvisioningException makeNetworkError(Context context, String message) {
         ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
         NetworkInfo networkInfo = cm.getActiveNetworkInfo();
diff --git a/src/com/android/remoteprovisioner/SettingsManager.java b/src/com/android/remoteprovisioner/SettingsManager.java
index fbf4261..ad6e6ac 100644
--- a/src/com/android/remoteprovisioner/SettingsManager.java
+++ b/src/com/android/remoteprovisioner/SettingsManager.java
@@ -21,6 +21,7 @@
 import android.util.Log;
 
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Random;
 
 /**
@@ -36,16 +37,90 @@
     public static final int EXPIRING_BY_MS_DEFAULT = 1000 * 60 * 60 * 24 * 3;
     public static final String URL_DEFAULT = "https://remoteprovisioning.googleapis.com/v1";
     public static final boolean IS_TEST_MODE = false;
+    // Limit data consumption from failures within a window of time to 1 MB.
+    public static final int FAILURE_DATA_USAGE_MAX = 1024 * 1024;
+    public static final Duration FAILURE_DATA_USAGE_WINDOW = Duration.ofDays(1);
 
     private static final String KEY_EXPIRING_BY = "expiring_by";
     private static final String KEY_EXTRA_KEYS = "extra_keys";
     private static final String KEY_ID = "settings_id";
+    private static final String KEY_FAILURE_DATA_WINDOW_START_TIME = "failure_start_time";
     private static final String KEY_FAILURE_COUNTER = "failure_counter";
+    private static final String KEY_FAILURE_BYTES = "failure_data";
     private static final String KEY_URL = "url";
     private static final String PREFERENCES_NAME = "com.android.remoteprovisioner.preferences";
     private static final String TAG = "RemoteProvisionerSettings";
 
     /**
+     * Determines whether or not there is enough data budget remaining to attempt provisioning.
+     * If {@code FAILURE_DATA_USAGE_MAX} bytes have already been used up in previous calls that
+     * resulted in errors, then false will be returned.
+     *
+     * Additionally, the rolling window of data usage is managed within this call. The used data
+     * budget will be reset if a time greater than @{code FAILURE_DATA_USAGE_WINDOW} has passed.
+     *
+     * @param context The application context
+     * @param curTime An instant representing the current time to measure the window against. If
+     *                null, then the code will use {@code Instant.now()} instead.
+     * @return whether or not the data budget has been exceeded.
+     */
+    public static boolean hasErrDataBudget(Context context, Instant curTime) {
+        if (curTime == null) {
+            curTime = Instant.now();
+        }
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        Instant logged =
+                Instant.ofEpochMilli(sharedPref.getLong(KEY_FAILURE_DATA_WINDOW_START_TIME, 0));
+        if (Duration.between(logged, curTime).compareTo(FAILURE_DATA_USAGE_WINDOW) > 0) {
+            SharedPreferences.Editor editor = sharedPref.edit();
+            editor.putLong(KEY_FAILURE_DATA_WINDOW_START_TIME, curTime.toEpochMilli());
+            editor.putInt(KEY_FAILURE_BYTES, 0);
+            editor.apply();
+            return true;
+        }
+        return sharedPref.getInt(KEY_FAILURE_BYTES, 0) < FAILURE_DATA_USAGE_MAX;
+    }
+
+    /**
+     * Fetches the amount of data currently consumed by calls within the current accounting window
+     * to the backend that resulted in errors and returns it.
+     *
+     * @param context the application context.
+     * @return the amount of data consumed.
+     */
+    public static int getErrDataBudgetConsumed(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return sharedPref.getInt(KEY_FAILURE_BYTES, 0);
+    }
+
+    /**
+     * Increments the counter of data currently used up in transactions with the backend server.
+     * This call will not check the current state of the rolling window, leaving that up to
+     * {@code hasDataBudget}.
+     *
+     * @param context the application context.
+     * @param bytesTransacted the number of bytes sent or received over the network. Must be a value
+     *                        greater than {@code 0}.
+     */
+    public static void consumeErrDataBudget(Context context, int bytesTransacted) {
+        if (bytesTransacted < 1) return;
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sharedPref.edit();
+        int budgetUsed = 0;
+        try {
+            budgetUsed = Math.addExact(sharedPref.getInt(KEY_FAILURE_BYTES, 0), bytesTransacted);
+        } catch (Exception e) {
+            Log.e(TAG, "Overflow on number of bytes sent over the network.");
+            budgetUsed = Integer.MAX_VALUE;
+        }
+        editor.putInt(KEY_FAILURE_BYTES, budgetUsed);
+        editor.apply();
+    }
+
+    /**
      * Generates a random ID for the use of gradual ramp up of remote provisioning.
      */
     public static void generateAndSetId(Context context) {
@@ -72,6 +147,15 @@
         return sharedPref.getInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND) /* defaultValue */);
     }
 
+    public static void resetDefaultConfig(Context context) {
+        setDeviceConfig(
+                context,
+                EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+                Duration.ofMillis(EXPIRING_BY_MS_DEFAULT),
+                URL_DEFAULT);
+        clearFailureCounter(context);
+    }
+
     /**
      * Sets the remote provisioning configuration values based on what was fetched from the server.
      * The server is not guaranteed to have sent every available parameter in the config that
@@ -128,6 +212,14 @@
     }
 
     /**
+     * Returns an Instant which represents the point in time that the provisioner should check
+     * keys for expiration.
+     */
+    public static Instant getExpirationTime(Context context) {
+        return Instant.now().plusMillis(getExpiringBy(context).toMillis());
+    }
+
+    /**
      * Gets the setting for what base URL the provisioner should use to talk to provisioning
      * servers.
      */
diff --git a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
index 6d8f48a..f4475ca 100644
--- a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
+++ b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
@@ -32,8 +32,8 @@
 import com.android.remoteprovisioner.PeriodicProvisioner;
 import com.android.remoteprovisioner.RemoteProvisioningException;
 import com.android.remoteprovisioner.ServerInterface;
+import com.android.remoteprovisioner.SettingsManager;
 
-import java.time.Duration;
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
@@ -41,7 +41,6 @@
  */
 public class GenerateRkpKeyService extends Service {
     private static final int KEY_GENERATION_PAUSE_MS = 1000;
-    private static final Duration LOOKAHEAD_TIME = Duration.ofDays(1);
 
     private static final String SERVICE = "android.security.remoteprovisioning";
     private static final String TAG = "RemoteProvisioningService";
@@ -104,8 +103,10 @@
 
                 Context context = getApplicationContext();
                 int keysToProvision =
-                        PeriodicProvisioner.generateNumKeysNeeded(binder, context,
-                                LOOKAHEAD_TIME.toMillis(),
+                        PeriodicProvisioner.generateNumKeysNeeded(
+                                binder,
+                                context,
+                                SettingsManager.getExpirationTime(context).toEpochMilli(),
                                 secLevel);
                 if (keysToProvision != 0) {
                     Log.i(TAG, "All signed keys are currently in use, provisioning more.");
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
index 42281f9..975370c 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.Manifest;
 import android.content.Context;
@@ -49,6 +50,7 @@
 import com.android.bedstead.nene.permissions.PermissionContext;
 import com.android.remoteprovisioner.GeekResponse;
 import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.RemoteProvisioningException;
 import com.android.remoteprovisioner.ServerInterface;
 import com.android.remoteprovisioner.SettingsManager;
 
@@ -187,11 +189,13 @@
         sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
         assertPoolStatus(numTestKeys, 0, 0, 0, mDuration);
         GeekResponse geek = ServerInterface.fetchGeek(sContext);
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
         assertNotNull(geek);
         int numProvisioned =
                 Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
                                            geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
                                            sContext);
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
         assertEquals(numTestKeys, numProvisioned);
         assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
         // Certificate duration sent back from the server may change, however ~6 months should be
@@ -245,6 +249,32 @@
     }
 
     @Test
+    public void testDataBudgetEmptyFetchGeek() throws Exception {
+        // Check the data budget in order to initialize a rolling window.
+        assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+        SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+        try {
+            ServerInterface.fetchGeek(sContext);
+            fail("Network transaction should not have proceeded.");
+        } catch (RemoteProvisioningException e) {
+            return;
+        }
+    }
+
+    @Test
+    public void testDataBudgetEmptySignCerts() throws Exception {
+        // Check the data budget in order to initialize a rolling window.
+        assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+        SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+        try {
+            ServerInterface.requestSignedCertificates(sContext, null, null);
+            fail("Network transaction should not have proceeded.");
+        } catch (RemoteProvisioningException e) {
+            return;
+        }
+    }
+
+    @Test
     public void testRetryableRkpError() throws Exception {
         try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) {
             SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */,
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
index 7db20a9..988d557 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
@@ -17,6 +17,7 @@
 package com.android.remoteprovisioner.unittest;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
@@ -33,6 +34,7 @@
 import org.junit.runner.RunWith;
 
 import java.time.Duration;
+import java.time.Instant;
 
 @RunWith(AndroidJUnit4.class)
 public class SettingsManagerTest {
@@ -80,6 +82,24 @@
     }
 
     @Test
+    public void testResetDefaults() throws Exception {
+        int extraKeys = 12;
+        Duration expiringBy = Duration.ofMillis(1000);
+        String url = "https://www.remoteprovisionalot";
+        assertTrue("Method did not return true on write.",
+                   SettingsManager.setDeviceConfig(sContext, extraKeys, expiringBy, url));
+        SettingsManager.incrementFailureCounter(sContext);
+        SettingsManager.resetDefaultConfig(sContext);
+        assertEquals(SettingsManager.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+                     SettingsManager.getExtraSignedKeysAvailable(sContext));
+        assertEquals(SettingsManager.EXPIRING_BY_MS_DEFAULT,
+                     SettingsManager.getExpiringBy(sContext).toMillis());
+        assertEquals(SettingsManager.URL_DEFAULT,
+                     SettingsManager.getUrl(sContext));
+        assertEquals(0, SettingsManager.getFailureCounter(sContext));
+    }
+
+    @Test
     public void testSetDeviceConfig() {
         int extraKeys = 12;
         Duration expiringBy = Duration.ofMillis(1000);
@@ -92,6 +112,14 @@
     }
 
     @Test
+    public void testGetExpirationTime() {
+        long expiringBy = SettingsManager.getExpiringBy(sContext).toMillis();
+        long timeDif = SettingsManager.getExpirationTime(sContext).toEpochMilli()
+                       - (expiringBy + System.currentTimeMillis());
+        assertTrue(Math.abs(timeDif) < 1000);
+    }
+
+    @Test
     public void testFailureCounter() {
         assertEquals(1, SettingsManager.incrementFailureCounter(sContext));
         assertEquals(1, SettingsManager.getFailureCounter(sContext));
@@ -103,4 +131,62 @@
         SettingsManager.incrementFailureCounter(sContext);
         assertEquals(1, SettingsManager.getFailureCounter(sContext));
     }
+
+    @Test
+    public void testDataBudgetUnused() {
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+    }
+
+    @Test
+    public void testDataBudgetIncrement() {
+        int[] bytesUsed = new int[]{1, 40, 100};
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+
+        SettingsManager.consumeErrDataBudget(sContext, bytesUsed[0]);
+        assertEquals(bytesUsed[0], SettingsManager.getErrDataBudgetConsumed(sContext));
+
+        SettingsManager.consumeErrDataBudget(sContext, bytesUsed[1]);
+        assertEquals(bytesUsed[0] + bytesUsed[1],
+                     SettingsManager.getErrDataBudgetConsumed(sContext));
+
+        SettingsManager.consumeErrDataBudget(sContext, bytesUsed[2]);
+        assertEquals(bytesUsed[0] + bytesUsed[1] + bytesUsed[2],
+                     SettingsManager.getErrDataBudgetConsumed(sContext));
+    }
+
+    @Test
+    public void testDataBudgetInvalidIncrement() {
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+        SettingsManager.consumeErrDataBudget(sContext, -20);
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+        SettingsManager.consumeErrDataBudget(sContext, 40);
+        SettingsManager.consumeErrDataBudget(sContext, -400);
+        SettingsManager.consumeErrDataBudget(sContext, 60);
+        assertEquals(100, SettingsManager.getErrDataBudgetConsumed(sContext));
+    }
+
+    @Test
+    public void testDataBudgetReset() {
+        // The first call to hasErrDataBudget will set the start of the bucket.
+        assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+
+        SettingsManager.consumeErrDataBudget(sContext, 100);
+        assertTrue(SettingsManager.hasErrDataBudget(sContext, null));
+        assertEquals(100, SettingsManager.getErrDataBudgetConsumed(sContext));
+
+        assertTrue(SettingsManager.hasErrDataBudget(sContext,
+                Instant.now().plusMillis(SettingsManager.FAILURE_DATA_USAGE_WINDOW.toMillis()
+                                         + 20)));
+        assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+    }
+
+    @Test
+    public void testDataBudgetExceeded() {
+        // The first call to hasErrDataBudget will set the start of the bucket.
+        assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+        SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX - 1);
+        assertTrue(SettingsManager.hasErrDataBudget(sContext, null));
+        SettingsManager.consumeErrDataBudget(sContext, 1);
+        assertFalse(SettingsManager.hasErrDataBudget(sContext, null));
+    }
 }
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
index ec35a0c..cb664c8 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
@@ -69,6 +69,8 @@
 import java.security.PublicKey;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Random;
@@ -197,10 +199,11 @@
             for (int j = 0; j < certChain[i].length; j++) {
                 os.write(certChain[i][j].getEncoded());
             }
+            Instant expiringBy = Instant.now().plusMillis(Duration.ofDays(4).toMillis());
             SystemInterface.provisionCertChain(X509Utils.getAndFormatRawPublicKey(certChain[i][0]),
                                                certChain[i][0].getEncoded() /* leafCert */,
                                                os.toByteArray() /* certChain */,
-                                               System.currentTimeMillis() + 25000 /* validity */,
+                                               expiringBy.toEpochMilli() /* validity */,
                                                SecurityLevel.TRUSTED_ENVIRONMENT,
                                                mBinder);
         }