Backup device information

Adding the device information to the backup will allow us to restore
data which is device specific if it's appropriate to do so (e.g. device
density)

The device specific settings which will be backed up are;

Settings.Secure.DISPLAY_DENSITY_FORCED

Test: atest SettingsProviderTest SettingsBackupTest
Test: Manual - Density changes during SUW when setting is restored
Fixes: 28437818
Change-Id: Ibc3595cdece3f1ccb4fccaff8212c1c3cb5c9756
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 7df1ebe..2159cf4 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7030,6 +7030,8 @@
          */
         public static final String DISPLAY_DENSITY_FORCED = "display_density_forced";
 
+        static final Validator DISPLAY_DENSITY_FORCED_VALIDATOR = NON_NEGATIVE_INTEGER_VALIDATOR;
+
         /**
          * Setting to always use the default text-to-speech settings regardless
          * of the application settings.
@@ -9083,8 +9085,22 @@
         };
 
         /**
-         * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
-         * otherwise they won't be restored.
+         * The settings values which should only be restored if the target device is the
+         * same as the source device
+         *
+         * NOTE: Settings are backed up and restored in the order they appear
+         *       in this array. If you have one setting depending on another,
+         *       make sure that they are ordered appropriately.
+         *
+         * @hide
+         */
+        public static final String[] DEVICE_SPECIFIC_SETTINGS_TO_BACKUP = {
+                DISPLAY_DENSITY_FORCED,
+        };
+
+        /**
+         * All settings in {@link SETTINGS_TO_BACKUP} and {@link DEVICE_SPECIFIC_SETTINGS_TO_BACKUP}
+         * array *must* have a non-null validator, otherwise they won't be restored.
          *
          * @hide
          */
@@ -9276,6 +9292,7 @@
             VALIDATORS.put(UI_NIGHT_MODE, UI_NIGHT_MODE_VALIDATOR);
             VALIDATORS.put(GLOBAL_ACTIONS_PANEL_ENABLED, GLOBAL_ACTIONS_PANEL_ENABLED_VALIDATOR);
             VALIDATORS.put(AWARE_LOCK_ENABLED, AWARE_LOCK_ENABLED_VALIDATOR);
+            VALIDATORS.put(DISPLAY_DENSITY_FORCED, DISPLAY_DENSITY_FORCED_VALIDATOR);
         }
 
         /**
@@ -13819,6 +13836,9 @@
          *       in this array. If you have one setting depending on another,
          *       make sure that they are ordered appropriately.
          *
+         * NOTE: This table should only be used for settings which should be restored
+         *       between different types of devices {@see #DEVICE_SPECIFIC_SETTINGS_TO_BACKUP}
+         *
          * @hide
          */
         public static final String[] SETTINGS_TO_BACKUP = {
@@ -13863,6 +13883,7 @@
          * @hide
          */
         public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
+
         static {
             VALIDATORS.put(APPLY_RAMPING_RINGER, APPLY_RAMPING_RINGER_VALIDATOR);
             VALIDATORS.put(BUGREPORT_IN_POWER_MENU, BUGREPORT_IN_POWER_MENU_VALIDATOR);
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 89ba3df..bdbf368 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -35,6 +35,7 @@
 import org.junit.runner.RunWith;
 
 import java.lang.reflect.Field;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -625,7 +626,6 @@
                  Settings.Secure.DIALER_DEFAULT_APPLICATION,
                  Settings.Secure.DISABLED_PRINT_SERVICES,
                  Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS,
-                 Settings.Secure.DISPLAY_DENSITY_FORCED,
                  Settings.Secure.DOCKED_CLOCK_FACE,
                  Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
                  Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION,
@@ -742,9 +742,12 @@
 
     @Test
     public void secureSettingsBackedUpOrBlacklisted() {
+        HashSet<String> keys = new HashSet<String>();
+        Collections.addAll(keys, Settings.Secure.SETTINGS_TO_BACKUP);
+        Collections.addAll(keys, Settings.Secure.DEVICE_SPECIFIC_SETTINGS_TO_BACKUP);
         checkSettingsBackedUpOrBlacklisted(
                 getCandidateSettings(Settings.Secure.class),
-                newHashSet(Settings.Secure.SETTINGS_TO_BACKUP),
+                keys,
             BACKUP_BLACKLISTED_SECURE_SETTINGS);
     }
 
@@ -758,9 +761,9 @@
                 is(empty()));
 
         assertThat(
-            "blacklisted settings backed up",
-            intersect(settingsToBackup, blacklist),
-            is(empty()));
+                "blacklisted settings backed up",
+                intersect(settingsToBackup, blacklist),
+                is(empty()));
     }
 
     private static Set<String> getCandidateSettings(Class<? extends Settings.NameValueTable> clazz) {
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 1c97fc3..e54b847 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -9,7 +9,10 @@
         "telephony-common",
         "ims-common",
     ],
