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/core/java/com/android/server/connectivity/IpConnectivityMetrics.java b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
index 5cc390a..f427819 100644
--- a/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
+++ b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
@@ -23,8 +23,6 @@
 import android.net.metrics.ApfProgramEvent;
 import android.net.metrics.IpConnectivityLog;
 import android.os.Binder;
-import android.os.IBinder;
-import android.os.Parcelable;
 import android.os.Process;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -322,22 +320,22 @@
         }
 
         @Override
-        public boolean registerNetdEventCallback(INetdEventCallback callback) {
+        public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) {
             enforceNetdEventListeningPermission();
             if (mNetdListener == null) {
                 return false;
             }
-            return mNetdListener.registerNetdEventCallback(callback);
+            return mNetdListener.addNetdEventCallback(callerType, callback);
         }
 
         @Override
-        public boolean unregisterNetdEventCallback() {
+        public boolean removeNetdEventCallback(int callerType) {
             enforceNetdEventListeningPermission();
             if (mNetdListener == null) {
                 // if the service is null, we aren't registered anyway
                 return true;
             }
-            return mNetdListener.unregisterNetdEventCallback();
+            return mNetdListener.removeNetdEventCallback(callerType);
         }
     };
 
diff --git a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
index 61b11e1..af138b93 100644
--- a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
+++ b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java
@@ -98,21 +98,55 @@
     @GuardedBy("this")
     private final TokenBucket mConnectTb =
             new TokenBucket(CONNECT_LATENCY_FILL_RATE, CONNECT_LATENCY_BURST_LIMIT);
-    // Callback should only be registered/unregistered when logging is being enabled/disabled in DPM
-    // by the device owner. It's DevicePolicyManager's responsibility to ensure that.
-    @GuardedBy("this")
-    private INetdEventCallback mNetdEventCallback;
 
