summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk1
-rw-r--r--core/java/android/app/SystemServiceRegistry.java13
-rw-r--r--core/java/android/content/Context.java2
-rw-r--r--core/java/android/net/IIpConnectivityMetrics.aidl6
-rw-r--r--core/java/android/net/INetdEventCallback.aidl4
-rw-r--r--core/java/android/net/NetworkWatchlistManager.java75
-rwxr-xr-xcore/java/android/provider/Settings.java7
-rw-r--r--core/java/com/android/internal/net/INetworkWatchlistManager.aidl26
-rw-r--r--core/res/AndroidManifest.xml4
-rw-r--r--core/tests/coretests/src/android/provider/SettingsBackupTest.java2
-rw-r--r--services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java10
-rw-r--r--services/core/java/com/android/server/connectivity/NetdEventListenerService.java64
-rw-r--r--services/core/java/com/android/server/net/watchlist/DigestUtils.java54
-rw-r--r--services/core/java/com/android/server/net/watchlist/HarmfulDigests.java55
-rw-r--r--services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java267
-rw-r--r--services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java76
-rw-r--r--services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java298
-rw-r--r--services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java203
-rw-r--r--services/core/java/com/android/server/net/watchlist/WatchlistSettings.java284
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java6
-rw-r--r--services/java/com/android/server/SystemServer.java5
-rw-r--r--services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml27
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java63
-rw-r--r--services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java217
-rw-r--r--services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java61
-rw-r--r--services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java195
27 files changed, 2003 insertions, 24 deletions
diff --git a/Android.mk b/Android.mk
index a7cb362b677e..c4f222ea7174 100644
--- a/Android.mk
+++ b/Android.mk
@@ -398,6 +398,7 @@ LOCAL_SRC_FILES += \
core/java/com/android/internal/backup/IObbBackupService.aidl \
core/java/com/android/internal/car/ICarServiceHelper.aidl \
core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl \
+ core/java/com/android/internal/net/INetworkWatchlistManager.aidl \
core/java/com/android/internal/policy/IKeyguardDrawnCallback.aidl \
core/java/com/android/internal/policy/IKeyguardDismissCallback.aidl \
core/java/com/android/internal/policy/IKeyguardExitCallback.aidl \
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 6989db64c42e..4efc2c79faed 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -81,6 +81,7 @@ import android.net.INetworkPolicyManager;
import android.net.IpSecManager;
import android.net.NetworkPolicyManager;
import android.net.NetworkScoreManager;
+import android.net.NetworkWatchlistManager;
import android.net.lowpan.ILowpanManager;
import android.net.lowpan.LowpanManager;
import android.net.nsd.INsdManager;
@@ -150,6 +151,7 @@ import com.android.internal.app.IAppOpsService;
import com.android.internal.app.IBatteryStats;
import com.android.internal.app.ISoundTriggerService;
import com.android.internal.appwidget.IAppWidgetService;
+import com.android.internal.net.INetworkWatchlistManager;
import com.android.internal.os.IDropBoxManagerService;
import com.android.internal.policy.PhoneLayoutInflater;
@@ -862,6 +864,17 @@ final class SystemServiceRegistry {
return new ShortcutManager(ctx, IShortcutService.Stub.asInterface(b));
}});
+ registerService(Context.NETWORK_WATCHLIST_SERVICE, NetworkWatchlistManager.class,
+ new CachedServiceFetcher<NetworkWatchlistManager>() {
+ @Override
+ public NetworkWatchlistManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder b =
+ ServiceManager.getServiceOrThrow(Context.NETWORK_WATCHLIST_SERVICE);
+ return new NetworkWatchlistManager(ctx,
+ INetworkWatchlistManager.Stub.asInterface(b));
+ }});
+
registerService(Context.SYSTEM_HEALTH_SERVICE, SystemHealthManager.class,
new CachedServiceFetcher<SystemHealthManager>() {
@Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index c165fb3e925c..01ad3ad2dcca 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3413,6 +3413,8 @@ public abstract class Context {
public static final String NETWORK_STATS_SERVICE = "netstats";
/** {@hide} */
public static final String NETWORK_POLICY_SERVICE = "netpolicy";
+ /** {@hide} */
+ public static final String NETWORK_WATCHLIST_SERVICE = "network_watchlist";
/**
* Use with {@link #getSystemService} to retrieve a {@link
diff --git a/core/java/android/net/IIpConnectivityMetrics.aidl b/core/java/android/net/IIpConnectivityMetrics.aidl
index 6f07b3153833..aeaf09d8fafe 100644
--- a/core/java/android/net/IIpConnectivityMetrics.aidl
+++ b/core/java/android/net/IIpConnectivityMetrics.aidl
@@ -30,11 +30,11 @@ interface IIpConnectivityMetrics {
int logEvent(in ConnectivityMetricsEvent event);
/**
- * At most one callback can be registered (by DevicePolicyManager).
+ * Callback can be registered by DevicePolicyManager or NetworkWatchlistService only.
* @return status {@code true} if registering/unregistering of the callback was successful,
* {@code false} otherwise (might happen if IIpConnectivityMetrics is not available,
* if it happens make sure you call it when the service is up in the caller)
*/
- boolean registerNetdEventCallback(in INetdEventCallback callback);
- boolean unregisterNetdEventCallback();
+ boolean addNetdEventCallback(in int callerType, in INetdEventCallback callback);
+ boolean removeNetdEventCallback(in int callerType);
}
diff --git a/core/java/android/net/INetdEventCallback.aidl b/core/java/android/net/INetdEventCallback.aidl
index 49436beadc51..1fd9423b6128 100644
--- a/core/java/android/net/INetdEventCallback.aidl
+++ b/core/java/android/net/INetdEventCallback.aidl
@@ -19,6 +19,10 @@ package android.net;
/** {@hide} */
oneway interface INetdEventCallback {
+ // Possible addNetdEventCallback callers.
+ const int CALLBACK_CALLER_DEVICE_POLICY = 0;
+ const int CALLBACK_CALLER_NETWORK_WATCHLIST = 1;
+
/**
* Reports a single DNS lookup function call.
* This method must not block or perform long-running operations.
diff --git a/core/java/android/net/NetworkWatchlistManager.java b/core/java/android/net/NetworkWatchlistManager.java
new file mode 100644
index 000000000000..42e43c8aea1e
--- /dev/null
+++ b/core/java/android/net/NetworkWatchlistManager.java
@@ -0,0 +1,75 @@
+/*
+ * 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 android.net;
+
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.net.INetworkWatchlistManager;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Class that manage network watchlist in system.
+ * @hide
+ */
+@SystemService(Context.NETWORK_WATCHLIST_SERVICE)
+public class NetworkWatchlistManager {
+
+ private static final String TAG = "NetworkWatchlistManager";
+ private static final String SHARED_MEMORY_TAG = "NETWORK_WATCHLIST_SHARED_MEMORY";
+
+ private final Context mContext;
+ private final INetworkWatchlistManager mNetworkWatchlistManager;
+
+ /**
+ * @hide
+ */
+ public NetworkWatchlistManager(Context context, INetworkWatchlistManager manager) {
+ mContext = context;
+ mNetworkWatchlistManager = manager;
+ }
+
+ /**
+ * @hide
+ */
+ public NetworkWatchlistManager(Context context) {
+ mContext = Preconditions.checkNotNull(context, "missing context");
+ mNetworkWatchlistManager = (INetworkWatchlistManager)
+ INetworkWatchlistManager.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_WATCHLIST_SERVICE));
+ }
+
+ /**
+ * Report network watchlist records if necessary.
+ *
+ * Watchlist report process will run summarize records into a single report, then the
+ * report will be processed by differential privacy framework and store it on disk.
+ *
+ * @hide
+ */
+ public void reportWatchlistIfNecessary() {
+ try {
+ mNetworkWatchlistManager.reportWatchlistIfNecessary();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot report records", e);
+ e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0a20c4348462..69c218076efa 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8513,6 +8513,13 @@ public final class Settings {
public static final String NETWORK_METERED_MULTIPATH_PREFERENCE =
"network_metered_multipath_preference";
+ /**
+ * Network watchlist last report time.
+ * @hide
+ */
+ public static final String NETWORK_WATCHLIST_LAST_REPORT_TIME =
+ "network_watchlist_last_report_time";
+
/**
* The thresholds of the wifi throughput badging (SD, HD etc.) as a comma-delimited list of
* colon-delimited key-value pairs. The key is the badging enum value defined in
diff --git a/core/java/com/android/internal/net/INetworkWatchlistManager.aidl b/core/java/com/android/internal/net/INetworkWatchlistManager.aidl
new file mode 100644
index 000000000000..7e88369055b8
--- /dev/null
+++ b/core/java/com/android/internal/net/INetworkWatchlistManager.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.internal.net;
+
+import android.os.SharedMemory;
+
+/** {@hide} */
+interface INetworkWatchlistManager {
+ boolean startWatchlistLogging();
+ boolean stopWatchlistLogging();
+ void reportWatchlistIfNecessary();
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 49de135d8ca5..b8d3a5744db5 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3944,6 +3944,10 @@
<service android:name="com.android.server.timezone.TimeZoneUpdateIdler"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
+
+ <service android:name="com.android.server.net.watchlist.ReportWatchlistJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE" >
+ </service>
</application>
</manifest>
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 1002939c7fa4..d36ed639b4b9 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -26,7 +26,6 @@ import static java.lang.reflect.Modifier.isFinal;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
-import android.platform.test.annotations.Presubmit;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -264,6 +263,7 @@ public class SettingsBackupTest {
Settings.Global.NETSTATS_UID_TAG_ROTATE_AGE,
Settings.Global.NETWORK_AVOID_BAD_WIFI,
Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE,
+ Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
Settings.Global.NETWORK_PREFERENCE,
Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE,
Settings.Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS,
diff --git a/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java
index 5cc390a02b86..f4278196b576 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.INetdEventCallback;
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 @@ final public class IpConnectivityMetrics extends SystemService {
}
@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 61b11e18e3cd..af138b936fa7 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 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
@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.
+
+
+ /**
+ * 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 mNetdEventCallback;
+ private INetdEventCallback[] mNetdEventCallbackList =
+ new INetdEventCallback[ALLOWED_CALLBACK_TYPES.length];
- public synchronized boolean registerNetdEventCallback(INetdEventCallback callback) {
- mNetdEventCallback = callback;
+ 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 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
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 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
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 000000000000..57becb002a59
--- /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 000000000000..27c22cea1782
--- /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 000000000000..171703ac8933
--- /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 000000000000..dfeb1b2d2eab
--- /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 000000000000..2247558476c4
--- /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 000000000000..f48463f5ae63
--- /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 000000000000..c50f0d56c992
--- /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 00859311af84..0aaf32cbe275 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 @@ final class NetworkLogger {
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 @@ final class NetworkLogger {
// 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 f8bcb73a8f83..e2e491a050de 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.MediaSessionService;
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 @@ public final class SystemServer {
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 000000000000..bb97e9431f72
--- /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 9d23fe9691b1..6de3395e6fdf 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 @@ public class DevicePolicyManagerTest extends DpmTestBase {
// 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 000000000000..a34f95eed040
--- /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 000000000000..ccd3cdd3a20b
--- /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 000000000000..e356b13d01d5
--- /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 000000000000..f3cb98078602
--- /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);
+ }
+ }
+}