-    static_libs: ["junit"],
+    static_libs: [
+        "junit",
+        "SettingsLib",
+    ],
     platform_apis: true,
     certificate: "platform",
     privileged: true,
@@ -21,11 +24,18 @@
     // because this test is not an instrumentation test. (because the target runs in the system process.)
     srcs: [
         "test/**/*.java",
+        "src/com/android/providers/settings/SettingsBackupAgent.java",
         "src/com/android/providers/settings/SettingsState.java",
         "src/com/android/providers/settings/SettingsHelper.java",
     ],
-    static_libs: ["androidx.test.rules"],
-    libs: ["android.test.base"],
+    static_libs: [
+        "androidx.test.rules",
+        "SettingsLib",
+    ],
+    libs: [
+        "android.test.base",
+        "android.test.mock",
+    ],
     resource_dirs: ["res"],
     aaptflags: [
         "--auto-add-overlay",
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 7d56868..2286f4c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -39,9 +39,13 @@
 import android.util.ArraySet;
 import android.util.BackupUtils;
 import android.util.Log;
+import android.util.Slog;
+import android.view.Display;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settingslib.display.DisplayDensityUtils;
 
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
@@ -52,11 +56,14 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.time.DateTimeException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.zip.CRC32;
 
 /**
@@ -78,10 +85,11 @@
     private static final String KEY_SOFTAP_CONFIG = "softap_config";
     private static final String KEY_NETWORK_POLICIES = "network_policies";
     private static final String KEY_WIFI_NEW_CONFIG = "wifi_new_config";
+    private static final String KEY_DEVICE_SPECIFIC_CONFIG = "device_specific_config";
 
     // Versioning of the state file.  Increment this version
     // number any time the set of state items is altered.
-    private static final int STATE_VERSION = 7;
+    private static final int STATE_VERSION = 8;
 
     // Versioning of the Network Policies backup payload.
     private static final int NETWORK_POLICIES_BACKUP_VERSION = 1;
@@ -99,8 +107,9 @@
     private static final int STATE_SOFTAP_CONFIG    = 7;
     private static final int STATE_NETWORK_POLICIES = 8;
     private static final int STATE_WIFI_NEW_CONFIG  = 9;
+    private static final int STATE_DEVICE_CONFIG    = 10;
 
-    private static final int STATE_SIZE             = 10; // The current number of state items
+    private static final int STATE_SIZE             = 11; // The current number of state items
 
     // Number of entries in the checksum array at various version numbers
     private static final int STATE_SIZES[] = {
@@ -111,17 +120,19 @@
             7,              // version 4 added STATE_LOCK_SETTINGS
             8,              // version 5 added STATE_SOFTAP_CONFIG
             9,              // version 6 added STATE_NETWORK_POLICIES
-            STATE_SIZE      // version 7 added STATE_WIFI_NEW_CONFIG
+            10,             // version 7 added STATE_WIFI_NEW_CONFIG
+            STATE_SIZE      // version 8 added STATE_DEVICE_CONFIG
     };
 
-    // Versioning of the 'full backup' format
-    // Increment this version any time a new item is added
-    private static final int FULL_BACKUP_VERSION = 6;
     private static final int FULL_BACKUP_ADDED_GLOBAL = 2;  // added the "global" entry
     private static final int FULL_BACKUP_ADDED_LOCK_SETTINGS = 3; // added the "lock_settings" entry
     private static final int FULL_BACKUP_ADDED_SOFTAP_CONF = 4; //added the "softap_config" entry
     private static final int FULL_BACKUP_ADDED_NETWORK_POLICIES = 5; //added "network_policies"
     private static final int FULL_BACKUP_ADDED_WIFI_NEW = 6; // added "wifi_new_config" entry
+    private static final int FULL_BACKUP_ADDED_DEVICE_SPECIFIC = 7; // added "device specific" entry
+    // Versioning of the 'full backup' format
+    // Increment this version any time a new item is added
+    private static final int FULL_BACKUP_VERSION = FULL_BACKUP_ADDED_DEVICE_SPECIFIC;
 
     private static final int INTEGER_BYTE_COUNT = Integer.SIZE / Byte.SIZE;
 
@@ -129,11 +140,17 @@
 
     private static final String TAG = "SettingsBackupAgent";
 
-    private static final String[] PROJECTION = {
+    @VisibleForTesting
+    static final String[] PROJECTION = {
             Settings.NameValueTable.NAME,
             Settings.NameValueTable.VALUE
     };
 
+    // Versioning of the 'device specific' section of a backup
+    // Increment this any time the format is changed or data added.
+    @VisibleForTesting
+    static final int DEVICE_SPECIFIC_VERSION = 1;
+
     // the key to store the WIFI data under, should be sorted as last, so restore happens last.
     // use very late unicode character to quasi-guarantee last sort position.
     private static final String KEY_WIFI_SUPPLICANT = "\uffedWIFI";
@@ -161,7 +178,8 @@
                 KEY_GLOBAL,
             }));
 
-    private SettingsHelper mSettingsHelper;
+    @VisibleForTesting
+    SettingsHelper mSettingsHelper;
 
     private WifiManager mWifiManager;
 
@@ -190,6 +208,7 @@
         byte[] softApConfigData = getSoftAPConfiguration();
         byte[] netPoliciesData = getNetworkPolicies();
         byte[] wifiFullConfigData = getNewWifiConfigData();
+        byte[] deviceSpecificInformation = getDeviceSpecificConfiguration();
 
         long[] stateChecksums = readOldChecksums(oldState);
 
@@ -215,6 +234,9 @@
         stateChecksums[STATE_WIFI_NEW_CONFIG] =
                 writeIfChanged(stateChecksums[STATE_WIFI_NEW_CONFIG], KEY_WIFI_NEW_CONFIG,
                         wifiFullConfigData, data);
+        stateChecksums[STATE_DEVICE_CONFIG] =
+                writeIfChanged(stateChecksums[STATE_DEVICE_CONFIG], KEY_DEVICE_SPECIFIC_CONFIG,
+                        deviceSpecificInformation, data);
 
         writeNewChecksums(stateChecksums, newState);
     }
@@ -313,6 +335,12 @@
                     restoreNewWifiConfigData(restoredWifiNewConfigData);
                     break;
 
+                case KEY_DEVICE_SPECIFIC_CONFIG:
+                    byte[] restoredDeviceSpecificConfig = new byte[size];
+                    data.readEntityData(restoredDeviceSpecificConfig, 0, size);
+                    restoreDeviceSpecificConfig(restoredDeviceSpecificConfig);
+                    break;
+
                 default :
                     data.skipEntityData();
 
@@ -591,6 +619,11 @@
 
     private void restoreSettings(byte[] settings, int bytes, Uri contentUri,
             HashSet<String> movedToGlobal, Set<String> movedToSecure) {
+        restoreSettings(settings, 0, bytes, contentUri, movedToGlobal, movedToSecure);
+    }
+
+    private void restoreSettings(byte[] settings, int pos, int bytes, Uri contentUri,
+                HashSet<String> movedToGlobal, Set<String> movedToSecure) {
         if (DEBUG) {
             Log.i(TAG, "restoreSettings: " + contentUri);
         }
@@ -601,7 +634,8 @@
         Map<String, Validator> validators = null;
         if (contentUri.equals(Settings.Secure.CONTENT_URI)) {
             whitelist = ArrayUtils.concatElements(String.class, Settings.Secure.SETTINGS_TO_BACKUP,
-                    Settings.Secure.LEGACY_RESTORE_SETTINGS);
+                    Settings.Secure.LEGACY_RESTORE_SETTINGS,
+                    Settings.Secure.DEVICE_SPECIFIC_SETTINGS_TO_BACKUP);
             validators = Settings.Secure.VALIDATORS;
         } else if (contentUri.equals(Settings.System.CONTENT_URI)) {
             whitelist = ArrayUtils.concatElements(String.class, Settings.System.SETTINGS_TO_BACKUP,
@@ -616,7 +650,6 @@
         }
 
         // Restore only the white list data.
-        int pos = 0;
         final ArrayMap<String, String> cachedEntries = new ArrayMap<>();
         ContentValues contentValues = new ContentValues(2);
         SettingsHelper settingsHelper = mSettingsHelper;
@@ -940,6 +973,150 @@
         }
     }
 
+    @VisibleForTesting
+    byte[] getDeviceSpecificConfiguration() throws IOException {
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            writeHeader(os);
+            os.write(getDeviceSpecificSettings());
+            return os.toByteArray();
+        }
+    }
+
+    @VisibleForTesting
+    void writeHeader(OutputStream os) throws IOException {
+        os.write(toByteArray(DEVICE_SPECIFIC_VERSION));
+        os.write(toByteArray(Build.MANUFACTURER));
+        os.write(toByteArray(Build.PRODUCT));
+    }
+
+    private byte[] getDeviceSpecificSettings() {
+        try (Cursor cursor =
+                     getContentResolver()
+                             .query(Settings.Secure.CONTENT_URI, PROJECTION, null, null, null)) {
+            return extractRelevantValues(
+                    cursor, Settings.Secure.DEVICE_SPECIFIC_SETTINGS_TO_BACKUP);
+        }
+    }
+
+    /**
+     * Restore the device specific settings.
+     *
+     * @param data The byte array holding a backed up version of another devices settings.
+     * @return true if the restore succeeded, false if it was stopped.
+     */
+    @VisibleForTesting
+    boolean restoreDeviceSpecificConfig(byte[] data) {
+        // We're using an AtomicInteger to wrap the position int and allow called methods to
+        // modify it.
+        AtomicInteger pos = new AtomicInteger(0);
+        if (!isSourceAcceptable(data, pos)) {
+            return false;
+        }
+
+        Integer originalDensity = getPreviousDensity();
+
+        int dataStart = pos.get();
+        restoreSettings(
+                data, dataStart, data.length, Settings.Secure.CONTENT_URI, null, null);
+
+        updateWindowManagerIfNeeded(originalDensity);
+
+        return true;
+    }
+
+    private void updateWindowManagerIfNeeded(Integer previousDensity) {
+        int newDensity;
+        try {
+            newDensity = getForcedDensity();
+        } catch (Settings.SettingNotFoundException e) {
+            // If there's not density setting we can't perform a change.
+            return;
+        }
+
+        if (previousDensity == null || previousDensity != newDensity) {
+            // From nothing to something is a change.
+            DisplayDensityUtils.setForcedDisplayDensity(Display.DEFAULT_DISPLAY, newDensity);
+        }
+    }
+
+    private Integer getPreviousDensity() {
+        try {
+            return getForcedDensity();
+        } catch (Settings.SettingNotFoundException e) {
+            return null;
+        }
+    }
+
+    private int getForcedDensity() throws Settings.SettingNotFoundException {
+        return Settings.Secure.getInt(getContentResolver(), Settings.Secure.DISPLAY_DENSITY_FORCED);
+    }
+
+    @VisibleForTesting
+    boolean isSourceAcceptable(byte[] data, AtomicInteger pos) {
+        int version = readInt(data, pos);
+        if (version > DEVICE_SPECIFIC_VERSION) {
+            Slog.w(TAG, "Unable to restore device specific information; Backup is too new");
+            return false;
+        }
+
+        String sourceManufacturer = readString(data, pos);
+        if (!Objects.equals(Build.MANUFACTURER, sourceManufacturer)) {
+            Log.w(
+                    TAG,
+                    "Unable to restore device specific information; Manufacturer mismatch "
+                            + "(\'"
+                            + Build.MANUFACTURER
+                            + "\' and \'"
+                            + sourceManufacturer
+                            + "\')");
+            return false;
+        }
+
+        String sourceProduct = readString(data, pos);
+        if (!Objects.equals(Build.PRODUCT, sourceProduct)) {
+            Log.w(
+                    TAG,
+                    "Unable to restore device specific information; Product mismatch (\'"
+                            + Build.PRODUCT
+                            + "\' and \'"
+                            + sourceProduct
+                            + "\')");
+            return false;
+        }
+
+        return true;
+    }
+
+    @VisibleForTesting
+    static byte[] toByteArray(String value) {
+        if (value == null) {
+            return toByteArray(NULL_SIZE);
+        }
+
+        byte[] stringBytes = value.getBytes();
+        byte[] sizeAndString = new byte[stringBytes.length + INTEGER_BYTE_COUNT];
+        writeInt(sizeAndString, 0, stringBytes.length);
+        writeBytes(sizeAndString, INTEGER_BYTE_COUNT, stringBytes);
+        return sizeAndString;
+    }
+
+    @VisibleForTesting
+    static byte[] toByteArray(int value) {
+        byte[] result = new byte[INTEGER_BYTE_COUNT];
+        writeInt(result, 0, value);
+        return result;
+    }
+
+    private String readString(byte[] data, AtomicInteger pos) {
+        int byteCount = readInt(data, pos);
+        if (byteCount == NULL_SIZE) {
+            return null;
+        }
+
+        int stringStart = pos.getAndAdd(byteCount);
+        return new String(data, stringStart, byteCount);
+    }
+
     /**
      * Write an int in BigEndian into the byte array.
      * @param out byte array
@@ -947,7 +1124,7 @@
      * @param value integer to write
      * @return the index after adding the size of an int (4) in bytes.
      */
-    private int writeInt(byte[] out, int pos, int value) {
+    private static int writeInt(byte[] out, int pos, int value) {
         out[pos + 0] = (byte) ((value >> 24) & 0xFF);
         out[pos + 1] = (byte) ((value >> 16) & 0xFF);
         out[pos + 2] = (byte) ((value >>  8) & 0xFF);
@@ -955,11 +1132,15 @@
         return pos + INTEGER_BYTE_COUNT;
     }
 
-    private int writeBytes(byte[] out, int pos, byte[] value) {
+    private static int writeBytes(byte[] out, int pos, byte[] value) {
         System.arraycopy(value, 0, out, pos, value.length);
         return pos + value.length;
     }
 
+    private int readInt(byte[] in, AtomicInteger pos) {
+        return readInt(in, pos.getAndAdd(INTEGER_BYTE_COUNT));
+    }
+
     private int readInt(byte[] in, int pos) {
         int result = ((in[pos] & 0xFF) << 24)
                 | ((in[pos + 1] & 0xFF) << 16)
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
new file mode 100644
index 0000000..cf8e1a5
--- /dev/null
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2019 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 com.android.providers.settings;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.Settings;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Tests for the SettingsHelperTest */
+@RunWith(AndroidJUnit4.class)
+public class SettingsBackupAgentTest extends BaseSettingsProviderTest {
+
+    private static final String TEST_DISPLAY_DENSITY_FORCED = "123";
+    private static final Map<String, String> TEST_VALUES = new HashMap<>();
+
+    static {
+        TEST_VALUES.put(Settings.Secure.DISPLAY_DENSITY_FORCED, TEST_DISPLAY_DENSITY_FORCED);
+    }
+
+    private TestFriendlySettingsBackupAgent mAgentUnderTest;
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = new ContextWithMockContentResolver(getContext());
+
+        mAgentUnderTest = new TestFriendlySettingsBackupAgent();
+        mAgentUnderTest.attach(mContext);
+    }
+
+    @Test
+    public void testRoundTripDeviceSpecificSettings() throws IOException {
+        TestSettingsHelper helper = new TestSettingsHelper(mContext);
+        mAgentUnderTest.mSettingsHelper = helper;
+
+        byte[] settingsBackup = mAgentUnderTest.getDeviceSpecificConfiguration();
+
+        assertEquals("Not all values backed up.", TEST_VALUES.keySet(), helper.mReadEntries);
+
+        mAgentUnderTest.restoreDeviceSpecificConfig(settingsBackup);
+
+        assertEquals("Not all values were restored.", TEST_VALUES, helper.mWrittenValues);
+    }
+
+    @Test
+    public void testGeneratedHeaderMatchesCurrentDevice() throws IOException {
+        mAgentUnderTest.mSettingsHelper = new TestSettingsHelper(mContext);
+
+        byte[] header = generateUncorruptedHeader();
+
+        AtomicInteger pos = new AtomicInteger(0);
+        assertTrue(
+                "Generated header is not correct for device.",
+                mAgentUnderTest.isSourceAcceptable(header, pos));
+    }
+
+    @Test
+    public void testTestHeaderGeneratorIsAccurate() throws IOException {
+        byte[] classGeneratedHeader = generateUncorruptedHeader();
+        byte[] testGeneratedHeader = generateCorruptedHeader(false, false, false);
+
+        assertArrayEquals(
+                "Difference in header generation", classGeneratedHeader, testGeneratedHeader);
+    }
+
+    @Test
+    public void testNewerHeaderVersionFailsMatch() throws IOException {
+        byte[] header = generateCorruptedHeader(true, false, false);
+
+        AtomicInteger pos = new AtomicInteger(0);
+        assertFalse(
+                "Newer header does not fail match",
+                mAgentUnderTest.isSourceAcceptable(header, pos));
+    }
+
+    @Test
+    public void testWrongManufacturerFailsMatch() throws IOException {
+        byte[] header = generateCorruptedHeader(false, true, false);
+
+        AtomicInteger pos = new AtomicInteger(0);
+        assertFalse(
+                "Wrong manufacturer does not fail match",
+                mAgentUnderTest.isSourceAcceptable(header, pos));
+    }
+
+    @Test
+    public void testWrongProductFailsMatch() throws IOException {
+        byte[] header = generateCorruptedHeader(false, false, true);
+
+        AtomicInteger pos = new AtomicInteger(0);
+        assertFalse(
+                "Wrong product does not fail match",
+                mAgentUnderTest.isSourceAcceptable(header, pos));
+    }
+
+    @Test
+    public void checkAcceptTestFailingBlockRestore() {
+        mAgentUnderTest.setForcedDeviceInfoRestoreAcceptability(false);
+        byte[] data = new byte[0];
+
+        assertFalse(
+                "Blocking isSourceAcceptable did not stop restore",
+                mAgentUnderTest.restoreDeviceSpecificConfig(data));
+    }
+
+    private byte[] generateUncorruptedHeader() throws IOException {
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            mAgentUnderTest.writeHeader(os);
+            return os.toByteArray();
+        }
+    }
+
+    private byte[] generateCorruptedHeader(
+            boolean corruptVersion, boolean corruptManufacturer, boolean corruptProduct)
+            throws IOException {
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            int version = SettingsBackupAgent.DEVICE_SPECIFIC_VERSION;
+            if (corruptVersion) {
+                version++;
+            }
+            os.write(SettingsBackupAgent.toByteArray(version));
+
+            String manufacturer = Build.MANUFACTURER;
+            if (corruptManufacturer) {
+                manufacturer = manufacturer == null ? "X" : manufacturer + "X";
+            }
+            os.write(SettingsBackupAgent.toByteArray(manufacturer));
+
+            String product = Build.PRODUCT;
+            if (corruptProduct) {
+                product = product == null ? "X" : product + "X";
+            }
+            os.write(SettingsBackupAgent.toByteArray(product));
+
+            return os.toByteArray();
+        }
+    }
+
+    private static class TestFriendlySettingsBackupAgent extends SettingsBackupAgent {
+        private Boolean mForcedDeviceInfoRestoreAcceptability = null;
+
+        void setForcedDeviceInfoRestoreAcceptability(boolean value) {
+            mForcedDeviceInfoRestoreAcceptability = value;
+        }
+
+        @VisibleForTesting
+        boolean isSourceAcceptable(byte[] data, AtomicInteger pos) {
+            return mForcedDeviceInfoRestoreAcceptability == null
+                    ? super.isSourceAcceptable(data, pos)
+                    : mForcedDeviceInfoRestoreAcceptability;
+        }
+    }
+
+    /** The TestSettingsHelper tracks which values have been backed up and/or restored. */
+    private static class TestSettingsHelper extends SettingsHelper {
+        private Set<String> mReadEntries;
+        private Map<String, String> mWrittenValues;
+
+        TestSettingsHelper(Context context) {
+            super(context);
+            mReadEntries = new HashSet<>();
+            mWrittenValues = new HashMap<>();
+        }
+
+        @Override
+        public String onBackupValue(String key, String value) {
+            mReadEntries.add(key);
+            String readValue = TEST_VALUES.get(key);
+            assert readValue != null;
+            return readValue;
+        }
+
+        @Override
+        public void restoreValue(
+                Context context,
+                ContentResolver cr,
+                ContentValues contentValues,
+                Uri destination,
+                String name,
+                String value,
+                int restoredFromSdkInt) {
+            mWrittenValues.put(name, value);
+        }
+    }
+
+    /**
+     * ContextWrapper which allows us to return a MockContentResolver to code which uses it to
+     * access settings. This allows us to override the ContentProvider for the Settings URIs to
+     * return known values.
+     */
+    private static class ContextWithMockContentResolver extends ContextWrapper {
+        private MockContentResolver mContentResolver;
+
+        ContextWithMockContentResolver(Context targetContext) {
+            super(targetContext);
+
+            mContentResolver = new MockContentResolver();
+            mContentResolver.addProvider(
+                    Settings.AUTHORITY, new DeviceSpecificInfoMockContentProvider());
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+    }
+
+    /** ContentProvider which returns a set of known test values. */
+    private static class DeviceSpecificInfoMockContentProvider extends MockContentProvider {
+        private static final Object[][] RESULT_ROWS = {
+            {Settings.Secure.DISPLAY_DENSITY_FORCED, TEST_DISPLAY_DENSITY_FORCED},
+        };
+
+        @Override
+        public Cursor query(
+                Uri uri,
+                String[] projection,
+                String selection,
+                String[] selectionArgs,
+                String sortOrder) {
+            MatrixCursor result = new MatrixCursor(SettingsBackupAgent.PROJECTION);
+            for (Object[] resultRow : RESULT_ROWS) {
+                result.addRow(resultRow);
+            }
+            return result;
+        }
+    }
+}