Merge history of ConnectivityT
Renamed files/directories: ConnectivityT/service/Android.bp --> service-t/Sources.bp ConnectivityT/framework-t/Android.bp --> framework-t/Sources.bp ConnectivityT/framework-t/aidl-export --> framework/aidl-export ConnectivityT/service --> service-t ConnectivityT/framework-t --> framework-t ConnectivityT/tests --> tests ConnectivityT/OWNERS --> (removed) BUG: 222234190 TEST: TH Ignore-AOSP-First: Move with history done per-branch Merged-In: I81893df9f327abb84f1561b2b33027a2d23a4d65 Merged-In: I67c703e3f7aa9d5787f032a79ed62e45412baf4f Change-Id: I27a91f1a94f9d807f92762436f533c4b0d0114d5
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007 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;
|
||||
|
||||
interface INativeDaemonConnectorCallbacks {
|
||||
|
||||
void onDaemonConnected();
|
||||
boolean onCheckHoldWakeLock(int code);
|
||||
boolean onEvent(int code, String raw, String[] cooked);
|
||||
}
|
||||
1878
service-t/src/com/android/server/IpSecService.java
Normal file
1878
service-t/src/com/android/server/IpSecService.java
Normal file
File diff suppressed because it is too large
Load Diff
704
service-t/src/com/android/server/NativeDaemonConnector.java
Normal file
704
service-t/src/com/android/server/NativeDaemonConnector.java
Normal file
@@ -0,0 +1,704 @@
|
||||
/*
|
||||
* Copyright (C) 2007 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;
|
||||
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
import android.util.LocalLog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Generic connector class for interfacing with a native daemon which uses the
|
||||
* {@code libsysutils} FrameworkListener protocol.
|
||||
*/
|
||||
final class NativeDaemonConnector implements Runnable, Handler.Callback {
|
||||
private final static boolean VDBG = false;
|
||||
|
||||
private final String TAG;
|
||||
|
||||
private String mSocket;
|
||||
private OutputStream mOutputStream;
|
||||
private LocalLog mLocalLog;
|
||||
|
||||
private volatile boolean mDebug = false;
|
||||
private volatile Object mWarnIfHeld;
|
||||
|
||||
private final ResponseQueue mResponseQueue;
|
||||
|
||||
private final PowerManager.WakeLock mWakeLock;
|
||||
|
||||
private final Looper mLooper;
|
||||
|
||||
private INativeDaemonConnectorCallbacks mCallbacks;
|
||||
private Handler mCallbackHandler;
|
||||
|
||||
private AtomicInteger mSequenceNumber;
|
||||
|
||||
private static final long DEFAULT_TIMEOUT = 1 * 60 * 1000; /* 1 minute */
|
||||
private static final long WARN_EXECUTE_DELAY_MS = 500; /* .5 sec */
|
||||
|
||||
/** Lock held whenever communicating with native daemon. */
|
||||
private final Object mDaemonLock = new Object();
|
||||
|
||||
private final int BUFFER_SIZE = 4096;
|
||||
|
||||
NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,
|
||||
int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl) {
|
||||
mCallbacks = callbacks;
|
||||
mSocket = socket;
|
||||
mResponseQueue = new ResponseQueue(responseQueueSize);
|
||||
mWakeLock = wl;
|
||||
if (mWakeLock != null) {
|
||||
mWakeLock.setReferenceCounted(true);
|
||||
}
|
||||
mSequenceNumber = new AtomicInteger(0);
|
||||
TAG = logTag != null ? logTag : "NativeDaemonConnector";
|
||||
mLocalLog = new LocalLog(maxLogSize);
|
||||
final HandlerThread thread = new HandlerThread(TAG);
|
||||
thread.start();
|
||||
mLooper = thread.getLooper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Set debugging mode, which causes messages to also be written to both
|
||||
* {@link Log} in addition to internal log.
|
||||
*/
|
||||
public void setDebug(boolean debug) {
|
||||
mDebug = debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like SystemClock.uptimeMillis, except truncated to an int so it will fit in a message arg.
|
||||
* Inaccurate across 49.7 days of uptime, but only used for debugging.
|
||||
*/
|
||||
private int uptimeMillisInt() {
|
||||
return (int) SystemClock.uptimeMillis() & Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yell loudly if someone tries making future {@link #execute(Command)}
|
||||
* calls while holding a lock on the given object.
|
||||
*/
|
||||
public void setWarnIfHeld(Object warnIfHeld) {
|
||||
if (mWarnIfHeld != null) {
|
||||
throw new IllegalStateException("warnIfHeld is already set.");
|
||||
}
|
||||
mWarnIfHeld = Objects.requireNonNull(warnIfHeld);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mCallbackHandler = new Handler(mLooper, this);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
listenToSocket();
|
||||
} catch (Exception e) {
|
||||
loge("Error in NativeDaemonConnector: " + e);
|
||||
SystemClock.sleep(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
final String event = (String) msg.obj;
|
||||
final int start = uptimeMillisInt();
|
||||
final int sent = msg.arg1;
|
||||
try {
|
||||
if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) {
|
||||
log(String.format("Unhandled event '%s'", event));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
loge("Error handling '" + event + "': " + e);
|
||||
} finally {
|
||||
if (mCallbacks.onCheckHoldWakeLock(msg.what) && mWakeLock != null) {
|
||||
mWakeLock.release();
|
||||
}
|
||||
final int end = uptimeMillisInt();
|
||||
if (start > sent && start - sent > WARN_EXECUTE_DELAY_MS) {
|
||||
loge(String.format("NDC event {%s} processed too late: %dms", event, start - sent));
|
||||
}
|
||||
if (end > start && end - start > WARN_EXECUTE_DELAY_MS) {
|
||||
loge(String.format("NDC event {%s} took too long: %dms", event, end - start));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private LocalSocketAddress determineSocketAddress() {
|
||||
// If we're testing, set up a socket in a namespace that's accessible to test code.
|
||||
// In order to ensure that unprivileged apps aren't able to impersonate native daemons on
|
||||
// production devices, even if said native daemons ill-advisedly pick a socket name that
|
||||
// starts with __test__, only allow this on debug builds.
|
||||
if (mSocket.startsWith("__test__") && Build.isDebuggable()) {
|
||||
return new LocalSocketAddress(mSocket);
|
||||
} else {
|
||||
return new LocalSocketAddress(mSocket, LocalSocketAddress.Namespace.RESERVED);
|
||||
}
|
||||
}
|
||||
|
||||
private void listenToSocket() throws IOException {
|
||||
LocalSocket socket = null;
|
||||
|
||||
try {
|
||||
socket = new LocalSocket();
|
||||
LocalSocketAddress address = determineSocketAddress();
|
||||
|
||||
socket.connect(address);
|
||||
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
synchronized (mDaemonLock) {
|
||||
mOutputStream = socket.getOutputStream();
|
||||
}
|
||||
|
||||
mCallbacks.onDaemonConnected();
|
||||
|
||||
FileDescriptor[] fdList = null;
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int start = 0;
|
||||
|
||||
while (true) {
|
||||
int count = inputStream.read(buffer, start, BUFFER_SIZE - start);
|
||||
if (count < 0) {
|
||||
loge("got " + count + " reading with start = " + start);
|
||||
break;
|
||||
}
|
||||
fdList = socket.getAncillaryFileDescriptors();
|
||||
|
||||
// Add our starting point to the count and reset the start.
|
||||
count += start;
|
||||
start = 0;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (buffer[i] == 0) {
|
||||
// Note - do not log this raw message since it may contain
|
||||
// sensitive data
|
||||
final String rawEvent = new String(
|
||||
buffer, start, i - start, StandardCharsets.UTF_8);
|
||||
|
||||
boolean releaseWl = false;
|
||||
try {
|
||||
final NativeDaemonEvent event =
|
||||
NativeDaemonEvent.parseRawEvent(rawEvent, fdList);
|
||||
|
||||
log("RCV <- {" + event + "}");
|
||||
|
||||
if (event.isClassUnsolicited()) {
|
||||
// TODO: migrate to sending NativeDaemonEvent instances
|
||||
if (mCallbacks.onCheckHoldWakeLock(event.getCode())
|
||||
&& mWakeLock != null) {
|
||||
mWakeLock.acquire();
|
||||
releaseWl = true;
|
||||
}
|
||||
Message msg = mCallbackHandler.obtainMessage(
|
||||
event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());
|
||||
if (mCallbackHandler.sendMessage(msg)) {
|
||||
releaseWl = false;
|
||||
}
|
||||
} else {
|
||||
mResponseQueue.add(event.getCmdNumber(), event);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
log("Problem parsing message " + e);
|
||||
} finally {
|
||||
if (releaseWl) {
|
||||
mWakeLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (start == 0) {
|
||||
log("RCV incomplete");
|
||||
}
|
||||
|
||||
// We should end at the amount we read. If not, compact then
|
||||
// buffer and read again.
|
||||
if (start != count) {
|
||||
final int remaining = BUFFER_SIZE - start;
|
||||
System.arraycopy(buffer, start, buffer, 0, remaining);
|
||||
start = remaining;
|
||||
} else {
|
||||
start = 0;
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
loge("Communications error: " + ex);
|
||||
throw ex;
|
||||
} finally {
|
||||
synchronized (mDaemonLock) {
|
||||
if (mOutputStream != null) {
|
||||
try {
|
||||
loge("closing stream for " + mSocket);
|
||||
mOutputStream.close();
|
||||
} catch (IOException e) {
|
||||
loge("Failed closing output stream: " + e);
|
||||
}
|
||||
mOutputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
loge("Failed closing socket: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around argument that indicates it's sensitive and shouldn't be
|
||||
* logged.
|
||||
*/
|
||||
public static class SensitiveArg {
|
||||
private final Object mArg;
|
||||
|
||||
public SensitiveArg(Object arg) {
|
||||
mArg = arg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(mArg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make command for daemon, escaping arguments as needed.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void makeCommand(StringBuilder rawBuilder, StringBuilder logBuilder, int sequenceNumber,
|
||||
String cmd, Object... args) {
|
||||
if (cmd.indexOf('\0') >= 0) {
|
||||
throw new IllegalArgumentException("Unexpected command: " + cmd);
|
||||
}
|
||||
if (cmd.indexOf(' ') >= 0) {
|
||||
throw new IllegalArgumentException("Arguments must be separate from command");
|
||||
}
|
||||
|
||||
rawBuilder.append(sequenceNumber).append(' ').append(cmd);
|
||||
logBuilder.append(sequenceNumber).append(' ').append(cmd);
|
||||
for (Object arg : args) {
|
||||
final String argString = String.valueOf(arg);
|
||||
if (argString.indexOf('\0') >= 0) {
|
||||
throw new IllegalArgumentException("Unexpected argument: " + arg);
|
||||
}
|
||||
|
||||
rawBuilder.append(' ');
|
||||
logBuilder.append(' ');
|
||||
|
||||
appendEscaped(rawBuilder, argString);
|
||||
if (arg instanceof SensitiveArg) {
|
||||
logBuilder.append("[scrubbed]");
|
||||
} else {
|
||||
appendEscaped(logBuilder, argString);
|
||||
}
|
||||
}
|
||||
|
||||
rawBuilder.append('\0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that waits until all asychronous notifications sent by the native daemon have
|
||||
* been processed. This method must not be called on the notification thread or an
|
||||
* exception will be thrown.
|
||||
*/
|
||||
public void waitForCallbacks() {
|
||||
if (Thread.currentThread() == mLooper.getThread()) {
|
||||
throw new IllegalStateException("Must not call this method on callback thread");
|
||||
}
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
mCallbackHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Log.wtf(TAG, "Interrupted while waiting for unsolicited response handling", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue the given command to the native daemon and return a single expected
|
||||
* response.
|
||||
*
|
||||
* @throws NativeDaemonConnectorException when problem communicating with
|
||||
* native daemon, or if the response matches
|
||||
* {@link NativeDaemonEvent#isClassClientError()} or
|
||||
* {@link NativeDaemonEvent#isClassServerError()}.
|
||||
*/
|
||||
public NativeDaemonEvent execute(Command cmd) throws NativeDaemonConnectorException {
|
||||
return execute(cmd.mCmd, cmd.mArguments.toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue the given command to the native daemon and return a single expected
|
||||
* response. Any arguments must be separated from base command so they can
|
||||
* be properly escaped.
|
||||
*
|
||||
* @throws NativeDaemonConnectorException when problem communicating with
|
||||
* native daemon, or if the response matches
|
||||
* {@link NativeDaemonEvent#isClassClientError()} or
|
||||
* {@link NativeDaemonEvent#isClassServerError()}.
|
||||
*/
|
||||
public NativeDaemonEvent execute(String cmd, Object... args)
|
||||
throws NativeDaemonConnectorException {
|
||||
return execute(DEFAULT_TIMEOUT, cmd, args);
|
||||
}
|
||||
|
||||
public NativeDaemonEvent execute(long timeoutMs, String cmd, Object... args)
|
||||
throws NativeDaemonConnectorException {
|
||||
final NativeDaemonEvent[] events = executeForList(timeoutMs, cmd, args);
|
||||
if (events.length != 1) {
|
||||
throw new NativeDaemonConnectorException(
|
||||
"Expected exactly one response, but received " + events.length);
|
||||
}
|
||||
return events[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue the given command to the native daemon and return any
|
||||
* {@link NativeDaemonEvent#isClassContinue()} responses, including the
|
||||
* final terminal response.
|
||||
*
|
||||
* @throws NativeDaemonConnectorException when problem communicating with
|
||||
* native daemon, or if the response matches
|
||||
* {@link NativeDaemonEvent#isClassClientError()} or
|
||||
* {@link NativeDaemonEvent#isClassServerError()}.
|
||||
*/
|
||||
public NativeDaemonEvent[] executeForList(Command cmd) throws NativeDaemonConnectorException {
|
||||
return executeForList(cmd.mCmd, cmd.mArguments.toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue the given command to the native daemon and return any
|
||||
* {@link NativeDaemonEvent#isClassContinue()} responses, including the
|
||||
* final terminal response. Any arguments must be separated from base
|
||||
* command so they can be properly escaped.
|
||||
*
|
||||
* @throws NativeDaemonConnectorException when problem communicating with
|
||||
* native daemon, or if the response matches
|
||||
* {@link NativeDaemonEvent#isClassClientError()} or
|
||||
* {@link NativeDaemonEvent#isClassServerError()}.
|
||||
*/
|
||||
public NativeDaemonEvent[] executeForList(String cmd, Object... args)
|
||||
throws NativeDaemonConnectorException {
|
||||
return executeForList(DEFAULT_TIMEOUT, cmd, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue the given command to the native daemon and return any {@linke
|
||||
* NativeDaemonEvent@isClassContinue()} responses, including the final
|
||||
* terminal response. Note that the timeout does not count time in deep
|
||||
* sleep. Any arguments must be separated from base command so they can be
|
||||
* properly escaped.
|
||||
*
|
||||
* @throws NativeDaemonConnectorException when problem communicating with
|
||||
* native daemon, or if the response matches
|
||||
* {@link NativeDaemonEvent#isClassClientError()} or
|
||||
* {@link NativeDaemonEvent#isClassServerError()}.
|
||||
*/
|
||||
public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args)
|
||||
throws NativeDaemonConnectorException {
|
||||
if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {
|
||||
Log.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"
|
||||
+ Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());
|
||||
}
|
||||
|
||||
final long startTime = SystemClock.elapsedRealtime();
|
||||
|
||||
final ArrayList<NativeDaemonEvent> events = new ArrayList<>();
|
||||
|
||||
final StringBuilder rawBuilder = new StringBuilder();
|
||||
final StringBuilder logBuilder = new StringBuilder();
|
||||
final int sequenceNumber = mSequenceNumber.incrementAndGet();
|
||||
|
||||
makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);
|
||||
|
||||
final String rawCmd = rawBuilder.toString();
|
||||
final String logCmd = logBuilder.toString();
|
||||
|
||||
log("SND -> {" + logCmd + "}");
|
||||
|
||||
synchronized (mDaemonLock) {
|
||||
if (mOutputStream == null) {
|
||||
throw new NativeDaemonConnectorException("missing output stream");
|
||||
} else {
|
||||
try {
|
||||
mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
throw new NativeDaemonConnectorException("problem sending command", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NativeDaemonEvent event = null;
|
||||
do {
|
||||
event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd);
|
||||
if (event == null) {
|
||||
loge("timed-out waiting for response to " + logCmd);
|
||||
throw new NativeDaemonTimeoutException(logCmd, event);
|
||||
}
|
||||
if (VDBG) log("RMV <- {" + event + "}");
|
||||
events.add(event);
|
||||
} while (event.isClassContinue());
|
||||
|
||||
final long endTime = SystemClock.elapsedRealtime();
|
||||
if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {
|
||||
loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");
|
||||
}
|
||||
|
||||
if (event.isClassClientError()) {
|
||||
throw new NativeDaemonArgumentException(logCmd, event);
|
||||
}
|
||||
if (event.isClassServerError()) {
|
||||
throw new NativeDaemonFailureException(logCmd, event);
|
||||
}
|
||||
|
||||
return events.toArray(new NativeDaemonEvent[events.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the given argument to {@link StringBuilder}, escaping as needed,
|
||||
* and surrounding with quotes when it contains spaces.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void appendEscaped(StringBuilder builder, String arg) {
|
||||
final boolean hasSpaces = arg.indexOf(' ') >= 0;
|
||||
if (hasSpaces) {
|
||||
builder.append('"');
|
||||
}
|
||||
|
||||
final int length = arg.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
final char c = arg.charAt(i);
|
||||
|
||||
if (c == '"') {
|
||||
builder.append("\\\"");
|
||||
} else if (c == '\\') {
|
||||
builder.append("\\\\");
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSpaces) {
|
||||
builder.append('"');
|
||||
}
|
||||
}
|
||||
|
||||
private static class NativeDaemonArgumentException extends NativeDaemonConnectorException {
|
||||
public NativeDaemonArgumentException(String command, NativeDaemonEvent event) {
|
||||
super(command, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IllegalArgumentException rethrowAsParcelableException() {
|
||||
throw new IllegalArgumentException(getMessage(), this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class NativeDaemonFailureException extends NativeDaemonConnectorException {
|
||||
public NativeDaemonFailureException(String command, NativeDaemonEvent event) {
|
||||
super(command, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command builder that handles argument list building. Any arguments must
|
||||
* be separated from base command so they can be properly escaped.
|
||||
*/
|
||||
public static class Command {
|
||||
private String mCmd;
|
||||
private ArrayList<Object> mArguments = new ArrayList<>();
|
||||
|
||||
public Command(String cmd, Object... args) {
|
||||
mCmd = cmd;
|
||||
for (Object arg : args) {
|
||||
appendArg(arg);
|
||||
}
|
||||
}
|
||||
|
||||
public Command appendArg(Object arg) {
|
||||
mArguments.add(arg);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
||||
mLocalLog.dump(fd, pw, args);
|
||||
pw.println();
|
||||
mResponseQueue.dump(fd, pw, args);
|
||||
}
|
||||
|
||||
private void log(String logstring) {
|
||||
if (mDebug) Log.d(TAG, logstring);
|
||||
mLocalLog.log(logstring);
|
||||
}
|
||||
|
||||
private void loge(String logstring) {
|
||||
Log.e(TAG, logstring);
|
||||
mLocalLog.log(logstring);
|
||||
}
|
||||
|
||||
private static class ResponseQueue {
|
||||
|
||||
private static class PendingCmd {
|
||||
public final int cmdNum;
|
||||
public final String logCmd;
|
||||
|
||||
public BlockingQueue<NativeDaemonEvent> responses =
|
||||
new ArrayBlockingQueue<NativeDaemonEvent>(10);
|
||||
|
||||
// The availableResponseCount member is used to track when we can remove this
|
||||
// instance from the ResponseQueue.
|
||||
// This is used under the protection of a sync of the mPendingCmds object.
|
||||
// A positive value means we've had more writers retreive this object while
|
||||
// a negative value means we've had more readers. When we've had an equal number
|
||||
// (it goes to zero) we can remove this object from the mPendingCmds list.
|
||||
// Note that we may have more responses for this command (and more readers
|
||||
// coming), but that would result in a new PendingCmd instance being created
|
||||
// and added with the same cmdNum.
|
||||
// Also note that when this goes to zero it just means a parity of readers and
|
||||
// writers have retrieved this object - not that they are done using it. The
|
||||
// responses queue may well have more responses yet to be read or may get more
|
||||
// responses added to it. But all those readers/writers have retreived and
|
||||
// hold references to this instance already so it can be removed from
|
||||
// mPendingCmds queue.
|
||||
public int availableResponseCount;
|
||||
|
||||
public PendingCmd(int cmdNum, String logCmd) {
|
||||
this.cmdNum = cmdNum;
|
||||
this.logCmd = logCmd;
|
||||
}
|
||||
}
|
||||
|
||||
private final LinkedList<PendingCmd> mPendingCmds;
|
||||
private int mMaxCount;
|
||||
|
||||
ResponseQueue(int maxCount) {
|
||||
mPendingCmds = new LinkedList<PendingCmd>();
|
||||
mMaxCount = maxCount;
|
||||
}
|
||||
|
||||
public void add(int cmdNum, NativeDaemonEvent response) {
|
||||
PendingCmd found = null;
|
||||
synchronized (mPendingCmds) {
|
||||
for (PendingCmd pendingCmd : mPendingCmds) {
|
||||
if (pendingCmd.cmdNum == cmdNum) {
|
||||
found = pendingCmd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == null) {
|
||||
// didn't find it - make sure our queue isn't too big before adding
|
||||
while (mPendingCmds.size() >= mMaxCount) {
|
||||
Log.e("NativeDaemonConnector.ResponseQueue",
|
||||
"more buffered than allowed: " + mPendingCmds.size() +
|
||||
" >= " + mMaxCount);
|
||||
// let any waiter timeout waiting for this
|
||||
PendingCmd pendingCmd = mPendingCmds.remove();
|
||||
Log.e("NativeDaemonConnector.ResponseQueue",
|
||||
"Removing request: " + pendingCmd.logCmd + " (" +
|
||||
pendingCmd.cmdNum + ")");
|
||||
}
|
||||
found = new PendingCmd(cmdNum, null);
|
||||
mPendingCmds.add(found);
|
||||
}
|
||||
found.availableResponseCount++;
|
||||
// if a matching remove call has already retrieved this we can remove this
|
||||
// instance from our list
|
||||
if (found.availableResponseCount == 0) mPendingCmds.remove(found);
|
||||
}
|
||||
try {
|
||||
found.responses.put(response);
|
||||
} catch (InterruptedException e) { }
|
||||
}
|
||||
|
||||
// note that the timeout does not count time in deep sleep. If you don't want
|
||||
// the device to sleep, hold a wakelock
|
||||
public NativeDaemonEvent remove(int cmdNum, long timeoutMs, String logCmd) {
|
||||
PendingCmd found = null;
|
||||
synchronized (mPendingCmds) {
|
||||
for (PendingCmd pendingCmd : mPendingCmds) {
|
||||
if (pendingCmd.cmdNum == cmdNum) {
|
||||
found = pendingCmd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == null) {
|
||||
found = new PendingCmd(cmdNum, logCmd);
|
||||
mPendingCmds.add(found);
|
||||
}
|
||||
found.availableResponseCount--;
|
||||
// if a matching add call has already retrieved this we can remove this
|
||||
// instance from our list
|
||||
if (found.availableResponseCount == 0) mPendingCmds.remove(found);
|
||||
}
|
||||
NativeDaemonEvent result = null;
|
||||
try {
|
||||
result = found.responses.poll(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {}
|
||||
if (result == null) {
|
||||
Log.e("NativeDaemonConnector.ResponseQueue", "Timeout waiting for response");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
||||
pw.println("Pending requests:");
|
||||
synchronized (mPendingCmds) {
|
||||
for (PendingCmd pendingCmd : mPendingCmds) {
|
||||
pw.println(" Cmd " + pendingCmd.cmdNum + " - " + pendingCmd.logCmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2006 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;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
/**
|
||||
* An exception that indicates there was an error with a
|
||||
* {@link NativeDaemonConnector} operation.
|
||||
*/
|
||||
public class NativeDaemonConnectorException extends Exception {
|
||||
private String mCmd;
|
||||
private NativeDaemonEvent mEvent;
|
||||
|
||||
public NativeDaemonConnectorException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public NativeDaemonConnectorException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
public NativeDaemonConnectorException(String cmd, NativeDaemonEvent event) {
|
||||
super("command '" + cmd + "' failed with '" + event + "'");
|
||||
mCmd = cmd;
|
||||
mEvent = event;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return mEvent != null ? mEvent.getCode() : -1;
|
||||
}
|
||||
|
||||
public String getCmd() {
|
||||
return mCmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rethrow as a {@link RuntimeException} subclass that is handled by
|
||||
* {@link Parcel#writeException(Exception)}.
|
||||
*/
|
||||
public IllegalArgumentException rethrowAsParcelableException() {
|
||||
throw new IllegalStateException(getMessage(), this);
|
||||
}
|
||||
}
|
||||
267
service-t/src/com/android/server/NativeDaemonEvent.java
Normal file
267
service-t/src/com/android/server/NativeDaemonEvent.java
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Parsed event from native side of {@link NativeDaemonConnector}.
|
||||
*/
|
||||
public class NativeDaemonEvent {
|
||||
|
||||
// TODO: keep class ranges in sync with ResponseCode.h
|
||||
// TODO: swap client and server error ranges to roughly mirror HTTP spec
|
||||
|
||||
private final int mCmdNumber;
|
||||
private final int mCode;
|
||||
private final String mMessage;
|
||||
private final String mRawEvent;
|
||||
private final String mLogMessage;
|
||||
private String[] mParsed;
|
||||
private FileDescriptor[] mFdList;
|
||||
|
||||
private NativeDaemonEvent(int cmdNumber, int code, String message,
|
||||
String rawEvent, String logMessage, FileDescriptor[] fdList) {
|
||||
mCmdNumber = cmdNumber;
|
||||
mCode = code;
|
||||
mMessage = message;
|
||||
mRawEvent = rawEvent;
|
||||
mLogMessage = logMessage;
|
||||
mParsed = null;
|
||||
mFdList = fdList;
|
||||
}
|
||||
|
||||
static public final String SENSITIVE_MARKER = "{{sensitive}}";
|
||||
|
||||
public int getCmdNumber() {
|
||||
return mCmdNumber;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return mCode;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
public FileDescriptor[] getFileDescriptors() {
|
||||
return mFdList;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public String getRawEvent() {
|
||||
return mRawEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mLogMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if event represents a partial response which is continued in
|
||||
* additional subsequent events.
|
||||
*/
|
||||
public boolean isClassContinue() {
|
||||
return mCode >= 100 && mCode < 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if event represents a command success.
|
||||
*/
|
||||
public boolean isClassOk() {
|
||||
return mCode >= 200 && mCode < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if event represents a remote native daemon error.
|
||||
*/
|
||||
public boolean isClassServerError() {
|
||||
return mCode >= 400 && mCode < 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if event represents a command syntax or argument error.
|
||||
*/
|
||||
public boolean isClassClientError() {
|
||||
return mCode >= 500 && mCode < 600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if event represents an unsolicited event from native daemon.
|
||||
*/
|
||||
public boolean isClassUnsolicited() {
|
||||
return isClassUnsolicited(mCode);
|
||||
}
|
||||
|
||||
private static boolean isClassUnsolicited(int code) {
|
||||
return code >= 600 && code < 700;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify this event matches the given code.
|
||||
*
|
||||
* @throws IllegalStateException if {@link #getCode()} doesn't match.
|
||||
*/
|
||||
public void checkCode(int code) {
|
||||
if (mCode != code) {
|
||||
throw new IllegalStateException("Expected " + code + " but was: " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given raw event into {@link NativeDaemonEvent} instance.
|
||||
*
|
||||
* @throws IllegalArgumentException when line doesn't match format expected
|
||||
* from native side.
|
||||
*/
|
||||
public static NativeDaemonEvent parseRawEvent(String rawEvent, FileDescriptor[] fdList) {
|
||||
final String[] parsed = rawEvent.split(" ");
|
||||
if (parsed.length < 2) {
|
||||
throw new IllegalArgumentException("Insufficient arguments");
|
||||
}
|
||||
|
||||
int skiplength = 0;
|
||||
|
||||
final int code;
|
||||
try {
|
||||
code = Integer.parseInt(parsed[0]);
|
||||
skiplength = parsed[0].length() + 1;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("problem parsing code", e);
|
||||
}
|
||||
|
||||
int cmdNumber = -1;
|
||||
if (isClassUnsolicited(code) == false) {
|
||||
if (parsed.length < 3) {
|
||||
throw new IllegalArgumentException("Insufficient arguemnts");
|
||||
}
|
||||
try {
|
||||
cmdNumber = Integer.parseInt(parsed[1]);
|
||||
skiplength += parsed[1].length() + 1;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("problem parsing cmdNumber", e);
|
||||
}
|
||||
}
|
||||
|
||||
String logMessage = rawEvent;
|
||||
if (parsed.length > 2 && parsed[2].equals(SENSITIVE_MARKER)) {
|
||||
skiplength += parsed[2].length() + 1;
|
||||
logMessage = parsed[0] + " " + parsed[1] + " {}";
|
||||
}
|
||||
|
||||
final String message = rawEvent.substring(skiplength);
|
||||
|
||||
return new NativeDaemonEvent(cmdNumber, code, message, rawEvent, logMessage, fdList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the given {@link NativeDaemonEvent} list, returning
|
||||
* {@link #getMessage()} for any events matching the requested code.
|
||||
*/
|
||||
public static String[] filterMessageList(NativeDaemonEvent[] events, int matchCode) {
|
||||
final ArrayList<String> result = new ArrayList<>();
|
||||
for (NativeDaemonEvent event : events) {
|
||||
if (event.getCode() == matchCode) {
|
||||
result.add(event.getMessage());
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Nth field of the event.
|
||||
*
|
||||
* This ignores and code or cmdNum, the first return value is given for N=0.
|
||||
* Also understands "\"quoted\" multiword responses" and tries them as a single field
|
||||
*/
|
||||
public String getField(int n) {
|
||||
if (mParsed == null) {
|
||||
mParsed = unescapeArgs(mRawEvent);
|
||||
}
|
||||
n += 2; // skip code and command#
|
||||
if (n > mParsed.length) return null;
|
||||
return mParsed[n];
|
||||
}
|
||||
|
||||
public static String[] unescapeArgs(String rawEvent) {
|
||||
final boolean DEBUG_ROUTINE = false;
|
||||
final String LOGTAG = "unescapeArgs";
|
||||
final ArrayList<String> parsed = new ArrayList<String>();
|
||||
final int length = rawEvent.length();
|
||||
int current = 0;
|
||||
int wordEnd = -1;
|
||||
boolean quoted = false;
|
||||
|
||||
if (DEBUG_ROUTINE) Log.e(LOGTAG, "parsing '" + rawEvent + "'");
|
||||
if (rawEvent.charAt(current) == '\"') {
|
||||
quoted = true;
|
||||
current++;
|
||||
}
|
||||
while (current < length) {
|
||||
// find the end of the word
|
||||
char terminator = quoted ? '\"' : ' ';
|
||||
wordEnd = current;
|
||||
while (wordEnd < length && rawEvent.charAt(wordEnd) != terminator) {
|
||||
if (rawEvent.charAt(wordEnd) == '\\') {
|
||||
// skip the escaped char
|
||||
++wordEnd;
|
||||
}
|
||||
++wordEnd;
|
||||
}
|
||||
if (wordEnd > length) wordEnd = length;
|
||||
String word = rawEvent.substring(current, wordEnd);
|
||||
current += word.length();
|
||||
if (!quoted) {
|
||||
word = word.trim();
|
||||
} else {
|
||||
current++; // skip the trailing quote
|
||||
}
|
||||
// unescape stuff within the word
|
||||
word = word.replace("\\\\", "\\");
|
||||
word = word.replace("\\\"", "\"");
|
||||
|
||||
if (DEBUG_ROUTINE) Log.e(LOGTAG, "found '" + word + "'");
|
||||
parsed.add(word);
|
||||
|
||||
// find the beginning of the next word - either of these options
|
||||
int nextSpace = rawEvent.indexOf(' ', current);
|
||||
int nextQuote = rawEvent.indexOf(" \"", current);
|
||||
if (DEBUG_ROUTINE) {
|
||||
Log.e(LOGTAG, "nextSpace=" + nextSpace + ", nextQuote=" + nextQuote);
|
||||
}
|
||||
if (nextQuote > -1 && nextQuote <= nextSpace) {
|
||||
quoted = true;
|
||||
current = nextQuote + 2;
|
||||
} else {
|
||||
quoted = false;
|
||||
if (nextSpace > -1) {
|
||||
current = nextSpace + 1;
|
||||
}
|
||||
} // else we just start the next word after the current and read til the end
|
||||
if (DEBUG_ROUTINE) {
|
||||
Log.e(LOGTAG, "next loop - current=" + current
|
||||
+ ", length=" + length + ", quoted=" + quoted);
|
||||
}
|
||||
}
|
||||
return parsed.toArray(new String[parsed.size()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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;
|
||||
|
||||
/**
|
||||
* An exception that indicates there was a timeout with a
|
||||
* {@link NativeDaemonConnector} operation.
|
||||
*/
|
||||
public class NativeDaemonTimeoutException extends NativeDaemonConnectorException {
|
||||
public NativeDaemonTimeoutException(String command, NativeDaemonEvent event) {
|
||||
super(command, event);
|
||||
}
|
||||
}
|
||||
|
||||
1146
service-t/src/com/android/server/NsdService.java
Normal file
1146
service-t/src/com/android/server/NsdService.java
Normal file
File diff suppressed because it is too large
Load Diff
139
service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
Normal file
139
service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.INetd;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceSpecificException;
|
||||
import android.system.ErrnoException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
|
||||
import com.android.net.module.util.BpfMap;
|
||||
import com.android.net.module.util.IBpfMap;
|
||||
import com.android.net.module.util.InterfaceParams;
|
||||
import com.android.net.module.util.Struct.U32;
|
||||
|
||||
/**
|
||||
* Monitor interface added (without removed) and right interface name and its index to bpf map.
|
||||
*/
|
||||
public class BpfInterfaceMapUpdater {
|
||||
private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
|
||||
// This is current path but may be changed soon.
|
||||
private static final String IFACE_INDEX_NAME_MAP_PATH =
|
||||
"/sys/fs/bpf/map_netd_iface_index_name_map";
|
||||
private final IBpfMap<U32, InterfaceMapValue> mBpfMap;
|
||||
private final INetd mNetd;
|
||||
private final Handler mHandler;
|
||||
private final Dependencies mDeps;
|
||||
|
||||
public BpfInterfaceMapUpdater(Context ctx, Handler handler) {
|
||||
this(ctx, handler, new Dependencies());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public BpfInterfaceMapUpdater(Context ctx, Handler handler, Dependencies deps) {
|
||||
mDeps = deps;
|
||||
mBpfMap = deps.getInterfaceMap();
|
||||
mNetd = deps.getINetd(ctx);
|
||||
mHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependencies of BpfInerfaceMapUpdater, for injection in tests.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static class Dependencies {
|
||||
/** Create BpfMap for updating interface and index mapping. */
|
||||
public IBpfMap<U32, InterfaceMapValue> getInterfaceMap() {
|
||||
try {
|
||||
return new BpfMap<>(IFACE_INDEX_NAME_MAP_PATH, BpfMap.BPF_F_RDWR,
|
||||
U32.class, InterfaceMapValue.class);
|
||||
} catch (ErrnoException e) {
|
||||
Log.e(TAG, "Cannot create interface map: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get InterfaceParams for giving interface name. */
|
||||
public InterfaceParams getInterfaceParams(String ifaceName) {
|
||||
return InterfaceParams.getByName(ifaceName);
|
||||
}
|
||||
|
||||
/** Get INetd binder object. */
|
||||
public INetd getINetd(Context ctx) {
|
||||
return INetd.Stub.asInterface((IBinder) ctx.getSystemService(Context.NETD_SERVICE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening interface update event.
|
||||
* Query current interface names before listening.
|
||||
*/
|
||||
public void start() {
|
||||
mHandler.post(() -> {
|
||||
if (mBpfMap == null) {
|
||||
Log.wtf(TAG, "Fail to start: Null bpf map");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: use a NetlinkMonitor and listen for RTM_NEWLINK messages instead.
|
||||
mNetd.registerUnsolicitedEventListener(new InterfaceChangeObserver());
|
||||
} catch (RemoteException e) {
|
||||
Log.wtf(TAG, "Unable to register netd UnsolicitedEventListener, " + e);
|
||||
}
|
||||
|
||||
final String[] ifaces;
|
||||
try {
|
||||
// TODO: use a netlink dump to get the current interface list.
|
||||
ifaces = mNetd.interfaceGetList();
|
||||
} catch (RemoteException | ServiceSpecificException e) {
|
||||
Log.wtf(TAG, "Unable to query interface names by netd, " + e);
|
||||
return;
|
||||
}
|
||||
|
||||
for (String ifaceName : ifaces) {
|
||||
addInterface(ifaceName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addInterface(String ifaceName) {
|
||||
final InterfaceParams iface = mDeps.getInterfaceParams(ifaceName);
|
||||
if (iface == null) {
|
||||
Log.e(TAG, "Unable to get InterfaceParams for " + ifaceName);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mBpfMap.updateEntry(new U32(iface.index), new InterfaceMapValue(ifaceName));
|
||||
} catch (ErrnoException e) {
|
||||
Log.e(TAG, "Unable to update entry for " + ifaceName + ", " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private class InterfaceChangeObserver extends BaseNetdUnsolicitedEventListener {
|
||||
@Override
|
||||
public void onInterfaceAdded(String ifName) {
|
||||
mHandler.post(() -> addInterface(ifName));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
service-t/src/com/android/server/net/CookieTagMapKey.java
Normal file
33
service-t/src/com/android/server/net/CookieTagMapKey.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.Struct.Field;
|
||||
import com.android.net.module.util.Struct.Type;
|
||||
|
||||
/**
|
||||
* Key for cookie tag map.
|
||||
*/
|
||||
public class CookieTagMapKey extends Struct {
|
||||
@Field(order = 0, type = Type.S64)
|
||||
public final long socketCookie;
|
||||
|
||||
public CookieTagMapKey(final long socketCookie) {
|
||||
this.socketCookie = socketCookie;
|
||||
}
|
||||
}
|
||||
37
service-t/src/com/android/server/net/CookieTagMapValue.java
Normal file
37
service-t/src/com/android/server/net/CookieTagMapValue.java
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.Struct.Field;
|
||||
import com.android.net.module.util.Struct.Type;
|
||||
|
||||
/**
|
||||
* Value for cookie tag map.
|
||||
*/
|
||||
public class CookieTagMapValue extends Struct {
|
||||
@Field(order = 0, type = Type.U32)
|
||||
public final long uid;
|
||||
|
||||
@Field(order = 1, type = Type.U32)
|
||||
public final long tag;
|
||||
|
||||
public CookieTagMapValue(final long uid, final long tag) {
|
||||
this.uid = uid;
|
||||
this.tag = tag;
|
||||
}
|
||||
}
|
||||
114
service-t/src/com/android/server/net/DelayedDiskWrite.java
Normal file
114
service-t/src/com/android/server/net/DelayedDiskWrite.java
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This class provides APIs to do a delayed data write to a given {@link OutputStream}.
|
||||
*/
|
||||
public class DelayedDiskWrite {
|
||||
private static final String TAG = "DelayedDiskWrite";
|
||||
|
||||
private HandlerThread mDiskWriteHandlerThread;
|
||||
private Handler mDiskWriteHandler;
|
||||
/* Tracks multiple writes on the same thread */
|
||||
private int mWriteSequence = 0;
|
||||
|
||||
/**
|
||||
* Used to do a delayed data write to a given {@link OutputStream}.
|
||||
*/
|
||||
public interface Writer {
|
||||
/**
|
||||
* write data to a given {@link OutputStream}.
|
||||
*/
|
||||
void onWriteCalled(DataOutputStream out) throws IOException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a delayed data write to a given output stream opened from filePath.
|
||||
*/
|
||||
public void write(final String filePath, final Writer w) {
|
||||
write(filePath, w, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a delayed data write to a given output stream opened from filePath.
|
||||
*/
|
||||
public void write(final String filePath, final Writer w, final boolean open) {
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
throw new IllegalArgumentException("empty file path");
|
||||
}
|
||||
|
||||
/* Do a delayed write to disk on a separate handler thread */
|
||||
synchronized (this) {
|
||||
if (++mWriteSequence == 1) {
|
||||
mDiskWriteHandlerThread = new HandlerThread("DelayedDiskWriteThread");
|
||||
mDiskWriteHandlerThread.start();
|
||||
mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper());
|
||||
}
|
||||
}
|
||||
|
||||
mDiskWriteHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doWrite(filePath, w, open);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void doWrite(String filePath, Writer w, boolean open) {
|
||||
DataOutputStream out = null;
|
||||
try {
|
||||
if (open) {
|
||||
out = new DataOutputStream(new BufferedOutputStream(
|
||||
new FileOutputStream(filePath)));
|
||||
}
|
||||
w.onWriteCalled(out);
|
||||
} catch (IOException e) {
|
||||
loge("Error writing data file " + filePath);
|
||||
} finally {
|
||||
if (out != null) {
|
||||
try {
|
||||
out.close();
|
||||
} catch (Exception e) { }
|
||||
}
|
||||
|
||||
// Quit if no more writes sent
|
||||
synchronized (this) {
|
||||
if (--mWriteSequence == 0) {
|
||||
mDiskWriteHandler.getLooper().quit();
|
||||
mDiskWriteHandler = null;
|
||||
mDiskWriteHandlerThread = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loge(String s) {
|
||||
Log.e(TAG, s);
|
||||
}
|
||||
}
|
||||
|
||||
35
service-t/src/com/android/server/net/InterfaceMapValue.java
Normal file
35
service-t/src/com/android/server/net/InterfaceMapValue.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.Struct.Field;
|
||||
import com.android.net.module.util.Struct.Type;
|
||||
|
||||
/**
|
||||
* The value of bpf interface index map which is used for NetworkStatsService.
|
||||
*/
|
||||
public class InterfaceMapValue extends Struct {
|
||||
@Field(order = 0, type = Type.ByteArray, arraysize = 16)
|
||||
public final byte[] interfaceName;
|
||||
|
||||
public InterfaceMapValue(String iface) {
|
||||
final byte[] ifaceArray = iface.getBytes();
|
||||
interfaceName = new byte[16];
|
||||
// All array bytes after the interface name, if any, must be 0.
|
||||
System.arraycopy(ifaceArray, 0, interfaceName, 0, ifaceArray.length);
|
||||
}
|
||||
}
|
||||
449
service-t/src/com/android/server/net/IpConfigStore.java
Normal file
449
service-t/src/com/android/server/net/IpConfigStore.java
Normal file
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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;
|
||||
|
||||
import android.net.InetAddresses;
|
||||
import android.net.IpConfiguration;
|
||||
import android.net.IpConfiguration.IpAssignment;
|
||||
import android.net.IpConfiguration.ProxySettings;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.ProxyInfo;
|
||||
import android.net.StaticIpConfiguration;
|
||||
import android.net.Uri;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.net.module.util.ProxyUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class provides an API to store and manage L3 network IP configuration.
|
||||
*/
|
||||
public class IpConfigStore {
|
||||
private static final String TAG = "IpConfigStore";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
protected final DelayedDiskWrite mWriter;
|
||||
|
||||
/* IP and proxy configuration keys */
|
||||
protected static final String ID_KEY = "id";
|
||||
protected static final String IP_ASSIGNMENT_KEY = "ipAssignment";
|
||||
protected static final String LINK_ADDRESS_KEY = "linkAddress";
|
||||
protected static final String GATEWAY_KEY = "gateway";
|
||||
protected static final String DNS_KEY = "dns";
|
||||
protected static final String PROXY_SETTINGS_KEY = "proxySettings";
|
||||
protected static final String PROXY_HOST_KEY = "proxyHost";
|
||||
protected static final String PROXY_PORT_KEY = "proxyPort";
|
||||
protected static final String PROXY_PAC_FILE = "proxyPac";
|
||||
protected static final String EXCLUSION_LIST_KEY = "exclusionList";
|
||||
protected static final String EOS = "eos";
|
||||
|
||||
protected static final int IPCONFIG_FILE_VERSION = 3;
|
||||
|
||||
public IpConfigStore(DelayedDiskWrite writer) {
|
||||
mWriter = writer;
|
||||
}
|
||||
|
||||
public IpConfigStore() {
|
||||
this(new DelayedDiskWrite());
|
||||
}
|
||||
|
||||
private static boolean writeConfig(DataOutputStream out, String configKey,
|
||||
IpConfiguration config) throws IOException {
|
||||
return writeConfig(out, configKey, config, IPCONFIG_FILE_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the IP configuration with the given parameters to {@link DataOutputStream}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static boolean writeConfig(DataOutputStream out, String configKey,
|
||||
IpConfiguration config, int version) throws IOException {
|
||||
boolean written = false;
|
||||
|
||||
try {
|
||||
switch (config.getIpAssignment()) {
|
||||
case STATIC:
|
||||
out.writeUTF(IP_ASSIGNMENT_KEY);
|
||||
out.writeUTF(config.getIpAssignment().toString());
|
||||
StaticIpConfiguration staticIpConfiguration = config.getStaticIpConfiguration();
|
||||
if (staticIpConfiguration != null) {
|
||||
if (staticIpConfiguration.getIpAddress() != null) {
|
||||
LinkAddress ipAddress = staticIpConfiguration.getIpAddress();
|
||||
out.writeUTF(LINK_ADDRESS_KEY);
|
||||
out.writeUTF(ipAddress.getAddress().getHostAddress());
|
||||
out.writeInt(ipAddress.getPrefixLength());
|
||||
}
|
||||
if (staticIpConfiguration.getGateway() != null) {
|
||||
out.writeUTF(GATEWAY_KEY);
|
||||
out.writeInt(0); // Default route.
|
||||
out.writeInt(1); // Have a gateway.
|
||||
out.writeUTF(staticIpConfiguration.getGateway().getHostAddress());
|
||||
}
|
||||
for (InetAddress inetAddr : staticIpConfiguration.getDnsServers()) {
|
||||
out.writeUTF(DNS_KEY);
|
||||
out.writeUTF(inetAddr.getHostAddress());
|
||||
}
|
||||
}
|
||||
written = true;
|
||||
break;
|
||||
case DHCP:
|
||||
out.writeUTF(IP_ASSIGNMENT_KEY);
|
||||
out.writeUTF(config.getIpAssignment().toString());
|
||||
written = true;
|
||||
break;
|
||||
case UNASSIGNED:
|
||||
/* Ignore */
|
||||
break;
|
||||
default:
|
||||
loge("Ignore invalid ip assignment while writing");
|
||||
break;
|
||||
}
|
||||
|
||||
switch (config.getProxySettings()) {
|
||||
case STATIC:
|
||||
ProxyInfo proxyProperties = config.getHttpProxy();
|
||||
String exclusionList = ProxyUtils.exclusionListAsString(
|
||||
proxyProperties.getExclusionList());
|
||||
out.writeUTF(PROXY_SETTINGS_KEY);
|
||||
out.writeUTF(config.getProxySettings().toString());
|
||||
out.writeUTF(PROXY_HOST_KEY);
|
||||
out.writeUTF(proxyProperties.getHost());
|
||||
out.writeUTF(PROXY_PORT_KEY);
|
||||
out.writeInt(proxyProperties.getPort());
|
||||
if (exclusionList != null) {
|
||||
out.writeUTF(EXCLUSION_LIST_KEY);
|
||||
out.writeUTF(exclusionList);
|
||||
}
|
||||
written = true;
|
||||
break;
|
||||
case PAC:
|
||||
ProxyInfo proxyPacProperties = config.getHttpProxy();
|
||||
out.writeUTF(PROXY_SETTINGS_KEY);
|
||||
out.writeUTF(config.getProxySettings().toString());
|
||||
out.writeUTF(PROXY_PAC_FILE);
|
||||
out.writeUTF(proxyPacProperties.getPacFileUrl().toString());
|
||||
written = true;
|
||||
break;
|
||||
case NONE:
|
||||
out.writeUTF(PROXY_SETTINGS_KEY);
|
||||
out.writeUTF(config.getProxySettings().toString());
|
||||
written = true;
|
||||
break;
|
||||
case UNASSIGNED:
|
||||
/* Ignore */
|
||||
break;
|
||||
default:
|
||||
loge("Ignore invalid proxy settings while writing");
|
||||
break;
|
||||
}
|
||||
|
||||
if (written) {
|
||||
out.writeUTF(ID_KEY);
|
||||
if (version < 3) {
|
||||
out.writeInt(Integer.valueOf(configKey));
|
||||
} else {
|
||||
out.writeUTF(configKey);
|
||||
}
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
loge("Failure in writing " + config + e);
|
||||
}
|
||||
out.writeUTF(EOS);
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #writeIpConfigurations(String, ArrayMap)} instead.
|
||||
* New method uses string as network identifier which could be interface name or MAC address or
|
||||
* other token.
|
||||
*/
|
||||
@Deprecated
|
||||
public void writeIpAndProxyConfigurationsToFile(String filePath,
|
||||
final SparseArray<IpConfiguration> networks) {
|
||||
mWriter.write(filePath, out -> {
|
||||
out.writeInt(IPCONFIG_FILE_VERSION);
|
||||
for (int i = 0; i < networks.size(); i++) {
|
||||
writeConfig(out, String.valueOf(networks.keyAt(i)), networks.valueAt(i));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the IP configuration associated to the target networks to the destination path.
|
||||
*/
|
||||
public void writeIpConfigurations(String filePath,
|
||||
ArrayMap<String, IpConfiguration> networks) {
|
||||
mWriter.write(filePath, out -> {
|
||||
out.writeInt(IPCONFIG_FILE_VERSION);
|
||||
for (int i = 0; i < networks.size(); i++) {
|
||||
writeConfig(out, networks.keyAt(i), networks.valueAt(i));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the IP configuration from the destination path to {@link BufferedInputStream}.
|
||||
*/
|
||||
public static ArrayMap<String, IpConfiguration> readIpConfigurations(String filePath) {
|
||||
BufferedInputStream bufferedInputStream;
|
||||
try {
|
||||
bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
|
||||
} catch (FileNotFoundException e) {
|
||||
// Return an empty array here because callers expect an empty array when the file is
|
||||
// not present.
|
||||
loge("Error opening configuration file: " + e);
|
||||
return new ArrayMap<>(0);
|
||||
}
|
||||
return readIpConfigurations(bufferedInputStream);
|
||||
}
|
||||
|
||||
/** @deprecated use {@link #readIpConfigurations(String)} */
|
||||
@Deprecated
|
||||
public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(String filePath) {
|
||||
BufferedInputStream bufferedInputStream;
|
||||
try {
|
||||
bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
|
||||
} catch (FileNotFoundException e) {
|
||||
// Return an empty array here because callers expect an empty array when the file is
|
||||
// not present.
|
||||
loge("Error opening configuration file: " + e);
|
||||
return new SparseArray<>();
|
||||
}
|
||||
return readIpAndProxyConfigurations(bufferedInputStream);
|
||||
}
|
||||
|
||||
/** @deprecated use {@link #readIpConfigurations(InputStream)} */
|
||||
@Deprecated
|
||||
public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(
|
||||
InputStream inputStream) {
|
||||
ArrayMap<String, IpConfiguration> networks = readIpConfigurations(inputStream);
|
||||
if (networks == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SparseArray<IpConfiguration> networksById = new SparseArray<>();
|
||||
for (int i = 0; i < networks.size(); i++) {
|
||||
int id = Integer.valueOf(networks.keyAt(i));
|
||||
networksById.put(id, networks.valueAt(i));
|
||||
}
|
||||
|
||||
return networksById;
|
||||
}
|
||||
|
||||
/** Returns a map of network identity token and {@link IpConfiguration}. */
|
||||
public static ArrayMap<String, IpConfiguration> readIpConfigurations(
|
||||
InputStream inputStream) {
|
||||
ArrayMap<String, IpConfiguration> networks = new ArrayMap<>();
|
||||
DataInputStream in = null;
|
||||
try {
|
||||
in = new DataInputStream(inputStream);
|
||||
|
||||
int version = in.readInt();
|
||||
if (version != 3 && version != 2 && version != 1) {
|
||||
loge("Bad version on IP configuration file, ignore read");
|
||||
return null;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
String uniqueToken = null;
|
||||
// Default is DHCP with no proxy
|
||||
IpAssignment ipAssignment = IpAssignment.DHCP;
|
||||
ProxySettings proxySettings = ProxySettings.NONE;
|
||||
StaticIpConfiguration staticIpConfiguration = new StaticIpConfiguration();
|
||||
LinkAddress linkAddress = null;
|
||||
InetAddress gatewayAddress = null;
|
||||
String proxyHost = null;
|
||||
String pacFileUrl = null;
|
||||
int proxyPort = -1;
|
||||
String exclusionList = null;
|
||||
String key;
|
||||
final List<InetAddress> dnsServers = new ArrayList<>();
|
||||
|
||||
do {
|
||||
key = in.readUTF();
|
||||
try {
|
||||
if (key.equals(ID_KEY)) {
|
||||
if (version < 3) {
|
||||
int id = in.readInt();
|
||||
uniqueToken = String.valueOf(id);
|
||||
} else {
|
||||
uniqueToken = in.readUTF();
|
||||
}
|
||||
} else if (key.equals(IP_ASSIGNMENT_KEY)) {
|
||||
ipAssignment = IpAssignment.valueOf(in.readUTF());
|
||||
} else if (key.equals(LINK_ADDRESS_KEY)) {
|
||||
LinkAddress parsedLinkAddress =
|
||||
new LinkAddress(
|
||||
InetAddresses.parseNumericAddress(in.readUTF()),
|
||||
in.readInt());
|
||||
if (parsedLinkAddress.getAddress() instanceof Inet4Address
|
||||
&& linkAddress == null) {
|
||||
linkAddress = parsedLinkAddress;
|
||||
} else {
|
||||
loge("Non-IPv4 or duplicate address: " + parsedLinkAddress);
|
||||
}
|
||||
} else if (key.equals(GATEWAY_KEY)) {
|
||||
LinkAddress dest = null;
|
||||
InetAddress gateway = null;
|
||||
if (version == 1) {
|
||||
// only supported default gateways - leave the dest/prefix empty
|
||||
gateway = InetAddresses.parseNumericAddress(in.readUTF());
|
||||
if (gatewayAddress == null) {
|
||||
gatewayAddress = gateway;
|
||||
} else {
|
||||
loge("Duplicate gateway: " + gateway.getHostAddress());
|
||||
}
|
||||
} else {
|
||||
if (in.readInt() == 1) {
|
||||
dest =
|
||||
new LinkAddress(
|
||||
InetAddresses.parseNumericAddress(in.readUTF()),
|
||||
in.readInt());
|
||||
}
|
||||
if (in.readInt() == 1) {
|
||||
gateway = InetAddresses.parseNumericAddress(in.readUTF());
|
||||
}
|
||||
// If the destination is a default IPv4 route, use the gateway
|
||||
// address unless already set. If there is no destination, assume
|
||||
// it is default route and use the gateway address in all cases.
|
||||
if (dest == null) {
|
||||
gatewayAddress = gateway;
|
||||
} else if (dest.getAddress() instanceof Inet4Address
|
||||
&& dest.getPrefixLength() == 0 && gatewayAddress == null) {
|
||||
gatewayAddress = gateway;
|
||||
} else {
|
||||
loge("Non-IPv4 default or duplicate route: "
|
||||
+ dest.getAddress());
|
||||
}
|
||||
}
|
||||
} else if (key.equals(DNS_KEY)) {
|
||||
dnsServers.add(InetAddresses.parseNumericAddress(in.readUTF()));
|
||||
} else if (key.equals(PROXY_SETTINGS_KEY)) {
|
||||
proxySettings = ProxySettings.valueOf(in.readUTF());
|
||||
} else if (key.equals(PROXY_HOST_KEY)) {
|
||||
proxyHost = in.readUTF();
|
||||
} else if (key.equals(PROXY_PORT_KEY)) {
|
||||
proxyPort = in.readInt();
|
||||
} else if (key.equals(PROXY_PAC_FILE)) {
|
||||
pacFileUrl = in.readUTF();
|
||||
} else if (key.equals(EXCLUSION_LIST_KEY)) {
|
||||
exclusionList = in.readUTF();
|
||||
} else if (key.equals(EOS)) {
|
||||
break;
|
||||
} else {
|
||||
loge("Ignore unknown key " + key + "while reading");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
loge("Ignore invalid address while reading" + e);
|
||||
}
|
||||
} while (true);
|
||||
|
||||
staticIpConfiguration = new StaticIpConfiguration.Builder()
|
||||
.setIpAddress(linkAddress)
|
||||
.setGateway(gatewayAddress)
|
||||
.setDnsServers(dnsServers)
|
||||
.build();
|
||||
|
||||
if (uniqueToken != null) {
|
||||
IpConfiguration config = new IpConfiguration();
|
||||
networks.put(uniqueToken, config);
|
||||
|
||||
switch (ipAssignment) {
|
||||
case STATIC:
|
||||
config.setStaticIpConfiguration(staticIpConfiguration);
|
||||
config.setIpAssignment(ipAssignment);
|
||||
break;
|
||||
case DHCP:
|
||||
config.setIpAssignment(ipAssignment);
|
||||
break;
|
||||
case UNASSIGNED:
|
||||
loge("BUG: Found UNASSIGNED IP on file, use DHCP");
|
||||
config.setIpAssignment(IpAssignment.DHCP);
|
||||
break;
|
||||
default:
|
||||
loge("Ignore invalid ip assignment while reading.");
|
||||
config.setIpAssignment(IpAssignment.UNASSIGNED);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (proxySettings) {
|
||||
case STATIC:
|
||||
ProxyInfo proxyInfo = ProxyInfo.buildDirectProxy(proxyHost, proxyPort,
|
||||
ProxyUtils.exclusionStringAsList(exclusionList));
|
||||
config.setProxySettings(proxySettings);
|
||||
config.setHttpProxy(proxyInfo);
|
||||
break;
|
||||
case PAC:
|
||||
ProxyInfo proxyPacProperties =
|
||||
ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
|
||||
config.setProxySettings(proxySettings);
|
||||
config.setHttpProxy(proxyPacProperties);
|
||||
break;
|
||||
case NONE:
|
||||
config.setProxySettings(proxySettings);
|
||||
break;
|
||||
case UNASSIGNED:
|
||||
loge("BUG: Found UNASSIGNED proxy on file, use NONE");
|
||||
config.setProxySettings(ProxySettings.NONE);
|
||||
break;
|
||||
default:
|
||||
loge("Ignore invalid proxy settings while reading");
|
||||
config.setProxySettings(ProxySettings.UNASSIGNED);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (DBG) log("Missing id while parsing configuration");
|
||||
}
|
||||
}
|
||||
} catch (EOFException ignore) {
|
||||
} catch (IOException e) {
|
||||
loge("Error parsing configuration: " + e);
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (Exception e) { }
|
||||
}
|
||||
}
|
||||
|
||||
return networks;
|
||||
}
|
||||
|
||||
protected static void loge(String s) {
|
||||
Log.e(TAG, s);
|
||||
}
|
||||
|
||||
protected static void log(String s) {
|
||||
Log.d(TAG, s);
|
||||
}
|
||||
}
|
||||
505
service-t/src/com/android/server/net/NetworkStatsFactory.java
Normal file
505
service-t/src/com/android/server/net/NetworkStatsFactory.java
Normal file
@@ -0,0 +1,505 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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;
|
||||
|
||||
import static android.net.NetworkStats.INTERFACES_ALL;
|
||||
import static android.net.NetworkStats.SET_ALL;
|
||||
import static android.net.NetworkStats.TAG_ALL;
|
||||
import static android.net.NetworkStats.TAG_NONE;
|
||||
import static android.net.NetworkStats.UID_ALL;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.UnderlyingNetworkInfo;
|
||||
import android.os.ServiceSpecificException;
|
||||
import android.os.StrictMode;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.ProcFileReader;
|
||||
import com.android.net.module.util.CollectionUtils;
|
||||
import com.android.server.BpfNetMaps;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ProtocolException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Creates {@link NetworkStats} instances by parsing various {@code /proc/}
|
||||
* files as needed.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class NetworkStatsFactory {
|
||||
static {
|
||||
System.loadLibrary("service-connectivity");
|
||||
}
|
||||
|
||||
private static final String TAG = "NetworkStatsFactory";
|
||||
|
||||
private static final boolean USE_NATIVE_PARSING = true;
|
||||
private static final boolean VALIDATE_NATIVE_STATS = false;
|
||||
|
||||
/** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */
|
||||
private final File mStatsXtIfaceAll;
|
||||
/** Path to {@code /proc/net/xt_qtaguid/iface_stat_fmt}. */
|
||||
private final File mStatsXtIfaceFmt;
|
||||
/** Path to {@code /proc/net/xt_qtaguid/stats}. */
|
||||
private final File mStatsXtUid;
|
||||
|
||||
private final boolean mUseBpfStats;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final BpfNetMaps mBpfNetMaps;
|
||||
|
||||
/**
|
||||
* Guards persistent data access in this class
|
||||
*
|
||||
* <p>In order to prevent deadlocks, critical sections protected by this lock SHALL NOT call out
|
||||
* to other code that will acquire other locks within the system server. See b/134244752.
|
||||
*/
|
||||
private final Object mPersistentDataLock = new Object();
|
||||
|
||||
/** Set containing info about active VPNs and their underlying networks. */
|
||||
private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0];
|
||||
|
||||
// A persistent snapshot of cumulative stats since device start
|
||||
@GuardedBy("mPersistentDataLock")
|
||||
private NetworkStats mPersistSnapshot;
|
||||
|
||||
// The persistent snapshot of tun and 464xlat adjusted stats since device start
|
||||
@GuardedBy("mPersistentDataLock")
|
||||
private NetworkStats mTunAnd464xlatAdjustedStats;
|
||||
|
||||
/**
|
||||
* (Stacked interface) -> (base interface) association for all connected ifaces since boot.
|
||||
*
|
||||
* Because counters must never roll backwards, once a given interface is stacked on top of an
|
||||
* underlying interface, the stacked interface can never be stacked on top of
|
||||
* another interface. */
|
||||
private final ConcurrentHashMap<String, String> mStackedIfaces
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
/** Informs the factory of a new stacked interface. */
|
||||
public void noteStackedIface(String stackedIface, String baseIface) {
|
||||
if (stackedIface != null && baseIface != null) {
|
||||
mStackedIfaces.put(stackedIface, baseIface);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active VPN information for data usage migration purposes
|
||||
*
|
||||
* <p>Traffic on TUN-based VPNs inherently all appear to be originated from the VPN providing
|
||||
* app's UID. This method is used to support migration of VPN data usage, ensuring data is
|
||||
* accurately billed to the real owner of the traffic.
|
||||
*
|
||||
* @param vpnArray The snapshot of the currently-running VPNs.
|
||||
*/
|
||||
public void updateUnderlyingNetworkInfos(UnderlyingNetworkInfo[] vpnArray) {
|
||||
mUnderlyingNetworkInfos = vpnArray.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a set of interfaces containing specified ifaces and stacked interfaces.
|
||||
*
|
||||
* <p>The added stacked interfaces are ifaces stacked on top of the specified ones, or ifaces
|
||||
* on which the specified ones are stacked. Stacked interfaces are those noted with
|
||||
* {@link #noteStackedIface(String, String)}, but only interfaces noted before this method
|
||||
* is called are guaranteed to be included.
|
||||
*/
|
||||
public String[] augmentWithStackedInterfaces(@Nullable String[] requiredIfaces) {
|
||||
if (requiredIfaces == NetworkStats.INTERFACES_ALL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HashSet<String> relatedIfaces = new HashSet<>(Arrays.asList(requiredIfaces));
|
||||
// ConcurrentHashMap's EntrySet iterators are "guaranteed to traverse
|
||||
// elements as they existed upon construction exactly once, and may
|
||||
// (but are not guaranteed to) reflect any modifications subsequent to construction".
|
||||
// This is enough here.
|
||||
for (Map.Entry<String, String> entry : mStackedIfaces.entrySet()) {
|
||||
if (relatedIfaces.contains(entry.getKey())) {
|
||||
relatedIfaces.add(entry.getValue());
|
||||
} else if (relatedIfaces.contains(entry.getValue())) {
|
||||
relatedIfaces.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
String[] outArray = new String[relatedIfaces.size()];
|
||||
return relatedIfaces.toArray(outArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies 464xlat adjustments with ifaces noted with {@link #noteStackedIface(String, String)}.
|
||||
* @see NetworkStats#apply464xlatAdjustments(NetworkStats, NetworkStats, Map)
|
||||
*/
|
||||
public void apply464xlatAdjustments(NetworkStats baseTraffic, NetworkStats stackedTraffic) {
|
||||
NetworkStats.apply464xlatAdjustments(baseTraffic, stackedTraffic, mStackedIfaces);
|
||||
}
|
||||
|
||||
public NetworkStatsFactory(@NonNull Context ctx) {
|
||||
this(ctx, new File("/proc/"), true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats) {
|
||||
mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
|
||||
mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
|
||||
mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
|
||||
mUseBpfStats = useBpfStats;
|
||||
mBpfNetMaps = new BpfNetMaps();
|
||||
synchronized (mPersistentDataLock) {
|
||||
mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
|
||||
mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
|
||||
}
|
||||
mContext = ctx;
|
||||
}
|
||||
|
||||
public NetworkStats readBpfNetworkStatsDev() throws IOException {
|
||||
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
|
||||
if (nativeReadNetworkStatsDev(stats) != 0) {
|
||||
throw new IOException("Failed to parse bpf iface stats");
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and return interface-level summary {@link NetworkStats} measured
|
||||
* using {@code /proc/net/dev} style hooks, which may include non IP layer
|
||||
* traffic. Values monotonically increase since device boot, and may include
|
||||
* details about inactive interfaces.
|
||||
*
|
||||
* @throws IllegalStateException when problem parsing stats.
|
||||
*/
|
||||
public NetworkStats readNetworkStatsSummaryDev() throws IOException {
|
||||
|
||||
// Return xt_bpf stats if switched to bpf module.
|
||||
if (mUseBpfStats)
|
||||
return readBpfNetworkStatsDev();
|
||||
|
||||
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
|
||||
|
||||
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
|
||||
final NetworkStats.Entry entry = new NetworkStats.Entry();
|
||||
|
||||
ProcFileReader reader = null;
|
||||
try {
|
||||
reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceAll));
|
||||
|
||||
while (reader.hasMoreData()) {
|
||||
entry.iface = reader.nextString();
|
||||
entry.uid = UID_ALL;
|
||||
entry.set = SET_ALL;
|
||||
entry.tag = TAG_NONE;
|
||||
|
||||
final boolean active = reader.nextInt() != 0;
|
||||
|
||||
// always include snapshot values
|
||||
entry.rxBytes = reader.nextLong();
|
||||
entry.rxPackets = reader.nextLong();
|
||||
entry.txBytes = reader.nextLong();
|
||||
entry.txPackets = reader.nextLong();
|
||||
|
||||
// fold in active numbers, but only when active
|
||||
if (active) {
|
||||
entry.rxBytes += reader.nextLong();
|
||||
entry.rxPackets += reader.nextLong();
|
||||
entry.txBytes += reader.nextLong();
|
||||
entry.txPackets += reader.nextLong();
|
||||
}
|
||||
|
||||
stats.insertEntry(entry);
|
||||
reader.finishLine();
|
||||
}
|
||||
} catch (NullPointerException|NumberFormatException e) {
|
||||
throw protocolExceptionWithCause("problem parsing stats", e);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(reader);
|
||||
StrictMode.setThreadPolicy(savedPolicy);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and return interface-level summary {@link NetworkStats}. Designed
|
||||
* to return only IP layer traffic. Values monotonically increase since
|
||||
* device boot, and may include details about inactive interfaces.
|
||||
*
|
||||
* @throws IllegalStateException when problem parsing stats.
|
||||
*/
|
||||
public NetworkStats readNetworkStatsSummaryXt() throws IOException {
|
||||
|
||||
// Return xt_bpf stats if qtaguid module is replaced.
|
||||
if (mUseBpfStats)
|
||||
return readBpfNetworkStatsDev();
|
||||
|
||||
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
|
||||
|
||||
// return null when kernel doesn't support
|
||||
if (!mStatsXtIfaceFmt.exists()) return null;
|
||||
|
||||
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
|
||||
final NetworkStats.Entry entry = new NetworkStats.Entry();
|
||||
|
||||
ProcFileReader reader = null;
|
||||
try {
|
||||
// open and consume header line
|
||||
reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceFmt));
|
||||
reader.finishLine();
|
||||
|
||||
while (reader.hasMoreData()) {
|
||||
entry.iface = reader.nextString();
|
||||
entry.uid = UID_ALL;
|
||||
entry.set = SET_ALL;
|
||||
entry.tag = TAG_NONE;
|
||||
|
||||
entry.rxBytes = reader.nextLong();
|
||||
entry.rxPackets = reader.nextLong();
|
||||
entry.txBytes = reader.nextLong();
|
||||
entry.txPackets = reader.nextLong();
|
||||
|
||||
stats.insertEntry(entry);
|
||||
reader.finishLine();
|
||||
}
|
||||
} catch (NullPointerException|NumberFormatException e) {
|
||||
throw protocolExceptionWithCause("problem parsing stats", e);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(reader);
|
||||
StrictMode.setThreadPolicy(savedPolicy);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
public NetworkStats readNetworkStatsDetail() throws IOException {
|
||||
return readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
|
||||
}
|
||||
|
||||
@GuardedBy("mPersistentDataLock")
|
||||
private void requestSwapActiveStatsMapLocked() throws IOException {
|
||||
try {
|
||||
// Do a active map stats swap. Once the swap completes, this code
|
||||
// can read and clean the inactive map without races.
|
||||
mBpfNetMaps.swapActiveStatsMap();
|
||||
} catch (ServiceSpecificException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the detailed UID stats based on the provided parameters
|
||||
*
|
||||
* @param limitUid the UID to limit this query to
|
||||
* @param limitIfaces the interfaces to limit this query to. Use {@link
|
||||
* NetworkStats.INTERFACES_ALL} to select all interfaces
|
||||
* @param limitTag the tags to limit this query to
|
||||
* @return the NetworkStats instance containing network statistics at the present time.
|
||||
*/
|
||||
public NetworkStats readNetworkStatsDetail(
|
||||
int limitUid, String[] limitIfaces, int limitTag) throws IOException {
|
||||
// In order to prevent deadlocks, anything protected by this lock MUST NOT call out to other
|
||||
// code that will acquire other locks within the system server. See b/134244752.
|
||||
synchronized (mPersistentDataLock) {
|
||||
// Take a reference. If this gets swapped out, we still have the old reference.
|
||||
final UnderlyingNetworkInfo[] vpnArray = mUnderlyingNetworkInfos;
|
||||
// Take a defensive copy. mPersistSnapshot is mutated in some cases below
|
||||
final NetworkStats prev = mPersistSnapshot.clone();
|
||||
|
||||
if (USE_NATIVE_PARSING) {
|
||||
final NetworkStats stats =
|
||||
new NetworkStats(SystemClock.elapsedRealtime(), 0 /* initialSize */);
|
||||
if (mUseBpfStats) {
|
||||
requestSwapActiveStatsMapLocked();
|
||||
// Stats are always read from the inactive map, so they must be read after the
|
||||
// swap
|
||||
if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
|
||||
INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
|
||||
throw new IOException("Failed to parse network stats");
|
||||
}
|
||||
|
||||
// BPF stats are incremental; fold into mPersistSnapshot.
|
||||
mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
|
||||
mPersistSnapshot.combineAllValues(stats);
|
||||
} else {
|
||||
if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
|
||||
INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
|
||||
throw new IOException("Failed to parse network stats");
|
||||
}
|
||||
if (VALIDATE_NATIVE_STATS) {
|
||||
final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid,
|
||||
UID_ALL, INTERFACES_ALL, TAG_ALL);
|
||||
assertEquals(javaStats, stats);
|
||||
}
|
||||
|
||||
mPersistSnapshot = stats;
|
||||
}
|
||||
} else {
|
||||
mPersistSnapshot = javaReadNetworkStatsDetail(mStatsXtUid, UID_ALL, INTERFACES_ALL,
|
||||
TAG_ALL);
|
||||
}
|
||||
|
||||
NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
|
||||
|
||||
// Filter return values
|
||||
adjustedStats.filter(limitUid, limitIfaces, limitTag);
|
||||
return adjustedStats;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mPersistentDataLock")
|
||||
private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats,
|
||||
NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) {
|
||||
// Calculate delta from last snapshot
|
||||
final NetworkStats delta = uidDetailStats.subtract(previousStats);
|
||||
|
||||
// Apply 464xlat adjustments before VPN adjustments. If VPNs are using v4 on a v6 only
|
||||
// network, the overhead is their fault.
|
||||
// No locking here: apply464xlatAdjustments behaves fine with an add-only
|
||||
// ConcurrentHashMap.
|
||||
delta.apply464xlatAdjustments(mStackedIfaces);
|
||||
|
||||
// Migrate data usage over a VPN to the TUN network.
|
||||
for (UnderlyingNetworkInfo info : vpnArray) {
|
||||
delta.migrateTun(info.getOwnerUid(), info.getInterface(),
|
||||
info.getUnderlyingInterfaces());
|
||||
// Filter out debug entries as that may lead to over counting.
|
||||
delta.filterDebugEntries();
|
||||
}
|
||||
|
||||
// Update mTunAnd464xlatAdjustedStats with migrated delta.
|
||||
mTunAnd464xlatAdjustedStats.combineAllValues(delta);
|
||||
mTunAnd464xlatAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
|
||||
|
||||
return mTunAnd464xlatAdjustedStats.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and return {@link NetworkStats} with UID-level details. Values are
|
||||
* expected to monotonically increase since device boot.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid,
|
||||
String[] limitIfaces, int limitTag)
|
||||
throws IOException {
|
||||
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
|
||||
|
||||
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24);
|
||||
final NetworkStats.Entry entry = new NetworkStats.Entry();
|
||||
|
||||
int idx = 1;
|
||||
int lastIdx = 1;
|
||||
|
||||
ProcFileReader reader = null;
|
||||
try {
|
||||
// open and consume header line
|
||||
reader = new ProcFileReader(new FileInputStream(detailPath));
|
||||
reader.finishLine();
|
||||
|
||||
while (reader.hasMoreData()) {
|
||||
idx = reader.nextInt();
|
||||
if (idx != lastIdx + 1) {
|
||||
throw new ProtocolException(
|
||||
"inconsistent idx=" + idx + " after lastIdx=" + lastIdx);
|
||||
}
|
||||
lastIdx = idx;
|
||||
|
||||
entry.iface = reader.nextString();
|
||||
entry.tag = kernelToTag(reader.nextString());
|
||||
entry.uid = reader.nextInt();
|
||||
entry.set = reader.nextInt();
|
||||
entry.rxBytes = reader.nextLong();
|
||||
entry.rxPackets = reader.nextLong();
|
||||
entry.txBytes = reader.nextLong();
|
||||
entry.txPackets = reader.nextLong();
|
||||
|
||||
if ((limitIfaces == null || CollectionUtils.contains(limitIfaces, entry.iface))
|
||||
&& (limitUid == UID_ALL || limitUid == entry.uid)
|
||||
&& (limitTag == TAG_ALL || limitTag == entry.tag)) {
|
||||
stats.insertEntry(entry);
|
||||
}
|
||||
|
||||
reader.finishLine();
|
||||
}
|
||||
} catch (NullPointerException|NumberFormatException e) {
|
||||
throw protocolExceptionWithCause("problem parsing idx " + idx, e);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(reader);
|
||||
StrictMode.setThreadPolicy(savedPolicy);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public void assertEquals(NetworkStats expected, NetworkStats actual) {
|
||||
if (expected.size() != actual.size()) {
|
||||
throw new AssertionError(
|
||||
"Expected size " + expected.size() + ", actual size " + actual.size());
|
||||
}
|
||||
|
||||
NetworkStats.Entry expectedRow = null;
|
||||
NetworkStats.Entry actualRow = null;
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
expectedRow = expected.getValues(i, expectedRow);
|
||||
actualRow = actual.getValues(i, actualRow);
|
||||
if (!expectedRow.equals(actualRow)) {
|
||||
throw new AssertionError(
|
||||
"Expected row " + i + ": " + expectedRow + ", actual row " + actualRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming
|
||||
* format like {@code 0x7fffffff00000000}.
|
||||
*/
|
||||
public static int kernelToTag(String string) {
|
||||
int length = string.length();
|
||||
if (length > 10) {
|
||||
return Long.decode(string.substring(0, length - 8)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse statistics from file into given {@link NetworkStats} object. Values
|
||||
* are expected to monotonically increase since device boot.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static native int nativeReadNetworkStatsDetail(NetworkStats stats, String path,
|
||||
int limitUid, String[] limitIfaces, int limitTag, boolean useBpfStats);
|
||||
|
||||
@VisibleForTesting
|
||||
public static native int nativeReadNetworkStatsDev(NetworkStats stats);
|
||||
|
||||
private static ProtocolException protocolExceptionWithCause(String message, Throwable cause) {
|
||||
ProtocolException pe = new ProtocolException(message);
|
||||
pe.initCause(cause);
|
||||
return pe;
|
||||
}
|
||||
}
|
||||
451
service-t/src/com/android/server/net/NetworkStatsObservers.java
Normal file
451
service-t/src/com/android/server/net/NetworkStatsObservers.java
Normal file
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* 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.net;
|
||||
|
||||
import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
|
||||
|
||||
import android.app.usage.NetworkStatsManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.DataUsageRequest;
|
||||
import android.net.NetworkIdentitySet;
|
||||
import android.net.NetworkStack;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.NetworkStatsAccess;
|
||||
import android.net.NetworkStatsCollection;
|
||||
import android.net.NetworkStatsHistory;
|
||||
import android.net.NetworkTemplate;
|
||||
import android.net.netstats.IUsageCallback;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Manages observers of {@link NetworkStats}. Allows observers to be notified when
|
||||
* data usage has been reported in {@link NetworkStatsService}. An observer can set
|
||||
* a threshold of how much data it cares about to be notified.
|
||||
*/
|
||||
class NetworkStatsObservers {
|
||||
private static final String TAG = "NetworkStatsObservers";
|
||||
private static final boolean LOGV = false;
|
||||
|
||||
private static final int MSG_REGISTER = 1;
|
||||
private static final int MSG_UNREGISTER = 2;
|
||||
private static final int MSG_UPDATE_STATS = 3;
|
||||
|
||||
// All access to this map must be done from the handler thread.
|
||||
// indexed by DataUsageRequest#requestId
|
||||
private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
|
||||
|
||||
// Sequence number of DataUsageRequests
|
||||
private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
|
||||
|
||||
// Lazily instantiated when an observer is registered.
|
||||
private volatile Handler mHandler;
|
||||
|
||||
/**
|
||||
* Creates a wrapper that contains the caller context and a normalized request.
|
||||
* The request should be returned to the caller app, and the wrapper should be sent to this
|
||||
* object through #addObserver by the service handler.
|
||||
*
|
||||
* <p>It will register the observer asynchronously, so it is safe to call from any thread.
|
||||
*
|
||||
* @return the normalized request wrapped within {@link RequestInfo}.
|
||||
*/
|
||||
public DataUsageRequest register(Context context, DataUsageRequest inputRequest,
|
||||
IUsageCallback callback, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
|
||||
DataUsageRequest request = buildRequest(context, inputRequest, callingUid);
|
||||
RequestInfo requestInfo = buildRequestInfo(request, callback, callingUid,
|
||||
accessLevel);
|
||||
|
||||
if (LOGV) Log.v(TAG, "Registering observer for " + request);
|
||||
getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a data usage observer.
|
||||
*
|
||||
* <p>It will unregister the observer asynchronously, so it is safe to call from any thread.
|
||||
*/
|
||||
public void unregister(DataUsageRequest request, int callingUid) {
|
||||
getHandler().sendMessage(mHandler.obtainMessage(MSG_UNREGISTER, callingUid, 0 /* ignore */,
|
||||
request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates data usage statistics of registered observers and notifies if limits are reached.
|
||||
*
|
||||
* <p>It will update stats asynchronously, so it is safe to call from any thread.
|
||||
*/
|
||||
public void updateStats(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
|
||||
ArrayMap<String, NetworkIdentitySet> activeIfaces,
|
||||
ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
|
||||
long currentTime) {
|
||||
StatsContext statsContext = new StatsContext(xtSnapshot, uidSnapshot, activeIfaces,
|
||||
activeUidIfaces, currentTime);
|
||||
getHandler().sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATS, statsContext));
|
||||
}
|
||||
|
||||
private Handler getHandler() {
|
||||
if (mHandler == null) {
|
||||
synchronized (this) {
|
||||
if (mHandler == null) {
|
||||
if (LOGV) Log.v(TAG, "Creating handler");
|
||||
mHandler = new Handler(getHandlerLooperLocked(), mHandlerCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mHandler;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected Looper getHandlerLooperLocked() {
|
||||
HandlerThread handlerThread = new HandlerThread(TAG);
|
||||
handlerThread.start();
|
||||
return handlerThread.getLooper();
|
||||
}
|
||||
|
||||
private Handler.Callback mHandlerCallback = new Handler.Callback() {
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_REGISTER: {
|
||||
handleRegister((RequestInfo) msg.obj);
|
||||
return true;
|
||||
}
|
||||
case MSG_UNREGISTER: {
|
||||
handleUnregister((DataUsageRequest) msg.obj, msg.arg1 /* callingUid */);
|
||||
return true;
|
||||
}
|
||||
case MSG_UPDATE_STATS: {
|
||||
handleUpdateStats((StatsContext) msg.obj);
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a {@link RequestInfo} as an observer.
|
||||
* Should only be called from the handler thread otherwise there will be a race condition
|
||||
* on mDataUsageRequests.
|
||||
*/
|
||||
private void handleRegister(RequestInfo requestInfo) {
|
||||
mDataUsageRequests.put(requestInfo.mRequest.requestId, requestInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link DataUsageRequest} if the calling uid is authorized.
|
||||
* Should only be called from the handler thread otherwise there will be a race condition
|
||||
* on mDataUsageRequests.
|
||||
*/
|
||||
private void handleUnregister(DataUsageRequest request, int callingUid) {
|
||||
RequestInfo requestInfo;
|
||||
requestInfo = mDataUsageRequests.get(request.requestId);
|
||||
if (requestInfo == null) {
|
||||
if (LOGV) Log.v(TAG, "Trying to unregister unknown request " + request);
|
||||
return;
|
||||
}
|
||||
if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
|
||||
Log.w(TAG, "Caller uid " + callingUid + " is not owner of " + request);
|
||||
return;
|
||||
}
|
||||
|
||||
if (LOGV) Log.v(TAG, "Unregistering " + request);
|
||||
mDataUsageRequests.remove(request.requestId);
|
||||
requestInfo.unlinkDeathRecipient();
|
||||
requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
|
||||
}
|
||||
|
||||
private void handleUpdateStats(StatsContext statsContext) {
|
||||
if (mDataUsageRequests.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < mDataUsageRequests.size(); i++) {
|
||||
RequestInfo requestInfo = mDataUsageRequests.valueAt(i);
|
||||
requestInfo.updateStats(statsContext);
|
||||
}
|
||||
}
|
||||
|
||||
private DataUsageRequest buildRequest(Context context, DataUsageRequest request,
|
||||
int callingUid) {
|
||||
// For non-NETWORK_STACK permission uid, cap the minimum threshold to a safe default to
|
||||
// avoid too many callbacks.
|
||||
final long thresholdInBytes = (context.checkPermission(
|
||||
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, Process.myPid(), callingUid)
|
||||
== PackageManager.PERMISSION_GRANTED ? request.thresholdInBytes
|
||||
: Math.max(MIN_THRESHOLD_BYTES, request.thresholdInBytes));
|
||||
if (thresholdInBytes > request.thresholdInBytes) {
|
||||
Log.w(TAG, "Threshold was too low for " + request
|
||||
+ ". Overriding to a safer default of " + thresholdInBytes + " bytes");
|
||||
}
|
||||
return new DataUsageRequest(mNextDataUsageRequestId.incrementAndGet(),
|
||||
request.template, thresholdInBytes);
|
||||
}
|
||||
|
||||
private RequestInfo buildRequestInfo(DataUsageRequest request, IUsageCallback callback,
|
||||
int callingUid, @NetworkStatsAccess.Level int accessLevel) {
|
||||
if (accessLevel <= NetworkStatsAccess.Level.USER) {
|
||||
return new UserUsageRequestInfo(this, request, callback, callingUid,
|
||||
accessLevel);
|
||||
} else {
|
||||
// Safety check in case a new access level is added and we forgot to update this
|
||||
if (accessLevel < NetworkStatsAccess.Level.DEVICESUMMARY) {
|
||||
throw new IllegalArgumentException(
|
||||
"accessLevel " + accessLevel + " is less than DEVICESUMMARY.");
|
||||
}
|
||||
return new NetworkUsageRequestInfo(this, request, callback, callingUid,
|
||||
accessLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks information relevant to a data usage observer.
|
||||
* It will notice when the calling process dies so we can self-expire.
|
||||
*/
|
||||
private abstract static class RequestInfo implements IBinder.DeathRecipient {
|
||||
private final NetworkStatsObservers mStatsObserver;
|
||||
protected final DataUsageRequest mRequest;
|
||||
private final IUsageCallback mCallback;
|
||||
protected final int mCallingUid;
|
||||
protected final @NetworkStatsAccess.Level int mAccessLevel;
|
||||
protected NetworkStatsRecorder mRecorder;
|
||||
protected NetworkStatsCollection mCollection;
|
||||
|
||||
RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
|
||||
IUsageCallback callback, int callingUid,
|
||||
@NetworkStatsAccess.Level int accessLevel) {
|
||||
mStatsObserver = statsObserver;
|
||||
mRequest = request;
|
||||
mCallback = callback;
|
||||
mCallingUid = callingUid;
|
||||
mAccessLevel = accessLevel;
|
||||
|
||||
try {
|
||||
mCallback.asBinder().linkToDeath(this, 0);
|
||||
} catch (RemoteException e) {
|
||||
binderDied();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
if (LOGV) {
|
||||
Log.v(TAG, "RequestInfo binderDied(" + mRequest + ", " + mCallback + ")");
|
||||
}
|
||||
mStatsObserver.unregister(mRequest, Process.SYSTEM_UID);
|
||||
callCallback(NetworkStatsManager.CALLBACK_RELEASED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RequestInfo from uid:" + mCallingUid
|
||||
+ " for " + mRequest + " accessLevel:" + mAccessLevel;
|
||||
}
|
||||
|
||||
private void unlinkDeathRecipient() {
|
||||
mCallback.asBinder().unlinkToDeath(this, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update stats given the samples and interface to identity mappings.
|
||||
*/
|
||||
private void updateStats(StatsContext statsContext) {
|
||||
if (mRecorder == null) {
|
||||
// First run; establish baseline stats
|
||||
resetRecorder();
|
||||
recordSample(statsContext);
|
||||
return;
|
||||
}
|
||||
recordSample(statsContext);
|
||||
|
||||
if (checkStats()) {
|
||||
resetRecorder();
|
||||
callCallback(NetworkStatsManager.CALLBACK_LIMIT_REACHED);
|
||||
}
|
||||
}
|
||||
|
||||
private void callCallback(int callbackType) {
|
||||
try {
|
||||
if (LOGV) {
|
||||
Log.v(TAG, "sending notification " + callbackTypeToName(callbackType)
|
||||
+ " for " + mRequest);
|
||||
}
|
||||
switch (callbackType) {
|
||||
case NetworkStatsManager.CALLBACK_LIMIT_REACHED:
|
||||
mCallback.onThresholdReached(mRequest);
|
||||
break;
|
||||
case NetworkStatsManager.CALLBACK_RELEASED:
|
||||
mCallback.onCallbackReleased(mRequest);
|
||||
break;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// May occur naturally in the race of binder death.
|
||||
Log.w(TAG, "RemoteException caught trying to send a callback msg for " + mRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetRecorder() {
|
||||
mRecorder = new NetworkStatsRecorder();
|
||||
mCollection = mRecorder.getSinceBoot();
|
||||
}
|
||||
|
||||
protected abstract boolean checkStats();
|
||||
|
||||
protected abstract void recordSample(StatsContext statsContext);
|
||||
|
||||
private String callbackTypeToName(int callbackType) {
|
||||
switch (callbackType) {
|
||||
case NetworkStatsManager.CALLBACK_LIMIT_REACHED:
|
||||
return "LIMIT_REACHED";
|
||||
case NetworkStatsManager.CALLBACK_RELEASED:
|
||||
return "RELEASED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NetworkUsageRequestInfo extends RequestInfo {
|
||||
NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
|
||||
IUsageCallback callback, int callingUid,
|
||||
@NetworkStatsAccess.Level int accessLevel) {
|
||||
super(statsObserver, request, callback, callingUid, accessLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkStats() {
|
||||
long bytesSoFar = getTotalBytesForNetwork(mRequest.template);
|
||||
if (LOGV) {
|
||||
Log.v(TAG, bytesSoFar + " bytes so far since notification for "
|
||||
+ mRequest.template);
|
||||
}
|
||||
if (bytesSoFar > mRequest.thresholdInBytes) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void recordSample(StatsContext statsContext) {
|
||||
// Recorder does not need to be locked in this context since only the handler
|
||||
// thread will update it. We pass a null VPN array because usage is aggregated by uid
|
||||
// for this snapshot, so VPN traffic can't be reattributed to responsible apps.
|
||||
mRecorder.recordSnapshotLocked(statsContext.mXtSnapshot, statsContext.mActiveIfaces,
|
||||
statsContext.mCurrentTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads stats matching the given template. {@link NetworkStatsCollection} will aggregate
|
||||
* over all buckets, which in this case should be only one since we built it big enough
|
||||
* that it will outlive the caller. If it doesn't, then there will be multiple buckets.
|
||||
*/
|
||||
private long getTotalBytesForNetwork(NetworkTemplate template) {
|
||||
NetworkStats stats = mCollection.getSummary(template,
|
||||
Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
|
||||
mAccessLevel, mCallingUid);
|
||||
return stats.getTotalBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private static class UserUsageRequestInfo extends RequestInfo {
|
||||
UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
|
||||
IUsageCallback callback, int callingUid,
|
||||
@NetworkStatsAccess.Level int accessLevel) {
|
||||
super(statsObserver, request, callback, callingUid, accessLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkStats() {
|
||||
int[] uidsToMonitor = mCollection.getRelevantUids(mAccessLevel, mCallingUid);
|
||||
|
||||
for (int i = 0; i < uidsToMonitor.length; i++) {
|
||||
long bytesSoFar = getTotalBytesForNetworkUid(mRequest.template, uidsToMonitor[i]);
|
||||
if (bytesSoFar > mRequest.thresholdInBytes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void recordSample(StatsContext statsContext) {
|
||||
// Recorder does not need to be locked in this context since only the handler
|
||||
// thread will update it. We pass the VPN info so VPN traffic is reattributed to
|
||||
// responsible apps.
|
||||
mRecorder.recordSnapshotLocked(statsContext.mUidSnapshot, statsContext.mActiveUidIfaces,
|
||||
statsContext.mCurrentTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all stats matching the given template and uid. Ther history will likely only
|
||||
* contain one bucket per ident since we build it big enough that it will outlive the
|
||||
* caller lifetime.
|
||||
*/
|
||||
private long getTotalBytesForNetworkUid(NetworkTemplate template, int uid) {
|
||||
try {
|
||||
NetworkStatsHistory history = mCollection.getHistory(template, null, uid,
|
||||
NetworkStats.SET_ALL, NetworkStats.TAG_NONE,
|
||||
NetworkStatsHistory.FIELD_ALL,
|
||||
Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
|
||||
mAccessLevel, mCallingUid);
|
||||
return history.getTotalBytes();
|
||||
} catch (SecurityException e) {
|
||||
if (LOGV) {
|
||||
Log.w(TAG, "CallerUid " + mCallingUid + " may have lost access to uid "
|
||||
+ uid);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class StatsContext {
|
||||
NetworkStats mXtSnapshot;
|
||||
NetworkStats mUidSnapshot;
|
||||
ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
|
||||
ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
|
||||
long mCurrentTime;
|
||||
|
||||
StatsContext(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
|
||||
ArrayMap<String, NetworkIdentitySet> activeIfaces,
|
||||
ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
|
||||
long currentTime) {
|
||||
mXtSnapshot = xtSnapshot;
|
||||
mUidSnapshot = uidSnapshot;
|
||||
mActiveIfaces = activeIfaces;
|
||||
mActiveUidIfaces = activeUidIfaces;
|
||||
mCurrentTime = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
507
service-t/src/com/android/server/net/NetworkStatsRecorder.java
Normal file
507
service-t/src/com/android/server/net/NetworkStatsRecorder.java
Normal file
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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;
|
||||
|
||||
import static android.net.NetworkStats.TAG_NONE;
|
||||
import static android.net.TrafficStats.KB_IN_BYTES;
|
||||
import static android.net.TrafficStats.MB_IN_BYTES;
|
||||
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
|
||||
|
||||
import android.net.NetworkIdentitySet;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.NetworkStats.NonMonotonicObserver;
|
||||
import android.net.NetworkStatsAccess;
|
||||
import android.net.NetworkStatsCollection;
|
||||
import android.net.NetworkStatsHistory;
|
||||
import android.net.NetworkTemplate;
|
||||
import android.net.TrafficStats;
|
||||
import android.os.Binder;
|
||||
import android.os.DropBoxManager;
|
||||
import android.service.NetworkStatsRecorderProto;
|
||||
import android.util.IndentingPrintWriter;
|
||||
import android.util.Log;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.internal.util.FileRotator;
|
||||
import com.android.net.module.util.NetworkStatsUtils;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Logic to record deltas between periodic {@link NetworkStats} snapshots into
|
||||
* {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}.
|
||||
* Keeps pending changes in memory until they pass a specific threshold, in
|
||||
* bytes. Uses {@link FileRotator} for persistence logic if present.
|
||||
* <p>
|
||||
* Not inherently thread safe.
|
||||
*/
|
||||
public class NetworkStatsRecorder {
|
||||
private static final String TAG = "NetworkStatsRecorder";
|
||||
private static final boolean LOGD = false;
|
||||
private static final boolean LOGV = false;
|
||||
|
||||
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
|
||||
|
||||
/** Dump before deleting in {@link #recoverFromWtf()}. */
|
||||
private static final boolean DUMP_BEFORE_DELETE = true;
|
||||
|
||||
private final FileRotator mRotator;
|
||||
private final NonMonotonicObserver<String> mObserver;
|
||||
private final DropBoxManager mDropBox;
|
||||
private final String mCookie;
|
||||
|
||||
private final long mBucketDuration;
|
||||
private final boolean mOnlyTags;
|
||||
|
||||
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
|
||||
private NetworkStats mLastSnapshot;
|
||||
|
||||
private final NetworkStatsCollection mPending;
|
||||
private final NetworkStatsCollection mSinceBoot;
|
||||
|
||||
private final CombiningRewriter mPendingRewriter;
|
||||
|
||||
private WeakReference<NetworkStatsCollection> mComplete;
|
||||
|
||||
/**
|
||||
* Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
|
||||
*/
|
||||
public NetworkStatsRecorder() {
|
||||
mRotator = null;
|
||||
mObserver = null;
|
||||
mDropBox = null;
|
||||
mCookie = null;
|
||||
|
||||
// set the bucket big enough to have all data in one bucket, but allow some
|
||||
// slack to avoid overflow
|
||||
mBucketDuration = YEAR_IN_MILLIS;
|
||||
mOnlyTags = false;
|
||||
|
||||
mPending = null;
|
||||
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
|
||||
|
||||
mPendingRewriter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persisted recorder.
|
||||
*/
|
||||
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
|
||||
DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
|
||||
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
|
||||
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
|
||||
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
|
||||
mCookie = cookie;
|
||||
|
||||
mBucketDuration = bucketDuration;
|
||||
mOnlyTags = onlyTags;
|
||||
|
||||
mPending = new NetworkStatsCollection(bucketDuration);
|
||||
mSinceBoot = new NetworkStatsCollection(bucketDuration);
|
||||
|
||||
mPendingRewriter = new CombiningRewriter(mPending);
|
||||
}
|
||||
|
||||
public void setPersistThreshold(long thresholdBytes) {
|
||||
if (LOGV) Log.v(TAG, "setPersistThreshold() with " + thresholdBytes);
|
||||
mPersistThresholdBytes = NetworkStatsUtils.constrain(
|
||||
thresholdBytes, 1 * KB_IN_BYTES, 100 * MB_IN_BYTES);
|
||||
}
|
||||
|
||||
public void resetLocked() {
|
||||
mLastSnapshot = null;
|
||||
if (mPending != null) {
|
||||
mPending.reset();
|
||||
}
|
||||
if (mSinceBoot != null) {
|
||||
mSinceBoot.reset();
|
||||
}
|
||||
if (mComplete != null) {
|
||||
mComplete.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) {
|
||||
return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE,
|
||||
NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotal(null);
|
||||
}
|
||||
|
||||
public NetworkStatsCollection getSinceBoot() {
|
||||
return mSinceBoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load complete history represented by {@link FileRotator}. Caches
|
||||
* internally as a {@link WeakReference}, and updated with future
|
||||
* {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long
|
||||
* as reference is valid.
|
||||
*/
|
||||
public NetworkStatsCollection getOrLoadCompleteLocked() {
|
||||
Objects.requireNonNull(mRotator, "missing FileRotator");
|
||||
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
|
||||
if (res == null) {
|
||||
res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
|
||||
mComplete = new WeakReference<NetworkStatsCollection>(res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) {
|
||||
Objects.requireNonNull(mRotator, "missing FileRotator");
|
||||
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
|
||||
if (res == null) {
|
||||
res = loadLocked(start, end);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private NetworkStatsCollection loadLocked(long start, long end) {
|
||||
if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
|
||||
final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
|
||||
try {
|
||||
mRotator.readMatching(res, start, end);
|
||||
res.recordCollection(mPending);
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem completely reading network stats", e);
|
||||
recoverFromWtf();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem completely reading network stats", e);
|
||||
recoverFromWtf();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record any delta that occurred since last {@link NetworkStats} snapshot, using the given
|
||||
* {@link Map} to identify network interfaces. First snapshot is considered bootstrap, and is
|
||||
* not counted as delta.
|
||||
*/
|
||||
public void recordSnapshotLocked(NetworkStats snapshot,
|
||||
Map<String, NetworkIdentitySet> ifaceIdent, long currentTimeMillis) {
|
||||
final HashSet<String> unknownIfaces = new HashSet<>();
|
||||
|
||||
// skip recording when snapshot missing
|
||||
if (snapshot == null) return;
|
||||
|
||||
// assume first snapshot is bootstrap and don't record
|
||||
if (mLastSnapshot == null) {
|
||||
mLastSnapshot = snapshot;
|
||||
return;
|
||||
}
|
||||
|
||||
final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
|
||||
|
||||
final NetworkStats delta = NetworkStats.subtract(
|
||||
snapshot, mLastSnapshot, mObserver, mCookie);
|
||||
final long end = currentTimeMillis;
|
||||
final long start = end - delta.getElapsedRealtime();
|
||||
|
||||
NetworkStats.Entry entry = null;
|
||||
for (int i = 0; i < delta.size(); i++) {
|
||||
entry = delta.getValues(i, entry);
|
||||
|
||||
// As a last-ditch check, report any negative values and
|
||||
// clamp them so recording below doesn't croak.
|
||||
if (entry.isNegative()) {
|
||||
if (mObserver != null) {
|
||||
mObserver.foundNonMonotonic(delta, i, mCookie);
|
||||
}
|
||||
entry.rxBytes = Math.max(entry.rxBytes, 0);
|
||||
entry.rxPackets = Math.max(entry.rxPackets, 0);
|
||||
entry.txBytes = Math.max(entry.txBytes, 0);
|
||||
entry.txPackets = Math.max(entry.txPackets, 0);
|
||||
entry.operations = Math.max(entry.operations, 0);
|
||||
}
|
||||
|
||||
final NetworkIdentitySet ident = ifaceIdent.get(entry.iface);
|
||||
if (ident == null) {
|
||||
unknownIfaces.add(entry.iface);
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip when no delta occurred
|
||||
if (entry.isEmpty()) continue;
|
||||
|
||||
// only record tag data when requested
|
||||
if ((entry.tag == TAG_NONE) != mOnlyTags) {
|
||||
if (mPending != null) {
|
||||
mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
|
||||
}
|
||||
|
||||
// also record against boot stats when present
|
||||
if (mSinceBoot != null) {
|
||||
mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
|
||||
}
|
||||
|
||||
// also record against complete dataset when present
|
||||
if (complete != null) {
|
||||
complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mLastSnapshot = snapshot;
|
||||
|
||||
if (LOGV && unknownIfaces.size() > 0) {
|
||||
Log.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider persisting any pending deltas, if they are beyond
|
||||
* {@link #mPersistThresholdBytes}.
|
||||
*/
|
||||
public void maybePersistLocked(long currentTimeMillis) {
|
||||
Objects.requireNonNull(mRotator, "missing FileRotator");
|
||||
final long pendingBytes = mPending.getTotalBytes();
|
||||
if (pendingBytes >= mPersistThresholdBytes) {
|
||||
forcePersistLocked(currentTimeMillis);
|
||||
} else {
|
||||
mRotator.maybeRotate(currentTimeMillis);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force persisting any pending deltas.
|
||||
*/
|
||||
public void forcePersistLocked(long currentTimeMillis) {
|
||||
Objects.requireNonNull(mRotator, "missing FileRotator");
|
||||
if (mPending.isDirty()) {
|
||||
if (LOGD) Log.d(TAG, "forcePersistLocked() writing for " + mCookie);
|
||||
try {
|
||||
mRotator.rewriteActive(mPendingRewriter, currentTimeMillis);
|
||||
mRotator.maybeRotate(currentTimeMillis);
|
||||
mPending.reset();
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem persisting pending stats", e);
|
||||
recoverFromWtf();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem persisting pending stats", e);
|
||||
recoverFromWtf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given UID from all {@link FileRotator} history, migrating it
|
||||
* to {@link TrafficStats#UID_REMOVED}.
|
||||
*/
|
||||
public void removeUidsLocked(int[] uids) {
|
||||
if (mRotator != null) {
|
||||
try {
|
||||
// Rewrite all persisted data to migrate UID stats
|
||||
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
||||
recoverFromWtf();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
||||
recoverFromWtf();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any pending stats
|
||||
if (mPending != null) {
|
||||
mPending.removeUids(uids);
|
||||
}
|
||||
if (mSinceBoot != null) {
|
||||
mSinceBoot.removeUids(uids);
|
||||
}
|
||||
|
||||
// Clear UID from current stats snapshot
|
||||
if (mLastSnapshot != null) {
|
||||
mLastSnapshot.removeUids(uids);
|
||||
}
|
||||
|
||||
final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
|
||||
if (complete != null) {
|
||||
complete.removeUids(uids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewriter that will combine current {@link NetworkStatsCollection} values
|
||||
* with anything read from disk, and write combined set to disk. Clears the
|
||||
* original {@link NetworkStatsCollection} when finished writing.
|
||||
*/
|
||||
private static class CombiningRewriter implements FileRotator.Rewriter {
|
||||
private final NetworkStatsCollection mCollection;
|
||||
|
||||
public CombiningRewriter(NetworkStatsCollection collection) {
|
||||
mCollection = Objects.requireNonNull(collection, "missing NetworkStatsCollection");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
// ignored
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(InputStream in) throws IOException {
|
||||
mCollection.read(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWrite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(OutputStream out) throws IOException {
|
||||
mCollection.write(out);
|
||||
mCollection.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewriter that will remove any {@link NetworkStatsHistory} attributed to
|
||||
* the requested UID, only writing data back when modified.
|
||||
*/
|
||||
public static class RemoveUidRewriter implements FileRotator.Rewriter {
|
||||
private final NetworkStatsCollection mTemp;
|
||||
private final int[] mUids;
|
||||
|
||||
public RemoveUidRewriter(long bucketDuration, int[] uids) {
|
||||
mTemp = new NetworkStatsCollection(bucketDuration);
|
||||
mUids = uids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
mTemp.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(InputStream in) throws IOException {
|
||||
mTemp.read(in);
|
||||
mTemp.clearDirty();
|
||||
mTemp.removeUids(mUids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWrite() {
|
||||
return mTemp.isDirty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(OutputStream out) throws IOException {
|
||||
mTemp.write(out);
|
||||
}
|
||||
}
|
||||
|
||||
public void importLegacyNetworkLocked(File file) throws IOException {
|
||||
Objects.requireNonNull(mRotator, "missing FileRotator");
|
||||
|
||||
// legacy file still exists; start empty to avoid double importing
|
||||
mRotator.deleteAll();
|
||||
|
||||
final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
|
||||
collection.readLegacyNetwork(file);
|
||||
|
||||
final long startMillis = collection.getStartMillis();
|
||||
final long endMillis = collection.getEndMillis();
|
||||
|
||||
if (!collection.isEmpty()) {
|
||||
// process legacy data, creating active file at starting time, then
|
||||
// using end time to possibly trigger rotation.
|
||||
mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
|
||||
mRotator.maybeRotate(endMillis);
|
||||
}
|
||||
}
|
||||
|
||||
public void importLegacyUidLocked(File file) throws IOException {
|
||||
Objects.requireNonNull(mRotator, "missing FileRotator");
|
||||
|
||||
// legacy file still exists; start empty to avoid double importing
|
||||
mRotator.deleteAll();
|
||||
|
||||
final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
|
||||
collection.readLegacyUid(file, mOnlyTags);
|
||||
|
||||
final long startMillis = collection.getStartMillis();
|
||||
final long endMillis = collection.getEndMillis();
|
||||
|
||||
if (!collection.isEmpty()) {
|
||||
// process legacy data, creating active file at starting time, then
|
||||
// using end time to possibly trigger rotation.
|
||||
mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
|
||||
mRotator.maybeRotate(endMillis);
|
||||
}
|
||||
}
|
||||
|
||||
public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
|
||||
if (mPending != null) {
|
||||
pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
|
||||
}
|
||||
if (fullHistory) {
|
||||
pw.println("Complete history:");
|
||||
getOrLoadCompleteLocked().dump(pw);
|
||||
} else {
|
||||
pw.println("History since boot:");
|
||||
mSinceBoot.dump(pw);
|
||||
}
|
||||
}
|
||||
|
||||
public void dumpDebugLocked(ProtoOutputStream proto, long tag) {
|
||||
final long start = proto.start(tag);
|
||||
if (mPending != null) {
|
||||
proto.write(NetworkStatsRecorderProto.PENDING_TOTAL_BYTES,
|
||||
mPending.getTotalBytes());
|
||||
}
|
||||
getOrLoadCompleteLocked().dumpDebug(proto,
|
||||
NetworkStatsRecorderProto.COMPLETE_HISTORY);
|
||||
proto.end(start);
|
||||
}
|
||||
|
||||
public void dumpCheckin(PrintWriter pw, long start, long end) {
|
||||
// Only load and dump stats from the requested window
|
||||
getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover from {@link FileRotator} failure by dumping state to
|
||||
* {@link DropBoxManager} and deleting contents.
|
||||
*/
|
||||
private void recoverFromWtf() {
|
||||
if (DUMP_BEFORE_DELETE) {
|
||||
final ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
mRotator.dumpAll(os);
|
||||
} catch (IOException e) {
|
||||
// ignore partial contents
|
||||
os.reset();
|
||||
} finally {
|
||||
IoUtils.closeQuietly(os);
|
||||
}
|
||||
mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
|
||||
}
|
||||
|
||||
mRotator.deleteAll();
|
||||
}
|
||||
}
|
||||
2528
service-t/src/com/android/server/net/NetworkStatsService.java
Normal file
2528
service-t/src/com/android/server/net/NetworkStatsService.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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;
|
||||
|
||||
import static android.app.usage.NetworkStatsManager.NETWORK_TYPE_5G_NSA;
|
||||
import static android.app.usage.NetworkStatsManager.getCollapsedRatType;
|
||||
import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED;
|
||||
import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA;
|
||||
import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.telephony.TelephonyCallback;
|
||||
import android.telephony.TelephonyDisplayInfo;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.net.module.util.CollectionUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Helper class that watches for events that are triggered per subscription.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||
public class NetworkStatsSubscriptionsMonitor extends
|
||||
SubscriptionManager.OnSubscriptionsChangedListener {
|
||||
|
||||
/**
|
||||
* Interface that this monitor uses to delegate event handling to NetworkStatsService.
|
||||
*/
|
||||
public interface Delegate {
|
||||
/**
|
||||
* Notify that the collapsed RAT type has been changed for any subscription. The method
|
||||
* will also be triggered for any existing sub when start and stop monitoring.
|
||||
*
|
||||
* @param subscriberId IMSI of the subscription.
|
||||
* @param collapsedRatType collapsed RAT type.
|
||||
* @see android.app.usage.NetworkStatsManager#getCollapsedRatType(int).
|
||||
*/
|
||||
void onCollapsedRatTypeChanged(@NonNull String subscriberId, int collapsedRatType);
|
||||
}
|
||||
private final Delegate mDelegate;
|
||||
|
||||
/**
|
||||
* Receivers that watches for {@link TelephonyDisplayInfo} changes for each subscription, to
|
||||
* monitor the transitioning between Radio Access Technology(RAT) types for each sub.
|
||||
*/
|
||||
@NonNull
|
||||
private final CopyOnWriteArrayList<RatTypeListener> mRatListeners =
|
||||
new CopyOnWriteArrayList<>();
|
||||
|
||||
@NonNull
|
||||
private final SubscriptionManager mSubscriptionManager;
|
||||
@NonNull
|
||||
private final TelephonyManager mTeleManager;
|
||||
|
||||
@NonNull
|
||||
private final Executor mExecutor;
|
||||
|
||||
NetworkStatsSubscriptionsMonitor(@NonNull Context context,
|
||||
@NonNull Executor executor, @NonNull Delegate delegate) {
|
||||
super();
|
||||
mSubscriptionManager = (SubscriptionManager) context.getSystemService(
|
||||
Context.TELEPHONY_SUBSCRIPTION_SERVICE);
|
||||
mTeleManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
mExecutor = executor;
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscriptionsChanged() {
|
||||
// Collect active subId list, hidden subId such as opportunistic subscriptions are
|
||||
// also needed to track CBRS.
|
||||
final List<Integer> newSubs = getActiveSubIdList(mSubscriptionManager);
|
||||
|
||||
// IMSI is needed for every newly added sub. Listener stores subscriberId into it to
|
||||
// prevent binder call to telephony when querying RAT. Keep listener registration with empty
|
||||
// IMSI is meaningless since the RAT type changed is ambiguous for multi-SIM if reported
|
||||
// with empty IMSI. So filter the subs w/o a valid IMSI to prevent such registration.
|
||||
final List<Pair<Integer, String>> filteredNewSubs = new ArrayList<>();
|
||||
for (final int subId : newSubs) {
|
||||
final String subscriberId =
|
||||
mTeleManager.createForSubscriptionId(subId).getSubscriberId();
|
||||
if (!TextUtils.isEmpty(subscriberId)) {
|
||||
filteredNewSubs.add(new Pair(subId, subscriberId));
|
||||
}
|
||||
}
|
||||
|
||||
for (final Pair<Integer, String> sub : filteredNewSubs) {
|
||||
// Fully match listener with subId and IMSI, since in some rare cases, IMSI might be
|
||||
// suddenly change regardless of subId, such as switch IMSI feature in modem side.
|
||||
// If that happens, register new listener with new IMSI and remove old one later.
|
||||
if (CollectionUtils.any(mRatListeners, it -> it.equalsKey(sub.first, sub.second))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final RatTypeListener listener = new RatTypeListener(this, sub.first, sub.second);
|
||||
mRatListeners.add(listener);
|
||||
|
||||
// Register listener to the telephony manager that associated with specific sub.
|
||||
mTeleManager.createForSubscriptionId(sub.first)
|
||||
.registerTelephonyCallback(mExecutor, listener);
|
||||
Log.d(NetworkStatsService.TAG, "RAT type listener registered for sub " + sub.first);
|
||||
}
|
||||
|
||||
for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
|
||||
// If there is no subId and IMSI matched the listener, removes it.
|
||||
if (!CollectionUtils.any(filteredNewSubs,
|
||||
it -> listener.equalsKey(it.first, it.second))) {
|
||||
handleRemoveRatTypeListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<Integer> getActiveSubIdList(@NonNull SubscriptionManager subscriptionManager) {
|
||||
final ArrayList<Integer> ret = new ArrayList<>();
|
||||
final int[] ids = subscriptionManager.getCompleteActiveSubscriptionIdList();
|
||||
for (int id : ids) ret.add(id);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collapsed RatType for the given subscriberId.
|
||||
*
|
||||
* @param subscriberId the target subscriberId
|
||||
* @return collapsed RatType for the given subscriberId
|
||||
*/
|
||||
public int getRatTypeForSubscriberId(@NonNull String subscriberId) {
|
||||
final int index = CollectionUtils.indexOf(mRatListeners,
|
||||
it -> TextUtils.equals(subscriberId, it.mSubscriberId));
|
||||
return index != -1 ? mRatListeners.get(index).mLastCollapsedRatType
|
||||
: TelephonyManager.NETWORK_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring events that triggered per subscription.
|
||||
*/
|
||||
public void start() {
|
||||
mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister subscription changes and all listeners for each subscription.
|
||||
*/
|
||||
public void stop() {
|
||||
mSubscriptionManager.removeOnSubscriptionsChangedListener(this);
|
||||
|
||||
for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
|
||||
handleRemoveRatTypeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRemoveRatTypeListener(@NonNull RatTypeListener listener) {
|
||||
mTeleManager.createForSubscriptionId(listener.mSubId)
|
||||
.unregisterTelephonyCallback(listener);
|
||||
Log.d(NetworkStatsService.TAG, "RAT type listener unregistered for sub " + listener.mSubId);
|
||||
mRatListeners.remove(listener);
|
||||
|
||||
// Removal of subscriptions doesn't generate RAT changed event, fire it for every
|
||||
// RatTypeListener.
|
||||
mDelegate.onCollapsedRatTypeChanged(
|
||||
listener.mSubscriberId, TelephonyManager.NETWORK_TYPE_UNKNOWN);
|
||||
}
|
||||
|
||||
static class RatTypeListener extends TelephonyCallback
|
||||
implements TelephonyCallback.DisplayInfoListener {
|
||||
// Unique id for the subscription. See {@link SubscriptionInfo#getSubscriptionId}.
|
||||
@NonNull
|
||||
private final int mSubId;
|
||||
|
||||
// IMSI to identifying the corresponding network from {@link NetworkState}.
|
||||
// See {@link TelephonyManager#getSubscriberId}.
|
||||
@NonNull
|
||||
private final String mSubscriberId;
|
||||
|
||||
private volatile int mLastCollapsedRatType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
|
||||
@NonNull
|
||||
private final NetworkStatsSubscriptionsMonitor mMonitor;
|
||||
|
||||
RatTypeListener(@NonNull NetworkStatsSubscriptionsMonitor monitor, int subId,
|
||||
@NonNull String subscriberId) {
|
||||
mSubId = subId;
|
||||
mSubscriberId = subscriberId;
|
||||
mMonitor = monitor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayInfoChanged(TelephonyDisplayInfo displayInfo) {
|
||||
// In 5G SA (Stand Alone) mode, the primary cell itself will be 5G hence telephony
|
||||
// would report RAT = 5G_NR.
|
||||
// However, in 5G NSA (Non Stand Alone) mode, the primary cell is still LTE and
|
||||
// network allocates a secondary 5G cell so telephony reports RAT = LTE along with
|
||||
// NR state as connected. In such case, attributes the data usage to NR.
|
||||
// See b/160727498.
|
||||
final boolean is5GNsa = displayInfo.getNetworkType() == NETWORK_TYPE_LTE
|
||||
&& (displayInfo.getOverrideNetworkType() == OVERRIDE_NETWORK_TYPE_NR_NSA
|
||||
|| displayInfo.getOverrideNetworkType() == OVERRIDE_NETWORK_TYPE_NR_ADVANCED);
|
||||
|
||||
final int networkType =
|
||||
(is5GNsa ? NETWORK_TYPE_5G_NSA : displayInfo.getNetworkType());
|
||||
final int collapsedRatType = getCollapsedRatType(networkType);
|
||||
if (collapsedRatType == mLastCollapsedRatType) return;
|
||||
|
||||
if (NetworkStatsService.LOGD) {
|
||||
Log.d(NetworkStatsService.TAG, "subtype changed for sub(" + mSubId + "): "
|
||||
+ mLastCollapsedRatType + " -> " + collapsedRatType);
|
||||
}
|
||||
mLastCollapsedRatType = collapsedRatType;
|
||||
mMonitor.mDelegate.onCollapsedRatTypeChanged(mSubscriberId, mLastCollapsedRatType);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public int getSubId() {
|
||||
return mSubId;
|
||||
}
|
||||
|
||||
boolean equalsKey(int subId, @NonNull String subscriberId) {
|
||||
return mSubId == subId && TextUtils.equals(mSubscriberId, subscriberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
service-t/src/com/android/server/net/StatsMapKey.java
Normal file
46
service-t/src/com/android/server/net/StatsMapKey.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.Struct.Field;
|
||||
import com.android.net.module.util.Struct.Type;
|
||||
|
||||
/**
|
||||
* Key for both stats maps.
|
||||
*/
|
||||
public class StatsMapKey extends Struct {
|
||||
@Field(order = 0, type = Type.U32)
|
||||
public final long uid;
|
||||
|
||||
@Field(order = 1, type = Type.U32)
|
||||
public final long tag;
|
||||
|
||||
@Field(order = 2, type = Type.U32)
|
||||
public final long counterSet;
|
||||
|
||||
@Field(order = 3, type = Type.U32)
|
||||
public final long ifaceIndex;
|
||||
|
||||
public StatsMapKey(final long uid, final long tag, final long counterSet,
|
||||
final long ifaceIndex) {
|
||||
this.uid = uid;
|
||||
this.tag = tag;
|
||||
this.counterSet = counterSet;
|
||||
this.ifaceIndex = ifaceIndex;
|
||||
}
|
||||
}
|
||||
46
service-t/src/com/android/server/net/StatsMapValue.java
Normal file
46
service-t/src/com/android/server/net/StatsMapValue.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.Struct.Field;
|
||||
import com.android.net.module.util.Struct.Type;
|
||||
|
||||
/**
|
||||
* Value used for both stats maps and uid stats map.
|
||||
*/
|
||||
public class StatsMapValue extends Struct {
|
||||
@Field(order = 0, type = Type.U63)
|
||||
public final long rxPackets;
|
||||
|
||||
@Field(order = 1, type = Type.U63)
|
||||
public final long rxBytes;
|
||||
|
||||
@Field(order = 2, type = Type.U63)
|
||||
public final long txPackets;
|
||||
|
||||
@Field(order = 3, type = Type.U63)
|
||||
public final long txBytes;
|
||||
|
||||
public StatsMapValue(final long rxPackets, final long rxBytes, final long txPackets,
|
||||
final long txBytes) {
|
||||
this.rxPackets = rxPackets;
|
||||
this.rxBytes = rxBytes;
|
||||
this.txPackets = txPackets;
|
||||
this.txBytes = txBytes;
|
||||
}
|
||||
}
|
||||
33
service-t/src/com/android/server/net/UidStatsMapKey.java
Normal file
33
service-t/src/com/android/server/net/UidStatsMapKey.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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;
|
||||
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.Struct.Field;
|
||||
import com.android.net.module.util.Struct.Type;
|
||||
|
||||
/**
|
||||
* Key for uid stats map.
|
||||
*/
|
||||
public class UidStatsMapKey extends Struct {
|
||||
@Field(order = 0, type = Type.U32)
|
||||
public final long uid;
|
||||
|
||||
public UidStatsMapKey(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user