Add Network security watchlist service

Network security watchlist service is a service to monitor all potential
harmful network traffic. By setting a network watchlist, any connections
that visit any site from watchlist will be logged.

Logs will be aggregated everyday and encoded using differential
privacy before exporting it from framework.

This feature is disabled now, run "setprop ro.network_watchlist_enabled true" to enable it.

All network events are handled in an async bg thread, it should not
cause any delay in netd. Also, it uses the hooks in enterprise network logging,
so we can run netd_benchmark to measure the impact to netd.

Here are the things not included in this CL:
- ConfigUpdater to get and set watchlist
- Differential privacy encoding logic and reporting
- CTS
- Memory and performance optimization for internal watchlist data structure

Test: manual - turn on the feature, hard code a watchlist xml, process
that visited that domain is being logged in sqlite.
Test: run netd_benchmark - seems no obvious performance change.
Test: bit FrameworksCoreTests:android.net.NetworkWatchlistManagerTests
Test: runtest frameworks-net
Test: runtest frameworks-services -p com.android.server.net.watchlist

Bug: 63908748

Change-Id: I09595178bac0070a867bc5e0501a7bf2c840e398
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 9d23fe9..6de3395 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -3193,7 +3193,7 @@
         // setUp() adds a secondary user for CALLER_USER_HANDLE. Remove it as otherwise the
         // feature is disabled because there are non-affiliated secondary users.
         getServices().removeUser(DpmMockContext.CALLER_USER_HANDLE);
-        when(getServices().iipConnectivityMetrics.registerNetdEventCallback(anyObject()))
+        when(getServices().iipConnectivityMetrics.addNetdEventCallback(anyInt(), anyObject()))
                 .thenReturn(true);
 
         // No logs were retrieved so far.
diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java
new file mode 100644
index 0000000..a34f95e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 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.server.net.watchlist;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * runtest frameworks-services -c com.android.server.net.watchlist.HarmfulDigestsTests
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class HarmfulDigestsTests {
+
+    private static final byte[] TEST_DIGEST_1 = HexDump.hexStringToByteArray("AAAAAA");
+    private static final byte[] TEST_DIGEST_2 = HexDump.hexStringToByteArray("BBBBBB");
+    private static final byte[] TEST_DIGEST_3 = HexDump.hexStringToByteArray("AAAABB");
+    private static final byte[] TEST_DIGEST_4 = HexDump.hexStringToByteArray("BBBBAA");
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testHarmfulDigests_setAndContains() throws Exception {
+        HarmfulDigests harmfulDigests = new HarmfulDigests(
+                Arrays.asList(new byte[][] {TEST_DIGEST_1}));
+        assertTrue(harmfulDigests.contains(TEST_DIGEST_1));
+        assertFalse(harmfulDigests.contains(TEST_DIGEST_2));
+        assertFalse(harmfulDigests.contains(TEST_DIGEST_3));
+        assertFalse(harmfulDigests.contains(TEST_DIGEST_4));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java
new file mode 100644
index 0000000..ccd3cdd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2017 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.server.net.watchlist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.ConnectivityMetricsEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.ServiceThread;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * runtest frameworks-services -c com.android.server.net.watchlist.NetworkWatchlistServiceTests
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class NetworkWatchlistServiceTests {
+
+    private static final long NETWOR_EVENT_TIMEOUT_SEC = 1;
+    private static final String TEST_HOST = "testhost.com";
+    private static final String TEST_IP = "7.6.8.9";
+    private static final String[] TEST_IPS =
+            new String[] {"1.2.3.4", "4.6.8.9", "2001:0db8:0001:0000:0000:0ab9:C0A8:0102"};
+
+    private static class TestHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case WatchlistLoggingHandler.LOG_WATCHLIST_EVENT_MSG:
+                    onLogEvent();
+                    break;
+                case WatchlistLoggingHandler.REPORT_RECORDS_IF_NECESSARY_MSG:
+                    onAggregateEvent();
+                    break;
+                default:
+                    fail("Unexpected message: " + msg.what);
+            }
+        }
+
+        public void onLogEvent() {}
+        public void onAggregateEvent() {}
+    }
+
+    private static class TestIIpConnectivityMetrics implements IIpConnectivityMetrics {
+
+        int counter = 0;
+        INetdEventCallback callback = null;
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+
+        @Override
+        public int logEvent(ConnectivityMetricsEvent connectivityMetricsEvent)
+                    throws RemoteException {
+            return 0;
+        }
+
+        @Override
+        public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) {
+            counter++;
+            this.callback = callback;
+            return true;
+        }
+
+        @Override
+        public boolean removeNetdEventCallback(int callerType) {
+            counter--;
+            return true;
+        }
+    };
+
+    ServiceThread mHandlerThread;
+    WatchlistLoggingHandler mWatchlistHandler;
+    NetworkWatchlistService mWatchlistService;
+
+    @Before
+    public void setUp() {
+        mHandlerThread = new ServiceThread("NetworkWatchlistServiceTests",
+                Process.THREAD_PRIORITY_BACKGROUND, /* allowIo */ false);
+        mHandlerThread.start();
+        mWatchlistHandler = new WatchlistLoggingHandler(InstrumentationRegistry.getContext(),
+                mHandlerThread.getLooper());
+        mWatchlistService = new NetworkWatchlistService(InstrumentationRegistry.getContext(),
+                mHandlerThread, mWatchlistHandler, null);
+    }
+
+    @After
+    public void tearDown() {
+        mHandlerThread.quitSafely();
+    }
+
+    @Test
+    public void testStartStopWatchlistLogging() throws Exception {
+        TestIIpConnectivityMetrics connectivityMetrics = new TestIIpConnectivityMetrics() {
+            @Override
+            public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) {
+                super.addNetdEventCallback(callerType, callback);
+                assertEquals(callerType, INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST);
+                return true;
+            }
+
+            @Override
+            public boolean removeNetdEventCallback(int callerType) {
+                super.removeNetdEventCallback(callerType);
+                assertEquals(callerType, INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST);
+                return true;
+            }
+        };
+        assertEquals(connectivityMetrics.counter, 0);
+        mWatchlistService.mIpConnectivityMetrics = connectivityMetrics;
+        assertTrue(mWatchlistService.startWatchlistLoggingImpl());
+        assertEquals(connectivityMetrics.counter, 1);
+        assertTrue(mWatchlistService.startWatchlistLoggingImpl());
+        assertEquals(connectivityMetrics.counter, 1);
+        assertTrue(mWatchlistService.stopWatchlistLoggingImpl());
+        assertEquals(connectivityMetrics.counter, 0);
+        assertTrue(mWatchlistService.stopWatchlistLoggingImpl());
+        assertEquals(connectivityMetrics.counter, 0);
+        assertTrue(mWatchlistService.startWatchlistLoggingImpl());
+        assertEquals(connectivityMetrics.counter, 1);
+        assertTrue(mWatchlistService.stopWatchlistLoggingImpl());
+        assertEquals(connectivityMetrics.counter, 0);
+    }
+
+    @Test
+    public void testNetworkEvents() throws Exception {
+        TestIIpConnectivityMetrics connectivityMetrics = new TestIIpConnectivityMetrics();
+        mWatchlistService.mIpConnectivityMetrics = connectivityMetrics;
+        assertTrue(mWatchlistService.startWatchlistLoggingImpl());
+
+        // Test DNS events
+        final CountDownLatch testDnsLatch = new CountDownLatch(1);
+        final Object[] dnsParams = new Object[3];
+        final WatchlistLoggingHandler testDnsHandler =
+                new WatchlistLoggingHandler(InstrumentationRegistry.getContext(),
+                        mHandlerThread.getLooper()) {
+                    @Override
+                    public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) {
+                        dnsParams[0] = host;
+                        dnsParams[1] = ipAddresses;
+                        dnsParams[2] = uid;
+                        testDnsLatch.countDown();
+                    }
+                };
+        mWatchlistService.mNetworkWatchlistHandler = testDnsHandler;
+        connectivityMetrics.callback.onDnsEvent(TEST_HOST, TEST_IPS, TEST_IPS.length, 123L, 456);
+        if (!testDnsLatch.await(NETWOR_EVENT_TIMEOUT_SEC, TimeUnit.SECONDS)) {
+            fail("Timed out waiting for network event");
+        }
+        assertEquals(TEST_HOST, dnsParams[0]);
+        for (int i = 0; i < TEST_IPS.length; i++) {
+            assertEquals(TEST_IPS[i], ((String[])dnsParams[1])[i]);
+        }
+        assertEquals(456, dnsParams[2]);
+
+        // Test connect events
+        final CountDownLatch testConnectLatch = new CountDownLatch(1);
+        final Object[] connectParams = new Object[3];
+        final WatchlistLoggingHandler testConnectHandler =
+                new WatchlistLoggingHandler(InstrumentationRegistry.getContext(),
+                        mHandlerThread.getLooper()) {
+                    @Override
+                    public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) {
+                        connectParams[0] = host;
+                        connectParams[1] = ipAddresses;
+                        connectParams[2] = uid;
+                        testConnectLatch.countDown();
+                    }
+                };
+        mWatchlistService.mNetworkWatchlistHandler = testConnectHandler;
+        connectivityMetrics.callback.onConnectEvent(TEST_IP, 80, 123L, 456);
+        if (!testConnectLatch.await(NETWOR_EVENT_TIMEOUT_SEC, TimeUnit.SECONDS)) {
+            fail("Timed out waiting for network event");
+        }
+        assertNull(connectParams[0]);
+        assertEquals(1, ((String[]) connectParams[1]).length);
+        assertEquals(TEST_IP, ((String[]) connectParams[1])[0]);
+        assertEquals(456, connectParams[2]);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java
new file mode 100644
index 0000000..e356b13
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 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.server.net.watchlist;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+/**
+ * runtest frameworks-services -c com.android.server.net.watchlist.WatchlistLoggingHandlerTests
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class WatchlistLoggingHandlerTests {
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testWatchlistLoggingHandler_getAllSubDomains() throws Exception {
+        String[] subDomains = WatchlistLoggingHandler.getAllSubDomains("abc.def.gh.i.jkl.mm");
+        assertTrue(Arrays.equals(subDomains, new String[] {"abc.def.gh.i.jkl.mm",
+                "def.gh.i.jkl.mm", "gh.i.jkl.mm", "i.jkl.mm", "jkl.mm", "mm"}));
+        subDomains = WatchlistLoggingHandler.getAllSubDomains(null);
+        assertNull(subDomains);
+        subDomains = WatchlistLoggingHandler.getAllSubDomains("jkl.mm");
+        assertTrue(Arrays.equals(subDomains, new String[] {"jkl.mm", "mm"}));
+        subDomains = WatchlistLoggingHandler.getAllSubDomains("abc");
+        assertTrue(Arrays.equals(subDomains, new String[] {"abc"}));
+        subDomains = WatchlistLoggingHandler.getAllSubDomains("jkl.mm.");
+        assertTrue(Arrays.equals(subDomains, new String[] {"jkl.mm.", "mm."}));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java
new file mode 100644
index 0000000..f3cb980
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 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.server.net.watchlist;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+
+/**
+ * runtest frameworks-services -c com.android.server.net.watchlist.WatchlistSettingsTests
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class WatchlistSettingsTests {
+
+    private static final String TEST_XML_1 = "NetworkWatchlistTest/watchlist_settings_test1.xml";
+    private static final String TEST_CC_DOMAIN = "test-cc-domain.com";
+    private static final String TEST_CC_IP = "127.0.0.2";
+    private static final String TEST_NOT_EXIST_CC_DOMAIN = "test-not-exist-cc-domain.com";
+    private static final String TEST_NOT_EXIST_CC_IP = "1.2.3.4";
+    private static final String TEST_SHA256_ONLY_DOMAIN = "test-cc-match-sha256-only.com";
+    private static final String TEST_SHA256_ONLY_IP = "127.0.0.3";
+    private static final String TEST_CRC32_ONLY_DOMAIN = "test-cc-match-crc32-only.com";
+    private static final String TEST_CRC32_ONLY_IP = "127.0.0.4";
+
+    private static final String TEST_NEW_CC_DOMAIN = "test-new-cc-domain.com";
+    private static final byte[] TEST_NEW_CC_DOMAIN_SHA256 = HexDump.hexStringToByteArray(
+            "B86F9D37425340B635F43D6BC2506630761ADA71F5E6BBDBCA4651C479F9FB43");
+    private static final byte[] TEST_NEW_CC_DOMAIN_CRC32 = HexDump.hexStringToByteArray("76795BD3");
+
+    private static final String TEST_NEW_CC_IP = "1.1.1.2";
+    private static final byte[] TEST_NEW_CC_IP_SHA256 = HexDump.hexStringToByteArray(
+            "721BAB5E313CF0CC76B10F9592F18B9D1B8996497501A3306A55B3AE9F1CC87C");
+    private static final byte[] TEST_NEW_CC_IP_CRC32 = HexDump.hexStringToByteArray("940B8BEE");
+
+    private Context mContext;
+    private File mTestXmlFile;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mTestXmlFile =  new File(mContext.getFilesDir(), "test_watchlist_settings.xml");
+        mTestXmlFile.delete();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTestXmlFile.delete();
+    }
+
+    @Test
+    public void testWatchlistSettings_parsing() throws Exception {
+        copyWatchlistSettingsXml(mContext, TEST_XML_1, mTestXmlFile);
+        WatchlistSettings settings = new WatchlistSettings(mTestXmlFile);
+        assertTrue(settings.containsDomain(TEST_CC_DOMAIN));
+        assertTrue(settings.containsIp(TEST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP));
+        assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP));
+    }
+
+    @Test
+    public void testWatchlistSettings_writeSettingsToDisk() throws Exception {
+        copyWatchlistSettingsXml(mContext, TEST_XML_1, mTestXmlFile);
+        WatchlistSettings settings = new WatchlistSettings(mTestXmlFile);
+        settings.writeSettingsToDisk(Arrays.asList(TEST_NEW_CC_DOMAIN_CRC32),
+                Arrays.asList(TEST_NEW_CC_DOMAIN_SHA256), Arrays.asList(TEST_NEW_CC_IP_CRC32),
+                Arrays.asList(TEST_NEW_CC_IP_SHA256));
+        // Ensure old watchlist is not in memory
+        assertFalse(settings.containsDomain(TEST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP));
+        assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP));
+        // Ensure new watchlist is in memory
+        assertTrue(settings.containsDomain(TEST_NEW_CC_DOMAIN));
+        assertTrue(settings.containsIp(TEST_NEW_CC_IP));
+        // Reload settings from disk and test again
+        settings = new WatchlistSettings(mTestXmlFile);
+        // Ensure old watchlist is not in memory
+        assertFalse(settings.containsDomain(TEST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP));
+        assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP));
+        // Ensure new watchlist is in memory
+        assertTrue(settings.containsDomain(TEST_NEW_CC_DOMAIN));
+        assertTrue(settings.containsIp(TEST_NEW_CC_IP));
+    }
+
+    @Test
+    public void testWatchlistSettings_writeSettingsToMemory() throws Exception {
+        copyWatchlistSettingsXml(mContext, TEST_XML_1, mTestXmlFile);
+        WatchlistSettings settings = new WatchlistSettings(mTestXmlFile);
+        settings.writeSettingsToMemory(Arrays.asList(TEST_NEW_CC_DOMAIN_CRC32),
+                Arrays.asList(TEST_NEW_CC_DOMAIN_SHA256), Arrays.asList(TEST_NEW_CC_IP_CRC32),
+                Arrays.asList(TEST_NEW_CC_IP_SHA256));
+        // Ensure old watchlist is not in memory
+        assertFalse(settings.containsDomain(TEST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP));
+        assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP));
+        // Ensure new watchlist is in memory
+        assertTrue(settings.containsDomain(TEST_NEW_CC_DOMAIN));
+        assertTrue(settings.containsIp(TEST_NEW_CC_IP));
+        // Reload settings from disk and test again
+        settings = new WatchlistSettings(mTestXmlFile);
+        // Ensure old watchlist is in memory
+        assertTrue(settings.containsDomain(TEST_CC_DOMAIN));
+        assertTrue(settings.containsIp(TEST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP));
+        assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP));
+        assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN));
+        assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP));
+        // Ensure new watchlist is not in memory
+        assertFalse(settings.containsDomain(TEST_NEW_CC_DOMAIN));
+        assertFalse(settings.containsIp(TEST_NEW_CC_IP));;
+    }
+
+    private static void copyWatchlistSettingsXml(Context context, String xmlAsset, File outFile)
+            throws IOException {
+        writeToFile(outFile, readAsset(context, xmlAsset));
+
+    }
+
+    private static String readAsset(Context context, String assetPath) throws IOException {
+        final StringBuilder sb = new StringBuilder();
+        try (BufferedReader br = new BufferedReader(
+                new InputStreamReader(
+                        context.getResources().getAssets().open(assetPath)))) {
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+                sb.append(System.lineSeparator());
+            }
+        }
+        return sb.toString();
+    }
+
+    private static void writeToFile(File path, String content)
+            throws IOException {
+        path.getParentFile().mkdirs();
+
+        try (FileWriter writer = new FileWriter(path)) {
+            writer.write(content);
+        }
+    }
+}