[DPM] Management and retrieval of network logs
This CL follows up on ag/1530343 and adds:
1) Various network events.
2) Retrieval method in DPM and APIs in DeviceAdminReceiver.
3) Extension of NetworkLogger and it's NetworkLoggingHandler.
Test: runtest --path frameworks/base/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
Bug: 29748723
Change-Id: I42a1a477e7c75c109a3982f809c22732b814e8b2
diff --git a/Android.mk b/Android.mk
index 09bdea2..862569d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -585,6 +585,9 @@
frameworks/base/graphics/java/android/graphics/drawable/Icon.aidl \
frameworks/base/core/java/android/accounts/AuthenticatorDescription.aidl \
frameworks/base/core/java/android/accounts/Account.aidl \
+ frameworks/base/core/java/android/app/admin/ConnectEvent.aidl \
+ frameworks/base/core/java/android/app/admin/DnsEvent.aidl \
+ frameworks/base/core/java/android/app/admin/NetworkEvent.aidl \
frameworks/base/core/java/android/app/admin/SystemUpdatePolicy.aidl \
frameworks/base/core/java/android/app/admin/PasswordMetrics.aidl \
frameworks/base/core/java/android/print/PrintDocumentInfo.aidl \
diff --git a/core/java/android/app/admin/ConnectEvent.aidl b/core/java/android/app/admin/ConnectEvent.aidl
new file mode 100644
index 0000000..bab40f5
--- /dev/null
+++ b/core/java/android/app/admin/ConnectEvent.aidl
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 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.app.admin;
+
+/** {@hide} */
+parcelable ConnectEvent;
+
diff --git a/core/java/android/app/admin/ConnectEvent.java b/core/java/android/app/admin/ConnectEvent.java
new file mode 100644
index 0000000..e05feaf
--- /dev/null
+++ b/core/java/android/app/admin/ConnectEvent.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 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.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class that represents a connect library call event.
+ * @hide
+ */
+public final class ConnectEvent extends NetworkEvent implements Parcelable {
+
+ /** The destination IP address. */
+ private final String ipAddress;
+
+ /** The destination port number. */
+ private final int port;
+
+ public ConnectEvent(String ipAddress, int port, String packageName, long timestamp) {
+ super(packageName, timestamp);
+ this.ipAddress = ipAddress;
+ this.port = port;
+ }
+
+ private ConnectEvent(Parcel in) {
+ this.ipAddress = in.readString();
+ this.port = in.readInt();
+ this.packageName = in.readString();
+ this.timestamp = in.readLong();
+ }
+
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ConnectEvent(%s, %d, %d, %s)", ipAddress, port, timestamp,
+ packageName);
+ }
+
+ public static final Parcelable.Creator<ConnectEvent> CREATOR
+ = new Parcelable.Creator<ConnectEvent>() {
+ @Override
+ public ConnectEvent createFromParcel(Parcel in) {
+ if (in.readInt() != PARCEL_TOKEN_CONNECT_EVENT) {
+ return null;
+ }
+ return new ConnectEvent(in);
+ }
+
+ @Override
+ public ConnectEvent[] newArray(int size) {
+ return new ConnectEvent[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // write parcel token first
+ out.writeInt(PARCEL_TOKEN_CONNECT_EVENT);
+ out.writeString(ipAddress);
+ out.writeInt(port);
+ out.writeString(packageName);
+ out.writeLong(timestamp);
+ }
+}
+
diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java
index dd70b5d..360087c 100644
--- a/core/java/android/app/admin/DeviceAdminReceiver.java
+++ b/core/java/android/app/admin/DeviceAdminReceiver.java
@@ -276,6 +276,15 @@
= "android.app.action.SECURITY_LOGS_AVAILABLE";
/**
+ * Broadcast action: notify that a new batch of network logs is ready to be collected.
+ * @see DeviceAdminReceiver#onNetworkLogsAvailable
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NETWORK_LOGS_AVAILABLE
+ = "android.app.action.NETWORK_LOGS_AVAILABLE";
+
+ /**
* A string containing the SHA-256 hash of the bugreport file.
*
* @see #ACTION_BUGREPORT_SHARE
@@ -635,6 +644,22 @@
}
/**
+ * Called when a new batch of network logs can be retrieved. This callback method will only ever
+ * be called when network logging is enabled. The logs can only be retrieved while network
+ * logging is enabled.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @see DevicePolicyManager#retrieveNetworkLogs(ComponentName)
+ *
+ * @hide
+ */
+ public void onNetworkLogsAvailable(Context context, Intent intent) {
+ }
+
+ /**
* Intercept standard device administrator broadcasts. Implementations
* should not override this method; it is better to implement the
* convenience callbacks for each action.
@@ -688,6 +713,8 @@
onBugreportFailed(context, intent, failureCode);
} else if (ACTION_SECURITY_LOGS_AVAILABLE.equals(action)) {
onSecurityLogsAvailable(context, intent);
+ } else if (ACTION_NETWORK_LOGS_AVAILABLE.equals(action)) {
+ onNetworkLogsAvailable(context, intent);
}
}
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 138ec02..94cfaca 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -26,6 +26,7 @@
import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.app.Activity;
+import android.app.admin.NetworkEvent;
import android.app.admin.PasswordMetrics;
import android.app.admin.SecurityLog.SecurityEvent;
import android.content.ComponentName;
@@ -6648,6 +6649,7 @@
* @throws {@link SecurityException} if {@code admin} is not a device owner.
* @throws {@link RemoteException} if network logging could not be enabled or disabled due to
* the logging service not being available
+ * @see #retrieveNetworkLogs
*
* @hide
*/
@@ -6677,4 +6679,31 @@
throw re.rethrowFromSystemServer();
}
}
+
+ /**
+ * Called by device owner to retrieve a new batch of network logging events.
+ *
+ * <p> {@link NetworkEvent} can be one of {@link DnsEvent} or {@link ConnectEvent}.
+ *
+ * <p> The list of network events is sorted chronologically, and contains at most 1200 events.
+ *
+ * <p> Access to the logs is rate limited and this method will only return a new batch of logs
+ * after the device device owner has been notified via
+ * {@link DeviceAdminReceiver#onNetworkLogsAvailable}.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @return A new batch of network logs which is a list of {@link NetworkEvent}. Returns
+ * {@code null} if there's no batch currently awaiting for retrieval or if logging is disabled.
+ * @throws {@link SecurityException} if {@code admin} is not a device owner.
+ *
+ * @hide
+ */
+ public List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin) {
+ throwIfParentInstance("retrieveNetworkLogs");
+ try {
+ return mService.retrieveNetworkLogs(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/app/admin/DnsEvent.aidl b/core/java/android/app/admin/DnsEvent.aidl
new file mode 100644
index 0000000..6da962a
--- /dev/null
+++ b/core/java/android/app/admin/DnsEvent.aidl
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 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.app.admin;
+
+/** {@hide} */
+parcelable DnsEvent;
+
diff --git a/core/java/android/app/admin/DnsEvent.java b/core/java/android/app/admin/DnsEvent.java
new file mode 100644
index 0000000..0ec134a
--- /dev/null
+++ b/core/java/android/app/admin/DnsEvent.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 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.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class that represents a DNS lookup event.
+ * @hide
+ */
+public final class DnsEvent extends NetworkEvent implements Parcelable {
+
+ /** The hostname that was looked up. */
+ private final String hostname;
+
+ /** Contains (possibly a subset of) the IP addresses returned. */
+ private final String[] ipAddresses;
+
+ /**
+ * The number of IP addresses returned from the DNS lookup event. May be different from the
+ * length of ipAddresses if there were too many addresses to log.
+ */
+ private final int ipAddressesCount;
+
+ public DnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount,
+ String packageName, long timestamp) {
+ super(packageName, timestamp);
+ this.hostname = hostname;
+ this.ipAddresses = ipAddresses;
+ this.ipAddressesCount = ipAddressesCount;
+ }
+
+ private DnsEvent(Parcel in) {
+ this.hostname = in.readString();
+ this.ipAddresses = in.createStringArray();
+ this.ipAddressesCount = in.readInt();
+ this.packageName = in.readString();
+ this.timestamp = in.readLong();
+ }
+
+ /** Returns the hostname that was looked up. */
+ public String getHostname() {
+ return hostname;
+ }
+
+ /** Returns (possibly a subset of) the IP addresses returned. */
+ public String[] getIpAddresses() {
+ return ipAddresses;
+ }
+
+ /**
+ * Returns the number of IP addresses returned from the DNS lookup event. May be different from
+ * the length of ipAddresses if there were too many addresses to log.
+ */
+ public int getIpAddressesCount() {
+ return ipAddressesCount;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("DnsEvent(%s, %s, %d, %d, %s)", hostname,
+ (ipAddresses == null) ? "NONE" : String.join(" ", ipAddresses),
+ ipAddressesCount, timestamp, packageName);
+ }
+
+ public static final Parcelable.Creator<DnsEvent> CREATOR
+ = new Parcelable.Creator<DnsEvent>() {
+ @Override
+ public DnsEvent createFromParcel(Parcel in) {
+ if (in.readInt() != PARCEL_TOKEN_DNS_EVENT) {
+ return null;
+ }
+ return new DnsEvent(in);
+ }
+
+ @Override
+ public DnsEvent[] newArray(int size) {
+ return new DnsEvent[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // write parcel token first
+ out.writeInt(PARCEL_TOKEN_DNS_EVENT);
+ out.writeString(hostname);
+ out.writeStringArray(ipAddresses);
+ out.writeInt(ipAddressesCount);
+ out.writeString(packageName);
+ out.writeLong(timestamp);
+ }
+}
+
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 3cfa1e8..b0aec8c 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -17,6 +17,7 @@
package android.app.admin;
+import android.app.admin.NetworkEvent;
import android.app.admin.SystemUpdatePolicy;
import android.app.admin.PasswordMetrics;
import android.content.ComponentName;
@@ -317,4 +318,5 @@
void setNetworkLoggingEnabled(in ComponentName admin, boolean enabled);
boolean isNetworkLoggingEnabled(in ComponentName admin);
+ List<NetworkEvent> retrieveNetworkLogs(in ComponentName admin);
}
diff --git a/core/java/android/app/admin/NetworkEvent.aidl b/core/java/android/app/admin/NetworkEvent.aidl
new file mode 100644
index 0000000..5fa5dbf
--- /dev/null
+++ b/core/java/android/app/admin/NetworkEvent.aidl
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 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.app.admin;
+
+/** {@hide} */
+parcelable NetworkEvent;
+
diff --git a/core/java/android/app/admin/NetworkEvent.java b/core/java/android/app/admin/NetworkEvent.java
new file mode 100644
index 0000000..ec7ed00
--- /dev/null
+++ b/core/java/android/app/admin/NetworkEvent.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 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.app.admin;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelFormatException;
+
+/**
+ * An abstract class that represents a network event.
+ * @hide
+ */
+public abstract class NetworkEvent implements Parcelable {
+
+ protected static final int PARCEL_TOKEN_DNS_EVENT = 1;
+ protected static final int PARCEL_TOKEN_CONNECT_EVENT = 2;
+
+ /** The package name of the UID that performed the query. */
+ protected String packageName;
+
+ /** The timestamp of the event being reported in milliseconds. */
+ protected long timestamp;
+
+ protected NetworkEvent() {
+ //empty constructor
+ }
+
+ protected NetworkEvent(String packageName, long timestamp) {
+ this.packageName = packageName;
+ this.timestamp = timestamp;
+ }
+
+ /** Returns the package name of the UID that performed the query. */
+ public String getPackageName() {
+ return packageName;
+ }
+
+ /** Returns the timestamp of the event being reported in milliseconds. */
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<NetworkEvent> CREATOR
+ = new Parcelable.Creator<NetworkEvent>() {
+ public NetworkEvent createFromParcel(Parcel in) {
+ final int initialPosition = in.dataPosition();
+ final int parcelToken = in.readInt();
+ // we need to move back to the position from before we read parcelToken
+ in.setDataPosition(initialPosition);
+ switch (parcelToken) {
+ case PARCEL_TOKEN_DNS_EVENT:
+ return DnsEvent.CREATOR.createFromParcel(in);
+ case PARCEL_TOKEN_CONNECT_EVENT:
+ return ConnectEvent.CREATOR.createFromParcel(in);
+ default:
+ throw new ParcelFormatException("Unexpected NetworkEvent token in parcel: "
+ + parcelToken);
+ }
+ }
+
+ public NetworkEvent[] newArray(int size) {
+ return new NetworkEvent[size];
+ }
+ };
+}
+
diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java
index da4eb2d..1f013ae 100644
--- a/core/java/android/content/pm/PackageManagerInternal.java
+++ b/core/java/android/content/pm/PackageManagerInternal.java
@@ -189,4 +189,17 @@
public abstract void revokeRuntimePermission(String packageName, String name, int userId,
boolean overridePolicy);
+ /**
+ * Retrieve the official name associated with a user id. This name is
+ * guaranteed to never change, though it is possible for the underlying
+ * user id to be changed. That is, if you are storing information about
+ * user ids in persistent storage, you should use the string returned
+ * by this function instead of the raw user-id.
+ *
+ * @param uid The user id for which you would like to retrieve a name.
+ * @return Returns a unique name for the given user id, or null if the
+ * user id is not currently assigned.
+ */
+ public abstract String getNameForUid(int uid);
+
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b38bb1e..7f657ea 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -454,6 +454,7 @@
<protected-broadcast android:name="com.android.server.Wifi.action.TOGGLE_PNO" />
<protected-broadcast android:name="intent.action.ACTION_RF_BAND_INFO" />
<protected-broadcast android:name="android.intent.action.MEDIA_RESOURCE_GRANTED" />
+ <protected-broadcast android:name="android.app.action.NETWORK_LOGS_AVAILABLE" />
<protected-broadcast android:name="android.app.action.SECURITY_LOGS_AVAILABLE" />
<protected-broadcast android:name="android.app.action.INTERRUPTION_FILTER_CHANGED" />
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 59c4fb9..3705814 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -21228,6 +21228,11 @@
PackageManagerService.this.revokeRuntimePermission(packageName, name, userId,
overridePolicy);
}
+
+ @Override
+ public String getNameForUid(int uid) {
+ return PackageManagerService.this.getNameForUid(uid);
+ }
}
@Override
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 42b5dc0..6006c6b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -51,6 +51,7 @@
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.app.admin.IDevicePolicyManager;
+import android.app.admin.NetworkEvent;
import android.app.admin.PasswordMetrics;
import android.app.admin.SecurityLog;
import android.app.admin.SecurityLog.SecurityEvent;
@@ -9558,4 +9559,23 @@
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
return (deviceOwner != null) && deviceOwner.isNetworkLoggingEnabled;
}
+
+ /*
+ * A maximum of 1200 events are returned, and the total marshalled size is in the order of
+ * 100kB, so returning a List instead of ParceledListSlice is acceptable.
+ * Ideally this would be done with ParceledList, however it only supports homogeneous types.
+ */
+ @Override
+ public synchronized List<NetworkEvent> retrieveNetworkLogs(ComponentName admin) {
+ if (!mHasFeature) {
+ return null;
+ }
+ Preconditions.checkNotNull(admin);
+ ensureDeviceOwnerManagingSingleUser(admin);
+
+ if (mNetworkLogger == null) {
+ return null;
+ }
+ return isNetworkLoggingEnabledInternal() ? mNetworkLogger.retrieveLogs() : null;
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
index db17ca2..185ccc2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
@@ -16,9 +16,16 @@
package com.android.server.devicepolicy;
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.app.admin.NetworkEvent;
import android.content.pm.PackageManagerInternal;
import android.net.IIpConnectivityMetrics;
import android.net.INetdEventCallback;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import android.util.Slog;
@@ -40,6 +47,8 @@
private final PackageManagerInternal mPm;
private IIpConnectivityMetrics mIpConnectivityMetrics;
+ private ServiceThread mHandlerThread;
+ private NetworkLoggingHandler mNetworkLoggingHandler;
private boolean mIsLoggingEnabled;
private final INetdEventCallback mNetdEventCallback = new INetdEventCallback.Stub() {
@@ -49,7 +58,9 @@
if (!mIsLoggingEnabled) {
return;
}
- // TODO(mkarpinski): send msg with data to Handler
+ DnsEvent dnsEvent = new DnsEvent(hostname, ipAddresses, ipAddressesCount,
+ mPm.getNameForUid(uid), timestamp);
+ sendNetworkEvent(dnsEvent);
}
@Override
@@ -57,7 +68,18 @@
if (!mIsLoggingEnabled) {
return;
}
- // TODO(mkarpinski): send msg with data to Handler
+ ConnectEvent connectEvent = new ConnectEvent(ipAddr, port, mPm.getNameForUid(uid),
+ timestamp);
+ sendNetworkEvent(connectEvent);
+ }
+
+ private void sendNetworkEvent(NetworkEvent event) {
+ Message msg = mNetworkLoggingHandler.obtainMessage(
+ NetworkLoggingHandler.LOG_NETWORK_EVENT_MSG);
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(NetworkLoggingHandler.NETWORK_EVENT_KEY, event);
+ msg.setData(bundle);
+ mNetworkLoggingHandler.sendMessage(msg);
}
};
@@ -87,7 +109,13 @@
}
try {
if (mIpConnectivityMetrics.registerNetdEventCallback(mNetdEventCallback)) {
- // TODO(mkarpinski): start a new ServiceThread, instantiate a Handler etc.
+ mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
+ /* allowIo */ false);
+ mHandlerThread.start();
+ mNetworkLoggingHandler = new NetworkLoggingHandler(mHandlerThread.getLooper(),
+ mDpm);
+ mNetworkLoggingHandler.scheduleBatchFinalization(
+ NetworkLoggingHandler.BATCH_FINALIZATION_TIMEOUT_MS);
mIsLoggingEnabled = true;
return true;
} else {
@@ -101,7 +129,7 @@
boolean stopNetworkLogging() {
Log.d(TAG, "Stopping network logging");
- // stop the logging regardless of whether we failed to unregister listener
+ // stop the logging regardless of whether we fail to unregister listener
mIsLoggingEnabled = false;
try {
if (!checkIpConnectivityMetricsService()) {
@@ -114,8 +142,12 @@
} catch (RemoteException re) {
Slog.wtf(TAG, "Failed to make remote calls to unregister the callback", re);
} finally {
- // TODO(mkarpinski): quitSafely() the Handler
+ mHandlerThread.quitSafely();
return true;
}
}
+
+ List<NetworkEvent> retrieveLogs() {
+ return mNetworkLoggingHandler.retrieveFullLogBatch();
+ }
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
new file mode 100644
index 0000000..96884e6
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 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.devicepolicy;
+
+import android.app.admin.DeviceAdminReceiver;
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.app.admin.NetworkEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A Handler class for managing network logging on a background thread.
+ */
+final class NetworkLoggingHandler extends Handler {
+
+ private static final String TAG = NetworkLoggingHandler.class.getSimpleName();
+
+ static final String NETWORK_EVENT_KEY = "network_event";
+
+ // est. ~128kB of memory usage per full batch TODO(mkarpinski): fine tune based on testing data
+ // If this value changes, update DevicePolicyManager#retrieveNetworkLogs() javadoc
+ private static final int MAX_EVENTS_PER_BATCH = 1200;
+ static final long BATCH_FINALIZATION_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(90);
+
+ static final int LOG_NETWORK_EVENT_MSG = 1;
+ static final int FINALIZE_BATCH_MSG = 2;
+
+ private final DevicePolicyManagerService mDpm;
+
+ // threadsafe as it's Handler's thread confined
+ private ArrayList<NetworkEvent> mNetworkEvents = new ArrayList<NetworkEvent>();
+
+ @GuardedBy("this")
+ private ArrayList<NetworkEvent> mFullBatch;
+
+ NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm) {
+ super(looper);
+ mDpm = dpm;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case LOG_NETWORK_EVENT_MSG: {
+ NetworkEvent networkEvent = msg.getData().getParcelable(NETWORK_EVENT_KEY);
+ if (networkEvent != null) {
+ mNetworkEvents.add(networkEvent);
+ if (mNetworkEvents.size() >= MAX_EVENTS_PER_BATCH) {
+ finalizeBatchAndNotifyDeviceOwner();
+ }
+ }
+ break;
+ }
+ case FINALIZE_BATCH_MSG: {
+ finalizeBatchAndNotifyDeviceOwner();
+ break;
+ }
+ }
+ }
+
+ void scheduleBatchFinalization(long delay) {
+ removeMessages(FINALIZE_BATCH_MSG);
+ sendMessageDelayed(obtainMessage(FINALIZE_BATCH_MSG), delay);
+ }
+
+ private synchronized void finalizeBatchAndNotifyDeviceOwner() {
+ mFullBatch = mNetworkEvents;
+ // start a new batch from scratch
+ mNetworkEvents = new ArrayList<NetworkEvent>();
+ scheduleBatchFinalization(BATCH_FINALIZATION_TIMEOUT_MS);
+ // notify DO that there's a new non-empty batch waiting
+ if (mFullBatch.size() > 0) {
+ mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE,
+ /* extras */ null);
+ } else {
+ mFullBatch = null;
+ }
+ }
+
+ synchronized List<NetworkEvent> retrieveFullLogBatch() {
+ List<NetworkEvent> ret = mFullBatch;
+ mFullBatch = null;
+ return ret;
+ }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
new file mode 100644
index 0000000..315d37c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 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.devicepolicy;
+
+import android.app.admin.ConnectEvent;
+import android.app.admin.DnsEvent;
+import android.os.Parcel;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import static junit.framework.Assert.assertEquals;
+
+@SmallTest
+public class NetworkEventTest extends DpmTestBase {
+
+ /**
+ * Test parceling and unparceling of a ConnectEvent.
+ */
+ public void testConnectEventParceling() {
+ ConnectEvent event = new ConnectEvent("127.0.0.1", 80, "com.android.whateverdude", 100000);
+ Parcel p = Parcel.obtain();
+ p.writeParcelable(event, 0);
+ p.setDataPosition(0);
+ ConnectEvent unparceledEvent = p.readParcelable(NetworkEventTest.class.getClassLoader());
+ p.recycle();
+ assertEquals(event.getIpAddress(), unparceledEvent.getIpAddress());
+ assertEquals(event.getPort(), unparceledEvent.getPort());
+ assertEquals(event.getPackageName(), unparceledEvent.getPackageName());
+ assertEquals(event.getTimestamp(), unparceledEvent.getTimestamp());
+ }
+
+ /**
+ * Test parceling and unparceling of a DnsEvent.
+ */
+ public void testDnsEventParceling() {
+ DnsEvent event = new DnsEvent("d.android.com", new String[]{"192.168.0.1", "127.0.0.1"}, 2,
+ "com.android.whateverdude", 100000);
+ Parcel p = Parcel.obtain();
+ p.writeParcelable(event, 0);
+ p.setDataPosition(0);
+ DnsEvent unparceledEvent = p.readParcelable(NetworkEventTest.class.getClassLoader());
+ p.recycle();
+ assertEquals(event.getHostname(), unparceledEvent.getHostname());
+ assertEquals(event.getIpAddresses()[0], unparceledEvent.getIpAddresses()[0]);
+ assertEquals(event.getIpAddresses()[1], unparceledEvent.getIpAddresses()[1]);
+ assertEquals(event.getIpAddressesCount(), unparceledEvent.getIpAddressesCount());
+ assertEquals(event.getPackageName(), unparceledEvent.getPackageName());
+ assertEquals(event.getTimestamp(), unparceledEvent.getTimestamp());
+ }
+}