-    public synchronized boolean registerNetdEventCallback(INetdEventCallback callback) {
-        mNetdEventCallback = callback;
+
+    /**
+     * There are only 2 possible callbacks.
+     *
+     * mNetdEventCallbackList[CALLBACK_CALLER_DEVICE_POLICY].
+     * Callback registered/unregistered when logging is being enabled/disabled in DPM
+     * by the device owner. It's DevicePolicyManager's responsibility to ensure that.
+     *
+     * mNetdEventCallbackList[CALLBACK_CALLER_NETWORK_WATCHLIST]
+     * Callback registered/unregistered by NetworkWatchlistService.
+     */
+    @GuardedBy("this")
+    private static final int[] ALLOWED_CALLBACK_TYPES = {
+        INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY,
+        INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST
+    };
+
+    @GuardedBy("this")
+    private INetdEventCallback[] mNetdEventCallbackList =
+            new INetdEventCallback[ALLOWED_CALLBACK_TYPES.length];
+
+    public synchronized boolean addNetdEventCallback(int callerType, INetdEventCallback callback) {
+        if (!isValidCallerType(callerType)) {
+            Log.e(TAG, "Invalid caller type: " + callerType);
+            return false;
+        }
+        mNetdEventCallbackList[callerType] = callback;
         return true;
     }
 
-    public synchronized boolean unregisterNetdEventCallback() {
-        mNetdEventCallback = null;
+    public synchronized boolean removeNetdEventCallback(int callerType) {
+        if (!isValidCallerType(callerType)) {
+            Log.e(TAG, "Invalid caller type: " + callerType);
+            return false;
+        }
+        mNetdEventCallbackList[callerType] = null;
         return true;
     }
 
+    private static boolean isValidCallerType(int callerType) {
+        for (int i = 0; i < ALLOWED_CALLBACK_TYPES.length; i++) {
+            if (callerType == ALLOWED_CALLBACK_TYPES[i]) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public NetdEventListenerService(Context context) {
         this(context.getSystemService(ConnectivityManager.class));
     }
@@ -169,8 +203,10 @@
         long timestamp = System.currentTimeMillis();
         getMetricsForNetwork(timestamp, netId).addDnsResult(eventType, returnCode, latencyMs);
 
-        if (mNetdEventCallback != null) {
-            mNetdEventCallback.onDnsEvent(hostname, ipAddresses, ipAddressesCount, timestamp, uid);
+        for (INetdEventCallback callback : mNetdEventCallbackList) {
+            if (callback != null) {
+                callback.onDnsEvent(hostname, ipAddresses, ipAddressesCount, timestamp, uid);
+            }
         }
     }
 
@@ -184,8 +220,14 @@
         long timestamp = System.currentTimeMillis();
         getMetricsForNetwork(timestamp, netId).addConnectResult(error, latencyMs, ipAddr);
 
-        if (mNetdEventCallback != null) {
-            mNetdEventCallback.onConnectEvent(ipAddr, port, timestamp, uid);
+        for (INetdEventCallback callback : mNetdEventCallbackList) {
+            if (callback != null) {
+                // TODO(rickywai): Remove this checking to collect ip in watchlist.
+                if (callback ==
+                        mNetdEventCallbackList[INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY]) {
+                    callback.onConnectEvent(ipAddr, port, timestamp, uid);
+                }
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/net/watchlist/DigestUtils.java b/services/core/java/com/android/server/net/watchlist/DigestUtils.java
new file mode 100644
index 0000000..57becb0
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/DigestUtils.java
@@ -0,0 +1,54 @@
+/*
+ * 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 java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Utils for calculating digests.
+ */
+public class DigestUtils {
+
+    private static final int FILE_READ_BUFFER_SIZE = 16 * 1024;
+
+    private DigestUtils() {}
+
+    /** @return SHA256 hash of the provided file */
+    public static byte[] getSha256Hash(File apkFile) throws IOException, NoSuchAlgorithmException {
+        try (InputStream stream = new FileInputStream(apkFile)) {
+            return getSha256Hash(stream);
+        }
+    }
+
+    /** @return SHA256 hash of data read from the provided input stream */
+    public static byte[] getSha256Hash(InputStream stream)
+            throws IOException, NoSuchAlgorithmException {
+        MessageDigest digester = MessageDigest.getInstance("SHA256");
+
+        int bytesRead;
+        byte[] buf = new byte[FILE_READ_BUFFER_SIZE];
+        while ((bytesRead = stream.read(buf)) >= 0) {
+            digester.update(buf, 0, bytesRead);
+        }
+        return digester.digest();
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/net/watchlist/HarmfulDigests.java b/services/core/java/com/android/server/net/watchlist/HarmfulDigests.java
new file mode 100644
index 0000000..27c22ce
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/HarmfulDigests.java
@@ -0,0 +1,55 @@
+/*
+ * 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 com.android.internal.util.HexDump;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class to store all harmful digests in memory.
+ * TODO: Optimize memory usage using byte array with binary search.
+ */
+class HarmfulDigests {
+
+    private final Set<String> mDigestSet;
+
+    HarmfulDigests(List<byte[]> digests) {
+        final HashSet<String> tmpDigestSet = new HashSet<>();
+        final int size = digests.size();
+        for (int i = 0; i < size; i++) {
+            tmpDigestSet.add(HexDump.toHexString(digests.get(i)));
+        }
+        mDigestSet = Collections.unmodifiableSet(tmpDigestSet);
+    }
+
+    public boolean contains(byte[] digest) {
+        return mDigestSet.contains(HexDump.toHexString(digest));
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        for (String digest : mDigestSet) {
+            pw.println(digest);
+        }
+        pw.println("");
+    }
+}
diff --git a/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java
new file mode 100644
index 0000000..171703a
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java
@@ -0,0 +1,267 @@
+/*
+ * 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 android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
+import android.net.NetworkWatchlistManager;
+import android.net.metrics.IpConnectivityLog;
+import android.os.Binder;
+import android.os.Process;
+import android.os.SharedMemory;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.net.INetworkWatchlistManager;
+import com.android.server.ServiceThread;
+import com.android.server.SystemService;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Implementation of network watchlist service.
+ */
+public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
+
+    private static final String TAG = NetworkWatchlistService.class.getSimpleName();
+    static final boolean DEBUG = false;
+
+    private static final String PROPERTY_NETWORK_WATCHLIST_ENABLED =
+            "ro.network_watchlist_enabled";
+
+    private static final int MAX_NUM_OF_WATCHLIST_DIGESTS = 10000;
+
+    public static class Lifecycle extends SystemService {
+        private NetworkWatchlistService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            if (!SystemProperties.getBoolean(PROPERTY_NETWORK_WATCHLIST_ENABLED, false)) {
+                // Watchlist service is disabled
+                return;
+            }
+            mService = new NetworkWatchlistService(getContext());
+            publishBinderService(Context.NETWORK_WATCHLIST_SERVICE, mService);
+        }
+
+        @Override
+        public void onBootPhase(int phase) {
+            if (!SystemProperties.getBoolean(PROPERTY_NETWORK_WATCHLIST_ENABLED, false)) {
+                // Watchlist service is disabled
+                return;
+            }
+            if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+                try {
+                    mService.initIpConnectivityMetrics();
+                    mService.startWatchlistLogging();
+                } catch (RemoteException e) {
+                    // Should not happen
+                }
+                ReportWatchlistJobService.schedule(getContext());
+            }
+        }
+    }
+
+    private volatile boolean mIsLoggingEnabled = false;
+    private final Object mLoggingSwitchLock = new Object();
+
+    private final WatchlistSettings mSettings;
+    private final Context mContext;
+
+    // Separate thread to handle expensive watchlist logging work.
+    private final ServiceThread mHandlerThread;
+
+    @VisibleForTesting
+    IIpConnectivityMetrics mIpConnectivityMetrics;
+    @VisibleForTesting
+    WatchlistLoggingHandler mNetworkWatchlistHandler;
+
+    public NetworkWatchlistService(Context context) {
+        mContext = context;
+        mSettings = WatchlistSettings.getInstance();
+        mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
+                        /* allowIo */ false);
+        mHandlerThread.start();
+        mNetworkWatchlistHandler = new WatchlistLoggingHandler(mContext,
+                mHandlerThread.getLooper());
+        mNetworkWatchlistHandler.reportWatchlistIfNecessary();
+    }
+
+    // For testing only
+    @VisibleForTesting
+    NetworkWatchlistService(Context context, ServiceThread handlerThread,
+            WatchlistLoggingHandler handler, IIpConnectivityMetrics ipConnectivityMetrics) {
+        mContext = context;
+        mSettings = WatchlistSettings.getInstance();
+        mHandlerThread = handlerThread;
+        mNetworkWatchlistHandler = handler;
+        mIpConnectivityMetrics = ipConnectivityMetrics;
+    }
+
+    private void initIpConnectivityMetrics() {
+        mIpConnectivityMetrics = (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface(
+                ServiceManager.getService(IpConnectivityLog.SERVICE_NAME));
+    }
+
+    private final INetdEventCallback mNetdEventCallback = new INetdEventCallback.Stub() {
+        @Override
+        public void onDnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount,
+                long timestamp, int uid) {
+            if (!mIsLoggingEnabled) {
+                return;
+            }
+            mNetworkWatchlistHandler.asyncNetworkEvent(hostname, ipAddresses, uid);
+        }
+
+        @Override
+        public void onConnectEvent(String ipAddr, int port, long timestamp, int uid) {
+            if (!mIsLoggingEnabled) {
+                return;
+            }
+            mNetworkWatchlistHandler.asyncNetworkEvent(null, new String[]{ipAddr}, uid);
+        }
+    };
+
+    @VisibleForTesting
+    protected boolean startWatchlistLoggingImpl() throws RemoteException {
+        if (DEBUG) {
+            Slog.i(TAG, "Starting watchlist logging.");
+        }
+        synchronized (mLoggingSwitchLock) {
+            if (mIsLoggingEnabled) {
+                Slog.w(TAG, "Watchlist logging is already running");
+                return true;
+            }
+            try {
+                if (mIpConnectivityMetrics.addNetdEventCallback(
+                        INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST, mNetdEventCallback)) {
+                    mIsLoggingEnabled = true;
+                    return true;
+                } else {
+                    return false;
+                }
+            } catch (RemoteException re) {
+                // Should not happen
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public boolean startWatchlistLogging() throws RemoteException {
+        enforceWatchlistLoggingPermission();
+        return startWatchlistLoggingImpl();
+    }
+
+    @VisibleForTesting
+    protected boolean stopWatchlistLoggingImpl() {
+        if (DEBUG) {
+            Slog.i(TAG, "Stopping watchlist logging");
+        }
+        synchronized (mLoggingSwitchLock) {
+            if (!mIsLoggingEnabled) {
+                Slog.w(TAG, "Watchlist logging is not running");
+                return true;
+            }
+            // stop the logging regardless of whether we fail to unregister listener
+            mIsLoggingEnabled = false;
+
+            try {
+                return mIpConnectivityMetrics.removeNetdEventCallback(
+                        INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST);
+            } catch (RemoteException re) {
+                // Should not happen
+                return false;
+            }
+        }
+    }
+
+    @Override
+    public boolean stopWatchlistLogging() throws RemoteException {
+        enforceWatchlistLoggingPermission();
+        return stopWatchlistLoggingImpl();
+    }
+
+    private void enforceWatchlistLoggingPermission() {
+        final int uid = Binder.getCallingUid();
+        if (uid != Process.SYSTEM_UID) {
+            throw new SecurityException(String.format("Uid %d has no permission to change watchlist"
+                    + " setting.", uid));
+        }
+    }
+
+    /**
+     * Set a new network watchlist.
+     * This method should be called by ConfigUpdater only.
+     *
+     * @return True if network watchlist is updated.
+     */
+    public boolean setNetworkSecurityWatchlist(List<byte[]> domainsCrc32Digests,
+            List<byte[]> domainsSha256Digests,
+            List<byte[]> ipAddressesCrc32Digests,
+            List<byte[]> ipAddressesSha256Digests) {
+        Slog.i(TAG, "Setting network watchlist");
+        if (domainsCrc32Digests == null || domainsSha256Digests == null
+                || ipAddressesCrc32Digests == null || ipAddressesSha256Digests == null) {
+            Slog.e(TAG, "Parameters cannot be null");
+            return false;
+        }
+        if (domainsCrc32Digests.size() != domainsSha256Digests.size()
+                || ipAddressesCrc32Digests.size() != ipAddressesSha256Digests.size()) {
+            Slog.e(TAG, "Must need to have the same number of CRC32 and SHA256 digests");
+            return false;
+        }
+        if (domainsSha256Digests.size() + ipAddressesSha256Digests.size()
+                > MAX_NUM_OF_WATCHLIST_DIGESTS) {
+            Slog.e(TAG, "Total watchlist size cannot exceed " + MAX_NUM_OF_WATCHLIST_DIGESTS);
+            return false;
+        }
+        mSettings.writeSettingsToDisk(domainsCrc32Digests, domainsSha256Digests,
+                ipAddressesCrc32Digests, ipAddressesSha256Digests);
+        Slog.i(TAG, "Set network watchlist: Success");
+        return true;
+    }
+
+    @Override
+    public void reportWatchlistIfNecessary() {
+        // Allow any apps to trigger report event, as we won't run it if it's too early.
+        mNetworkWatchlistHandler.reportWatchlistIfNecessary();
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        mSettings.dump(fd, pw, args);
+    }
+
+}
diff --git a/services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java b/services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java
new file mode 100644
index 0000000..dfeb1b2
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.NetworkWatchlistManager;
+import android.util.Slog;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A job that periodically report watchlist records.
+ */
+public class ReportWatchlistJobService extends JobService {
+
+    private static final boolean DEBUG = NetworkWatchlistService.DEBUG;
+    private static final String TAG = "WatchlistJobService";
+
+    // Unique job id used in system service, other jobs should not use the same value.
+    public static final int REPORT_WATCHLIST_RECORDS_JOB_ID = 0xd7689;
+    public static final long REPORT_WATCHLIST_RECORDS_PERIOD_MILLIS =
+            TimeUnit.HOURS.toMillis(12);
+
+    @Override
+    public boolean onStartJob(final JobParameters jobParameters) {
+        if (jobParameters.getJobId() != REPORT_WATCHLIST_RECORDS_JOB_ID) {
+            return false;
+        }
+        if (DEBUG) Slog.d(TAG, "Start scheduled job.");
+        new NetworkWatchlistManager(this).reportWatchlistIfNecessary();
+        jobFinished(jobParameters, false);
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters jobParameters) {
+        return true; // Reschedule when possible.
+    }
+
+    /**
+     * Schedule the {@link ReportWatchlistJobService} to run periodically.
+     */
+    public static void schedule(Context context) {
+        if (DEBUG) Slog.d(TAG, "Scheduling records aggregator task");
+        final JobScheduler scheduler =
+                (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        scheduler.schedule(new JobInfo.Builder(REPORT_WATCHLIST_RECORDS_JOB_ID,
+                new ComponentName(context, ReportWatchlistJobService.class))
+                //.setOverrideDeadline(45 * 1000) // Schedule job soon, for testing.
+                .setPeriodic(REPORT_WATCHLIST_RECORDS_PERIOD_MILLIS)
+                .setRequiresDeviceIdle(true)
+                .setRequiresBatteryNotLow(true)
+                .setPersisted(false)
+                .build());
+    }
+
+}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
new file mode 100644
index 0000000..2247558
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java
@@ -0,0 +1,298 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.DropBoxManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A Handler class for network watchlist logging on a background thread.
+ */
+class WatchlistLoggingHandler extends Handler {
+
+    private static final String TAG = WatchlistLoggingHandler.class.getSimpleName();
+    private static final boolean DEBUG = NetworkWatchlistService.DEBUG;
+
+    @VisibleForTesting
+    static final int LOG_WATCHLIST_EVENT_MSG = 1;
+    @VisibleForTesting
+    static final int REPORT_RECORDS_IF_NECESSARY_MSG = 2;
+
+    private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
+    private static final String DROPBOX_TAG = "network_watchlist_report";
+
+    private final Context mContext;
+    private final ContentResolver mResolver;
+    private final PackageManager mPm;
+    private final WatchlistReportDbHelper mDbHelper;
+    private final WatchlistSettings mSettings;
+    // A cache for uid and apk digest mapping.
+    // As uid won't be reused until reboot, it's safe to assume uid is unique per signature and app.
+    // TODO: Use more efficient data structure.
+    private final HashMap<Integer, byte[]> mCachedUidDigestMap = new HashMap<>();
+
+    private interface WatchlistEventKeys {
+        String HOST = "host";
+        String IP_ADDRESSES = "ipAddresses";
+        String UID = "uid";
+        String TIMESTAMP = "timestamp";
+    }
+
+    WatchlistLoggingHandler(Context context, Looper looper) {
+        super(looper);
+        mContext = context;
+        mPm = mContext.getPackageManager();
+        mResolver = mContext.getContentResolver();
+        mDbHelper = WatchlistReportDbHelper.getInstance(context);
+        mSettings = WatchlistSettings.getInstance();
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case LOG_WATCHLIST_EVENT_MSG: {
+                final Bundle data = msg.getData();
+                handleNetworkEvent(
+                        data.getString(WatchlistEventKeys.HOST),
+                        data.getStringArray(WatchlistEventKeys.IP_ADDRESSES),
+                        data.getInt(WatchlistEventKeys.UID),
+                        data.getLong(WatchlistEventKeys.TIMESTAMP)
+                );
+                break;
+            }
+            case REPORT_RECORDS_IF_NECESSARY_MSG:
+                tryAggregateRecords();
+                break;
+            default: {
+                Slog.d(TAG, "WatchlistLoggingHandler received an unknown of message.");
+                break;
+            }
+        }
+    }
+
+    /**
+     * Report network watchlist records if we collected enough data.
+     */
+    public void reportWatchlistIfNecessary() {
+        final Message msg = obtainMessage(REPORT_RECORDS_IF_NECESSARY_MSG);
+        sendMessage(msg);
+    }
+
+    /**
+     * Insert network traffic event to watchlist async queue processor.
+     */
+    public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) {
+        final Message msg = obtainMessage(LOG_WATCHLIST_EVENT_MSG);
+        final Bundle bundle = new Bundle();
+        bundle.putString(WatchlistEventKeys.HOST, host);
+        bundle.putStringArray(WatchlistEventKeys.IP_ADDRESSES, ipAddresses);
+        bundle.putInt(WatchlistEventKeys.UID, uid);
+        bundle.putLong(WatchlistEventKeys.TIMESTAMP, System.currentTimeMillis());
+        msg.setData(bundle);
+        sendMessage(msg);
+    }
+
+    private void handleNetworkEvent(String hostname, String[] ipAddresses,
+            int uid, long timestamp) {
+        if (DEBUG) {
+            Slog.i(TAG, "handleNetworkEvent with host: " + hostname + ", uid: " + uid);
+        }
+        final String cncDomain = searchAllSubDomainsInWatchlist(hostname);
+        if (cncDomain != null) {
+            insertRecord(getDigestFromUid(uid), cncDomain, timestamp);
+        } else {
+            final String cncIp = searchIpInWatchlist(ipAddresses);
+            if (cncIp != null) {
+                insertRecord(getDigestFromUid(uid), cncIp, timestamp);
+            }
+        }
+    }
+
+    private boolean insertRecord(byte[] digest, String cncHost, long timestamp) {
+        final boolean result = mDbHelper.insertNewRecord(digest, cncHost, timestamp);
+        tryAggregateRecords();
+        return result;
+    }
+
+    private boolean shouldReportNetworkWatchlist() {
+        final long lastReportTime = Settings.Global.getLong(mResolver,
+                Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, 0L);
+        final long currentTimestamp = System.currentTimeMillis();
+        if (currentTimestamp < lastReportTime) {
+            Slog.i(TAG, "Last report time is larger than current time, reset report");
+            mDbHelper.cleanup();
+            return false;
+        }
+        return currentTimestamp >= lastReportTime + ONE_DAY_MS;
+    }
+
+    private void tryAggregateRecords() {
+        if (shouldReportNetworkWatchlist()) {
+            Slog.i(TAG, "Start aggregating watchlist records.");
+            final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
+            if (dbox != null && !dbox.isTagEnabled(DROPBOX_TAG)) {
+                final WatchlistReportDbHelper.AggregatedResult aggregatedResult =
+                        mDbHelper.getAggregatedRecords();
+                final byte[] encodedResult = encodeAggregatedResult(aggregatedResult);
+                if (encodedResult != null) {
+                    addEncodedReportToDropBox(encodedResult);
+                }
+            }
+            mDbHelper.cleanup();
+            Settings.Global.putLong(mResolver, Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
+                    System.currentTimeMillis());
+        } else {
+            Slog.i(TAG, "No need to aggregate record yet.");
+        }
+    }
+
+    private byte[] encodeAggregatedResult(
+            WatchlistReportDbHelper.AggregatedResult aggregatedResult) {
+        // TODO: Encode results using differential privacy.
+        return null;
+    }
+
+    private void addEncodedReportToDropBox(byte[] encodedReport) {
+        final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
+        dbox.addData(DROPBOX_TAG, encodedReport, 0);
+    }
+
+    /**
+     * Get app digest from app uid.
+     */
+    private byte[] getDigestFromUid(int uid) {
+        final byte[] cachedDigest = mCachedUidDigestMap.get(uid);
+        if (cachedDigest != null) {
+            return cachedDigest;
+        }
+        final String[] packageNames = mPm.getPackagesForUid(uid);
+        final int userId = UserHandle.getUserId(uid);
+        if (!ArrayUtils.isEmpty(packageNames)) {
+            for (String packageName : packageNames) {
+                try {
+                    final String apkPath = mPm.getPackageInfoAsUser(packageName,
+                            PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId)
+                            .applicationInfo.publicSourceDir;
+                    if (TextUtils.isEmpty(apkPath)) {
+                        Slog.w(TAG, "Cannot find apkPath for " + packageName);
+                        continue;
+                    }
+                    final byte[] digest = DigestUtils.getSha256Hash(new File(apkPath));
+                    mCachedUidDigestMap.put(uid, digest);
+                    return digest;
+                } catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) {
+                    Slog.e(TAG, "Should not happen", e);
+                    return null;
+                }
+            }
+        } else {
+            Slog.e(TAG, "Should not happen");
+        }
+        return null;
+    }
+
+    /**
+     * Search if any ip addresses are in watchlist.
+     *
+     * @param ipAddresses Ip address that you want to search in watchlist.
+     * @return Ip address that exists in watchlist, null if it does not match anything.
+     */
+    private String searchIpInWatchlist(String[] ipAddresses) {
+        for (String ipAddress : ipAddresses) {
+            if (isIpInWatchlist(ipAddress)) {
+                return ipAddress;
+            }
+        }
+        return null;
+    }
+
+    /** Search if the ip is in watchlist */
+    private boolean isIpInWatchlist(String ipAddr) {
+        if (ipAddr == null) {
+            return false;
+        }
+        return mSettings.containsIp(ipAddr);
+    }
+
+    /** Search if the host is in watchlist */
+    private boolean isHostInWatchlist(String host) {
+        if (host == null) {
+            return false;
+        }
+        return mSettings.containsDomain(host);
+    }
+
+    /**
+     * Search if any sub-domain in host is in watchlist.
+     *
+     * @param host Host that we want to search.
+     * @return Domain that exists in watchlist, null if it does not match anything.
+     */
+    private String searchAllSubDomainsInWatchlist(String host) {
+        if (host == null) {
+            return null;
+        }
+        final String[] subDomains = getAllSubDomains(host);
+        for (String subDomain : subDomains) {
+            if (isHostInWatchlist(subDomain)) {
+                return subDomain;
+            }
+        }
+        return null;
+    }
+
+    /** Get all sub-domains in a host */
+    @VisibleForTesting
+    static String[] getAllSubDomains(String host) {
+        if (host == null) {
+            return null;
+        }
+        final ArrayList<String> subDomainList = new ArrayList<>();
+        subDomainList.add(host);
+        int index = host.indexOf(".");
+        while (index != -1) {
+            host = host.substring(index + 1);
+            if (!TextUtils.isEmpty(host)) {
+                subDomainList.add(host);
+            }
+            index = host.indexOf(".");
+        }
+        return subDomainList.toArray(new String[0]);
+    }
+}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
new file mode 100644
index 0000000..f48463f
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -0,0 +1,203 @@
+/*
+ * 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 android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Pair;
+
+import com.android.internal.util.HexDump;
+
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class to process watchlist read / save watchlist reports.
+ */
+class WatchlistReportDbHelper extends SQLiteOpenHelper {
+
+    private static final String TAG = "WatchlistReportDbHelper";
+
+    private static final String NAME = "watchlist_report.db";
+    private static final int VERSION = 2;
+
+    private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
+
+    private static class WhiteListReportContract {
+        private static final String TABLE = "records";
+        private static final String APP_DIGEST = "app_digest";
+        private static final String CNC_DOMAIN = "cnc_domain";
+        private static final String TIMESTAMP = "timestamp";
+    }
+
+    private static final String CREATE_TABLE_MODEL = "CREATE TABLE "
+            + WhiteListReportContract.TABLE + "("
+            + WhiteListReportContract.APP_DIGEST + " BLOB,"
+            + WhiteListReportContract.CNC_DOMAIN + " TEXT,"
+            + WhiteListReportContract.TIMESTAMP + " INTEGER DEFAULT 0" + " )";
+
+    private static final int INDEX_DIGEST = 0;
+    private static final int INDEX_CNC_DOMAIN = 1;
+    private static final int INDEX_TIMESTAMP = 2;
+
+    private static final String[] DIGEST_DOMAIN_PROJECTION =
+            new String[] {
+                    WhiteListReportContract.APP_DIGEST,
+                    WhiteListReportContract.CNC_DOMAIN
+            };
+
+    private static WatchlistReportDbHelper sInstance;
+
+    /**
+     * Aggregated watchlist records.
+     */
+    public static class AggregatedResult {
+        // A list of digests that visited c&c domain or ip before.
+        Set<String> appDigestList;
+
+        // The c&c domain or ip visited before.
+        String cncDomainVisited;
+
+        // A list of app digests and c&c domain visited.
+        HashMap<String, String> appDigestCNCList;
+    }
+
+    private WatchlistReportDbHelper(Context context) {
+        super(context, WatchlistSettings.getSystemWatchlistFile(NAME).getAbsolutePath(),
+                null, VERSION);
+        // Memory optimization - close idle connections after 30s of inactivity
+        setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
+    }
+
+    public static synchronized WatchlistReportDbHelper getInstance(Context context) {
+        if (sInstance != null) {
+            return sInstance;
+        }
+        sInstance = new WatchlistReportDbHelper(context);
+        return sInstance;
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(CREATE_TABLE_MODEL);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        // TODO: For now, drop older tables and recreate new ones.
+        db.execSQL("DROP TABLE IF EXISTS " + WhiteListReportContract.TABLE);
+        onCreate(db);
+    }
+
+    /**
+     * Insert new watchlist record.
+     *
+     * @param appDigest The digest of an app.
+     * @param cncDomain C&C domain that app visited.
+     * @return True if success.
+     */
+    public boolean insertNewRecord(byte[] appDigest, String cncDomain,
+            long timestamp) {
+        final SQLiteDatabase db = getWritableDatabase();
+        final ContentValues values = new ContentValues();
+        values.put(WhiteListReportContract.APP_DIGEST, appDigest);
+        values.put(WhiteListReportContract.CNC_DOMAIN, cncDomain);
+        values.put(WhiteListReportContract.TIMESTAMP, timestamp);
+        return db.insert(WhiteListReportContract.TABLE, null, values) != -1;
+    }
+
+    /**
+     * Aggregate the records in database, and return a rappor encoded result.
+     */
+    public AggregatedResult getAggregatedRecords() {
+        final long twoDaysBefore = getTwoDaysBeforeTimestamp();
+        final long yesterday = getYesterdayTimestamp();
+        final String selectStatement = WhiteListReportContract.TIMESTAMP + " >= ? AND " +
+                WhiteListReportContract.TIMESTAMP + " <= ?";
+
+        final SQLiteDatabase db = getReadableDatabase();
+        Cursor c = null;
+        try {
+            c = db.query(true /* distinct */,
+                    WhiteListReportContract.TABLE, DIGEST_DOMAIN_PROJECTION, selectStatement,
+                    new String[]{"" + twoDaysBefore, "" + yesterday}, null, null,
+                    null, null);
+            if (c == null || c.getCount() == 0) {
+                return null;
+            }
+            final AggregatedResult result = new AggregatedResult();
+            result.cncDomainVisited = null;
+            // After aggregation, each digest maximum will have only 1 record.
+            result.appDigestList = new HashSet<>();
+            result.appDigestCNCList = new HashMap<>();
+            while (c.moveToNext()) {
+                // We use hex string here as byte[] cannot be a key in HashMap.
+                String digestHexStr = HexDump.toHexString(c.getBlob(INDEX_DIGEST));
+                String cncDomain = c.getString(INDEX_CNC_DOMAIN);
+
+                result.appDigestList.add(digestHexStr);
+                if (result.cncDomainVisited != null) {
+                    result.cncDomainVisited = cncDomain;
+                }
+                result.appDigestCNCList.put(digestHexStr, cncDomain);
+            }
+            return result;
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    /**
+     * Remove all the records before yesterday.
+     *
+     * @return True if success.
+     */
+    public boolean cleanup() {
+        final SQLiteDatabase db = getWritableDatabase();
+        final long twoDaysBefore = getTwoDaysBeforeTimestamp();
+        final String clause = WhiteListReportContract.TIMESTAMP + "< " + twoDaysBefore;
+        return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+    }
+
+    static long getTwoDaysBeforeTimestamp() {
+        return getMidnightTimestamp(2);
+    }
+
+    static long getYesterdayTimestamp() {
+        return getMidnightTimestamp(1);
+    }
+
+    static long getMidnightTimestamp(int daysBefore) {
+        java.util.Calendar date = new GregorianCalendar();
+        // reset hour, minutes, seconds and millis
+        date.set(java.util.Calendar.HOUR_OF_DAY, 0);
+        date.set(java.util.Calendar.MINUTE, 0);
+        date.set(java.util.Calendar.SECOND, 0);
+        date.set(java.util.Calendar.MILLISECOND, 0);
+        date.add(java.util.Calendar.DAY_OF_MONTH, -daysBefore);
+        return date.getTimeInMillis();
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
new file mode 100644
index 0000000..c50f0d5
--- /dev/null
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
@@ -0,0 +1,284 @@
+/*
+ * 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 android.os.Environment;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+
+/**
+ * A util class to do watchlist settings operations, like setting watchlist, query if a domain
+ * exists in watchlist.
+ */
+class WatchlistSettings {
+    private static final String TAG = "WatchlistSettings";
+
+    // Settings xml will be stored in /data/system/network_watchlist/watchlist_settings.xml
+    static final String SYSTEM_WATCHLIST_DIR = "network_watchlist";
+
+    private static final String WATCHLIST_XML_FILE = "watchlist_settings.xml";
+
+    private static class XmlTags {
+        private static final String WATCHLIST_SETTINGS = "watchlist-settings";
+        private static final String SHA256_DOMAIN = "sha256-domain";
+        private static final String CRC32_DOMAIN = "crc32-domain";
+        private static final String SHA256_IP = "sha256-ip";
+        private static final String CRC32_IP = "crc32-ip";
+        private static final String HASH = "hash";
+    }
+
+    private static WatchlistSettings sInstance = new WatchlistSettings();
+    private final AtomicFile mXmlFile;
+    private final Object mLock = new Object();
+    private HarmfulDigests mCrc32DomainDigests = new HarmfulDigests(new ArrayList<>());
+    private HarmfulDigests mSha256DomainDigests = new HarmfulDigests(new ArrayList<>());
+    private HarmfulDigests mCrc32IpDigests = new HarmfulDigests(new ArrayList<>());
+    private HarmfulDigests mSha256IpDigests = new HarmfulDigests(new ArrayList<>());
+
+    public static synchronized WatchlistSettings getInstance() {
+        return sInstance;
+    }
+
+    private WatchlistSettings() {
+        this(getSystemWatchlistFile(WATCHLIST_XML_FILE));
+    }
+
+    @VisibleForTesting
+    protected WatchlistSettings(File xmlFile) {
+        mXmlFile = new AtomicFile(xmlFile);
+        readSettingsLocked();
+    }
+
+    static File getSystemWatchlistFile(String filename) {
+        final File dataSystemDir = Environment.getDataSystemDirectory();
+        final File systemWatchlistDir = new File(dataSystemDir, SYSTEM_WATCHLIST_DIR);
+        systemWatchlistDir.mkdirs();
+        return new File(systemWatchlistDir, filename);
+    }
+
+    private void readSettingsLocked() {
+        synchronized (mLock) {
+            FileInputStream stream;
+            try {
+                stream = mXmlFile.openRead();
+            } catch (FileNotFoundException e) {
+                Log.i(TAG, "No watchlist settings: " + mXmlFile.getBaseFile().getAbsolutePath());
+                return;
+            }
+
+            final List<byte[]> crc32DomainList = new ArrayList<>();
+            final List<byte[]> sha256DomainList = new ArrayList<>();
+            final List<byte[]> crc32IpList = new ArrayList<>();
+            final List<byte[]> sha256IpList = new ArrayList<>();
+
+            try {
+                XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(stream, StandardCharsets.UTF_8.name());
+                parser.nextTag();
+                parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_SETTINGS);
+                while (parser.nextTag() == XmlPullParser.START_TAG) {
+                    String tagName = parser.getName();
+                    switch (tagName) {
+                        case XmlTags.CRC32_DOMAIN:
+                            parseHash(parser, tagName, crc32DomainList);
+                            break;
+                        case XmlTags.CRC32_IP:
+                            parseHash(parser, tagName, crc32IpList);
+                            break;
+                        case XmlTags.SHA256_DOMAIN:
+                            parseHash(parser, tagName, sha256DomainList);
+                            break;
+                        case XmlTags.SHA256_IP:
+                            parseHash(parser, tagName, sha256IpList);
+                            break;
+                        default:
+                            Log.w(TAG, "Unknown element: " + parser.getName());
+                            XmlUtils.skipCurrentTag(parser);
+                    }
+                }
+                parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_SETTINGS);
+                writeSettingsToMemory(crc32DomainList, sha256DomainList, crc32IpList, sha256IpList);
+            } catch (IllegalStateException | NullPointerException | NumberFormatException |
+                    XmlPullParserException | IOException | IndexOutOfBoundsException e) {
+                Log.w(TAG, "Failed parsing " + e);
+            } finally {
+                try {
+                    stream.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    private void parseHash(XmlPullParser parser, String tagName, List<byte[]> hashSet)
+            throws IOException, XmlPullParserException {
+        parser.require(XmlPullParser.START_TAG, null, tagName);
+        while (parser.nextTag() == XmlPullParser.START_TAG) {
+            parser.require(XmlPullParser.START_TAG, null, XmlTags.HASH);
+            byte[] hash = HexDump.hexStringToByteArray(parser.nextText());
+            parser.require(XmlPullParser.END_TAG, null, XmlTags.HASH);
+            hashSet.add(hash);
+        }
+        parser.require(XmlPullParser.END_TAG, null, tagName);
+    }
+
+    /**
+     * Write network watchlist settings to disk.
+     * Adb should not use it, should use writeSettingsToMemory directly instead.
+     */
+    public void writeSettingsToDisk(List<byte[]> newCrc32DomainList,
+            List<byte[]> newSha256DomainList,
+            List<byte[]> newCrc32IpList,
+            List<byte[]> newSha256IpList) {
+        synchronized (mLock) {
+            FileOutputStream stream;
+            try {
+                stream = mXmlFile.startWrite();
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to write display settings: " + e);
+                return;
+            }
+
+            try {
+                XmlSerializer out = new FastXmlSerializer();
+                out.setOutput(stream, StandardCharsets.UTF_8.name());
+                out.startDocument(null, true);
+                out.startTag(null, XmlTags.WATCHLIST_SETTINGS);
+
+                writeHashSetToXml(out, XmlTags.SHA256_DOMAIN, newSha256DomainList);
+                writeHashSetToXml(out, XmlTags.SHA256_IP, newSha256IpList);
+                writeHashSetToXml(out, XmlTags.CRC32_DOMAIN, newCrc32DomainList);
+                writeHashSetToXml(out, XmlTags.CRC32_IP, newCrc32IpList);
+
+                out.endTag(null, XmlTags.WATCHLIST_SETTINGS);
+                out.endDocument();
+                mXmlFile.finishWrite(stream);
+                writeSettingsToMemory(newCrc32DomainList, newSha256DomainList, newCrc32IpList,
+                        newSha256IpList);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to write display settings, restoring backup.", e);
+                mXmlFile.failWrite(stream);
+            }
+        }
+    }
+
+    /**
+     * Write network watchlist settings to memory.
+     */
+    public void writeSettingsToMemory(List<byte[]> newCrc32DomainList,
+            List<byte[]> newSha256DomainList,
+            List<byte[]> newCrc32IpList,
+            List<byte[]> newSha256IpList) {
+        synchronized (mLock) {
+            mCrc32DomainDigests = new HarmfulDigests(newCrc32DomainList);
+            mCrc32IpDigests = new HarmfulDigests(newCrc32IpList);
+            mSha256DomainDigests = new HarmfulDigests(newSha256DomainList);
+            mSha256IpDigests = new HarmfulDigests(newSha256IpList);
+        }
+    }
+
+    private static void writeHashSetToXml(XmlSerializer out, String tagName, List<byte[]> hashSet)
+            throws IOException {
+        out.startTag(null, tagName);
+        for (byte[] hash : hashSet) {
+            out.startTag(null, XmlTags.HASH);
+            out.text(HexDump.toHexString(hash));
+            out.endTag(null, XmlTags.HASH);
+        }
+        out.endTag(null, tagName);
+    }
+
+    public boolean containsDomain(String domain) {
+        // First it does a quick CRC32 check.
+        final byte[] crc32 = getCrc32(domain);
+        if (!mCrc32DomainDigests.contains(crc32)) {
+            return false;
+        }
+        // Now we do a slow SHA256 check.
+        final byte[] sha256 = getSha256(domain);
+        return mSha256DomainDigests.contains(sha256);
+    }
+
+    public boolean containsIp(String ip) {
+        // First it does a quick CRC32 check.
+        final byte[] crc32 = getCrc32(ip);
+        if (!mCrc32IpDigests.contains(crc32)) {
+            return false;
+        }
+        // Now we do a slow SHA256 check.
+        final byte[] sha256 = getSha256(ip);
+        return mSha256IpDigests.contains(sha256);
+    }
+
+
+    /** Get CRC32 of a string */
+    private byte[] getCrc32(String str) {
+        final CRC32 crc = new CRC32();
+        crc.update(str.getBytes());
+        final long tmp = crc.getValue();
+        return new byte[]{(byte)(tmp >> 24 & 255), (byte)(tmp >> 16 & 255),
+                (byte)(tmp >> 8 & 255), (byte)(tmp & 255)};
+    }
+
+    /** Get SHA256 of a string */
+    private byte[] getSha256(String str) {
+        MessageDigest messageDigest;
+        try {
+            messageDigest = MessageDigest.getInstance("SHA256");
+        } catch (NoSuchAlgorithmException e) {
+            /* can't happen */
+            return null;
+        }
+        messageDigest.update(str.getBytes());
+        return messageDigest.digest();
+    }
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("Domain CRC32 digest list:");
+        mCrc32DomainDigests.dump(fd, pw, args);
+        pw.println("Domain SHA256 digest list:");
+        mSha256DomainDigests.dump(fd, pw, args);
+        pw.println("Ip CRC32 digest list:");
+        mCrc32IpDigests.dump(fd, pw, args);
+        pw.println("Ip SHA256 digest list:");
+        mSha256IpDigests.dump(fd, pw, args);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
index 0085931..0aaf32c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
@@ -107,7 +107,8 @@
             return false;
         }
         try {
-           if (mIpConnectivityMetrics.registerNetdEventCallback(mNetdEventCallback)) {
+           if (mIpConnectivityMetrics.addNetdEventCallback(
+                   INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY, mNetdEventCallback)) {
                 mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
                         /* allowIo */ false);
                 mHandlerThread.start();
@@ -138,7 +139,8 @@
                 // logging is forcefully disabled even if unregistering fails
                 return true;
             }
-            return mIpConnectivityMetrics.unregisterNetdEventCallback();
+            return mIpConnectivityMetrics.removeNetdEventCallback(
+                    INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY);
         } catch (RemoteException re) {
             Slog.wtf(TAG, "Failed to make remote calls to unregister the callback", re);
             return true;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index f8bcb73..e2e491a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -83,6 +83,7 @@
 import com.android.server.media.projection.MediaProjectionManagerService;
 import com.android.server.net.NetworkPolicyManagerService;
 import com.android.server.net.NetworkStatsService;
+import com.android.server.net.watchlist.NetworkWatchlistService;
 import com.android.server.notification.NotificationManagerService;
 import com.android.server.oemlock.OemLockService;
 import com.android.server.om.OverlayManagerService;
@@ -884,6 +885,10 @@
             mSystemServiceManager.startService(IpConnectivityMetrics.class);
             traceEnd();
 
+            traceBeginAndSlog("NetworkWatchlistService");
+            mSystemServiceManager.startService(NetworkWatchlistService.Lifecycle.class);
+            traceEnd();
+
             traceBeginAndSlog("PinnerService");
             mSystemServiceManager.startService(PinnerService.class);
             traceEnd();
diff --git a/services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml b/services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml
new file mode 100644
index 0000000..bb97e94
--- /dev/null
+++ b/services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml
@@ -0,0 +1,27 @@
+<?xml version='1.0'?>
+<watchlist-settings>
+    <sha256-domain>
+        <!-- test-cc-domain.com -->
+        <hash>8E7DCD2AEB4F364358242BB3F403263E61E3B4AECE4E2500FF28BF32E52FF0F1</hash>
+        <!-- test-cc-match-sha256-only.com -->
+        <hash>F0905DA7549614957B449034C281EF7BDEFDBC2B6E050AD1E78D6DE18FBD0D5F</hash>
+    </sha256-domain>
+    <sha256-ip>
+        <!-- 127.0.0.2 -->
+        <hash>1EDD62868F2767A1FFF68DF0A4CB3C23448E45100715768DB9310B5E719536A1</hash>
+        <!-- 127.0.0.3, match in sha256 only -->
+        <hash>18DD41C9F2E8E4879A1575FB780514EF33CF6E1F66578C4AE7CCA31F49B9F2ED</hash>
+    </sha256-ip>
+    <crc32-domain>
+        <!-- test-cc-domain.com -->
+        <hash>6C67059D</hash>
+        <!-- test-cc-match-crc32-only.com -->
+        <hash>3DC775F8</hash>
+    </crc32-domain>
+    <crc32-ip>
+        <!-- 127.0.0.2 -->
+        <hash>4EBEB612</hash>
+        <!-- 127.0.0.4, match in crc32 only -->
+        <hash>A7DD1327</hash>
+    </crc32-ip>
+</watchlist-settings>
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);
+        }
+    }
+}