DO NOT MERGE Create a key value settings observer for backup parameters

Extracts an abstract class to observe changes in backup parameter settings that
are stored as a comma-separated key value list. This class is
responsible for registering and unregistering a content observer on the
setting and updating local references to the parameters.

Refactor BackupManagerConstants and LocalTransportParameters to use this
implementation. This will also be used for the new backup timeout
setting.

Bug: 74346317
Test: 1) m -j RunFrameworksServicesRoboTests ROBOTEST_FILTER=BackupManagerConstantsTest
2) gts-tradefed run commandAndExit gts-dev -m GtsBackupHostTestCases -t com.google.android.gts.backup.TransportFlagsHostSideTest

Change-Id: Id4c50fbcf7479c925515887e3fa70e166dd9955c
diff --git a/core/java/android/util/KeyValueSettingObserver.java b/core/java/android/util/KeyValueSettingObserver.java
new file mode 100644
index 0000000..9fca8b2
--- /dev/null
+++ b/core/java/android/util/KeyValueSettingObserver.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+/**
+ * Abstract class for observing changes to a specified setting stored as a comma-separated key value
+ * list of parameters. Registers and unregisters a {@link ContentObserver} and handles updates when
+ * the setting changes.
+ *
+ * <p>Subclasses should pass in the relevant setting's {@link Uri} in the constructor and implement
+ * {@link #update(KeyValueListParser)} to receive updates when the value changes.
+ * Calls to {@link #update(KeyValueListParser)} only trigger after calling {@link
+ * #start()}.
+ *
+ * <p>To get the most up-to-date parameter values, first call {@link #start()} before accessing the
+ * values to start observing changes, and then call {@link #stop()} once finished.
+ *
+ * @hide
+ */
+public abstract class KeyValueSettingObserver {
+    private static final String TAG = "KeyValueSettingObserver";
+
+    private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+    private final ContentObserver mObserver;
+    private final ContentResolver mResolver;
+    private final Uri mSettingUri;
+
+    public KeyValueSettingObserver(Handler handler, ContentResolver resolver,
+            Uri uri) {
+        mObserver = new SettingObserver(handler);
+        mResolver = resolver;
+        mSettingUri = uri;
+    }
+
+    /** Starts observing changes for the setting. Pair with {@link #stop()}. */
+    public void start() {
+        mResolver.registerContentObserver(mSettingUri, false, mObserver);
+        setParserValue();
+        update(mParser);
+    }
+
+    /** Stops observing changes for the setting. */
+    public void stop() {
+        mResolver.unregisterContentObserver(mObserver);
+    }
+
+    /**
+     * Returns the {@link String} representation of the setting. Subclasses should implement this
+     * for their setting.
+     */
+    public abstract String getSettingValue(ContentResolver resolver);
+
+    /** Updates the parser with the current setting value. */
+    private void setParserValue() {
+        String setting = getSettingValue(mResolver);
+        try {
+            mParser.setString(setting);
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, "Malformed setting: " + setting);
+        }
+    }
+
+    /** Subclasses should implement this to update references to their parameters. */
+    public abstract void update(KeyValueListParser parser);
+
+    private class SettingObserver extends ContentObserver {
+        private SettingObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            setParserValue();
+            update(mParser);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/backup/LocalTransportParameters.java b/core/java/com/android/internal/backup/LocalTransportParameters.java
index 390fae9..154e79d 100644
--- a/core/java/com/android/internal/backup/LocalTransportParameters.java
+++ b/core/java/com/android/internal/backup/LocalTransportParameters.java
@@ -16,62 +16,32 @@
 
 package com.android.internal.backup;
 
+import android.util.KeyValueSettingObserver;
 import android.content.ContentResolver;
-import android.database.ContentObserver;
 import android.os.Handler;
 import android.provider.Settings;
 import android.util.KeyValueListParser;
-import android.util.Slog;
 
-class LocalTransportParameters {
+class LocalTransportParameters extends KeyValueSettingObserver {
     private static final String TAG = "LocalTransportParams";
     private static final String SETTING = Settings.Secure.BACKUP_LOCAL_TRANSPORT_PARAMETERS;
     private static final String KEY_FAKE_ENCRYPTION_FLAG = "fake_encryption_flag";
 
-    private final KeyValueListParser mParser = new KeyValueListParser(',');
-    private final ContentObserver mObserver;
-    private final ContentResolver mResolver;
     private boolean mFakeEncryptionFlag;
 
     LocalTransportParameters(Handler handler, ContentResolver resolver) {
-        mObserver = new Observer(handler);
-        mResolver = resolver;
-    }
-
-    /** Observes for changes in the setting. This method MUST be paired with {@link #stop()}. */
-    void start() {
-        mResolver.registerContentObserver(Settings.Secure.getUriFor(SETTING), false, mObserver);
-        update();
-    }
-
-    /** Stop observing for changes in the setting. */
-    void stop() {
-        mResolver.unregisterContentObserver(mObserver);
+        super(handler, resolver, Settings.Secure.getUriFor(SETTING));
     }
 
     boolean isFakeEncryptionFlag() {
         return mFakeEncryptionFlag;
     }
 
-    private void update() {
-        String parameters = "";
-        try {
-            parameters = Settings.Secure.getString(mResolver, SETTING);
-        } catch (IllegalArgumentException e) {
-            Slog.e(TAG, "Malformed " + SETTING + " setting: " + e.getMessage());
-        }
-        mParser.setString(parameters);
-        mFakeEncryptionFlag = mParser.getBoolean(KEY_FAKE_ENCRYPTION_FLAG, false);
+    public String getSettingValue(ContentResolver resolver) {
+        return Settings.Secure.getString(resolver, SETTING);
     }
 
-    private class Observer extends ContentObserver {
-        private Observer(Handler handler) {
-            super(handler);
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            update();
-        }
+    public void update(KeyValueListParser parser) {
+        mFakeEncryptionFlag = parser.getBoolean(KEY_FAKE_ENCRYPTION_FLAG, false);
     }
 }
diff --git a/services/backup/java/com/android/server/backup/BackupManagerConstants.java b/services/backup/java/com/android/server/backup/BackupManagerConstants.java
index 99160c2..dd6e6ab 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerConstants.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerConstants.java
@@ -18,53 +18,76 @@
 
 import android.app.AlarmManager;
 import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.net.Uri;
 import android.os.Handler;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.KeyValueListParser;
+import android.util.KeyValueSettingObserver;
 import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
 
 /**
  * Class to access backup manager constants.
  *
- * The backup manager constants are encoded as a key value list separated by commas
- * and stored as a Settings.Secure.
+ * <p>The backup manager constants are encoded as a key value list separated by commas and stored as
+ * a Settings.Secure.
  */
-class BackupManagerConstants extends ContentObserver {
+class BackupManagerConstants extends KeyValueSettingObserver {
     private static final String TAG = "BackupManagerConstants";
+    private static final String SETTING = Settings.Secure.BACKUP_MANAGER_CONSTANTS;
 
     // Key names stored in the secure settings value.
-    private static final String KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
             "key_value_backup_interval_milliseconds";
-    private static final String KEY_VALUE_BACKUP_FUZZ_MILLISECONDS =
+
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_FUZZ_MILLISECONDS =
             "key_value_backup_fuzz_milliseconds";
-    private static final String KEY_VALUE_BACKUP_REQUIRE_CHARGING =
+
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_REQUIRE_CHARGING =
             "key_value_backup_require_charging";
-    private static final String KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE =
+
+    @VisibleForTesting
+    public static final String KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE =
             "key_value_backup_required_network_type";
-    private static final String FULL_BACKUP_INTERVAL_MILLISECONDS =
+
+    @VisibleForTesting
+    public static final String FULL_BACKUP_INTERVAL_MILLISECONDS =
             "full_backup_interval_milliseconds";
-    private static final String FULL_BACKUP_REQUIRE_CHARGING =
-            "full_backup_require_charging";
-    private static final String FULL_BACKUP_REQUIRED_NETWORK_TYPE =
+
+    @VisibleForTesting
+    public static final String FULL_BACKUP_REQUIRE_CHARGING = "full_backup_require_charging";
+
+    @VisibleForTesting
+    public static final String FULL_BACKUP_REQUIRED_NETWORK_TYPE =
             "full_backup_required_network_type";
-    private static final String BACKUP_FINISHED_NOTIFICATION_RECEIVERS =
+
+    @VisibleForTesting
+    public static final String BACKUP_FINISHED_NOTIFICATION_RECEIVERS =
             "backup_finished_notification_receivers";
 
     // Hard coded default values.
-    private static final long DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
+    @VisibleForTesting
+    public static final long DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
             4 * AlarmManager.INTERVAL_HOUR;
-    private static final long DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS =
-            10 * 60 * 1000;
-    private static final boolean DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING = true;
-    private static final int DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE = 1;
-    private static final long DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS =
+
+    @VisibleForTesting
+    public static final long DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS = 10 * 60 * 1000;
+
+    @VisibleForTesting public static final boolean DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING = true;
+    @VisibleForTesting public static final int DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE = 1;
+
+    @VisibleForTesting
+    public static final long DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS =
             24 * AlarmManager.INTERVAL_HOUR;
-    private static final boolean DEFAULT_FULL_BACKUP_REQUIRE_CHARGING = true;
-    private static final int DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE = 2;
-    private static final String DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS = "";
+
+    @VisibleForTesting public static final boolean DEFAULT_FULL_BACKUP_REQUIRE_CHARGING = true;
+    @VisibleForTesting public static final int DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE = 2;
+
+    @VisibleForTesting
+    public static final String DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS = "";
 
     // Backup manager constants.
     private long mKeyValueBackupIntervalMilliseconds;
@@ -76,49 +99,46 @@
     private int mFullBackupRequiredNetworkType;
     private String[] mBackupFinishedNotificationReceivers;
 
-    private ContentResolver mResolver;
-    private final KeyValueListParser mParser = new KeyValueListParser(',');
-
     public BackupManagerConstants(Handler handler, ContentResolver resolver) {
-        super(handler);
-        mResolver = resolver;
-        updateSettings();
-        mResolver.registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.BACKUP_MANAGER_CONSTANTS), false, this);
+        super(handler, resolver, Settings.Secure.getUriFor(SETTING));
     }
 
-    @Override
-    public void onChange(boolean selfChange, Uri uri) {
-        updateSettings();
+    public String getSettingValue(ContentResolver resolver) {
+        return Settings.Secure.getString(resolver, SETTING);
     }
 
-    private synchronized void updateSettings() {
-        try {
-            mParser.setString(Settings.Secure.getString(mResolver,
-                    Settings.Secure.BACKUP_MANAGER_CONSTANTS));
-        } catch (IllegalArgumentException e) {
-            // Failed to parse the settings string. Use defaults.
-            Slog.e(TAG, "Bad backup manager constants: " + e.getMessage());
-        }
-
-        mKeyValueBackupIntervalMilliseconds = mParser.getLong(
-                KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS,
-                DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
-        mKeyValueBackupFuzzMilliseconds = mParser.getLong(KEY_VALUE_BACKUP_FUZZ_MILLISECONDS,
-                DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
-        mKeyValueBackupRequireCharging = mParser.getBoolean(KEY_VALUE_BACKUP_REQUIRE_CHARGING,
-                DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
-        mKeyValueBackupRequiredNetworkType = mParser.getInt(KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE,
-                DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
-        mFullBackupIntervalMilliseconds = mParser.getLong(FULL_BACKUP_INTERVAL_MILLISECONDS,
-                DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
-        mFullBackupRequireCharging = mParser.getBoolean(FULL_BACKUP_REQUIRE_CHARGING,
-                DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
-        mFullBackupRequiredNetworkType = mParser.getInt(FULL_BACKUP_REQUIRED_NETWORK_TYPE,
-                DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
-        String backupFinishedNotificationReceivers = mParser.getString(
-                BACKUP_FINISHED_NOTIFICATION_RECEIVERS,
-                DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS);
+    public synchronized void update(KeyValueListParser parser) {
+        mKeyValueBackupIntervalMilliseconds =
+                parser.getLong(
+                        KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS,
+                        DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
+        mKeyValueBackupFuzzMilliseconds =
+                parser.getLong(
+                        KEY_VALUE_BACKUP_FUZZ_MILLISECONDS,
+                        DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+        mKeyValueBackupRequireCharging =
+                parser.getBoolean(
+                        KEY_VALUE_BACKUP_REQUIRE_CHARGING,
+                        DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
+        mKeyValueBackupRequiredNetworkType =
+                parser.getInt(
+                        KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE,
+                        DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+        mFullBackupIntervalMilliseconds =
+                parser.getLong(
+                        FULL_BACKUP_INTERVAL_MILLISECONDS,
+                        DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
+        mFullBackupRequireCharging =
+                parser.getBoolean(
+                        FULL_BACKUP_REQUIRE_CHARGING, DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
+        mFullBackupRequiredNetworkType =
+                parser.getInt(
+                        FULL_BACKUP_REQUIRED_NETWORK_TYPE,
+                        DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+        String backupFinishedNotificationReceivers =
+                parser.getString(
+                        BACKUP_FINISHED_NOTIFICATION_RECEIVERS,
+                        DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS);
         if (backupFinishedNotificationReceivers.isEmpty()) {
             mBackupFinishedNotificationReceivers = new String[] {};
         } else {
@@ -132,40 +152,50 @@
     // a reference of this object.
     public synchronized long getKeyValueBackupIntervalMilliseconds() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getKeyValueBackupIntervalMilliseconds(...) returns "
-                    + mKeyValueBackupIntervalMilliseconds);
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupIntervalMilliseconds(...) returns "
+                            + mKeyValueBackupIntervalMilliseconds);
         }
         return mKeyValueBackupIntervalMilliseconds;
     }
 
     public synchronized long getKeyValueBackupFuzzMilliseconds() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getKeyValueBackupFuzzMilliseconds(...) returns "
-                    + mKeyValueBackupFuzzMilliseconds);
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupFuzzMilliseconds(...) returns "
+                            + mKeyValueBackupFuzzMilliseconds);
         }
         return mKeyValueBackupFuzzMilliseconds;
     }
 
     public synchronized boolean getKeyValueBackupRequireCharging() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getKeyValueBackupRequireCharging(...) returns "
-                    + mKeyValueBackupRequireCharging);
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupRequireCharging(...) returns "
+                            + mKeyValueBackupRequireCharging);
         }
         return mKeyValueBackupRequireCharging;
     }
 
     public synchronized int getKeyValueBackupRequiredNetworkType() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getKeyValueBackupRequiredNetworkType(...) returns "
-                    + mKeyValueBackupRequiredNetworkType);
+            Slog.v(
+                    TAG,
+                    "getKeyValueBackupRequiredNetworkType(...) returns "
+                            + mKeyValueBackupRequiredNetworkType);
         }
         return mKeyValueBackupRequiredNetworkType;
     }
 
     public synchronized long getFullBackupIntervalMilliseconds() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getFullBackupIntervalMilliseconds(...) returns "
-                    + mFullBackupIntervalMilliseconds);
+            Slog.v(
+                    TAG,
+                    "getFullBackupIntervalMilliseconds(...) returns "
+                            + mFullBackupIntervalMilliseconds);
         }
         return mFullBackupIntervalMilliseconds;
     }
@@ -179,19 +209,21 @@
 
     public synchronized int getFullBackupRequiredNetworkType() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getFullBackupRequiredNetworkType(...) returns "
-                    + mFullBackupRequiredNetworkType);
+            Slog.v(
+                    TAG,
+                    "getFullBackupRequiredNetworkType(...) returns "
+                            + mFullBackupRequiredNetworkType);
         }
         return mFullBackupRequiredNetworkType;
     }
 
-    /**
-     * Returns an array of package names that should be notified whenever a backup finishes.
-     */
+    /** Returns an array of package names that should be notified whenever a backup finishes. */
     public synchronized String[] getBackupFinishedNotificationReceivers() {
         if (BackupManagerService.DEBUG_SCHEDULING) {
-            Slog.v(TAG, "getBackupFinishedNotificationReceivers(...) returns "
-                    + TextUtils.join(", ", mBackupFinishedNotificationReceivers));
+            Slog.v(
+                    TAG,
+                    "getBackupFinishedNotificationReceivers(...) returns "
+                            + TextUtils.join(", ", mBackupFinishedNotificationReceivers));
         }
         return mBackupFinishedNotificationReceivers;
     }
diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java
index 6469091..b6e2dca 100644
--- a/services/backup/java/com/android/server/backup/BackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/BackupManagerService.java
@@ -851,6 +851,10 @@
         mJournal = null;        // will be created on first use
 
         mConstants = new BackupManagerConstants(mBackupHandler, mContext.getContentResolver());
+        // We are observing changes to the constants throughout the lifecycle of BMS. This is
+        // because we reference the constants in multiple areas of BMS, which otherwise would
+        // require frequent starting and stopping.
+        mConstants.start();
 
         // Set up the various sorts of package tracking we do
         mFullBackupScheduleFile = new File(mBaseStateDir, "fb-schedule");
diff --git a/services/robotests/Android.mk b/services/robotests/Android.mk
index aed57e3..3d7fdbdd 100644
--- a/services/robotests/Android.mk
+++ b/services/robotests/Android.mk
@@ -62,7 +62,8 @@
     $(call all-java-files-under, ../../core/java/android/app/backup) \
     $(call all-Iaidl-files-under, ../../core/java/android/app/backup) \
     ../../core/java/android/content/pm/PackageInfo.java \
-    ../../core/java/android/app/IBackupAgent.aidl
+    ../../core/java/android/app/IBackupAgent.aidl \
+    ../../core/java/android/util/KeyValueSettingObserver.java
 
 LOCAL_AIDL_INCLUDES := \
     $(call all-Iaidl-files-under, $(INTERNAL_BACKUP)) \
diff --git a/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java b/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java
index 0752537..2a32c2e 100644
--- a/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java
+++ b/services/robotests/src/com/android/server/backup/BackupManagerConstantsTest.java
@@ -18,79 +18,218 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.AlarmManager;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Handler;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
-
+import android.util.KeyValueSettingObserver;
 import com.android.server.testing.FrameworkRobolectricTestRunner;
 import com.android.server.testing.SystemLoaderClasses;
 import com.android.server.testing.SystemLoaderPackages;
-
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.MockitoAnnotations;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
 @RunWith(FrameworkRobolectricTestRunner.class)
 @Config(manifest = Config.NONE, sdk = 26)
 @SystemLoaderPackages({"com.android.server.backup"})
+@SystemLoaderClasses({KeyValueSettingObserver.class})
 @Presubmit
 public class BackupManagerConstantsTest {
     private static final String PACKAGE_NAME = "some.package.name";
     private static final String ANOTHER_PACKAGE_NAME = "another.package.name";
 
+    private ContentResolver mContentResolver;
+    private BackupManagerConstants mConstants;
+
     @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
+    public void setUp() {
+        final Context context = RuntimeEnvironment.application.getApplicationContext();
+
+        mContentResolver = context.getContentResolver();
+        mConstants = new BackupManagerConstants(new Handler(), mContentResolver);
+        mConstants.start();
+    }
+
+    @After
+    public void tearDown() {
+        mConstants.stop();
     }
 
     @Test
-    public void testDefaultValues() throws Exception {
-        final Context context = RuntimeEnvironment.application.getApplicationContext();
-        final Handler handler = new Handler();
+    public void testGetConstants_afterConstructorWithStart_returnsDefaultValues() {
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        long keyValueBackupFuzzMilliseconds = mConstants.getKeyValueBackupFuzzMilliseconds();
+        boolean keyValueBackupRequireCharging = mConstants.getKeyValueBackupRequireCharging();
+        int keyValueBackupRequiredNetworkType = mConstants.getKeyValueBackupRequiredNetworkType();
+        long fullBackupIntervalMilliseconds = mConstants.getFullBackupIntervalMilliseconds();
+        boolean fullBackupRequireCharging = mConstants.getFullBackupRequireCharging();
+        int fullBackupRequiredNetworkType = mConstants.getFullBackupRequiredNetworkType();
+        String[] backupFinishedNotificationReceivers =
+                mConstants.getBackupFinishedNotificationReceivers();
 
-        Settings.Secure.putString(
-                context.getContentResolver(), Settings.Secure.BACKUP_MANAGER_CONSTANTS, null);
+        assertThat(keyValueBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(keyValueBackupFuzzMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+        assertThat(keyValueBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
+        assertThat(keyValueBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(fullBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(fullBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
+        assertThat(fullBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(backupFinishedNotificationReceivers).isEqualTo(new String[0]);
+    }
 
-        final BackupManagerConstants constants =
-                new BackupManagerConstants(handler, context.getContentResolver());
+    /**
+     * Tests that if there is a setting change when we are not currently observing the setting, that
+     * once we start observing again, we receive the most up-to-date value.
+     */
+    @Test
+    public void testGetConstant_withSettingChangeBeforeStart_updatesValues() {
+        mConstants.stop();
+        long testInterval =
+                BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS * 2;
+        final String setting =
+                BackupManagerConstants.KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS + "=" + testInterval;
+        putStringAndNotify(setting);
 
-        assertThat(constants.getKeyValueBackupIntervalMilliseconds())
-                .isEqualTo(4 * AlarmManager.INTERVAL_HOUR);
-        assertThat(constants.getKeyValueBackupFuzzMilliseconds()).isEqualTo(10 * 60 * 1000);
-        assertThat(constants.getKeyValueBackupRequireCharging()).isEqualTo(true);
-        assertThat(constants.getKeyValueBackupRequiredNetworkType()).isEqualTo(1);
+        mConstants.start();
 
-        assertThat(constants.getFullBackupIntervalMilliseconds())
-                .isEqualTo(24 * AlarmManager.INTERVAL_HOUR);
-        assertThat(constants.getFullBackupRequireCharging()).isEqualTo(true);
-        assertThat(constants.getFullBackupRequiredNetworkType()).isEqualTo(2);
-        assertThat(constants.getBackupFinishedNotificationReceivers()).isEqualTo(new String[0]);
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        assertThat(keyValueBackupIntervalMilliseconds).isEqualTo(testInterval);
     }
 
     @Test
-    public void testParseNotificationReceivers() throws Exception {
-        final Context context = RuntimeEnvironment.application.getApplicationContext();
-        final Handler handler = new Handler();
+    public void testGetConstants_whenSettingIsNull_returnsDefaultValues() {
+        putStringAndNotify(null);
 
-        final String recieversSetting =
-                "backup_finished_notification_receivers="
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        long keyValueBackupFuzzMilliseconds = mConstants.getKeyValueBackupFuzzMilliseconds();
+        boolean keyValueBackupRequireCharging = mConstants.getKeyValueBackupRequireCharging();
+        int keyValueBackupRequiredNetworkType = mConstants.getKeyValueBackupRequiredNetworkType();
+        long fullBackupIntervalMilliseconds = mConstants.getFullBackupIntervalMilliseconds();
+        boolean fullBackupRequireCharging = mConstants.getFullBackupRequireCharging();
+        int fullBackupRequiredNetworkType = mConstants.getFullBackupRequiredNetworkType();
+        String[] backupFinishedNotificationReceivers =
+                mConstants.getBackupFinishedNotificationReceivers();
+
+        assertThat(keyValueBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(keyValueBackupFuzzMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+        assertThat(keyValueBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRE_CHARGING);
+        assertThat(keyValueBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(fullBackupIntervalMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS);
+        assertThat(fullBackupRequireCharging)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
+        assertThat(fullBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+        assertThat(backupFinishedNotificationReceivers).isEqualTo(new String[0]);
+    }
+
+    /**
+     * Test passing in a malformed setting string. The setting expects
+     * "key1=value,key2=value,key3=value..." but we pass in "key1=,value"
+     */
+    @Test
+    public void testGetConstant_whenSettingIsMalformed_doesNotUpdateParamsOrThrow() {
+        long testFuzz = BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS * 2;
+        final String invalidSettingFormat =
+                BackupManagerConstants.KEY_VALUE_BACKUP_FUZZ_MILLISECONDS + "=," + testFuzz;
+        putStringAndNotify(invalidSettingFormat);
+
+        long keyValueBackupFuzzMilliseconds = mConstants.getKeyValueBackupFuzzMilliseconds();
+
+        assertThat(keyValueBackupFuzzMilliseconds)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_FUZZ_MILLISECONDS);
+    }
+
+    /**
+     * Test passing in an invalid value type. {@link
+     * BackupManagerConstants#KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE} expects an integer, but we
+     * pass in a boolean.
+     */
+    @Test
+    public void testGetConstant_whenSettingHasInvalidType_doesNotUpdateParamsOrThrow() {
+        boolean testValue = true;
+        final String invalidSettingType =
+                BackupManagerConstants.KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE + "=" + testValue;
+        putStringAndNotify(invalidSettingType);
+
+        int keyValueBackupRequiredNetworkType = mConstants.getKeyValueBackupRequiredNetworkType();
+
+        assertThat(keyValueBackupRequiredNetworkType)
+                .isEqualTo(BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_REQUIRED_NETWORK_TYPE);
+    }
+
+    @Test
+    public void testGetConstants_afterSettingChange_updatesValues() {
+        long testKVInterval =
+                BackupManagerConstants.DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS * 2;
+        long testFullInterval =
+                BackupManagerConstants.DEFAULT_FULL_BACKUP_INTERVAL_MILLISECONDS * 2;
+        final String intervalSetting =
+                BackupManagerConstants.KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS
+                        + "="
+                        + testKVInterval
+                        + ","
+                        + BackupManagerConstants.FULL_BACKUP_INTERVAL_MILLISECONDS
+                        + "="
+                        + testFullInterval;
+        putStringAndNotify(intervalSetting);
+
+        long keyValueBackupIntervalMilliseconds =
+                mConstants.getKeyValueBackupIntervalMilliseconds();
+        long fullBackupIntervalMilliseconds = mConstants.getFullBackupIntervalMilliseconds();
+
+        assertThat(keyValueBackupIntervalMilliseconds).isEqualTo(testKVInterval);
+        assertThat(fullBackupIntervalMilliseconds).isEqualTo(testFullInterval);
+    }
+
+    @Test
+    public void testBackupNotificationReceivers_afterSetting_updatesAndParsesCorrectly() {
+        final String receiversSetting =
+                BackupManagerConstants.BACKUP_FINISHED_NOTIFICATION_RECEIVERS
+                        + "="
                         + PACKAGE_NAME
                         + ':'
                         + ANOTHER_PACKAGE_NAME;
-        Settings.Secure.putString(
-                context.getContentResolver(),
-                Settings.Secure.BACKUP_MANAGER_CONSTANTS,
-                recieversSetting);
+        putStringAndNotify(receiversSetting);
 
-        final BackupManagerConstants constants =
-                new BackupManagerConstants(handler, context.getContentResolver());
-
-        assertThat(constants.getBackupFinishedNotificationReceivers())
+        String[] backupFinishedNotificationReceivers =
+                mConstants.getBackupFinishedNotificationReceivers();
+        assertThat(backupFinishedNotificationReceivers)
                 .isEqualTo(new String[] {PACKAGE_NAME, ANOTHER_PACKAGE_NAME});
     }
+
+    /**
+     * Robolectric does not notify observers of changes to settings so we have to trigger it here.
+     * Currently, the mock of {@link Settings.Secure#putString(ContentResolver, String, String)}
+     * only stores the value. TODO: Implement properly in ShadowSettings.
+     */
+    private void putStringAndNotify(String value) {
+        Settings.Secure.putString(
+                mContentResolver, Settings.Secure.BACKUP_MANAGER_CONSTANTS, value);
+
+        // We pass null as the observer since notifyChange iterates over all available observers and
+        // we don't have access to the local observer.
+        mContentResolver.notifyChange(
+                Settings.Secure.getUriFor(Settings.Secure.BACKUP_MANAGER_CONSTANTS),
+                /*observer*/ null);
+    }
 }