diff --git a/framework-t/Sources.bp b/framework-t/Sources.bp new file mode 100644 index 0000000000..bc27852857 --- /dev/null +++ b/framework-t/Sources.bp @@ -0,0 +1,205 @@ +// +// Copyright (C) 2021 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 { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +// NetworkStats related libraries. + +filegroup { + name: "framework-connectivity-netstats-internal-sources", + srcs: [ + "src/android/app/usage/*.java", + "src/android/net/DataUsageRequest.*", + "src/android/net/INetworkStatsService.aidl", + "src/android/net/INetworkStatsSession.aidl", + "src/android/net/NetworkIdentity.java", + "src/android/net/NetworkIdentitySet.java", + "src/android/net/NetworkStateSnapshot.*", + "src/android/net/NetworkStats.*", + "src/android/net/NetworkStatsAccess.*", + "src/android/net/NetworkStatsCollection.*", + "src/android/net/NetworkStatsHistory.*", + "src/android/net/NetworkTemplate.*", + "src/android/net/TrafficStats.java", + "src/android/net/UnderlyingNetworkInfo.*", + "src/android/net/netstats/**/*.*", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +filegroup { + name: "framework-connectivity-netstats-aidl-export-sources", + srcs: [ + "aidl-export/android/net/NetworkStats.aidl", + "aidl-export/android/net/NetworkTemplate.aidl", + ], + path: "aidl-export", + visibility: [ + "//visibility:private", + ], +} + +filegroup { + name: "framework-connectivity-netstats-sources", + srcs: [ + ":framework-connectivity-netstats-internal-sources", + ":framework-connectivity-netstats-aidl-export-sources", + ], + visibility: [ + "//visibility:private", + ], +} + +// Nsd related libraries. + +filegroup { + name: "framework-connectivity-nsd-internal-sources", + srcs: [ + "src/android/net/nsd/*.aidl", + "src/android/net/nsd/*.java", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +filegroup { + name: "framework-connectivity-nsd-aidl-export-sources", + srcs: [ + "aidl-export/android/net/nsd/*.aidl", + ], + path: "aidl-export", + visibility: [ + "//visibility:private", + ], +} + +filegroup { + name: "framework-connectivity-nsd-sources", + srcs: [ + ":framework-connectivity-nsd-internal-sources", + ":framework-connectivity-nsd-aidl-export-sources", + ], + visibility: [ + "//visibility:private", + ], +} + +// IpSec related libraries. + +filegroup { + name: "framework-connectivity-ipsec-sources", + srcs: [ + "src/android/net/IIpSecService.aidl", + "src/android/net/IpSec*.*", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +// Ethernet related libraries. + +filegroup { + name: "framework-connectivity-ethernet-sources", + srcs: [ + "src/android/net/EthernetManager.java", + "src/android/net/EthernetNetworkManagementException.java", + "src/android/net/EthernetNetworkManagementException.aidl", + "src/android/net/EthernetNetworkSpecifier.java", + "src/android/net/EthernetNetworkUpdateRequest.java", + "src/android/net/EthernetNetworkUpdateRequest.aidl", + "src/android/net/IEthernetManager.aidl", + "src/android/net/IEthernetServiceListener.aidl", + "src/android/net/INetworkInterfaceOutcomeReceiver.aidl", + "src/android/net/ITetheredInterfaceCallback.aidl", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +// Connectivity-T common libraries. + +filegroup { + name: "framework-connectivity-tiramisu-internal-sources", + srcs: [ + "src/android/net/ConnectivityFrameworkInitializerTiramisu.java", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +// TODO: remove this empty filegroup. +filegroup { + name: "framework-connectivity-tiramisu-sources", + srcs: [], + visibility: ["//frameworks/base"], +} + +filegroup { + name: "framework-connectivity-tiramisu-updatable-sources", + srcs: [ + ":framework-connectivity-ethernet-sources", + ":framework-connectivity-ipsec-sources", + ":framework-connectivity-netstats-sources", + ":framework-connectivity-nsd-sources", + ":framework-connectivity-tiramisu-internal-sources", + ], + visibility: [ + "//frameworks/base", + "//packages/modules/Connectivity:__subpackages__", + ], +} + +cc_library_shared { + name: "libframework-connectivity-tiramisu-jni", + min_sdk_version: "30", + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + // Don't warn about S API usage even with + // min_sdk 30: the library is only loaded + // on S+ devices + "-Wno-unguarded-availability", + "-Wthread-safety", + ], + srcs: [ + "jni/android_net_TrafficStats.cpp", + "jni/onload.cpp", + ], + shared_libs: [ + "libandroid", + "liblog", + "libnativehelper", + ], + stl: "none", + apex_available: [ + "com.android.tethering", + ], +} diff --git a/framework-t/jni/android_net_TrafficStats.cpp b/framework-t/jni/android_net_TrafficStats.cpp new file mode 100644 index 0000000000..f3c58b112f --- /dev/null +++ b/framework-t/jni/android_net_TrafficStats.cpp @@ -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. + */ + +#include +#include +#include + +namespace android { + +static jint tagSocketFd(JNIEnv* env, jclass, jobject fileDescriptor, jint tag, jint uid) { + int fd = AFileDescriptor_getFd(env, fileDescriptor); + if (fd == -1) return -EBADF; + return android_tag_socket_with_uid(fd, tag, uid); +} + +static jint untagSocketFd(JNIEnv* env, jclass, jobject fileDescriptor) { + int fd = AFileDescriptor_getFd(env, fileDescriptor); + if (fd == -1) return -EBADF; + return android_untag_socket(fd); +} + +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "native_tagSocketFd", "(Ljava/io/FileDescriptor;II)I", (void*) tagSocketFd }, + { "native_untagSocketFd", "(Ljava/io/FileDescriptor;)I", (void*) untagSocketFd }, +}; + +int register_android_net_TrafficStats(JNIEnv* env) { + return jniRegisterNativeMethods(env, "android/net/TrafficStats", gMethods, NELEM(gMethods)); +} + +}; // namespace android + diff --git a/framework-t/jni/onload.cpp b/framework-t/jni/onload.cpp new file mode 100644 index 0000000000..1fb42c6347 --- /dev/null +++ b/framework-t/jni/onload.cpp @@ -0,0 +1,39 @@ +/* + * 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. + */ + +#define LOG_TAG "FrameworkConnectivityJNI" + +#include +#include + +namespace android { + +int register_android_net_TrafficStats(JNIEnv* env); + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ERROR: GetEnv failed"); + return JNI_ERR; + } + + if (register_android_net_TrafficStats(env) < 0) return JNI_ERR; + + return JNI_VERSION_1_6; +} + +}; // namespace android + diff --git a/framework-t/src/android/app/usage/NetworkStats.java b/framework-t/src/android/app/usage/NetworkStats.java new file mode 100644 index 0000000000..2b6570a6ec --- /dev/null +++ b/framework-t/src/android/app/usage/NetworkStats.java @@ -0,0 +1,742 @@ +/** + * 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 android.app.usage; + +import android.annotation.IntDef; +import android.content.Context; +import android.net.INetworkStatsService; +import android.net.INetworkStatsSession; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.net.TrafficStats; +import android.os.RemoteException; +import android.util.Log; + +import com.android.net.module.util.CollectionUtils; + +import dalvik.system.CloseGuard; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Class providing enumeration over buckets of network usage statistics. {@link NetworkStats} objects + * are returned as results to various queries in {@link NetworkStatsManager}. + */ +public final class NetworkStats implements AutoCloseable { + private final static String TAG = "NetworkStats"; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + /** + * Start timestamp of stats collected + */ + private final long mStartTimeStamp; + + /** + * End timestamp of stats collected + */ + private final long mEndTimeStamp; + + /** + * Non-null array indicates the query enumerates over uids. + */ + private int[] mUids; + + /** + * Index of the current uid in mUids when doing uid enumeration or a single uid value, + * depending on query type. + */ + private int mUidOrUidIndex; + + /** + * Tag id in case if was specified in the query. + */ + private int mTag = android.net.NetworkStats.TAG_NONE; + + /** + * State in case it was not specified in the query. + */ + private int mState = Bucket.STATE_ALL; + + /** + * The session while the query requires it, null if all the stats have been collected or close() + * has been called. + */ + private INetworkStatsSession mSession; + private NetworkTemplate mTemplate; + + /** + * Results of a summary query. + */ + private android.net.NetworkStats mSummary = null; + + /** + * Results of detail queries. + */ + private NetworkStatsHistory mHistory = null; + + /** + * Where we are in enumerating over the current result. + */ + private int mEnumerationIndex = 0; + + /** + * Recycling entry objects to prevent heap fragmentation. + */ + private android.net.NetworkStats.Entry mRecycledSummaryEntry = null; + private NetworkStatsHistory.Entry mRecycledHistoryEntry = null; + + /** @hide */ + NetworkStats(Context context, NetworkTemplate template, int flags, long startTimestamp, + long endTimestamp, INetworkStatsService statsService) + throws RemoteException, SecurityException { + // Open network stats session + mSession = statsService.openSessionForUsageStats(flags, context.getOpPackageName()); + mCloseGuard.open("close"); + mTemplate = template; + mStartTimeStamp = startTimestamp; + mEndTimeStamp = endTimestamp; + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } finally { + super.finalize(); + } + } + + // -------------------------BEGINNING OF PUBLIC API----------------------------------- + + /** + * Buckets are the smallest elements of a query result. As some dimensions of a result may be + * aggregated (e.g. time or state) some values may be equal across all buckets. + */ + public static class Bucket { + /** @hide */ + @IntDef(prefix = { "STATE_" }, value = { + STATE_ALL, + STATE_DEFAULT, + STATE_FOREGROUND + }) + @Retention(RetentionPolicy.SOURCE) + public @interface State {} + + /** + * Combined usage across all states. + */ + public static final int STATE_ALL = -1; + + /** + * Usage not accounted for in any other state. + */ + public static final int STATE_DEFAULT = 0x1; + + /** + * Foreground usage. + */ + public static final int STATE_FOREGROUND = 0x2; + + /** + * Special UID value for aggregate/unspecified. + */ + public static final int UID_ALL = android.net.NetworkStats.UID_ALL; + + /** + * Special UID value for removed apps. + */ + public static final int UID_REMOVED = TrafficStats.UID_REMOVED; + + /** + * Special UID value for data usage by tethering. + */ + public static final int UID_TETHERING = TrafficStats.UID_TETHERING; + + /** @hide */ + @IntDef(prefix = { "METERED_" }, value = { + METERED_ALL, + METERED_NO, + METERED_YES + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Metered {} + + /** + * Combined usage across all metered states. Covers metered and unmetered usage. + */ + public static final int METERED_ALL = -1; + + /** + * Usage that occurs on an unmetered network. + */ + public static final int METERED_NO = 0x1; + + /** + * Usage that occurs on a metered network. + * + *

A network is classified as metered when the user is sensitive to heavy data usage on + * that connection. + */ + public static final int METERED_YES = 0x2; + + /** @hide */ + @IntDef(prefix = { "ROAMING_" }, value = { + ROAMING_ALL, + ROAMING_NO, + ROAMING_YES + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Roaming {} + + /** + * Combined usage across all roaming states. Covers both roaming and non-roaming usage. + */ + public static final int ROAMING_ALL = -1; + + /** + * Usage that occurs on a home, non-roaming network. + * + *

Any cellular usage in this bucket was incurred while the device was connected to a + * tower owned or operated by the user's wireless carrier, or a tower that the user's + * wireless carrier has indicated should be treated as a home network regardless. + * + *

This is also the default value for network types that do not support roaming. + */ + public static final int ROAMING_NO = 0x1; + + /** + * Usage that occurs on a roaming network. + * + *

Any cellular usage in this bucket as incurred while the device was roaming on another + * carrier's network, for which additional charges may apply. + */ + public static final int ROAMING_YES = 0x2; + + /** @hide */ + @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = { + DEFAULT_NETWORK_ALL, + DEFAULT_NETWORK_NO, + DEFAULT_NETWORK_YES + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DefaultNetworkStatus {} + + /** + * Combined usage for this network regardless of default network status. + */ + public static final int DEFAULT_NETWORK_ALL = -1; + + /** + * Usage that occurs while this network is not a default network. + * + *

This implies that the app responsible for this usage requested that it occur on a + * specific network different from the one(s) the system would have selected for it. + */ + public static final int DEFAULT_NETWORK_NO = 0x1; + + /** + * Usage that occurs while this network is a default network. + * + *

This implies that the app either did not select a specific network for this usage, + * or it selected a network that the system could have selected for app traffic. + */ + public static final int DEFAULT_NETWORK_YES = 0x2; + + /** + * Special TAG value for total data across all tags + */ + public static final int TAG_NONE = android.net.NetworkStats.TAG_NONE; + + private int mUid; + private int mTag; + private int mState; + private int mDefaultNetworkStatus; + private int mMetered; + private int mRoaming; + private long mBeginTimeStamp; + private long mEndTimeStamp; + private long mRxBytes; + private long mRxPackets; + private long mTxBytes; + private long mTxPackets; + + private static int convertSet(@State int state) { + switch (state) { + case STATE_ALL: return android.net.NetworkStats.SET_ALL; + case STATE_DEFAULT: return android.net.NetworkStats.SET_DEFAULT; + case STATE_FOREGROUND: return android.net.NetworkStats.SET_FOREGROUND; + } + return 0; + } + + private static @State int convertState(int networkStatsSet) { + switch (networkStatsSet) { + case android.net.NetworkStats.SET_ALL : return STATE_ALL; + case android.net.NetworkStats.SET_DEFAULT : return STATE_DEFAULT; + case android.net.NetworkStats.SET_FOREGROUND : return STATE_FOREGROUND; + } + return 0; + } + + private static int convertUid(int uid) { + switch (uid) { + case TrafficStats.UID_REMOVED: return UID_REMOVED; + case TrafficStats.UID_TETHERING: return UID_TETHERING; + } + return uid; + } + + private static int convertTag(int tag) { + switch (tag) { + case android.net.NetworkStats.TAG_NONE: return TAG_NONE; + } + return tag; + } + + private static @Metered int convertMetered(int metered) { + switch (metered) { + case android.net.NetworkStats.METERED_ALL : return METERED_ALL; + case android.net.NetworkStats.METERED_NO: return METERED_NO; + case android.net.NetworkStats.METERED_YES: return METERED_YES; + } + return 0; + } + + private static @Roaming int convertRoaming(int roaming) { + switch (roaming) { + case android.net.NetworkStats.ROAMING_ALL : return ROAMING_ALL; + case android.net.NetworkStats.ROAMING_NO: return ROAMING_NO; + case android.net.NetworkStats.ROAMING_YES: return ROAMING_YES; + } + return 0; + } + + private static @DefaultNetworkStatus int convertDefaultNetworkStatus( + int defaultNetworkStatus) { + switch (defaultNetworkStatus) { + case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL; + case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO; + case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES; + } + return 0; + } + + public Bucket() { + } + + /** + * Key of the bucket. Usually an app uid or one of the following special values:

+ *

    + *
  • {@link #UID_REMOVED}
  • + *
  • {@link #UID_TETHERING}
  • + *
  • {@link android.os.Process#SYSTEM_UID}
  • + *
+ * @return Bucket key. + */ + public int getUid() { + return mUid; + } + + /** + * Tag of the bucket.

+ * @return Bucket tag. + */ + public int getTag() { + return mTag; + } + + /** + * Usage state. One of the following values:

+ *

    + *
  • {@link #STATE_ALL}
  • + *
  • {@link #STATE_DEFAULT}
  • + *
  • {@link #STATE_FOREGROUND}
  • + *
+ * @return Usage state. + */ + public @State int getState() { + return mState; + } + + /** + * Metered state. One of the following values:

+ *

    + *
  • {@link #METERED_ALL}
  • + *
  • {@link #METERED_NO}
  • + *
  • {@link #METERED_YES}
  • + *
+ *

A network is classified as metered when the user is sensitive to heavy data usage on + * that connection. Apps may warn before using these networks for large downloads. The + * metered state can be set by the user within data usage network restrictions. + */ + public @Metered int getMetered() { + return mMetered; + } + + /** + * Roaming state. One of the following values:

+ *

    + *
  • {@link #ROAMING_ALL}
  • + *
  • {@link #ROAMING_NO}
  • + *
  • {@link #ROAMING_YES}
  • + *
+ */ + public @Roaming int getRoaming() { + return mRoaming; + } + + /** + * Default network status. One of the following values:

+ *

    + *
  • {@link #DEFAULT_NETWORK_ALL}
  • + *
  • {@link #DEFAULT_NETWORK_NO}
  • + *
  • {@link #DEFAULT_NETWORK_YES}
  • + *
+ */ + public @DefaultNetworkStatus int getDefaultNetworkStatus() { + return mDefaultNetworkStatus; + } + + /** + * Start timestamp of the bucket's time interval. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @return Start of interval. + */ + public long getStartTimeStamp() { + return mBeginTimeStamp; + } + + /** + * End timestamp of the bucket's time interval. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @return End of interval. + */ + public long getEndTimeStamp() { + return mEndTimeStamp; + } + + /** + * Number of bytes received during the bucket's time interval. Statistics are measured at + * the network layer, so they include both TCP and UDP usage. + * @return Number of bytes. + */ + public long getRxBytes() { + return mRxBytes; + } + + /** + * Number of bytes transmitted during the bucket's time interval. Statistics are measured at + * the network layer, so they include both TCP and UDP usage. + * @return Number of bytes. + */ + public long getTxBytes() { + return mTxBytes; + } + + /** + * Number of packets received during the bucket's time interval. Statistics are measured at + * the network layer, so they include both TCP and UDP usage. + * @return Number of packets. + */ + public long getRxPackets() { + return mRxPackets; + } + + /** + * Number of packets transmitted during the bucket's time interval. Statistics are measured + * at the network layer, so they include both TCP and UDP usage. + * @return Number of packets. + */ + public long getTxPackets() { + return mTxPackets; + } + } + + /** + * Fills the recycled bucket with data of the next bin in the enumeration. + * @param bucketOut Bucket to be filled with data. + * @return true if successfully filled the bucket, false otherwise. + */ + public boolean getNextBucket(Bucket bucketOut) { + if (mSummary != null) { + return getNextSummaryBucket(bucketOut); + } else { + return getNextHistoryBucket(bucketOut); + } + } + + /** + * Check if it is possible to ask for a next bucket in the enumeration. + * @return true if there is at least one more bucket. + */ + public boolean hasNextBucket() { + if (mSummary != null) { + return mEnumerationIndex < mSummary.size(); + } else if (mHistory != null) { + return mEnumerationIndex < mHistory.size() + || hasNextUid(); + } + return false; + } + + /** + * Closes the enumeration. Call this method before this object gets out of scope. + */ + @Override + public void close() { + if (mSession != null) { + try { + mSession.close(); + } catch (RemoteException e) { + Log.w(TAG, e); + // Otherwise, meh + } + } + mSession = null; + if (mCloseGuard != null) { + mCloseGuard.close(); + } + } + + // -------------------------END OF PUBLIC API----------------------------------- + + /** + * Collects device summary results into a Bucket. + * @throws RemoteException + */ + Bucket getDeviceSummaryForNetwork() throws RemoteException { + mSummary = mSession.getDeviceSummaryForNetwork(mTemplate, mStartTimeStamp, mEndTimeStamp); + + // Setting enumeration index beyond end to avoid accidental enumeration over data that does + // not belong to the calling user. + mEnumerationIndex = mSummary.size(); + + return getSummaryAggregate(); + } + + /** + * Collects summary results and sets summary enumeration mode. + * @throws RemoteException + */ + void startSummaryEnumeration() throws RemoteException { + mSummary = mSession.getSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp, + false /* includeTags */); + mEnumerationIndex = 0; + } + + /** + * Collects tagged summary results and sets summary enumeration mode. + * @throws RemoteException + */ + void startTaggedSummaryEnumeration() throws RemoteException { + mSummary = mSession.getTaggedSummaryForAllUid(mTemplate, mStartTimeStamp, mEndTimeStamp); + mEnumerationIndex = 0; + } + + /** + * Collects history results for uid and resets history enumeration index. + */ + void startHistoryUidEnumeration(int uid, int tag, int state) { + mHistory = null; + try { + mHistory = mSession.getHistoryIntervalForUid(mTemplate, uid, + Bucket.convertSet(state), tag, NetworkStatsHistory.FIELD_ALL, + mStartTimeStamp, mEndTimeStamp); + setSingleUidTagState(uid, tag, state); + } catch (RemoteException e) { + Log.w(TAG, e); + // Leaving mHistory null + } + mEnumerationIndex = 0; + } + + /** + * Collects history results for network and resets history enumeration index. + */ + void startHistoryDeviceEnumeration() { + try { + mHistory = mSession.getHistoryIntervalForNetwork( + mTemplate, NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp); + } catch (RemoteException e) { + Log.w(TAG, e); + mHistory = null; + } + mEnumerationIndex = 0; + } + + /** + * Starts uid enumeration for current user. + * @throws RemoteException + */ + void startUserUidEnumeration() throws RemoteException { + // TODO: getRelevantUids should be sensitive to time interval. When that's done, + // the filtering logic below can be removed. + int[] uids = mSession.getRelevantUids(); + // Filtering of uids with empty history. + final ArrayList filteredUids = new ArrayList<>(); + for (int uid : uids) { + try { + NetworkStatsHistory history = mSession.getHistoryIntervalForUid(mTemplate, uid, + android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE, + NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp); + if (history != null && history.size() > 0) { + filteredUids.add(uid); + } + } catch (RemoteException e) { + Log.w(TAG, "Error while getting history of uid " + uid, e); + } + } + mUids = CollectionUtils.toIntArray(filteredUids); + mUidOrUidIndex = -1; + stepHistory(); + } + + /** + * Steps to next uid in enumeration and collects history for that. + */ + private void stepHistory(){ + if (hasNextUid()) { + stepUid(); + mHistory = null; + try { + mHistory = mSession.getHistoryIntervalForUid(mTemplate, getUid(), + android.net.NetworkStats.SET_ALL, android.net.NetworkStats.TAG_NONE, + NetworkStatsHistory.FIELD_ALL, mStartTimeStamp, mEndTimeStamp); + } catch (RemoteException e) { + Log.w(TAG, e); + // Leaving mHistory null + } + mEnumerationIndex = 0; + } + } + + private void fillBucketFromSummaryEntry(Bucket bucketOut) { + bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid); + bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag); + bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set); + bucketOut.mDefaultNetworkStatus = Bucket.convertDefaultNetworkStatus( + mRecycledSummaryEntry.defaultNetwork); + bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered); + bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming); + bucketOut.mBeginTimeStamp = mStartTimeStamp; + bucketOut.mEndTimeStamp = mEndTimeStamp; + bucketOut.mRxBytes = mRecycledSummaryEntry.rxBytes; + bucketOut.mRxPackets = mRecycledSummaryEntry.rxPackets; + bucketOut.mTxBytes = mRecycledSummaryEntry.txBytes; + bucketOut.mTxPackets = mRecycledSummaryEntry.txPackets; + } + + /** + * Getting the next item in summary enumeration. + * @param bucketOut Next item will be set here. + * @return true if a next item could be set. + */ + private boolean getNextSummaryBucket(Bucket bucketOut) { + if (bucketOut != null && mEnumerationIndex < mSummary.size()) { + mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry); + fillBucketFromSummaryEntry(bucketOut); + return true; + } + return false; + } + + Bucket getSummaryAggregate() { + if (mSummary == null) { + return null; + } + Bucket bucket = new Bucket(); + if (mRecycledSummaryEntry == null) { + mRecycledSummaryEntry = new android.net.NetworkStats.Entry(); + } + mSummary.getTotal(mRecycledSummaryEntry); + fillBucketFromSummaryEntry(bucket); + return bucket; + } + + /** + * Getting the next item in a history enumeration. + * @param bucketOut Next item will be set here. + * @return true if a next item could be set. + */ + private boolean getNextHistoryBucket(Bucket bucketOut) { + if (bucketOut != null && mHistory != null) { + if (mEnumerationIndex < mHistory.size()) { + mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++, + mRecycledHistoryEntry); + bucketOut.mUid = Bucket.convertUid(getUid()); + bucketOut.mTag = Bucket.convertTag(mTag); + bucketOut.mState = mState; + bucketOut.mDefaultNetworkStatus = Bucket.DEFAULT_NETWORK_ALL; + bucketOut.mMetered = Bucket.METERED_ALL; + bucketOut.mRoaming = Bucket.ROAMING_ALL; + bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart; + bucketOut.mEndTimeStamp = mRecycledHistoryEntry.bucketStart + + mRecycledHistoryEntry.bucketDuration; + bucketOut.mRxBytes = mRecycledHistoryEntry.rxBytes; + bucketOut.mRxPackets = mRecycledHistoryEntry.rxPackets; + bucketOut.mTxBytes = mRecycledHistoryEntry.txBytes; + bucketOut.mTxPackets = mRecycledHistoryEntry.txPackets; + return true; + } else if (hasNextUid()) { + stepHistory(); + return getNextHistoryBucket(bucketOut); + } + } + return false; + } + + // ------------------ UID LOGIC------------------------ + + private boolean isUidEnumeration() { + return mUids != null; + } + + private boolean hasNextUid() { + return isUidEnumeration() && (mUidOrUidIndex + 1) < mUids.length; + } + + private int getUid() { + // Check if uid enumeration. + if (isUidEnumeration()) { + if (mUidOrUidIndex < 0 || mUidOrUidIndex >= mUids.length) { + throw new IndexOutOfBoundsException( + "Index=" + mUidOrUidIndex + " mUids.length=" + mUids.length); + } + return mUids[mUidOrUidIndex]; + } + // Single uid mode. + return mUidOrUidIndex; + } + + private void setSingleUidTagState(int uid, int tag, int state) { + mUidOrUidIndex = uid; + mTag = tag; + mState = state; + } + + private void stepUid() { + if (mUids != null) { + ++mUidOrUidIndex; + } + } +} diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java new file mode 100644 index 0000000000..bf518b2d5a --- /dev/null +++ b/framework-t/src/android/app/usage/NetworkStatsManager.java @@ -0,0 +1,1181 @@ +/** + * 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 android.app.usage; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import android.Manifest; +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.annotation.WorkerThread; +import android.app.usage.NetworkStats.Bucket; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.DataUsageRequest; +import android.net.INetworkStatsService; +import android.net.Network; +import android.net.NetworkStack; +import android.net.NetworkStateSnapshot; +import android.net.NetworkTemplate; +import android.net.UnderlyingNetworkInfo; +import android.net.netstats.IUsageCallback; +import android.net.netstats.NetworkStatsDataMigrationUtils; +import android.net.netstats.provider.INetworkStatsProviderCallback; +import android.net.netstats.provider.NetworkStatsProvider; +import android.os.Build; +import android.os.Handler; +import android.os.RemoteException; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.NetworkIdentityUtils; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Provides access to network usage history and statistics. Usage data is collected in + * discrete bins of time called 'Buckets'. See {@link NetworkStats.Bucket} for details. + *

+ * Queries can define a time interval in the form of start and end timestamps (Long.MIN_VALUE and + * Long.MAX_VALUE can be used to simulate open ended intervals). By default, apps can only obtain + * data about themselves. See the below note for special cases in which apps can obtain data about + * other applications. + *

+ * Summary queries + *

+ * {@link #querySummaryForDevice}

+ * {@link #querySummaryForUser}

+ * {@link #querySummary}

+ * These queries aggregate network usage across the whole interval. Therefore there will be only one + * bucket for a particular key, state, metered and roaming combination. In case of the user-wide + * and device-wide summaries a single bucket containing the totalised network usage is returned. + *

+ * History queries + *

+ * {@link #queryDetailsForUid}

+ * {@link #queryDetails}

+ * These queries do not aggregate over time but do aggregate over state, metered and roaming. + * Therefore there can be multiple buckets for a particular key. However, all Buckets will have + * {@code state} {@link NetworkStats.Bucket#STATE_ALL}, + * {@code defaultNetwork} {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL}, + * {@code metered } {@link NetworkStats.Bucket#METERED_ALL}, + * {@code roaming} {@link NetworkStats.Bucket#ROAMING_ALL}. + *

+ * NOTE: Calling {@link #querySummaryForDevice} or accessing stats for apps other than the + * calling app requires the permission {@link android.Manifest.permission#PACKAGE_USAGE_STATS}, + * which is a system-level permission and will not be granted to third-party apps. However, + * declaring the permission implies intention to use the API and the user of the device can grant + * permission through the Settings application. + *

+ * Profile owner apps are automatically granted permission to query data on the profile they manage + * (that is, for any query except {@link #querySummaryForDevice}). Device owner apps and carrier- + * privileged apps likewise get access to usage data for all users on the device. + *

+ * In addition to tethering usage, usage by removed users and apps, and usage by the system + * is also included in the results for callers with one of these higher levels of access. + *

+ * NOTE: Prior to API level {@value android.os.Build.VERSION_CODES#N}, all calls to these APIs required + * the above permission, even to access an app's own data usage, and carrier-privileged apps were + * not included. + */ +@SystemService(Context.NETWORK_STATS_SERVICE) +public class NetworkStatsManager { + private static final String TAG = "NetworkStatsManager"; + private static final boolean DBG = false; + + /** @hide */ + public static final int CALLBACK_LIMIT_REACHED = 0; + /** @hide */ + public static final int CALLBACK_RELEASED = 1; + + /** + * Minimum data usage threshold for registering usage callbacks. + * + * Requests registered with a threshold lower than this will only be triggered once this minimum + * is reached. + * @hide + */ + public static final long MIN_THRESHOLD_BYTES = 2 * 1_048_576L; // 2MiB + + private final Context mContext; + private final INetworkStatsService mService; + + /** + * @deprecated Use {@link NetworkStatsDataMigrationUtils#PREFIX_XT} + * instead. + * @hide + */ + @Deprecated + public static final String PREFIX_DEV = "dev"; + + /** @hide */ + public static final int FLAG_POLL_ON_OPEN = 1 << 0; + /** @hide */ + public static final int FLAG_POLL_FORCE = 1 << 1; + /** @hide */ + public static final int FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN = 1 << 2; + + /** + * Virtual RAT type to represent 5G NSA (Non Stand Alone) mode, where the primary cell is + * still LTE and network allocates a secondary 5G cell so telephony reports RAT = LTE along + * with NR state as connected. This is a concept added by NetworkStats on top of the telephony + * constants for backward compatibility of metrics so this should not be overlapped with any of + * the {@code TelephonyManager.NETWORK_TYPE_*} constants. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int NETWORK_TYPE_5G_NSA = -2; + + private int mFlags; + + /** @hide */ + @VisibleForTesting + public NetworkStatsManager(Context context, INetworkStatsService service) { + mContext = context; + mService = service; + setPollOnOpen(true); + setAugmentWithSubscriptionPlan(true); + } + + /** @hide */ + public INetworkStatsService getBinder() { + return mService; + } + + /** + * Set poll on open flag to indicate the poll is needed before service gets statistics + * result. This is default enabled. However, for any non-privileged caller, the poll might + * be omitted in case of rate limiting. + * + * @param pollOnOpen true if poll is needed. + * @hide + */ + // The system will ignore any non-default values for non-privileged + // processes, so processes that don't hold the appropriate permissions + // can make no use of this API. + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public void setPollOnOpen(boolean pollOnOpen) { + if (pollOnOpen) { + mFlags |= FLAG_POLL_ON_OPEN; + } else { + mFlags &= ~FLAG_POLL_ON_OPEN; + } + } + + /** + * Set poll force flag to indicate that calling any subsequent query method will force a stats + * poll. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @SystemApi(client = MODULE_LIBRARIES) + public void setPollForce(boolean pollForce) { + if (pollForce) { + mFlags |= FLAG_POLL_FORCE; + } else { + mFlags &= ~FLAG_POLL_FORCE; + } + } + + /** @hide */ + public void setAugmentWithSubscriptionPlan(boolean augmentWithSubscriptionPlan) { + if (augmentWithSubscriptionPlan) { + mFlags |= FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN; + } else { + mFlags &= ~FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN; + } + } + + /** + * Query network usage statistics summaries. + * + * Result is summarised data usage for the whole + * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and + * roaming. This means the bucket's start and end timestamp will be the same as the + * 'startTime' and 'endTime' arguments. State is going to be + * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL}, + * tag {@link NetworkStats.Bucket#TAG_NONE}, + * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL}, + * metered {@link NetworkStats.Bucket#METERED_ALL}, + * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param template Template used to match networks. See {@link NetworkTemplate}. + * @param startTime Start of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @return Bucket Summarised data usage. + * + * @hide + */ + @NonNull + @WorkerThread + @SystemApi(client = MODULE_LIBRARIES) + public Bucket querySummaryForDevice(@NonNull NetworkTemplate template, + long startTime, long endTime) { + Objects.requireNonNull(template); + try { + NetworkStats stats = + new NetworkStats(mContext, template, mFlags, startTime, endTime, mService); + Bucket bucket = stats.getDeviceSummaryForNetwork(); + stats.close(); + return bucket; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return null; // To make the compiler happy. + } + + /** + * Query network usage statistics summaries. Result is summarised data usage for the whole + * device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and + * roaming. This means the bucket's start and end timestamp are going to be the same as the + * 'startTime' and 'endTime' parameters. State is going to be + * {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL}, + * tag {@link NetworkStats.Bucket#TAG_NONE}, + * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL}, + * metered {@link NetworkStats.Bucket#METERED_ALL}, + * and roaming {@link NetworkStats.Bucket#ROAMING_ALL}. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param networkType As defined in {@link ConnectivityManager}, e.g. + * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI} + * etc. + * @param subscriberId If applicable, the subscriber id of the network interface. + *

Starting with API level 29, the {@code subscriberId} is guarded by + * additional restrictions. Calling apps that do not meet the new + * requirements to access the {@code subscriberId} can provide a {@code + * null} value when querying for the mobile network type to receive usage + * for all mobile networks. For additional details see {@link + * TelephonyManager#getSubscriberId()}. + *

Starting with API level 31, calling apps can provide a + * {@code subscriberId} with wifi network type to receive usage for + * wifi networks which is under the given subscription if applicable. + * Otherwise, pass {@code null} when querying all wifi networks. + * @param startTime Start of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @return Bucket object or null if permissions are insufficient or error happened during + * statistics collection. + */ + @WorkerThread + public Bucket querySummaryForDevice(int networkType, String subscriberId, + long startTime, long endTime) throws SecurityException, RemoteException { + NetworkTemplate template; + try { + template = createTemplate(networkType, subscriberId); + } catch (IllegalArgumentException e) { + if (DBG) Log.e(TAG, "Cannot create template", e); + return null; + } + + return querySummaryForDevice(template, startTime, endTime); + } + + /** + * Query network usage statistics summaries. Result is summarised data usage for all uids + * belonging to calling user. Result is a single Bucket aggregated over time, state and uid. + * This means the bucket's start and end timestamp are going to be the same as the 'startTime' + * and 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL}, + * uid {@link NetworkStats.Bucket#UID_ALL}, tag {@link NetworkStats.Bucket#TAG_NONE}, + * metered {@link NetworkStats.Bucket#METERED_ALL}, and roaming + * {@link NetworkStats.Bucket#ROAMING_ALL}. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param networkType As defined in {@link ConnectivityManager}, e.g. + * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI} + * etc. + * @param subscriberId If applicable, the subscriber id of the network interface. + *

Starting with API level 29, the {@code subscriberId} is guarded by + * additional restrictions. Calling apps that do not meet the new + * requirements to access the {@code subscriberId} can provide a {@code + * null} value when querying for the mobile network type to receive usage + * for all mobile networks. For additional details see {@link + * TelephonyManager#getSubscriberId()}. + *

Starting with API level 31, calling apps can provide a + * {@code subscriberId} with wifi network type to receive usage for + * wifi networks which is under the given subscription if applicable. + * Otherwise, pass {@code null} when querying all wifi networks. + * @param startTime Start of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @return Bucket object or null if permissions are insufficient or error happened during + * statistics collection. + */ + @WorkerThread + public Bucket querySummaryForUser(int networkType, String subscriberId, long startTime, + long endTime) throws SecurityException, RemoteException { + NetworkTemplate template; + try { + template = createTemplate(networkType, subscriberId); + } catch (IllegalArgumentException e) { + if (DBG) Log.e(TAG, "Cannot create template", e); + return null; + } + + NetworkStats stats; + stats = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService); + stats.startSummaryEnumeration(); + + stats.close(); + return stats.getSummaryAggregate(); + } + + /** + * Query network usage statistics summaries. Result filtered to include only uids belonging to + * calling user. Result is aggregated over time, hence all buckets will have the same start and + * end timestamps. Not aggregated over state, uid, default network, metered, or roaming. This + * means buckets' start and end timestamps are going to be the same as the 'startTime' and + * 'endTime' parameters. State, uid, metered, and roaming are going to vary, and tag is going to + * be the same. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param networkType As defined in {@link ConnectivityManager}, e.g. + * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI} + * etc. + * @param subscriberId If applicable, the subscriber id of the network interface. + *

Starting with API level 29, the {@code subscriberId} is guarded by + * additional restrictions. Calling apps that do not meet the new + * requirements to access the {@code subscriberId} can provide a {@code + * null} value when querying for the mobile network type to receive usage + * for all mobile networks. For additional details see {@link + * TelephonyManager#getSubscriberId()}. + *

Starting with API level 31, calling apps can provide a + * {@code subscriberId} with wifi network type to receive usage for + * wifi networks which is under the given subscription if applicable. + * Otherwise, pass {@code null} when querying all wifi networks. + * @param startTime Start of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @return Statistics object or null if permissions are insufficient or error happened during + * statistics collection. + */ + @WorkerThread + public NetworkStats querySummary(int networkType, String subscriberId, long startTime, + long endTime) throws SecurityException, RemoteException { + NetworkTemplate template; + try { + template = createTemplate(networkType, subscriberId); + } catch (IllegalArgumentException e) { + if (DBG) Log.e(TAG, "Cannot create template", e); + return null; + } + + return querySummary(template, startTime, endTime); + } + + /** + * Query network usage statistics summaries. + * + * The results will only include traffic made by UIDs belonging to the calling user profile. + * The results are aggregated over time, so that all buckets will have the same start and + * end timestamps as the passed arguments. Not aggregated over state, uid, default network, + * metered, or roaming. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param template Template used to match networks. See {@link NetworkTemplate}. + * @param startTime Start of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @return Statistics which is described above. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + @WorkerThread + public NetworkStats querySummary(@NonNull NetworkTemplate template, long startTime, + long endTime) throws SecurityException { + Objects.requireNonNull(template); + try { + NetworkStats result = + new NetworkStats(mContext, template, mFlags, startTime, endTime, mService); + result.startSummaryEnumeration(); + return result; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return null; // To make the compiler happy. + } + + /** + * Query tagged network usage statistics summaries. + * + * The results will only include tagged traffic made by UIDs belonging to the calling user + * profile. The results are aggregated over time, so that all buckets will have the same + * start and end timestamps as the passed arguments. Not aggregated over state, uid, + * default network, metered, or roaming. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param template Template used to match networks. See {@link NetworkTemplate}. + * @param startTime Start of period, in milliseconds since the Unix epoch, see + * {@link System#currentTimeMillis}. + * @param endTime End of period, in milliseconds since the Unix epoch, see + * {@link System#currentTimeMillis}. + * @return Statistics which is described above. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + @WorkerThread + public NetworkStats queryTaggedSummary(@NonNull NetworkTemplate template, long startTime, + long endTime) throws SecurityException { + Objects.requireNonNull(template); + try { + NetworkStats result = + new NetworkStats(mContext, template, mFlags, startTime, endTime, mService); + result.startTaggedSummaryEnumeration(); + return result; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return null; // To make the compiler happy. + } + + /** + * Query usage statistics details for networks matching a given {@link NetworkTemplate}. + * + * Result is not aggregated over time. This means buckets' start and + * end timestamps will be between 'startTime' and 'endTime' parameters. + *

Only includes buckets whose entire time period is included between + * startTime and endTime. Doesn't interpolate or return partial buckets. + * Since bucket length is in the order of hours, this + * method cannot be used to measure data usage on a fine grained time scale. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param template Template used to match networks. See {@link NetworkTemplate}. + * @param startTime Start of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @return Statistics which is described above. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + @WorkerThread + public NetworkStats queryDetailsForDevice(@NonNull NetworkTemplate template, + long startTime, long endTime) { + Objects.requireNonNull(template); + try { + final NetworkStats result = + new NetworkStats(mContext, template, mFlags, startTime, endTime, mService); + result.startHistoryDeviceEnumeration(); + return result; + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + return null; // To make the compiler happy. + } + + /** + * Query network usage statistics details for a given uid. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int) + */ + @WorkerThread + public NetworkStats queryDetailsForUid(int networkType, String subscriberId, + long startTime, long endTime, int uid) throws SecurityException { + return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid, + NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL); + } + + /** @hide */ + public NetworkStats queryDetailsForUid(NetworkTemplate template, + long startTime, long endTime, int uid) throws SecurityException { + return queryDetailsForUidTagState(template, startTime, endTime, uid, + NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL); + } + + /** + * Query network usage statistics details for a given uid and tag. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int) + */ + @WorkerThread + public NetworkStats queryDetailsForUidTag(int networkType, String subscriberId, + long startTime, long endTime, int uid, int tag) throws SecurityException { + return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid, + tag, NetworkStats.Bucket.STATE_ALL); + } + + /** + * Query network usage statistics details for a given uid, tag, and state. Only usable for uids + * belonging to calling user. Result is not aggregated over time. This means buckets' start and + * end timestamps are going to be between 'startTime' and 'endTime' parameters. The uid is going + * to be the same as the 'uid' parameter, the tag the same as the 'tag' parameter, and the state + * the same as the 'state' parameter. + * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL}, + * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and + * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}. + *

Only includes buckets that atomically occur in the inclusive time range. Doesn't + * interpolate across partial buckets. Since bucket length is in the order of hours, this + * method cannot be used to measure data usage on a fine grained time scale. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param networkType As defined in {@link ConnectivityManager}, e.g. + * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI} + * etc. + * @param subscriberId If applicable, the subscriber id of the network interface. + *

Starting with API level 29, the {@code subscriberId} is guarded by + * additional restrictions. Calling apps that do not meet the new + * requirements to access the {@code subscriberId} can provide a {@code + * null} value when querying for the mobile network type to receive usage + * for all mobile networks. For additional details see {@link + * TelephonyManager#getSubscriberId()}. + *

Starting with API level 31, calling apps can provide a + * {@code subscriberId} with wifi network type to receive usage for + * wifi networks which is under the given subscription if applicable. + * Otherwise, pass {@code null} when querying all wifi networks. + * @param startTime Start of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @param uid UID of app + * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data + * across all the tags. + * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate + * traffic from all states. + * @return Statistics object or null if an error happened during statistics collection. + * @throws SecurityException if permissions are insufficient to read network statistics. + */ + @WorkerThread + public NetworkStats queryDetailsForUidTagState(int networkType, String subscriberId, + long startTime, long endTime, int uid, int tag, int state) throws SecurityException { + NetworkTemplate template; + template = createTemplate(networkType, subscriberId); + + return queryDetailsForUidTagState(template, startTime, endTime, uid, tag, state); + } + + /** + * Query network usage statistics details for a given template, uid, tag, and state. + * + * Only usable for uids belonging to calling user. Result is not aggregated over time. + * This means buckets' start and end timestamps are going to be between 'startTime' and + * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag + * the same as the 'tag' parameter, and the state the same as the 'state' parameter. + * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL}, + * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and + * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}. + *

Only includes buckets that atomically occur in the inclusive time range. Doesn't + * interpolate across partial buckets. Since bucket length is in the order of hours, this + * method cannot be used to measure data usage on a fine grained time scale. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param template Template used to match networks. See {@link NetworkTemplate}. + * @param startTime Start of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period, in milliseconds since the Unix epoch, see + * {@link java.lang.System#currentTimeMillis}. + * @param uid UID of app + * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data + * across all the tags. + * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate + * traffic from all states. + * @return Statistics which is described above. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + @WorkerThread + public NetworkStats queryDetailsForUidTagState(@NonNull NetworkTemplate template, + long startTime, long endTime, int uid, int tag, int state) throws SecurityException { + Objects.requireNonNull(template); + try { + final NetworkStats result = new NetworkStats( + mContext, template, mFlags, startTime, endTime, mService); + result.startHistoryUidEnumeration(uid, tag, state); + return result; + } catch (RemoteException e) { + Log.e(TAG, "Error while querying stats for uid=" + uid + " tag=" + tag + + " state=" + state, e); + e.rethrowFromSystemServer(); + } + + return null; // To make the compiler happy. + } + + /** + * Query network usage statistics details. Result filtered to include only uids belonging to + * calling user. Result is aggregated over state but not aggregated over time, uid, tag, + * metered, nor roaming. This means buckets' start and end timestamps are going to be between + * 'startTime' and 'endTime' parameters. State is going to be + * {@link NetworkStats.Bucket#STATE_ALL}, uid will vary, + * tag {@link NetworkStats.Bucket#TAG_NONE}, + * default network is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL}, + * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, + * and roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}. + *

Only includes buckets that atomically occur in the inclusive time range. Doesn't + * interpolate across partial buckets. Since bucket length is in the order of hours, this + * method cannot be used to measure data usage on a fine grained time scale. + * This may take a long time, and apps should avoid calling this on their main thread. + * + * @param networkType As defined in {@link ConnectivityManager}, e.g. + * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI} + * etc. + * @param subscriberId If applicable, the subscriber id of the network interface. + *

Starting with API level 29, the {@code subscriberId} is guarded by + * additional restrictions. Calling apps that do not meet the new + * requirements to access the {@code subscriberId} can provide a {@code + * null} value when querying for the mobile network type to receive usage + * for all mobile networks. For additional details see {@link + * TelephonyManager#getSubscriberId()}. + *

Starting with API level 31, calling apps can provide a + * {@code subscriberId} with wifi network type to receive usage for + * wifi networks which is under the given subscription if applicable. + * Otherwise, pass {@code null} when querying all wifi networks. + * @param startTime Start of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @param endTime End of period. Defined in terms of "Unix time", see + * {@link java.lang.System#currentTimeMillis}. + * @return Statistics object or null if permissions are insufficient or error happened during + * statistics collection. + */ + @WorkerThread + public NetworkStats queryDetails(int networkType, String subscriberId, long startTime, + long endTime) throws SecurityException, RemoteException { + NetworkTemplate template; + try { + template = createTemplate(networkType, subscriberId); + } catch (IllegalArgumentException e) { + if (DBG) Log.e(TAG, "Cannot create template", e); + return null; + } + + NetworkStats result; + result = new NetworkStats(mContext, template, mFlags, startTime, endTime, mService); + result.startUserUidEnumeration(); + return result; + } + + /** + * Query realtime mobile network usage statistics. + * + * Return a snapshot of current UID network statistics, as it applies + * to the mobile radios of the device. The snapshot will include any + * tethering traffic, video calling data usage and count of + * network operations set by {@link TrafficStats#incrementOperationCount} + * made over a mobile radio. + * The snapshot will not include any statistics that cannot be seen by + * the kernel, e.g. statistics reported by {@link NetworkStatsProvider}s. + * + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + @NonNull public android.net.NetworkStats getMobileUidStats() { + try { + return mService.getUidStatsForTransport(TRANSPORT_CELLULAR); + } catch (RemoteException e) { + if (DBG) Log.d(TAG, "Remote exception when get Mobile uid stats"); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Query realtime Wi-Fi network usage statistics. + * + * Return a snapshot of current UID network statistics, as it applies + * to the Wi-Fi radios of the device. The snapshot will include any + * tethering traffic, video calling data usage and count of + * network operations set by {@link TrafficStats#incrementOperationCount} + * made over a Wi-Fi radio. + * The snapshot will not include any statistics that cannot be seen by + * the kernel, e.g. statistics reported by {@link NetworkStatsProvider}s. + * + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + @NonNull public android.net.NetworkStats getWifiUidStats() { + try { + return mService.getUidStatsForTransport(TRANSPORT_WIFI); + } catch (RemoteException e) { + if (DBG) Log.d(TAG, "Remote exception when get WiFi uid stats"); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers to receive notifications about data usage on specified networks. + * + *

The callbacks will continue to be called as long as the process is alive or + * {@link #unregisterUsageCallback} is called. + * + * @param template Template used to match networks. See {@link NetworkTemplate}. + * @param thresholdBytes Threshold in bytes to be notified on. Provided values lower than 2MiB + * will be clamped for callers except callers with the NETWORK_STACK + * permission. + * @param executor The executor on which callback will be invoked. The provided {@link Executor} + * must run callback sequentially, otherwise the order of callbacks cannot be + * guaranteed. + * @param callback The {@link UsageCallback} that the system will call when data usage + * has exceeded the specified threshold. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}, conditional = true) + public void registerUsageCallback(@NonNull NetworkTemplate template, long thresholdBytes, + @NonNull @CallbackExecutor Executor executor, @NonNull UsageCallback callback) { + Objects.requireNonNull(template, "NetworkTemplate cannot be null"); + Objects.requireNonNull(callback, "UsageCallback cannot be null"); + Objects.requireNonNull(executor, "Executor cannot be null"); + + final DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET, + template, thresholdBytes); + try { + final UsageCallbackWrapper callbackWrapper = + new UsageCallbackWrapper(executor, callback); + callback.request = mService.registerUsageCallback( + mContext.getOpPackageName(), request, callbackWrapper); + if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request); + + if (callback.request == null) { + Log.e(TAG, "Request from callback is null; should not happen"); + } + } catch (RemoteException e) { + if (DBG) Log.d(TAG, "Remote exception when registering callback"); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers to receive notifications about data usage on specified networks. + * + * @see #registerUsageCallback(int, String, long, UsageCallback, Handler) + */ + public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes, + UsageCallback callback) { + registerUsageCallback(networkType, subscriberId, thresholdBytes, callback, + null /* handler */); + } + + /** + * Registers to receive notifications about data usage on specified networks. + * + *

The callbacks will continue to be called as long as the process is live or + * {@link #unregisterUsageCallback} is called. + * + * @param networkType Type of network to monitor. Either + {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}. + * @param subscriberId If applicable, the subscriber id of the network interface. + *

Starting with API level 29, the {@code subscriberId} is guarded by + * additional restrictions. Calling apps that do not meet the new + * requirements to access the {@code subscriberId} can provide a {@code + * null} value when registering for the mobile network type to receive + * notifications for all mobile networks. For additional details see {@link + * TelephonyManager#getSubscriberId()}. + *

Starting with API level 31, calling apps can provide a + * {@code subscriberId} with wifi network type to receive usage for + * wifi networks which is under the given subscription if applicable. + * Otherwise, pass {@code null} when querying all wifi networks. + * @param thresholdBytes Threshold in bytes to be notified on. + * @param callback The {@link UsageCallback} that the system will call when data usage + * has exceeded the specified threshold. + * @param handler to dispatch callback events through, otherwise if {@code null} it uses + * the calling thread. + */ + public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes, + UsageCallback callback, @Nullable Handler handler) { + NetworkTemplate template = createTemplate(networkType, subscriberId); + if (DBG) { + Log.d(TAG, "registerUsageCallback called with: {" + + " networkType=" + networkType + + " subscriberId=" + subscriberId + + " thresholdBytes=" + thresholdBytes + + " }"); + } + + final Executor executor = handler == null ? r -> r.run() : r -> handler.post(r); + + registerUsageCallback(template, thresholdBytes, executor, callback); + } + + /** + * Unregisters callbacks on data usage. + * + * @param callback The {@link UsageCallback} used when registering. + */ + public void unregisterUsageCallback(UsageCallback callback) { + if (callback == null || callback.request == null + || callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) { + throw new IllegalArgumentException("Invalid UsageCallback"); + } + try { + mService.unregisterUsageRequest(callback.request); + } catch (RemoteException e) { + if (DBG) Log.d(TAG, "Remote exception when unregistering callback"); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Base class for usage callbacks. Should be extended by applications wanting notifications. + */ + public static abstract class UsageCallback { + /** + * Called when data usage has reached the given threshold. + * + * Called by {@code NetworkStatsService} when the registered threshold is reached. + * If a caller implements {@link #onThresholdReached(NetworkTemplate)}, the system + * will not call {@link #onThresholdReached(int, String)}. + * + * @param template The {@link NetworkTemplate} that associated with this callback. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public void onThresholdReached(@NonNull NetworkTemplate template) { + // Backward compatibility for those who didn't override this function. + final int networkType = networkTypeForTemplate(template); + if (networkType != ConnectivityManager.TYPE_NONE) { + final String subscriberId = template.getSubscriberIds().isEmpty() ? null + : template.getSubscriberIds().iterator().next(); + onThresholdReached(networkType, subscriberId); + } + } + + /** + * Called when data usage has reached the given threshold. + */ + public abstract void onThresholdReached(int networkType, String subscriberId); + + /** + * @hide used for internal bookkeeping + */ + private DataUsageRequest request; + + /** + * Get network type from a template if feasible. + * + * @param template the target {@link NetworkTemplate}. + * @return legacy network type, only supports for the types which is already supported in + * {@link #registerUsageCallback(int, String, long, UsageCallback, Handler)}. + * {@link ConnectivityManager#TYPE_NONE} for other types. + */ + private static int networkTypeForTemplate(@NonNull NetworkTemplate template) { + switch (template.getMatchRule()) { + case NetworkTemplate.MATCH_MOBILE: + return ConnectivityManager.TYPE_MOBILE; + case NetworkTemplate.MATCH_WIFI: + return ConnectivityManager.TYPE_WIFI; + default: + return ConnectivityManager.TYPE_NONE; + } + } + } + + /** + * Registers a custom provider of {@link android.net.NetworkStats} to provide network statistics + * to the system. To unregister, invoke {@link #unregisterNetworkStatsProvider}. + * Note that no de-duplication of statistics between providers is performed, so each provider + * must only report network traffic that is not being reported by any other provider. Also note + * that the provider cannot be re-registered after unregistering. + * + * @param tag a human readable identifier of the custom network stats provider. This is only + * used for debugging. + * @param provider the subclass of {@link NetworkStatsProvider} that needs to be + * registered to the system. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_STATS_PROVIDER, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) + @NonNull public void registerNetworkStatsProvider( + @NonNull String tag, + @NonNull NetworkStatsProvider provider) { + try { + if (provider.getProviderCallbackBinder() != null) { + throw new IllegalArgumentException("provider is already registered"); + } + final INetworkStatsProviderCallback cbBinder = + mService.registerNetworkStatsProvider(tag, provider.getProviderBinder()); + provider.setProviderCallbackBinder(cbBinder); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Unregisters an instance of {@link NetworkStatsProvider}. + * + * @param provider the subclass of {@link NetworkStatsProvider} that needs to be + * unregistered to the system. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_STATS_PROVIDER, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) + @NonNull public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) { + try { + provider.getProviderCallbackBinderOrThrow().unregister(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + private static NetworkTemplate createTemplate(int networkType, String subscriberId) { + final NetworkTemplate template; + switch (networkType) { + case ConnectivityManager.TYPE_MOBILE: + template = subscriberId == null + ? NetworkTemplate.buildTemplateMobileWildcard() + : NetworkTemplate.buildTemplateMobileAll(subscriberId); + break; + case ConnectivityManager.TYPE_WIFI: + template = TextUtils.isEmpty(subscriberId) + ? NetworkTemplate.buildTemplateWifiWildcard() + : NetworkTemplate.buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL, + subscriberId); + break; + default: + throw new IllegalArgumentException("Cannot create template for network type " + + networkType + ", subscriberId '" + + NetworkIdentityUtils.scrubSubscriberId(subscriberId) + "'."); + } + return template; + } + + /** + * Notify {@code NetworkStatsService} about network status changed. + * + * Notifies NetworkStatsService of network state changes for data usage accounting purposes. + * + * To avoid races that attribute data usage to wrong network, such as new network with + * the same interface after SIM hot-swap, this function will not return until + * {@code NetworkStatsService} finishes its work of retrieving traffic statistics from + * all data sources. + * + * @param defaultNetworks the list of all networks that could be used by network traffic that + * does not explicitly select a network. + * @param networkStateSnapshots a list of {@link NetworkStateSnapshot}s, one for + * each network that is currently connected. + * @param activeIface the active (i.e., connected) default network interface for the calling + * uid. Used to determine on which network future calls to + * {@link android.net.TrafficStats#incrementOperationCount} applies to. + * @param underlyingNetworkInfos the list of underlying network information for all + * currently-connected VPNs. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public void notifyNetworkStatus( + @NonNull List defaultNetworks, + @NonNull List networkStateSnapshots, + @Nullable String activeIface, + @NonNull List underlyingNetworkInfos) { + try { + Objects.requireNonNull(defaultNetworks); + Objects.requireNonNull(networkStateSnapshots); + Objects.requireNonNull(underlyingNetworkInfos); + mService.notifyNetworkStatus(defaultNetworks.toArray(new Network[0]), + networkStateSnapshots.toArray(new NetworkStateSnapshot[0]), activeIface, + underlyingNetworkInfos.toArray(new UnderlyingNetworkInfo[0])); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static class UsageCallbackWrapper extends IUsageCallback.Stub { + // Null if unregistered. + private volatile UsageCallback mCallback; + + private final Executor mExecutor; + + UsageCallbackWrapper(@NonNull Executor executor, @NonNull UsageCallback callback) { + mCallback = callback; + mExecutor = executor; + } + + @Override + public void onThresholdReached(DataUsageRequest request) { + // Copy it to a local variable in case mCallback changed inside the if condition. + final UsageCallback callback = mCallback; + if (callback != null) { + mExecutor.execute(() -> callback.onThresholdReached(request.template)); + } else { + Log.e(TAG, "onThresholdReached with released callback for " + request); + } + } + + @Override + public void onCallbackReleased(DataUsageRequest request) { + if (DBG) Log.d(TAG, "callback released for " + request); + mCallback = null; + } + } + + /** + * Mark given UID as being in foreground for stats purposes. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public void noteUidForeground(int uid, boolean uidForeground) { + try { + mService.noteUidForeground(uid, uidForeground); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set default value of global alert bytes, the value will be clamped to [128kB, 2MB]. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + Manifest.permission.NETWORK_STACK}) + public void setDefaultGlobalAlert(long alertBytes) { + try { + // TODO: Sync internal naming with the API surface. + mService.advisePersistThreshold(alertBytes); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Force update of statistics. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public void forceUpdate() { + try { + mService.forceUpdate(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the warning and limit to all registered custom network stats providers. + * Note that invocation of any interface will be sent to all providers. + * + * Asynchronicity notes : because traffic may be happening on the device at the same time, it + * doesn't make sense to wait for the warning and limit to be set – a caller still wouldn't + * know when exactly it was effective. All that can matter is that it's done quickly. Also, + * this method can't fail, so there is no status to return. All providers will see the new + * values soon. + * As such, this method returns immediately and sends the warning and limit to all providers + * as soon as possible through a one-way binder call. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public void setStatsProviderWarningAndLimitAsync(@NonNull String iface, long warning, + long limit) { + try { + mService.setStatsProviderWarningAndLimitAsync(iface, warning, limit); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get a RAT type representative of a group of RAT types for network statistics. + * + * Collapse the given Radio Access Technology (RAT) type into a bucket that + * is representative of the original RAT type for network statistics. The + * mapping mostly corresponds to {@code TelephonyManager#NETWORK_CLASS_BIT_MASK_*} + * but with adaptations specific to the virtual types introduced by + * networks stats. + * + * @param ratType An integer defined in {@code TelephonyManager#NETWORK_TYPE_*}. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static int getCollapsedRatType(int ratType) { + switch (ratType) { + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_GSM: + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_IDEN: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_1xRTT: + return TelephonyManager.NETWORK_TYPE_GSM; + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_EVDO_B: + case TelephonyManager.NETWORK_TYPE_EHRPD: + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_HSPAP: + case TelephonyManager.NETWORK_TYPE_TD_SCDMA: + return TelephonyManager.NETWORK_TYPE_UMTS; + case TelephonyManager.NETWORK_TYPE_LTE: + case TelephonyManager.NETWORK_TYPE_IWLAN: + return TelephonyManager.NETWORK_TYPE_LTE; + case TelephonyManager.NETWORK_TYPE_NR: + return TelephonyManager.NETWORK_TYPE_NR; + // Virtual RAT type for 5G NSA mode, see + // {@link NetworkStatsManager#NETWORK_TYPE_5G_NSA}. + case NetworkStatsManager.NETWORK_TYPE_5G_NSA: + return NetworkStatsManager.NETWORK_TYPE_5G_NSA; + default: + return TelephonyManager.NETWORK_TYPE_UNKNOWN; + } + } +} diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java new file mode 100644 index 0000000000..61b34d0bcf --- /dev/null +++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.net.nsd.INsdManager; +import android.net.nsd.NsdManager; + +/** + * Class for performing registration for Connectivity services which are exposed via updatable APIs + * since Android T. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class ConnectivityFrameworkInitializerTiramisu { + private ConnectivityFrameworkInitializerTiramisu() {} + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers NetworkStats, nsd, + * ipsec and ethernet services to {@link Context}, so that {@link Context#getSystemService} can + * return them. + * + * @throws IllegalStateException if this is called anywhere besides + * {@link SystemServiceRegistry}. + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService( + Context.NSD_SERVICE, + NsdManager.class, + (context, serviceBinder) -> { + INsdManager service = INsdManager.Stub.asInterface(serviceBinder); + return new NsdManager(context, service); + } + ); + + SystemServiceRegistry.registerContextAwareService( + Context.IPSEC_SERVICE, + IpSecManager.class, + (context, serviceBinder) -> { + IIpSecService service = IIpSecService.Stub.asInterface(serviceBinder); + return new IpSecManager(context, service); + } + ); + + SystemServiceRegistry.registerContextAwareService( + Context.NETWORK_STATS_SERVICE, + NetworkStatsManager.class, + (context, serviceBinder) -> { + INetworkStatsService service = + INetworkStatsService.Stub.asInterface(serviceBinder); + return new NetworkStatsManager(context, service); + } + ); + + SystemServiceRegistry.registerContextAwareService( + Context.ETHERNET_SERVICE, + EthernetManager.class, + (context, serviceBinder) -> { + IEthernetManager service = IEthernetManager.Stub.asInterface(serviceBinder); + return new EthernetManager(context, service); + } + ); + } +} diff --git a/framework-t/src/android/net/DataUsageRequest.aidl b/framework-t/src/android/net/DataUsageRequest.aidl new file mode 100644 index 0000000000..d1937c7b8c --- /dev/null +++ b/framework-t/src/android/net/DataUsageRequest.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable DataUsageRequest; diff --git a/framework-t/src/android/net/DataUsageRequest.java b/framework-t/src/android/net/DataUsageRequest.java new file mode 100644 index 0000000000..b06d515b3a --- /dev/null +++ b/framework-t/src/android/net/DataUsageRequest.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package android.net; + +import android.annotation.Nullable; +import android.net.NetworkTemplate; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Defines a request to register a callbacks. Used to be notified on data usage via + * {@link android.app.usage.NetworkStatsManager#registerDataUsageCallback}. + * If no {@code uid}s are set, callbacks are restricted to device-owners, + * carrier-privileged apps, or system apps. + * + * @hide + */ +public final class DataUsageRequest implements Parcelable { + + public static final String PARCELABLE_KEY = "DataUsageRequest"; + public static final int REQUEST_ID_UNSET = 0; + + /** + * Identifies the request. {@link DataUsageRequest}s should only be constructed by + * the Framework and it is used internally to identify the request. + */ + public final int requestId; + + /** + * {@link NetworkTemplate} describing the network to monitor. + */ + public final NetworkTemplate template; + + /** + * Threshold in bytes to be notified on. + */ + public final long thresholdInBytes; + + public DataUsageRequest(int requestId, NetworkTemplate template, long thresholdInBytes) { + this.requestId = requestId; + this.template = template; + this.thresholdInBytes = thresholdInBytes; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(requestId); + dest.writeParcelable(template, flags); + dest.writeLong(thresholdInBytes); + } + + public static final @android.annotation.NonNull Creator CREATOR = + new Creator() { + @Override + public DataUsageRequest createFromParcel(Parcel in) { + int requestId = in.readInt(); + NetworkTemplate template = in.readParcelable(null); + long thresholdInBytes = in.readLong(); + DataUsageRequest result = new DataUsageRequest(requestId, template, + thresholdInBytes); + return result; + } + + @Override + public DataUsageRequest[] newArray(int size) { + return new DataUsageRequest[size]; + } + }; + + @Override + public String toString() { + return "DataUsageRequest [ requestId=" + requestId + + ", networkTemplate=" + template + + ", thresholdInBytes=" + thresholdInBytes + " ]"; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof DataUsageRequest == false) return false; + DataUsageRequest that = (DataUsageRequest) obj; + return that.requestId == this.requestId + && Objects.equals(that.template, this.template) + && that.thresholdInBytes == this.thresholdInBytes; + } + + @Override + public int hashCode() { + return Objects.hash(requestId, template, thresholdInBytes); + } + +} diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java new file mode 100644 index 0000000000..e02ea897db --- /dev/null +++ b/framework-t/src/android/net/EthernetManager.java @@ -0,0 +1,729 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresFeature; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.OutcomeReceiver; +import android.os.RemoteException; + +import com.android.internal.annotations.GuardedBy; +import com.android.modules.utils.BackgroundThread; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.IntConsumer; + +/** + * A class that manages and configures Ethernet interfaces. + * + * @hide + */ +@SystemApi +@SystemService(Context.ETHERNET_SERVICE) +public class EthernetManager { + private static final String TAG = "EthernetManager"; + + private final IEthernetManager mService; + @GuardedBy("mListenerLock") + private final ArrayList> mIfaceListeners = + new ArrayList<>(); + @GuardedBy("mListenerLock") + private final ArrayList> mEthernetStateListeners = + new ArrayList<>(); + final Object mListenerLock = new Object(); + private final IEthernetServiceListener.Stub mServiceListener = + new IEthernetServiceListener.Stub() { + @Override + public void onEthernetStateChanged(int state) { + synchronized (mListenerLock) { + for (ListenerInfo li : mEthernetStateListeners) { + li.executor.execute(() -> { + li.listener.accept(state); + }); + } + } + } + + @Override + public void onInterfaceStateChanged(String iface, int state, int role, + IpConfiguration configuration) { + synchronized (mListenerLock) { + for (ListenerInfo li : mIfaceListeners) { + li.executor.execute(() -> + li.listener.onInterfaceStateChanged(iface, state, role, + configuration)); + } + } + } + }; + + /** + * Indicates that Ethernet is disabled. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int ETHERNET_STATE_DISABLED = 0; + + /** + * Indicates that Ethernet is enabled. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int ETHERNET_STATE_ENABLED = 1; + + private static class ListenerInfo { + @NonNull + public final Executor executor; + @NonNull + public final T listener; + + private ListenerInfo(@NonNull Executor executor, @NonNull T listener) { + this.executor = executor; + this.listener = listener; + } + } + + /** + * The interface is absent. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int STATE_ABSENT = 0; + + /** + * The interface is present but link is down. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int STATE_LINK_DOWN = 1; + + /** + * The interface is present and link is up. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int STATE_LINK_UP = 2; + + /** @hide */ + @IntDef(prefix = "STATE_", value = {STATE_ABSENT, STATE_LINK_DOWN, STATE_LINK_UP}) + @Retention(RetentionPolicy.SOURCE) + public @interface InterfaceState {} + + /** + * The interface currently does not have any specific role. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int ROLE_NONE = 0; + + /** + * The interface is in client mode (e.g., connected to the Internet). + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int ROLE_CLIENT = 1; + + /** + * Ethernet interface is in server mode (e.g., providing Internet access to tethered devices). + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int ROLE_SERVER = 2; + + /** @hide */ + @IntDef(prefix = "ROLE_", value = {ROLE_NONE, ROLE_CLIENT, ROLE_SERVER}) + @Retention(RetentionPolicy.SOURCE) + public @interface Role {} + + /** + * A listener that receives notifications about the state of Ethernet interfaces on the system. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public interface InterfaceStateListener { + /** + * Called when an Ethernet interface changes state. + * + * @param iface the name of the interface. + * @param state the current state of the interface, or {@link #STATE_ABSENT} if the + * interface was removed. + * @param role whether the interface is in client mode or server mode. + * @param configuration the current IP configuration of the interface. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + void onInterfaceStateChanged(@NonNull String iface, @InterfaceState int state, + @Role int role, @Nullable IpConfiguration configuration); + } + + /** + * A listener interface to receive notification on changes in Ethernet. + * This has never been a supported API. Use {@link InterfaceStateListener} instead. + * @hide + */ + public interface Listener extends InterfaceStateListener { + /** + * Called when Ethernet port's availability is changed. + * @param iface Ethernet interface name + * @param isAvailable {@code true} if Ethernet port exists. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + void onAvailabilityChanged(String iface, boolean isAvailable); + + /** Default implementation for backwards compatibility. Only calls the legacy listener. */ + default void onInterfaceStateChanged(@NonNull String iface, @InterfaceState int state, + @Role int role, @Nullable IpConfiguration configuration) { + onAvailabilityChanged(iface, (state >= STATE_LINK_UP)); + } + + } + + /** + * Create a new EthernetManager instance. + * Applications will almost always want to use + * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve + * the standard {@link android.content.Context#ETHERNET_SERVICE Context.ETHERNET_SERVICE}. + * @hide + */ + public EthernetManager(Context context, IEthernetManager service) { + mService = service; + } + + /** + * Get Ethernet configuration. + * @return the Ethernet Configuration, contained in {@link IpConfiguration}. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public IpConfiguration getConfiguration(String iface) { + try { + return mService.getConfiguration(iface); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set Ethernet configuration. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void setConfiguration(@NonNull String iface, @NonNull IpConfiguration config) { + try { + mService.setConfiguration(iface, config); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Indicates whether the system currently has one or more Ethernet interfaces. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public boolean isAvailable() { + return getAvailableInterfaces().length > 0; + } + + /** + * Indicates whether the system has given interface. + * + * @param iface Ethernet interface name + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public boolean isAvailable(String iface) { + try { + return mService.isAvailable(iface); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adds a listener. + * This has never been a supported API. Use {@link #addInterfaceStateListener} instead. + * + * @param listener A {@link Listener} to add. + * @throws IllegalArgumentException If the listener is null. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void addListener(@NonNull Listener listener) { + addListener(listener, BackgroundThread.getExecutor()); + } + + /** + * Adds a listener. + * This has never been a supported API. Use {@link #addInterfaceStateListener} instead. + * + * @param listener A {@link Listener} to add. + * @param executor Executor to run callbacks on. + * @throws IllegalArgumentException If the listener or executor is null. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void addListener(@NonNull Listener listener, @NonNull Executor executor) { + addInterfaceStateListener(executor, listener); + } + + /** + * Listen to changes in the state of Ethernet interfaces. + * + * Adds a listener to receive notification for any state change of all existing Ethernet + * interfaces. + *

{@link Listener#onInterfaceStateChanged} will be triggered immediately for all + * existing interfaces upon adding a listener. The same method will be called on the + * listener every time any of the interface changes state. In particular, if an + * interface is removed, it will be called with state {@link #STATE_ABSENT}. + *

Use {@link #removeInterfaceStateListener} with the same object to stop listening. + * + * @param executor Executor to run callbacks on. + * @param listener A {@link Listener} to add. + * @hide + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @SystemApi(client = MODULE_LIBRARIES) + public void addInterfaceStateListener(@NonNull Executor executor, + @NonNull InterfaceStateListener listener) { + if (listener == null || executor == null) { + throw new NullPointerException("listener and executor must not be null"); + } + synchronized (mListenerLock) { + maybeAddServiceListener(); + mIfaceListeners.add(new ListenerInfo(executor, listener)); + } + } + + @GuardedBy("mListenerLock") + private void maybeAddServiceListener() { + if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return; + + try { + mService.addListener(mServiceListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + } + + /** + * Returns an array of available Ethernet interface names. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public String[] getAvailableInterfaces() { + try { + return mService.getAvailableInterfaces(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + /** + * Removes a listener. + * + * @param listener A {@link Listener} to remove. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public void removeInterfaceStateListener(@NonNull InterfaceStateListener listener) { + Objects.requireNonNull(listener); + synchronized (mListenerLock) { + mIfaceListeners.removeIf(l -> l.listener == listener); + maybeRemoveServiceListener(); + } + } + + @GuardedBy("mListenerLock") + private void maybeRemoveServiceListener() { + if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return; + + try { + mService.removeListener(mServiceListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a listener. + * This has never been a supported API. Use {@link #removeInterfaceStateListener} instead. + * @param listener A {@link Listener} to remove. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void removeListener(@NonNull Listener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + removeInterfaceStateListener(listener); + } + + /** + * Whether to treat interfaces created by {@link TestNetworkManager#createTapInterface} + * as Ethernet interfaces. The effects of this method apply to any test interfaces that are + * already present on the system. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public void setIncludeTestInterfaces(boolean include) { + try { + mService.setIncludeTestInterfaces(include); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * A request for a tethered interface. + */ + public static class TetheredInterfaceRequest { + private final IEthernetManager mService; + private final ITetheredInterfaceCallback mCb; + + private TetheredInterfaceRequest(@NonNull IEthernetManager service, + @NonNull ITetheredInterfaceCallback cb) { + this.mService = service; + this.mCb = cb; + } + + /** + * Release the request, causing the interface to revert back from tethering mode if there + * is no other requestor. + */ + public void release() { + try { + mService.releaseTetheredInterface(mCb); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + } + + /** + * Callback for {@link #requestTetheredInterface(TetheredInterfaceCallback)}. + */ + public interface TetheredInterfaceCallback { + /** + * Called when the tethered interface is available. + * @param iface The name of the interface. + */ + void onAvailable(@NonNull String iface); + + /** + * Called when the tethered interface is now unavailable. + */ + void onUnavailable(); + } + + /** + * Request a tethered interface in tethering mode. + * + *

When this method is called and there is at least one ethernet interface available, the + * system will designate one to act as a tethered interface. If there is already a tethered + * interface, the existing interface will be used. + * @param callback A callback to be called once the request has been fulfilled. + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_STACK, + android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + @NonNull + public TetheredInterfaceRequest requestTetheredInterface(@NonNull final Executor executor, + @NonNull final TetheredInterfaceCallback callback) { + Objects.requireNonNull(callback, "Callback must be non-null"); + Objects.requireNonNull(executor, "Executor must be non-null"); + final ITetheredInterfaceCallback cbInternal = new ITetheredInterfaceCallback.Stub() { + @Override + public void onAvailable(String iface) { + executor.execute(() -> callback.onAvailable(iface)); + } + + @Override + public void onUnavailable() { + executor.execute(() -> callback.onUnavailable()); + } + }; + + try { + mService.requestTetheredInterface(cbInternal); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return new TetheredInterfaceRequest(mService, cbInternal); + } + + private static final class NetworkInterfaceOutcomeReceiver + extends INetworkInterfaceOutcomeReceiver.Stub { + @NonNull + private final Executor mExecutor; + @NonNull + private final OutcomeReceiver mCallback; + + NetworkInterfaceOutcomeReceiver( + @NonNull final Executor executor, + @NonNull final OutcomeReceiver + callback) { + Objects.requireNonNull(executor, "Pass a non-null executor"); + Objects.requireNonNull(callback, "Pass a non-null callback"); + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResult(@NonNull String iface) { + mExecutor.execute(() -> mCallback.onResult(iface)); + } + + @Override + public void onError(@NonNull EthernetNetworkManagementException e) { + mExecutor.execute(() -> mCallback.onError(e)); + } + } + + private NetworkInterfaceOutcomeReceiver makeNetworkInterfaceOutcomeReceiver( + @Nullable final Executor executor, + @Nullable final OutcomeReceiver callback) { + if (null != callback) { + Objects.requireNonNull(executor, "Pass a non-null executor, or a null callback"); + } + final NetworkInterfaceOutcomeReceiver proxy; + if (null == callback) { + proxy = null; + } else { + proxy = new NetworkInterfaceOutcomeReceiver(executor, callback); + } + return proxy; + } + + /** + * Updates the configuration of an automotive device's ethernet network. + * + * The {@link EthernetNetworkUpdateRequest} {@code request} argument describes how to update the + * configuration for this network. + * Use {@link StaticIpConfiguration.Builder} to build a {@code StaticIpConfiguration} object for + * this network to put inside the {@code request}. + * Similarly, use {@link NetworkCapabilities.Builder} to build a {@code NetworkCapabilities} + * object for this network to put inside the {@code request}. + * + * This function accepts an {@link OutcomeReceiver} that is called once the operation has + * finished execution. + * + * @param iface the name of the interface to act upon. + * @param request the {@link EthernetNetworkUpdateRequest} used to set an ethernet network's + * {@link StaticIpConfiguration} and {@link NetworkCapabilities} values. + * @param executor an {@link Executor} to execute the callback on. Optional if callback is null. + * @param callback an optional {@link OutcomeReceiver} to listen for completion of the + * operation. On success, {@link OutcomeReceiver#onResult} is called with the + * interface name. On error, {@link OutcomeReceiver#onError} is called with more + * information about the error. + * @throws SecurityException if the process doesn't hold + * {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}. + * @throws UnsupportedOperationException if called on a non-automotive device or on an + * unsupported interface. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) + public void updateConfiguration( + @NonNull String iface, + @NonNull EthernetNetworkUpdateRequest request, + @Nullable @CallbackExecutor Executor executor, + @Nullable OutcomeReceiver callback) { + Objects.requireNonNull(iface, "iface must be non-null"); + Objects.requireNonNull(request, "request must be non-null"); + final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver( + executor, callback); + try { + mService.updateConfiguration(iface, request, proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Enable a network interface. + * + * Enables a previously disabled network interface. + * This function accepts an {@link OutcomeReceiver} that is called once the operation has + * finished execution. + * + * @param iface the name of the interface to enable. + * @param executor an {@link Executor} to execute the callback on. Optional if callback is null. + * @param callback an optional {@link OutcomeReceiver} to listen for completion of the + * operation. On success, {@link OutcomeReceiver#onResult} is called with the + * interface name. On error, {@link OutcomeReceiver#onError} is called with more + * information about the error. + * @throws SecurityException if the process doesn't hold + * {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) + @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE) + public void enableInterface( + @NonNull String iface, + @Nullable @CallbackExecutor Executor executor, + @Nullable OutcomeReceiver callback) { + Objects.requireNonNull(iface, "iface must be non-null"); + final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver( + executor, callback); + try { + mService.connectNetwork(iface, proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Disable a network interface. + * + * Disables the use of a network interface to fulfill network requests. If the interface + * currently serves a request, the network will be torn down. + * This function accepts an {@link OutcomeReceiver} that is called once the operation has + * finished execution. + * + * @param iface the name of the interface to disable. + * @param executor an {@link Executor} to execute the callback on. Optional if callback is null. + * @param callback an optional {@link OutcomeReceiver} to listen for completion of the + * operation. On success, {@link OutcomeReceiver#onResult} is called with the + * interface name. On error, {@link OutcomeReceiver#onError} is called with more + * information about the error. + * @throws SecurityException if the process doesn't hold + * {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}) + @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE) + public void disableInterface( + @NonNull String iface, + @Nullable @CallbackExecutor Executor executor, + @Nullable OutcomeReceiver callback) { + Objects.requireNonNull(iface, "iface must be non-null"); + final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver( + executor, callback); + try { + mService.disconnectNetwork(iface, proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Change ethernet setting. + * + * @param enabled enable or disable ethernet settings. + * + * @hide + */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + @SystemApi(client = MODULE_LIBRARIES) + public void setEthernetEnabled(boolean enabled) { + try { + mService.setEthernetEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Listen to changes in the state of ethernet. + * + * @param executor to run callbacks on. + * @param listener to listen ethernet state changed. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @SystemApi(client = MODULE_LIBRARIES) + public void addEthernetStateListener(@NonNull Executor executor, + @NonNull IntConsumer listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mListenerLock) { + maybeAddServiceListener(); + mEthernetStateListeners.add(new ListenerInfo(executor, listener)); + } + } + + /** + * Removes a listener. + * + * @param listener to listen ethernet state changed. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @SystemApi(client = MODULE_LIBRARIES) + public void removeEthernetStateListener(@NonNull IntConsumer listener) { + Objects.requireNonNull(listener); + synchronized (mListenerLock) { + mEthernetStateListeners.removeIf(l -> l.listener == listener); + maybeRemoveServiceListener(); + } + } + + /** + * Returns an array of existing Ethernet interface names regardless whether the interface + * is available or not currently. + * @hide + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @SystemApi(client = MODULE_LIBRARIES) + @NonNull + public List getInterfaceList() { + try { + return mService.getInterfaceList(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } +} diff --git a/framework-t/src/android/net/EthernetNetworkManagementException.aidl b/framework-t/src/android/net/EthernetNetworkManagementException.aidl new file mode 100644 index 0000000000..adf9e5a4db --- /dev/null +++ b/framework-t/src/android/net/EthernetNetworkManagementException.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.net; + + parcelable EthernetNetworkManagementException; \ No newline at end of file diff --git a/framework-t/src/android/net/EthernetNetworkManagementException.java b/framework-t/src/android/net/EthernetNetworkManagementException.java new file mode 100644 index 0000000000..a69cc55363 --- /dev/null +++ b/framework-t/src/android/net/EthernetNetworkManagementException.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** @hide */ +@SystemApi +public final class EthernetNetworkManagementException + extends RuntimeException implements Parcelable { + + /* @hide */ + public EthernetNetworkManagementException(@NonNull final String errorMessage) { + super(errorMessage); + } + + @Override + public int hashCode() { + return Objects.hash(getMessage()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + final EthernetNetworkManagementException that = (EthernetNetworkManagementException) obj; + + return Objects.equals(getMessage(), that.getMessage()); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(getMessage()); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public EthernetNetworkManagementException[] newArray(int size) { + return new EthernetNetworkManagementException[size]; + } + + @Override + public EthernetNetworkManagementException createFromParcel(@NonNull Parcel source) { + return new EthernetNetworkManagementException(source.readString()); + } + }; +} diff --git a/framework-t/src/android/net/EthernetNetworkSpecifier.java b/framework-t/src/android/net/EthernetNetworkSpecifier.java new file mode 100644 index 0000000000..e4d6e248d4 --- /dev/null +++ b/framework-t/src/android/net/EthernetNetworkSpecifier.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * A {@link NetworkSpecifier} used to identify ethernet interfaces. + * + * @see EthernetManager + */ +public final class EthernetNetworkSpecifier extends NetworkSpecifier implements Parcelable { + + /** + * Name of the network interface. + */ + @NonNull + private final String mInterfaceName; + + /** + * Create a new EthernetNetworkSpecifier. + * @param interfaceName Name of the ethernet interface the specifier refers to. + */ + public EthernetNetworkSpecifier(@NonNull String interfaceName) { + if (TextUtils.isEmpty(interfaceName)) { + throw new IllegalArgumentException(); + } + mInterfaceName = interfaceName; + } + + /** + * Get the name of the ethernet interface the specifier refers to. + */ + @Nullable + public String getInterfaceName() { + // This may be null in the future to support specifiers based on data other than the + // interface name. + return mInterfaceName; + } + + /** @hide */ + @Override + public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) { + return equals(other); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof EthernetNetworkSpecifier)) return false; + return TextUtils.equals(mInterfaceName, ((EthernetNetworkSpecifier) o).mInterfaceName); + } + + @Override + public int hashCode() { + return Objects.hashCode(mInterfaceName); + } + + @Override + public String toString() { + return "EthernetNetworkSpecifier (" + mInterfaceName + ")"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mInterfaceName); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public EthernetNetworkSpecifier createFromParcel(Parcel in) { + return new EthernetNetworkSpecifier(in.readString()); + } + public EthernetNetworkSpecifier[] newArray(int size) { + return new EthernetNetworkSpecifier[size]; + } + }; +} diff --git a/framework-t/src/android/net/EthernetNetworkUpdateRequest.aidl b/framework-t/src/android/net/EthernetNetworkUpdateRequest.aidl new file mode 100644 index 0000000000..debc348ea3 --- /dev/null +++ b/framework-t/src/android/net/EthernetNetworkUpdateRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.net; + + parcelable EthernetNetworkUpdateRequest; \ No newline at end of file diff --git a/framework-t/src/android/net/EthernetNetworkUpdateRequest.java b/framework-t/src/android/net/EthernetNetworkUpdateRequest.java new file mode 100644 index 0000000000..1691942c36 --- /dev/null +++ b/framework-t/src/android/net/EthernetNetworkUpdateRequest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Represents a request to update an existing Ethernet interface. + * + * @see EthernetManager#updateConfiguration + * + * @hide + */ +@SystemApi +public final class EthernetNetworkUpdateRequest implements Parcelable { + @Nullable + private final IpConfiguration mIpConfig; + @Nullable + private final NetworkCapabilities mNetworkCapabilities; + + /** + * Setting the {@link IpConfiguration} is optional in {@link EthernetNetworkUpdateRequest}. + * When set to null, the existing IpConfiguration is not updated. + * + * @return the new {@link IpConfiguration} or null. + */ + @Nullable + public IpConfiguration getIpConfiguration() { + return mIpConfig == null ? null : new IpConfiguration(mIpConfig); + } + + /** + * Setting the {@link NetworkCapabilities} is optional in {@link EthernetNetworkUpdateRequest}. + * When set to null, the existing NetworkCapabilities are not updated. + * + * @return the new {@link NetworkCapabilities} or null. + */ + @Nullable + public NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities == null ? null : new NetworkCapabilities(mNetworkCapabilities); + } + + private EthernetNetworkUpdateRequest(@Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities networkCapabilities) { + mIpConfig = ipConfig; + mNetworkCapabilities = networkCapabilities; + } + + private EthernetNetworkUpdateRequest(@NonNull final Parcel source) { + Objects.requireNonNull(source); + mIpConfig = source.readParcelable(IpConfiguration.class.getClassLoader(), + IpConfiguration.class); + mNetworkCapabilities = source.readParcelable(NetworkCapabilities.class.getClassLoader(), + NetworkCapabilities.class); + } + + /** + * Builder used to create {@link EthernetNetworkUpdateRequest} objects. + */ + public static final class Builder { + @Nullable + private IpConfiguration mBuilderIpConfig; + @Nullable + private NetworkCapabilities mBuilderNetworkCapabilities; + + public Builder(){} + + /** + * Constructor to populate the builder's values with an already built + * {@link EthernetNetworkUpdateRequest}. + * @param request the {@link EthernetNetworkUpdateRequest} to populate with. + */ + public Builder(@NonNull final EthernetNetworkUpdateRequest request) { + Objects.requireNonNull(request); + mBuilderIpConfig = null == request.mIpConfig + ? null : new IpConfiguration(request.mIpConfig); + mBuilderNetworkCapabilities = null == request.mNetworkCapabilities + ? null : new NetworkCapabilities(request.mNetworkCapabilities); + } + + /** + * Set the {@link IpConfiguration} to be used with the {@code Builder}. + * @param ipConfig the {@link IpConfiguration} to set. + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder setIpConfiguration(@Nullable final IpConfiguration ipConfig) { + mBuilderIpConfig = ipConfig == null ? null : new IpConfiguration(ipConfig); + return this; + } + + /** + * Set the {@link NetworkCapabilities} to be used with the {@code Builder}. + * @param nc the {@link NetworkCapabilities} to set. + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder setNetworkCapabilities(@Nullable final NetworkCapabilities nc) { + mBuilderNetworkCapabilities = nc == null ? null : new NetworkCapabilities(nc); + return this; + } + + /** + * Build {@link EthernetNetworkUpdateRequest} return the current update request. + * + * @throws IllegalStateException when both mBuilderNetworkCapabilities and mBuilderIpConfig + * are null. + */ + @NonNull + public EthernetNetworkUpdateRequest build() { + if (mBuilderIpConfig == null && mBuilderNetworkCapabilities == null) { + throw new IllegalStateException( + "Cannot construct an empty EthernetNetworkUpdateRequest"); + } + return new EthernetNetworkUpdateRequest(mBuilderIpConfig, mBuilderNetworkCapabilities); + } + } + + @Override + public String toString() { + return "EthernetNetworkUpdateRequest{" + + "mIpConfig=" + mIpConfig + + ", mNetworkCapabilities=" + mNetworkCapabilities + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EthernetNetworkUpdateRequest that = (EthernetNetworkUpdateRequest) o; + + return Objects.equals(that.getIpConfiguration(), mIpConfig) + && Objects.equals(that.getNetworkCapabilities(), mNetworkCapabilities); + } + + @Override + public int hashCode() { + return Objects.hash(mIpConfig, mNetworkCapabilities); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mIpConfig, flags); + dest.writeParcelable(mNetworkCapabilities, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public EthernetNetworkUpdateRequest[] newArray(int size) { + return new EthernetNetworkUpdateRequest[size]; + } + + @Override + public EthernetNetworkUpdateRequest createFromParcel(@NonNull Parcel source) { + return new EthernetNetworkUpdateRequest(source); + } + }; +} diff --git a/framework-t/src/android/net/IEthernetManager.aidl b/framework-t/src/android/net/IEthernetManager.aidl new file mode 100644 index 0000000000..42e4c1ac55 --- /dev/null +++ b/framework-t/src/android/net/IEthernetManager.aidl @@ -0,0 +1,50 @@ +/* + * 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 android.net; + +import android.net.IpConfiguration; +import android.net.IEthernetServiceListener; +import android.net.EthernetNetworkManagementException; +import android.net.EthernetNetworkUpdateRequest; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.ITetheredInterfaceCallback; + +import java.util.List; + +/** + * Interface that answers queries about, and allows changing + * ethernet configuration. + */ +/** {@hide} */ +interface IEthernetManager +{ + String[] getAvailableInterfaces(); + IpConfiguration getConfiguration(String iface); + void setConfiguration(String iface, in IpConfiguration config); + boolean isAvailable(String iface); + void addListener(in IEthernetServiceListener listener); + void removeListener(in IEthernetServiceListener listener); + void setIncludeTestInterfaces(boolean include); + void requestTetheredInterface(in ITetheredInterfaceCallback callback); + void releaseTetheredInterface(in ITetheredInterfaceCallback callback); + void updateConfiguration(String iface, in EthernetNetworkUpdateRequest request, + in INetworkInterfaceOutcomeReceiver listener); + void connectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener); + void disconnectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener); + void setEthernetEnabled(boolean enabled); + List getInterfaceList(); +} diff --git a/framework-t/src/android/net/IEthernetServiceListener.aidl b/framework-t/src/android/net/IEthernetServiceListener.aidl new file mode 100644 index 0000000000..751605bb38 --- /dev/null +++ b/framework-t/src/android/net/IEthernetServiceListener.aidl @@ -0,0 +1,27 @@ +/* + * 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 android.net; + +import android.net.IpConfiguration; + +/** @hide */ +oneway interface IEthernetServiceListener +{ + void onEthernetStateChanged(int state); + void onInterfaceStateChanged(String iface, int state, int role, + in IpConfiguration configuration); +} diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl new file mode 100644 index 0000000000..933256a3b4 --- /dev/null +++ b/framework-t/src/android/net/IIpSecService.aidl @@ -0,0 +1,78 @@ +/* +** Copyright 2017, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.net; + +import android.net.LinkAddress; +import android.net.Network; +import android.net.IpSecConfig; +import android.net.IpSecUdpEncapResponse; +import android.net.IpSecSpiResponse; +import android.net.IpSecTransformResponse; +import android.net.IpSecTunnelInterfaceResponse; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; + +/** + * @hide + */ +interface IIpSecService +{ + IpSecSpiResponse allocateSecurityParameterIndex( + in String destinationAddress, int requestedSpi, in IBinder binder); + + void releaseSecurityParameterIndex(int resourceId); + + IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, in IBinder binder); + + void closeUdpEncapsulationSocket(int resourceId); + + IpSecTunnelInterfaceResponse createTunnelInterface( + in String localAddr, + in String remoteAddr, + in Network underlyingNetwork, + in IBinder binder, + in String callingPackage); + + void addAddressToTunnelInterface( + int tunnelResourceId, + in LinkAddress localAddr, + in String callingPackage); + + void removeAddressFromTunnelInterface( + int tunnelResourceId, + in LinkAddress localAddr, + in String callingPackage); + + void setNetworkForTunnelInterface( + int tunnelResourceId, in Network underlyingNetwork, in String callingPackage); + + void deleteTunnelInterface(int resourceId, in String callingPackage); + + IpSecTransformResponse createTransform( + in IpSecConfig c, in IBinder binder, in String callingPackage); + + void deleteTransform(int transformId); + + void applyTransportModeTransform( + in ParcelFileDescriptor socket, int direction, int transformId); + + void applyTunnelModeTransform( + int tunnelResourceId, int direction, int transformResourceId, in String callingPackage); + + void removeTransportModeTransforms(in ParcelFileDescriptor socket); +} diff --git a/framework-t/src/android/net/INetworkInterfaceOutcomeReceiver.aidl b/framework-t/src/android/net/INetworkInterfaceOutcomeReceiver.aidl new file mode 100644 index 0000000000..85795ead7a --- /dev/null +++ b/framework-t/src/android/net/INetworkInterfaceOutcomeReceiver.aidl @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2021, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.EthernetNetworkManagementException; + +/** @hide */ +oneway interface INetworkInterfaceOutcomeReceiver { + void onResult(in String iface); + void onError(in EthernetNetworkManagementException e); +} \ No newline at end of file diff --git a/framework-t/src/android/net/INetworkStatsService.aidl b/framework-t/src/android/net/INetworkStatsService.aidl new file mode 100644 index 0000000000..c86f7fd089 --- /dev/null +++ b/framework-t/src/android/net/INetworkStatsService.aidl @@ -0,0 +1,104 @@ +/* + * 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 android.net; + +import android.net.DataUsageRequest; +import android.net.INetworkStatsSession; +import android.net.Network; +import android.net.NetworkStateSnapshot; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.net.UnderlyingNetworkInfo; +import android.net.netstats.IUsageCallback; +import android.net.netstats.provider.INetworkStatsProvider; +import android.net.netstats.provider.INetworkStatsProviderCallback; +import android.os.IBinder; +import android.os.Messenger; + +/** {@hide} */ +interface INetworkStatsService { + + /** Start a statistics query session. */ + @UnsupportedAppUsage + INetworkStatsSession openSession(); + + /** Start a statistics query session. If calling package is profile or device owner then it is + * granted automatic access if apiLevel is NetworkStatsManager.API_LEVEL_DPC_ALLOWED. If + * apiLevel is at least NetworkStatsManager.API_LEVEL_REQUIRES_PACKAGE_USAGE_STATS then + * PACKAGE_USAGE_STATS permission is always checked. If PACKAGE_USAGE_STATS is not granted + * READ_NETWORK_USAGE_STATS is checked for. + */ + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage); + + /** Return data layer snapshot of UID network usage. */ + @UnsupportedAppUsage + NetworkStats getDataLayerSnapshotForUid(int uid); + + /** Get the transport NetworkStats for all UIDs since boot. */ + NetworkStats getUidStatsForTransport(int transport); + + /** Return set of any ifaces associated with mobile networks since boot. */ + @UnsupportedAppUsage + String[] getMobileIfaces(); + + /** Increment data layer count of operations performed for UID and tag. */ + void incrementOperationCount(int uid, int tag, int operationCount); + + /** Notify {@code NetworkStatsService} about network status changed. */ + void notifyNetworkStatus( + in Network[] defaultNetworks, + in NetworkStateSnapshot[] snapshots, + in String activeIface, + in UnderlyingNetworkInfo[] underlyingNetworkInfos); + /** Force update of statistics. */ + @UnsupportedAppUsage + void forceUpdate(); + + /** Registers a callback on data usage. */ + DataUsageRequest registerUsageCallback(String callingPackage, + in DataUsageRequest request, in IUsageCallback callback); + + /** Unregisters a callback on data usage. */ + void unregisterUsageRequest(in DataUsageRequest request); + + /** Get the uid stats information since boot */ + long getUidStats(int uid, int type); + + /** Get the iface stats information since boot */ + long getIfaceStats(String iface, int type); + + /** Get the total network stats information since boot */ + long getTotalStats(int type); + + /** Registers a network stats provider */ + INetworkStatsProviderCallback registerNetworkStatsProvider(String tag, + in INetworkStatsProvider provider); + + /** Mark given UID as being in foreground for stats purposes. */ + void noteUidForeground(int uid, boolean uidForeground); + + /** Advise persistence threshold; may be overridden internally. */ + void advisePersistThreshold(long thresholdBytes); + + /** + * Set the warning and limit to all registered custom network stats providers. + * Note that invocation of any interface will be sent to all providers. + */ + void setStatsProviderWarningAndLimitAsync(String iface, long warning, long limit); +} diff --git a/framework-t/src/android/net/INetworkStatsSession.aidl b/framework-t/src/android/net/INetworkStatsSession.aidl new file mode 100644 index 0000000000..ab70be826f --- /dev/null +++ b/framework-t/src/android/net/INetworkStatsSession.aidl @@ -0,0 +1,70 @@ +/* + * 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 android.net; + +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; + +/** {@hide} */ +interface INetworkStatsSession { + + /** Return device aggregated network layer usage summary for traffic that matches template. */ + NetworkStats getDeviceSummaryForNetwork(in NetworkTemplate template, long start, long end); + + /** Return network layer usage summary for traffic that matches template. */ + @UnsupportedAppUsage + NetworkStats getSummaryForNetwork(in NetworkTemplate template, long start, long end); + /** Return historical network layer stats for traffic that matches template. */ + @UnsupportedAppUsage + NetworkStatsHistory getHistoryForNetwork(in NetworkTemplate template, int fields); + /** + * Return historical network layer stats for traffic that matches template, start and end + * timestamp. + */ + NetworkStatsHistory getHistoryIntervalForNetwork(in NetworkTemplate template, int fields, long start, long end); + + /** + * Return network layer usage summary per UID for traffic that matches template. + * + *

The resulting {@code NetworkStats#getElapsedRealtime()} contains time delta between + * {@code start} and {@code end}. + * + * @param template - a predicate to filter netstats. + * @param start - start of the range, timestamp in milliseconds since the epoch. + * @param end - end of the range, timestamp in milliseconds since the epoch. + * @param includeTags - includes data usage tags if true. + */ + @UnsupportedAppUsage + NetworkStats getSummaryForAllUid(in NetworkTemplate template, long start, long end, boolean includeTags); + + /** Return network layer usage summary per UID for tagged traffic that matches template. */ + NetworkStats getTaggedSummaryForAllUid(in NetworkTemplate template, long start, long end); + + /** Return historical network layer stats for specific UID traffic that matches template. */ + @UnsupportedAppUsage + NetworkStatsHistory getHistoryForUid(in NetworkTemplate template, int uid, int set, int tag, int fields); + /** Return historical network layer stats for specific UID traffic that matches template. */ + NetworkStatsHistory getHistoryIntervalForUid(in NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end); + + /** Return array of uids that have stats and are accessible to the calling user */ + int[] getRelevantUids(); + + @UnsupportedAppUsage + void close(); + +} diff --git a/framework-t/src/android/net/ITetheredInterfaceCallback.aidl b/framework-t/src/android/net/ITetheredInterfaceCallback.aidl new file mode 100644 index 0000000000..14aa0237f2 --- /dev/null +++ b/framework-t/src/android/net/ITetheredInterfaceCallback.aidl @@ -0,0 +1,23 @@ +/* + * 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 android.net; + +/** @hide */ +oneway interface ITetheredInterfaceCallback { + void onAvailable(in String iface); + void onUnavailable(); +} \ No newline at end of file diff --git a/framework-t/src/android/net/IpSecAlgorithm.java b/framework-t/src/android/net/IpSecAlgorithm.java new file mode 100644 index 0000000000..10a22ac360 --- /dev/null +++ b/framework-t/src/android/net/IpSecAlgorithm.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.annotation.NonNull; +import android.annotation.StringDef; +import android.content.res.Resources; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * This class represents a single algorithm that can be used by an {@link IpSecTransform}. + * + * @see RFC 4301, Security Architecture for the + * Internet Protocol + */ +public final class IpSecAlgorithm implements Parcelable { + private static final String TAG = "IpSecAlgorithm"; + + /** + * Null cipher. + * + * @hide + */ + public static final String CRYPT_NULL = "ecb(cipher_null)"; + + /** + * AES-CBC Encryption/Ciphering Algorithm. + * + *

Valid lengths for this key are {128, 192, 256}. + */ + public static final String CRYPT_AES_CBC = "cbc(aes)"; + + /** + * AES-CTR Encryption/Ciphering Algorithm. + * + *

Valid lengths for keying material are {160, 224, 288}. + * + *

As per RFC3686 (Section + * 5.1), keying material consists of a 128, 192, or 256 bit AES key followed by a 32-bit + * nonce. RFC compliance requires that the nonce must be unique per security association. + * + *

This algorithm may be available on the device. Caller MUST check if it is supported before + * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is + * included in the returned algorithm set. The returned algorithm set will not change unless the + * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is + * requested on an unsupported device. + * + *

@see {@link #getSupportedAlgorithms()} + */ + // This algorithm may be available on devices released before Android 12, and is guaranteed + // to be available on devices first shipped with Android 12 or later. + public static final String CRYPT_AES_CTR = "rfc3686(ctr(aes))"; + + /** + * MD5 HMAC Authentication/Integrity Algorithm. This algorithm is not recommended for use in + * new applications and is provided for legacy compatibility with 3gpp infrastructure. + * + *

Keys for this algorithm must be 128 bits in length. + * + *

Valid truncation lengths are multiples of 8 bits from 96 to 128. + */ + public static final String AUTH_HMAC_MD5 = "hmac(md5)"; + + /** + * SHA1 HMAC Authentication/Integrity Algorithm. This algorithm is not recommended for use in + * new applications and is provided for legacy compatibility with 3gpp infrastructure. + * + *

Keys for this algorithm must be 160 bits in length. + * + *

Valid truncation lengths are multiples of 8 bits from 96 to 160. + */ + public static final String AUTH_HMAC_SHA1 = "hmac(sha1)"; + + /** + * SHA256 HMAC Authentication/Integrity Algorithm. + * + *

Keys for this algorithm must be 256 bits in length. + * + *

Valid truncation lengths are multiples of 8 bits from 96 to 256. + */ + public static final String AUTH_HMAC_SHA256 = "hmac(sha256)"; + + /** + * SHA384 HMAC Authentication/Integrity Algorithm. + * + *

Keys for this algorithm must be 384 bits in length. + * + *

Valid truncation lengths are multiples of 8 bits from 192 to 384. + */ + public static final String AUTH_HMAC_SHA384 = "hmac(sha384)"; + + /** + * SHA512 HMAC Authentication/Integrity Algorithm. + * + *

Keys for this algorithm must be 512 bits in length. + * + *

Valid truncation lengths are multiples of 8 bits from 256 to 512. + */ + public static final String AUTH_HMAC_SHA512 = "hmac(sha512)"; + + /** + * AES-XCBC Authentication/Integrity Algorithm. + * + *

Keys for this algorithm must be 128 bits in length. + * + *

The only valid truncation length is 96 bits. + * + *

This algorithm may be available on the device. Caller MUST check if it is supported before + * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is + * included in the returned algorithm set. The returned algorithm set will not change unless the + * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is + * requested on an unsupported device. + * + *

@see {@link #getSupportedAlgorithms()} + */ + // This algorithm may be available on devices released before Android 12, and is guaranteed + // to be available on devices first shipped with Android 12 or later. + public static final String AUTH_AES_XCBC = "xcbc(aes)"; + + /** + * AES-CMAC Authentication/Integrity Algorithm. + * + *

Keys for this algorithm must be 128 bits in length. + * + *

The only valid truncation length is 96 bits. + * + *

This algorithm may be available on the device. Caller MUST check if it is supported before + * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is + * included in the returned algorithm set. The returned algorithm set will not change unless the + * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is + * requested on an unsupported device. + * + *

@see {@link #getSupportedAlgorithms()} + */ + // This algorithm may be available on devices released before Android 12, and is guaranteed + // to be available on devices first shipped with Android 12 or later. + public static final String AUTH_AES_CMAC = "cmac(aes)"; + + /** + * AES-GCM Authentication/Integrity + Encryption/Ciphering Algorithm. + * + *

Valid lengths for keying material are {160, 224, 288}. + * + *

As per RFC4106 (Section + * 8.1), keying material consists of a 128, 192, or 256 bit AES key followed by a 32-bit + * salt. RFC compliance requires that the salt must be unique per invocation with the same key. + * + *

Valid ICV (truncation) lengths are {64, 96, 128}. + */ + public static final String AUTH_CRYPT_AES_GCM = "rfc4106(gcm(aes))"; + + /** + * ChaCha20-Poly1305 Authentication/Integrity + Encryption/Ciphering Algorithm. + * + *

Keys for this algorithm must be 288 bits in length. + * + *

As per RFC7634 (Section 2), + * keying material consists of a 256 bit key followed by a 32-bit salt. The salt is fixed per + * security association. + * + *

The only valid ICV (truncation) length is 128 bits. + * + *

This algorithm may be available on the device. Caller MUST check if it is supported before + * using it by calling {@link #getSupportedAlgorithms()} and checking if this algorithm is + * included in the returned algorithm set. The returned algorithm set will not change unless the + * device is rebooted. {@link IllegalArgumentException} will be thrown if this algorithm is + * requested on an unsupported device. + * + *

@see {@link #getSupportedAlgorithms()} + */ + // This algorithm may be available on devices released before Android 12, and is guaranteed + // to be available on devices first shipped with Android 12 or later. + public static final String AUTH_CRYPT_CHACHA20_POLY1305 = "rfc7539esp(chacha20,poly1305)"; + + /** @hide */ + @StringDef({ + CRYPT_AES_CBC, + CRYPT_AES_CTR, + AUTH_HMAC_MD5, + AUTH_HMAC_SHA1, + AUTH_HMAC_SHA256, + AUTH_HMAC_SHA384, + AUTH_HMAC_SHA512, + AUTH_AES_XCBC, + AUTH_AES_CMAC, + AUTH_CRYPT_AES_GCM, + AUTH_CRYPT_CHACHA20_POLY1305 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AlgorithmName {} + + /** @hide */ + @VisibleForTesting + public static final Map ALGO_TO_REQUIRED_FIRST_SDK = new HashMap<>(); + + private static final int SDK_VERSION_ZERO = 0; + + static { + ALGO_TO_REQUIRED_FIRST_SDK.put(CRYPT_AES_CBC, SDK_VERSION_ZERO); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_MD5, SDK_VERSION_ZERO); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA1, SDK_VERSION_ZERO); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA256, SDK_VERSION_ZERO); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA384, SDK_VERSION_ZERO); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_HMAC_SHA512, SDK_VERSION_ZERO); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_CRYPT_AES_GCM, SDK_VERSION_ZERO); + + ALGO_TO_REQUIRED_FIRST_SDK.put(CRYPT_AES_CTR, Build.VERSION_CODES.S); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_AES_XCBC, Build.VERSION_CODES.S); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_AES_CMAC, Build.VERSION_CODES.S); + ALGO_TO_REQUIRED_FIRST_SDK.put(AUTH_CRYPT_CHACHA20_POLY1305, Build.VERSION_CODES.S); + } + + private static final Set ENABLED_ALGOS = + Collections.unmodifiableSet(loadAlgos(Resources.getSystem())); + + private final String mName; + private final byte[] mKey; + private final int mTruncLenBits; + + /** + * Creates an IpSecAlgorithm of one of the supported types. Supported algorithm names are + * defined as constants in this class. + * + *

For algorithms that produce an integrity check value, the truncation length is a required + * parameter. See {@link #IpSecAlgorithm(String algorithm, byte[] key, int truncLenBits)} + * + * @param algorithm name of the algorithm. + * @param key key padded to a multiple of 8 bits. + * @throws IllegalArgumentException if algorithm or key length is invalid. + */ + public IpSecAlgorithm(@NonNull @AlgorithmName String algorithm, @NonNull byte[] key) { + this(algorithm, key, 0); + } + + /** + * Creates an IpSecAlgorithm of one of the supported types. Supported algorithm names are + * defined as constants in this class. + * + *

This constructor only supports algorithms that use a truncation length. i.e. + * Authentication and Authenticated Encryption algorithms. + * + * @param algorithm name of the algorithm. + * @param key key padded to a multiple of 8 bits. + * @param truncLenBits number of bits of output hash to use. + * @throws IllegalArgumentException if algorithm, key length or truncation length is invalid. + */ + public IpSecAlgorithm( + @NonNull @AlgorithmName String algorithm, @NonNull byte[] key, int truncLenBits) { + mName = algorithm; + mKey = key.clone(); + mTruncLenBits = truncLenBits; + checkValidOrThrow(mName, mKey.length * 8, mTruncLenBits); + } + + /** Get the algorithm name */ + @NonNull + public String getName() { + return mName; + } + + /** Get the key for this algorithm */ + @NonNull + public byte[] getKey() { + return mKey.clone(); + } + + /** Get the truncation length of this algorithm, in bits */ + public int getTruncationLengthBits() { + return mTruncLenBits; + } + + /** Parcelable Implementation */ + public int describeContents() { + return 0; + } + + /** Write to parcel */ + public void writeToParcel(Parcel out, int flags) { + out.writeString(mName); + out.writeByteArray(mKey); + out.writeInt(mTruncLenBits); + } + + /** Parcelable Creator */ + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public IpSecAlgorithm createFromParcel(Parcel in) { + final String name = in.readString(); + final byte[] key = in.createByteArray(); + final int truncLenBits = in.readInt(); + + return new IpSecAlgorithm(name, key, truncLenBits); + } + + public IpSecAlgorithm[] newArray(int size) { + return new IpSecAlgorithm[size]; + } + }; + + /** + * Returns supported IPsec algorithms for the current device. + * + *

Some algorithms may not be supported on old devices. Callers MUST check if an algorithm is + * supported before using it. + */ + @NonNull + public static Set getSupportedAlgorithms() { + return ENABLED_ALGOS; + } + + /** @hide */ + @VisibleForTesting + public static Set loadAlgos(Resources systemResources) { + final Set enabledAlgos = new HashSet<>(); + + // Load and validate the optional algorithm resource. Undefined or duplicate algorithms in + // the resource are not allowed. + final String[] resourceAlgos = systemResources.getStringArray( + android.R.array.config_optionalIpSecAlgorithms); + for (String str : resourceAlgos) { + if (!ALGO_TO_REQUIRED_FIRST_SDK.containsKey(str) || !enabledAlgos.add(str)) { + // This error should be caught by CTS and never be thrown to API callers + throw new IllegalArgumentException("Invalid or repeated algorithm " + str); + } + } + + for (Entry entry : ALGO_TO_REQUIRED_FIRST_SDK.entrySet()) { + if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= entry.getValue()) { + enabledAlgos.add(entry.getKey()); + } + } + + return enabledAlgos; + } + + private static void checkValidOrThrow(String name, int keyLen, int truncLen) { + final boolean isValidLen; + final boolean isValidTruncLen; + + if (!getSupportedAlgorithms().contains(name)) { + throw new IllegalArgumentException("Unsupported algorithm: " + name); + } + + switch (name) { + case CRYPT_AES_CBC: + isValidLen = keyLen == 128 || keyLen == 192 || keyLen == 256; + isValidTruncLen = true; + break; + case CRYPT_AES_CTR: + // The keying material for AES-CTR is a key plus a 32-bit salt + isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32; + isValidTruncLen = true; + break; + case AUTH_HMAC_MD5: + isValidLen = keyLen == 128; + isValidTruncLen = truncLen >= 96 && truncLen <= 128; + break; + case AUTH_HMAC_SHA1: + isValidLen = keyLen == 160; + isValidTruncLen = truncLen >= 96 && truncLen <= 160; + break; + case AUTH_HMAC_SHA256: + isValidLen = keyLen == 256; + isValidTruncLen = truncLen >= 96 && truncLen <= 256; + break; + case AUTH_HMAC_SHA384: + isValidLen = keyLen == 384; + isValidTruncLen = truncLen >= 192 && truncLen <= 384; + break; + case AUTH_HMAC_SHA512: + isValidLen = keyLen == 512; + isValidTruncLen = truncLen >= 256 && truncLen <= 512; + break; + case AUTH_AES_XCBC: + isValidLen = keyLen == 128; + isValidTruncLen = truncLen == 96; + break; + case AUTH_AES_CMAC: + isValidLen = keyLen == 128; + isValidTruncLen = truncLen == 96; + break; + case AUTH_CRYPT_AES_GCM: + // The keying material for GCM is a key plus a 32-bit salt + isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32; + isValidTruncLen = truncLen == 64 || truncLen == 96 || truncLen == 128; + break; + case AUTH_CRYPT_CHACHA20_POLY1305: + // The keying material for ChaCha20Poly1305 is a key plus a 32-bit salt + isValidLen = keyLen == 256 + 32; + isValidTruncLen = truncLen == 128; + break; + default: + // Should never hit here. + throw new IllegalArgumentException("Couldn't find an algorithm: " + name); + } + + if (!isValidLen) { + throw new IllegalArgumentException("Invalid key material keyLength: " + keyLen); + } + if (!isValidTruncLen) { + throw new IllegalArgumentException("Invalid truncation keyLength: " + truncLen); + } + } + + /** @hide */ + public boolean isAuthentication() { + switch (getName()) { + // Fallthrough + case AUTH_HMAC_MD5: + case AUTH_HMAC_SHA1: + case AUTH_HMAC_SHA256: + case AUTH_HMAC_SHA384: + case AUTH_HMAC_SHA512: + case AUTH_AES_XCBC: + case AUTH_AES_CMAC: + return true; + default: + return false; + } + } + + /** @hide */ + public boolean isEncryption() { + switch (getName()) { + case CRYPT_AES_CBC: // fallthrough + case CRYPT_AES_CTR: + return true; + default: + return false; + } + } + + /** @hide */ + public boolean isAead() { + switch (getName()) { + case AUTH_CRYPT_AES_GCM: // fallthrough + case AUTH_CRYPT_CHACHA20_POLY1305: + return true; + default: + return false; + } + } + + @Override + @NonNull + public String toString() { + return new StringBuilder() + .append("{mName=") + .append(mName) + .append(", mTruncLenBits=") + .append(mTruncLenBits) + .append("}") + .toString(); + } + + /** @hide */ + @VisibleForTesting + public static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) { + if (lhs == null || rhs == null) return (lhs == rhs); + return (lhs.mName.equals(rhs.mName) + && Arrays.equals(lhs.mKey, rhs.mKey) + && lhs.mTruncLenBits == rhs.mTruncLenBits); + } +}; diff --git a/framework-t/src/android/net/IpSecConfig.aidl b/framework-t/src/android/net/IpSecConfig.aidl new file mode 100644 index 0000000000..eaefca74d3 --- /dev/null +++ b/framework-t/src/android/net/IpSecConfig.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** @hide */ +parcelable IpSecConfig; diff --git a/framework-t/src/android/net/IpSecConfig.java b/framework-t/src/android/net/IpSecConfig.java new file mode 100644 index 0000000000..575c5ed968 --- /dev/null +++ b/framework-t/src/android/net/IpSecConfig.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * This class encapsulates all the configuration parameters needed to create IPsec transforms and + * policies. + * + * @hide + */ +public final class IpSecConfig implements Parcelable { + private static final String TAG = "IpSecConfig"; + + // MODE_TRANSPORT or MODE_TUNNEL + private int mMode = IpSecTransform.MODE_TRANSPORT; + + // Preventing this from being null simplifies Java->Native binder + private String mSourceAddress = ""; + + // Preventing this from being null simplifies Java->Native binder + private String mDestinationAddress = ""; + + // The underlying Network that represents the "gateway" Network + // for outbound packets. It may also be used to select packets. + private Network mNetwork; + + // Minimum requirements for identifying a transform + // SPI identifying the IPsec SA in packet processing + // and a destination IP address + private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID; + + // Encryption Algorithm + private IpSecAlgorithm mEncryption; + + // Authentication Algorithm + private IpSecAlgorithm mAuthentication; + + // Authenticated Encryption Algorithm + private IpSecAlgorithm mAuthenticatedEncryption; + + // For tunnel mode IPv4 UDP Encapsulation + // IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE + private int mEncapType = IpSecTransform.ENCAP_NONE; + private int mEncapSocketResourceId = IpSecManager.INVALID_RESOURCE_ID; + private int mEncapRemotePort; + + // An interval, in seconds between the NattKeepalive packets + private int mNattKeepaliveInterval; + + // XFRM mark and mask; defaults to 0 (no mark/mask) + private int mMarkValue; + private int mMarkMask; + + // XFRM interface id + private int mXfrmInterfaceId; + + /** Set the mode for this IPsec transform */ + public void setMode(int mode) { + mMode = mode; + } + + /** Set the source IP addres for this IPsec transform */ + public void setSourceAddress(String sourceAddress) { + mSourceAddress = sourceAddress; + } + + /** Set the destination IP address for this IPsec transform */ + public void setDestinationAddress(String destinationAddress) { + mDestinationAddress = destinationAddress; + } + + /** Set the SPI by resource ID */ + public void setSpiResourceId(int resourceId) { + mSpiResourceId = resourceId; + } + + /** Set the encryption algorithm */ + public void setEncryption(IpSecAlgorithm encryption) { + mEncryption = encryption; + } + + /** Set the authentication algorithm */ + public void setAuthentication(IpSecAlgorithm authentication) { + mAuthentication = authentication; + } + + /** Set the authenticated encryption algorithm */ + public void setAuthenticatedEncryption(IpSecAlgorithm authenticatedEncryption) { + mAuthenticatedEncryption = authenticatedEncryption; + } + + /** Set the underlying network that will carry traffic for this transform */ + public void setNetwork(Network network) { + mNetwork = network; + } + + public void setEncapType(int encapType) { + mEncapType = encapType; + } + + public void setEncapSocketResourceId(int resourceId) { + mEncapSocketResourceId = resourceId; + } + + public void setEncapRemotePort(int port) { + mEncapRemotePort = port; + } + + public void setNattKeepaliveInterval(int interval) { + mNattKeepaliveInterval = interval; + } + + /** + * Sets the mark value + * + *

Internal (System server) use only. Marks passed in by users will be overwritten or + * ignored. + */ + public void setMarkValue(int mark) { + mMarkValue = mark; + } + + /** + * Sets the mark mask + * + *

Internal (System server) use only. Marks passed in by users will be overwritten or + * ignored. + */ + public void setMarkMask(int mask) { + mMarkMask = mask; + } + + public void setXfrmInterfaceId(int xfrmInterfaceId) { + mXfrmInterfaceId = xfrmInterfaceId; + } + + // Transport or Tunnel + public int getMode() { + return mMode; + } + + public String getSourceAddress() { + return mSourceAddress; + } + + public int getSpiResourceId() { + return mSpiResourceId; + } + + public String getDestinationAddress() { + return mDestinationAddress; + } + + public IpSecAlgorithm getEncryption() { + return mEncryption; + } + + public IpSecAlgorithm getAuthentication() { + return mAuthentication; + } + + public IpSecAlgorithm getAuthenticatedEncryption() { + return mAuthenticatedEncryption; + } + + public Network getNetwork() { + return mNetwork; + } + + public int getEncapType() { + return mEncapType; + } + + public int getEncapSocketResourceId() { + return mEncapSocketResourceId; + } + + public int getEncapRemotePort() { + return mEncapRemotePort; + } + + public int getNattKeepaliveInterval() { + return mNattKeepaliveInterval; + } + + public int getMarkValue() { + return mMarkValue; + } + + public int getMarkMask() { + return mMarkMask; + } + + public int getXfrmInterfaceId() { + return mXfrmInterfaceId; + } + + // Parcelable Methods + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mMode); + out.writeString(mSourceAddress); + out.writeString(mDestinationAddress); + out.writeParcelable(mNetwork, flags); + out.writeInt(mSpiResourceId); + out.writeParcelable(mEncryption, flags); + out.writeParcelable(mAuthentication, flags); + out.writeParcelable(mAuthenticatedEncryption, flags); + out.writeInt(mEncapType); + out.writeInt(mEncapSocketResourceId); + out.writeInt(mEncapRemotePort); + out.writeInt(mNattKeepaliveInterval); + out.writeInt(mMarkValue); + out.writeInt(mMarkMask); + out.writeInt(mXfrmInterfaceId); + } + + @VisibleForTesting + public IpSecConfig() {} + + /** Copy constructor */ + @VisibleForTesting + public IpSecConfig(IpSecConfig c) { + mMode = c.mMode; + mSourceAddress = c.mSourceAddress; + mDestinationAddress = c.mDestinationAddress; + mNetwork = c.mNetwork; + mSpiResourceId = c.mSpiResourceId; + mEncryption = c.mEncryption; + mAuthentication = c.mAuthentication; + mAuthenticatedEncryption = c.mAuthenticatedEncryption; + mEncapType = c.mEncapType; + mEncapSocketResourceId = c.mEncapSocketResourceId; + mEncapRemotePort = c.mEncapRemotePort; + mNattKeepaliveInterval = c.mNattKeepaliveInterval; + mMarkValue = c.mMarkValue; + mMarkMask = c.mMarkMask; + mXfrmInterfaceId = c.mXfrmInterfaceId; + } + + private IpSecConfig(Parcel in) { + mMode = in.readInt(); + mSourceAddress = in.readString(); + mDestinationAddress = in.readString(); + mNetwork = (Network) in.readParcelable(Network.class.getClassLoader()); + mSpiResourceId = in.readInt(); + mEncryption = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); + mAuthentication = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); + mAuthenticatedEncryption = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader()); + mEncapType = in.readInt(); + mEncapSocketResourceId = in.readInt(); + mEncapRemotePort = in.readInt(); + mNattKeepaliveInterval = in.readInt(); + mMarkValue = in.readInt(); + mMarkMask = in.readInt(); + mXfrmInterfaceId = in.readInt(); + } + + @Override + public String toString() { + StringBuilder strBuilder = new StringBuilder(); + strBuilder + .append("{mMode=") + .append(mMode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT") + .append(", mSourceAddress=") + .append(mSourceAddress) + .append(", mDestinationAddress=") + .append(mDestinationAddress) + .append(", mNetwork=") + .append(mNetwork) + .append(", mEncapType=") + .append(mEncapType) + .append(", mEncapSocketResourceId=") + .append(mEncapSocketResourceId) + .append(", mEncapRemotePort=") + .append(mEncapRemotePort) + .append(", mNattKeepaliveInterval=") + .append(mNattKeepaliveInterval) + .append("{mSpiResourceId=") + .append(mSpiResourceId) + .append(", mEncryption=") + .append(mEncryption) + .append(", mAuthentication=") + .append(mAuthentication) + .append(", mAuthenticatedEncryption=") + .append(mAuthenticatedEncryption) + .append(", mMarkValue=") + .append(mMarkValue) + .append(", mMarkMask=") + .append(mMarkMask) + .append(", mXfrmInterfaceId=") + .append(mXfrmInterfaceId) + .append("}"); + + return strBuilder.toString(); + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public IpSecConfig createFromParcel(Parcel in) { + return new IpSecConfig(in); + } + + public IpSecConfig[] newArray(int size) { + return new IpSecConfig[size]; + } + }; + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof IpSecConfig)) return false; + final IpSecConfig rhs = (IpSecConfig) other; + return (mMode == rhs.mMode + && mSourceAddress.equals(rhs.mSourceAddress) + && mDestinationAddress.equals(rhs.mDestinationAddress) + && ((mNetwork != null && mNetwork.equals(rhs.mNetwork)) + || (mNetwork == rhs.mNetwork)) + && mEncapType == rhs.mEncapType + && mEncapSocketResourceId == rhs.mEncapSocketResourceId + && mEncapRemotePort == rhs.mEncapRemotePort + && mNattKeepaliveInterval == rhs.mNattKeepaliveInterval + && mSpiResourceId == rhs.mSpiResourceId + && IpSecAlgorithm.equals(mEncryption, rhs.mEncryption) + && IpSecAlgorithm.equals(mAuthenticatedEncryption, rhs.mAuthenticatedEncryption) + && IpSecAlgorithm.equals(mAuthentication, rhs.mAuthentication) + && mMarkValue == rhs.mMarkValue + && mMarkMask == rhs.mMarkMask + && mXfrmInterfaceId == rhs.mXfrmInterfaceId); + } +} diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java new file mode 100644 index 0000000000..9cb0947b23 --- /dev/null +++ b/framework-t/src/android/net/IpSecManager.java @@ -0,0 +1,1065 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresFeature; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.annotation.TestApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.system.ErrnoException; +import android.system.OsConstants; +import android.util.AndroidException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import dalvik.system.CloseGuard; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; +import java.util.Objects; + +/** + * This class contains methods for managing IPsec sessions. Once configured, the kernel will apply + * confidentiality (encryption) and integrity (authentication) to IP traffic. + * + *

Note that not all aspects of IPsec are permitted by this API. Applications may create + * transport mode security associations and apply them to individual sockets. Applications looking + * to create an IPsec VPN should use {@link VpnManager} and {@link Ikev2VpnProfile}. + * + * @see RFC 4301, Security Architecture for the + * Internet Protocol + */ +@SystemService(Context.IPSEC_SERVICE) +public class IpSecManager { + private static final String TAG = "IpSecManager"; + + /** + * Used when applying a transform to direct traffic through an {@link IpSecTransform} + * towards the host. + * + *

See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}. + */ + public static final int DIRECTION_IN = 0; + + /** + * Used when applying a transform to direct traffic through an {@link IpSecTransform} + * away from the host. + * + *

See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}. + */ + public static final int DIRECTION_OUT = 1; + + /** + * Used when applying a transform to direct traffic through an {@link IpSecTransform} for + * forwarding between interfaces. + * + *

See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int DIRECTION_FWD = 2; + + /** @hide */ + @IntDef(value = {DIRECTION_IN, DIRECTION_OUT}) + @Retention(RetentionPolicy.SOURCE) + public @interface PolicyDirection {} + + /** + * The Security Parameter Index (SPI) 0 indicates an unknown or invalid index. + * + *

No IPsec packet may contain an SPI of 0. + * + * @hide + */ + @TestApi public static final int INVALID_SECURITY_PARAMETER_INDEX = 0; + + /** @hide */ + public interface Status { + int OK = 0; + int RESOURCE_UNAVAILABLE = 1; + int SPI_UNAVAILABLE = 2; + } + + /** @hide */ + public static final int INVALID_RESOURCE_ID = -1; + + /** + * Thrown to indicate that a requested SPI is in use. + * + *

The combination of remote {@code InetAddress} and SPI must be unique across all apps on + * one device. If this error is encountered, a new SPI is required before a transform may be + * created. This error can be avoided by calling {@link + * IpSecManager#allocateSecurityParameterIndex}. + */ + public static final class SpiUnavailableException extends AndroidException { + private final int mSpi; + + /** + * Construct an exception indicating that a transform with the given SPI is already in use + * or otherwise unavailable. + * + * @param msg description indicating the colliding SPI + * @param spi the SPI that could not be used due to a collision + */ + SpiUnavailableException(String msg, int spi) { + super(msg + " (spi: " + spi + ")"); + mSpi = spi; + } + + /** Get the SPI that caused a collision. */ + public int getSpi() { + return mSpi; + } + } + + /** + * Thrown to indicate that an IPsec resource is unavailable. + * + *

This could apply to resources such as sockets, {@link SecurityParameterIndex}, {@link + * IpSecTransform}, or other system resources. If this exception is thrown, users should release + * allocated objects of the type requested. + */ + public static final class ResourceUnavailableException extends AndroidException { + + ResourceUnavailableException(String msg) { + super(msg); + } + } + + private final Context mContext; + private final IIpSecService mService; + + /** + * This class represents a reserved SPI. + * + *

Objects of this type are used to track reserved security parameter indices. They can be + * obtained by calling {@link IpSecManager#allocateSecurityParameterIndex} and must be released + * by calling {@link #close()} when they are no longer needed. + */ + public static final class SecurityParameterIndex implements AutoCloseable { + private final IIpSecService mService; + private final InetAddress mDestinationAddress; + private final CloseGuard mCloseGuard = CloseGuard.get(); + private int mSpi = INVALID_SECURITY_PARAMETER_INDEX; + private int mResourceId = INVALID_RESOURCE_ID; + + /** Get the underlying SPI held by this object. */ + public int getSpi() { + return mSpi; + } + + /** + * Release an SPI that was previously reserved. + * + *

Release an SPI for use by other users in the system. If a SecurityParameterIndex is + * applied to an IpSecTransform, it will become unusable for future transforms but should + * still be closed to ensure system resources are released. + */ + @Override + public void close() { + try { + mService.releaseSecurityParameterIndex(mResourceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (Exception e) { + // On close we swallow all random exceptions since failure to close is not + // actionable by the user. + Log.e(TAG, "Failed to close " + this + ", Exception=" + e); + } finally { + mResourceId = INVALID_RESOURCE_ID; + mCloseGuard.close(); + } + } + + /** Check that the SPI was closed properly. */ + @Override + protected void finalize() throws Throwable { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + + close(); + } + + private SecurityParameterIndex( + @NonNull IIpSecService service, InetAddress destinationAddress, int spi) + throws ResourceUnavailableException, SpiUnavailableException { + mService = service; + mDestinationAddress = destinationAddress; + try { + IpSecSpiResponse result = + mService.allocateSecurityParameterIndex( + destinationAddress.getHostAddress(), spi, new Binder()); + + if (result == null) { + throw new NullPointerException("Received null response from IpSecService"); + } + + int status = result.status; + switch (status) { + case Status.OK: + break; + case Status.RESOURCE_UNAVAILABLE: + throw new ResourceUnavailableException( + "No more SPIs may be allocated by this requester."); + case Status.SPI_UNAVAILABLE: + throw new SpiUnavailableException("Requested SPI is unavailable", spi); + default: + throw new RuntimeException( + "Unknown status returned by IpSecService: " + status); + } + mSpi = result.spi; + mResourceId = result.resourceId; + + if (mSpi == INVALID_SECURITY_PARAMETER_INDEX) { + throw new RuntimeException("Invalid SPI returned by IpSecService: " + status); + } + + if (mResourceId == INVALID_RESOURCE_ID) { + throw new RuntimeException( + "Invalid Resource ID returned by IpSecService: " + status); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mCloseGuard.open("open"); + } + + /** @hide */ + @VisibleForTesting + public int getResourceId() { + return mResourceId; + } + + @Override + public String toString() { + return new StringBuilder() + .append("SecurityParameterIndex{spi=") + .append(mSpi) + .append(",resourceId=") + .append(mResourceId) + .append("}") + .toString(); + } + } + + /** + * Reserve a random SPI for traffic bound to or from the specified destination address. + * + *

If successful, this SPI is guaranteed available until released by a call to {@link + * SecurityParameterIndex#close()}. + * + * @param destinationAddress the destination address for traffic bearing the requested SPI. + * For inbound traffic, the destination should be an address currently assigned on-device. + * @return the reserved SecurityParameterIndex + * @throws ResourceUnavailableException indicating that too many SPIs are + * currently allocated for this user + */ + @NonNull + public SecurityParameterIndex allocateSecurityParameterIndex( + @NonNull InetAddress destinationAddress) throws ResourceUnavailableException { + try { + return new SecurityParameterIndex( + mService, + destinationAddress, + IpSecManager.INVALID_SECURITY_PARAMETER_INDEX); + } catch (ServiceSpecificException e) { + throw rethrowUncheckedExceptionFromServiceSpecificException(e); + } catch (SpiUnavailableException unlikely) { + // Because this function allocates a totally random SPI, it really shouldn't ever + // fail to allocate an SPI; we simply need this because the exception is checked. + throw new ResourceUnavailableException("No SPIs available"); + } + } + + /** + * Reserve the requested SPI for traffic bound to or from the specified destination address. + * + *

If successful, this SPI is guaranteed available until released by a call to {@link + * SecurityParameterIndex#close()}. + * + * @param destinationAddress the destination address for traffic bearing the requested SPI. + * For inbound traffic, the destination should be an address currently assigned on-device. + * @param requestedSpi the requested SPI. The range 1-255 is reserved and may not be used. See + * RFC 4303 Section 2.1. + * @return the reserved SecurityParameterIndex + * @throws ResourceUnavailableException indicating that too many SPIs are + * currently allocated for this user + * @throws SpiUnavailableException indicating that the requested SPI could not be + * reserved + */ + @NonNull + public SecurityParameterIndex allocateSecurityParameterIndex( + @NonNull InetAddress destinationAddress, int requestedSpi) + throws SpiUnavailableException, ResourceUnavailableException { + if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) { + throw new IllegalArgumentException("Requested SPI must be a valid (non-zero) SPI"); + } + try { + return new SecurityParameterIndex(mService, destinationAddress, requestedSpi); + } catch (ServiceSpecificException e) { + throw rethrowUncheckedExceptionFromServiceSpecificException(e); + } + } + + /** + * Apply an IPsec transform to a stream socket. + * + *

This applies transport mode encapsulation to the given socket. Once applied, I/O on the + * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When + * the transform is removed from the socket by calling {@link #removeTransportModeTransforms}, + * unprotected traffic can resume on that socket. + * + *

For security reasons, the destination address of any traffic on the socket must match the + * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any + * other IP address will result in an IOException. In addition, reads and writes on the socket + * will throw IOException if the user deactivates the transform (by calling {@link + * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}. + * + *

Note that when applied to TCP sockets, calling {@link IpSecTransform#close()} on an + * applied transform before completion of graceful shutdown may result in the shutdown sequence + * failing to complete. As such, applications requiring graceful shutdown MUST close the socket + * prior to deactivating the applied transform. Socket closure may be performed asynchronously + * (in batches), so the returning of a close function does not guarantee shutdown of a socket. + * Setting an SO_LINGER timeout results in socket closure being performed synchronously, and is + * sufficient to ensure shutdown. + * + * Specifically, if the transform is deactivated (by calling {@link IpSecTransform#close()}), + * prior to the socket being closed, the standard [FIN - FIN/ACK - ACK], or the reset [RST] + * packets are dropped due to the lack of a valid Transform. Similarly, if a socket without the + * SO_LINGER option set is closed, the delayed/batched FIN packets may be dropped. + * + *

Rekey Procedure

+ * + *

When applying a new tranform to a socket in the outbound direction, the previous transform + * will be removed and the new transform will take effect immediately, sending all traffic on + * the new transform; however, when applying a transform in the inbound direction, traffic + * on the old transform will continue to be decrypted and delivered until that transform is + * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey + * procedures where both transforms are valid until both endpoints are using the new transform + * and all in-flight packets have been received. + * + * @param socket a stream socket + * @param direction the direction in which the transform should be applied + * @param transform a transport mode {@code IpSecTransform} + * @throws IOException indicating that the transform could not be applied + */ + public void applyTransportModeTransform(@NonNull Socket socket, + @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { + // Ensure creation of FD. See b/77548890 for more details. + socket.getSoLinger(); + + applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform); + } + + /** + * Apply an IPsec transform to a datagram socket. + * + *

This applies transport mode encapsulation to the given socket. Once applied, I/O on the + * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When + * the transform is removed from the socket by calling {@link #removeTransportModeTransforms}, + * unprotected traffic can resume on that socket. + * + *

For security reasons, the destination address of any traffic on the socket must match the + * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any + * other IP address will result in an IOException. In addition, reads and writes on the socket + * will throw IOException if the user deactivates the transform (by calling {@link + * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}. + * + *

Rekey Procedure

+ * + *

When applying a new tranform to a socket in the outbound direction, the previous transform + * will be removed and the new transform will take effect immediately, sending all traffic on + * the new transform; however, when applying a transform in the inbound direction, traffic + * on the old transform will continue to be decrypted and delivered until that transform is + * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey + * procedures where both transforms are valid until both endpoints are using the new transform + * and all in-flight packets have been received. + * + * @param socket a datagram socket + * @param direction the direction in which the transform should be applied + * @param transform a transport mode {@code IpSecTransform} + * @throws IOException indicating that the transform could not be applied + */ + public void applyTransportModeTransform(@NonNull DatagramSocket socket, + @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { + applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform); + } + + /** + * Apply an IPsec transform to a socket. + * + *

This applies transport mode encapsulation to the given socket. Once applied, I/O on the + * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When + * the transform is removed from the socket by calling {@link #removeTransportModeTransforms}, + * unprotected traffic can resume on that socket. + * + *

For security reasons, the destination address of any traffic on the socket must match the + * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any + * other IP address will result in an IOException. In addition, reads and writes on the socket + * will throw IOException if the user deactivates the transform (by calling {@link + * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}. + * + *

Note that when applied to TCP sockets, calling {@link IpSecTransform#close()} on an + * applied transform before completion of graceful shutdown may result in the shutdown sequence + * failing to complete. As such, applications requiring graceful shutdown MUST close the socket + * prior to deactivating the applied transform. Socket closure may be performed asynchronously + * (in batches), so the returning of a close function does not guarantee shutdown of a socket. + * Setting an SO_LINGER timeout results in socket closure being performed synchronously, and is + * sufficient to ensure shutdown. + * + * Specifically, if the transform is deactivated (by calling {@link IpSecTransform#close()}), + * prior to the socket being closed, the standard [FIN - FIN/ACK - ACK], or the reset [RST] + * packets are dropped due to the lack of a valid Transform. Similarly, if a socket without the + * SO_LINGER option set is closed, the delayed/batched FIN packets may be dropped. + * + *

Rekey Procedure

+ * + *

When applying a new tranform to a socket in the outbound direction, the previous transform + * will be removed and the new transform will take effect immediately, sending all traffic on + * the new transform; however, when applying a transform in the inbound direction, traffic + * on the old transform will continue to be decrypted and delivered until that transform is + * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey + * procedures where both transforms are valid until both endpoints are using the new transform + * and all in-flight packets have been received. + * + * @param socket a socket file descriptor + * @param direction the direction in which the transform should be applied + * @param transform a transport mode {@code IpSecTransform} + * @throws IOException indicating that the transform could not be applied + */ + public void applyTransportModeTransform(@NonNull FileDescriptor socket, + @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { + // We dup() the FileDescriptor here because if we don't, then the ParcelFileDescriptor() + // constructor takes control and closes the user's FD when we exit the method. + try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) { + mService.applyTransportModeTransform(pfd, direction, transform.getResourceId()); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove an IPsec transform from a stream socket. + * + *

Once removed, traffic on the socket will not be encrypted. Removing transforms from a + * socket allows the socket to be reused for communication in the clear. + * + *

If an {@code IpSecTransform} object applied to this socket was deallocated by calling + * {@link IpSecTransform#close()}, then communication on the socket will fail until this method + * is called. + * + * @param socket a socket that previously had a transform applied to it + * @throws IOException indicating that the transform could not be removed from the socket + */ + public void removeTransportModeTransforms(@NonNull Socket socket) throws IOException { + // Ensure creation of FD. See b/77548890 for more details. + socket.getSoLinger(); + + removeTransportModeTransforms(socket.getFileDescriptor$()); + } + + /** + * Remove an IPsec transform from a datagram socket. + * + *

Once removed, traffic on the socket will not be encrypted. Removing transforms from a + * socket allows the socket to be reused for communication in the clear. + * + *

If an {@code IpSecTransform} object applied to this socket was deallocated by calling + * {@link IpSecTransform#close()}, then communication on the socket will fail until this method + * is called. + * + * @param socket a socket that previously had a transform applied to it + * @throws IOException indicating that the transform could not be removed from the socket + */ + public void removeTransportModeTransforms(@NonNull DatagramSocket socket) throws IOException { + removeTransportModeTransforms(socket.getFileDescriptor$()); + } + + /** + * Remove an IPsec transform from a socket. + * + *

Once removed, traffic on the socket will not be encrypted. Removing transforms from a + * socket allows the socket to be reused for communication in the clear. + * + *

If an {@code IpSecTransform} object applied to this socket was deallocated by calling + * {@link IpSecTransform#close()}, then communication on the socket will fail until this method + * is called. + * + * @param socket a socket that previously had a transform applied to it + * @throws IOException indicating that the transform could not be removed from the socket + */ + public void removeTransportModeTransforms(@NonNull FileDescriptor socket) throws IOException { + try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) { + mService.removeTransportModeTransforms(pfd); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove a Tunnel Mode IPsec Transform from a {@link Network}. This must be used as part of + * cleanup if a tunneled Network experiences a change in default route. The Network will drop + * all traffic that cannot be routed to the Tunnel's outbound interface. If that interface is + * lost, all traffic will drop. + * + *

TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked. + * + * @param net a network that currently has transform applied to it. + * @param transform a Tunnel Mode IPsec Transform that has been previously applied to the given + * network + * @hide + */ + public void removeTunnelModeTransform(Network net, IpSecTransform transform) {} + + /** + * This class provides access to a UDP encapsulation Socket. + * + *

{@code UdpEncapsulationSocket} wraps a system-provided datagram socket intended for IKEv2 + * signalling and UDP encapsulated IPsec traffic. Instances can be obtained by calling {@link + * IpSecManager#openUdpEncapsulationSocket}. The provided socket cannot be re-bound by the + * caller. The caller should not close the {@code FileDescriptor} returned by {@link + * #getFileDescriptor}, but should use {@link #close} instead. + * + *

Allowing the user to close or unbind a UDP encapsulation socket could impact the traffic + * of the next user who binds to that port. To prevent this scenario, these sockets are held + * open by the system so that they may only be closed by calling {@link #close} or when the user + * process exits. + */ + public static final class UdpEncapsulationSocket implements AutoCloseable { + private final ParcelFileDescriptor mPfd; + private final IIpSecService mService; + private int mResourceId = INVALID_RESOURCE_ID; + private final int mPort; + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private UdpEncapsulationSocket(@NonNull IIpSecService service, int port) + throws ResourceUnavailableException, IOException { + mService = service; + try { + IpSecUdpEncapResponse result = + mService.openUdpEncapsulationSocket(port, new Binder()); + switch (result.status) { + case Status.OK: + break; + case Status.RESOURCE_UNAVAILABLE: + throw new ResourceUnavailableException( + "No more Sockets may be allocated by this requester."); + default: + throw new RuntimeException( + "Unknown status returned by IpSecService: " + result.status); + } + mResourceId = result.resourceId; + mPort = result.port; + mPfd = result.fileDescriptor; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mCloseGuard.open("constructor"); + } + + /** Get the encapsulation socket's file descriptor. */ + public FileDescriptor getFileDescriptor() { + if (mPfd == null) { + return null; + } + return mPfd.getFileDescriptor(); + } + + /** Get the bound port of the wrapped socket. */ + public int getPort() { + return mPort; + } + + /** + * Close this socket. + * + *

This closes the wrapped socket. Open encapsulation sockets count against a user's + * resource limits, and forgetting to close them eventually will result in {@link + * ResourceUnavailableException} being thrown. + */ + @Override + public void close() throws IOException { + try { + mService.closeUdpEncapsulationSocket(mResourceId); + mResourceId = INVALID_RESOURCE_ID; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (Exception e) { + // On close we swallow all random exceptions since failure to close is not + // actionable by the user. + Log.e(TAG, "Failed to close " + this + ", Exception=" + e); + } finally { + mResourceId = INVALID_RESOURCE_ID; + mCloseGuard.close(); + } + + try { + mPfd.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close UDP Encapsulation Socket with Port= " + mPort); + throw e; + } + } + + /** Check that the socket was closed properly. */ + @Override + protected void finalize() throws Throwable { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } + + /** @hide */ + @SystemApi(client = MODULE_LIBRARIES) + public int getResourceId() { + return mResourceId; + } + + @Override + public String toString() { + return new StringBuilder() + .append("UdpEncapsulationSocket{port=") + .append(mPort) + .append(",resourceId=") + .append(mResourceId) + .append("}") + .toString(); + } + }; + + /** + * Open a socket for UDP encapsulation and bind to the given port. + * + *

See {@link UdpEncapsulationSocket} for the proper way to close the returned socket. + * + * @param port a local UDP port + * @return a socket that is bound to the given port + * @throws IOException indicating that the socket could not be opened or bound + * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open + */ + // Returning a socket in this fashion that has been created and bound by the system + // is the only safe way to ensure that a socket is both accessible to the user and + // safely usable for Encapsulation without allowing a user to possibly unbind from/close + // the port, which could potentially impact the traffic of the next user who binds to that + // socket. + @NonNull + public UdpEncapsulationSocket openUdpEncapsulationSocket(int port) + throws IOException, ResourceUnavailableException { + /* + * Most range checking is done in the service, but this version of the constructor expects + * a valid port number, and zero cannot be checked after being passed to the service. + */ + if (port == 0) { + throw new IllegalArgumentException("Specified port must be a valid port number!"); + } + try { + return new UdpEncapsulationSocket(mService, port); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } + } + + /** + * Open a socket for UDP encapsulation. + * + *

See {@link UdpEncapsulationSocket} for the proper way to close the returned socket. + * + *

The local port of the returned socket can be obtained by calling {@link + * UdpEncapsulationSocket#getPort()}. + * + * @return a socket that is bound to a local port + * @throws IOException indicating that the socket could not be opened or bound + * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open + */ + // Returning a socket in this fashion that has been created and bound by the system + // is the only safe way to ensure that a socket is both accessible to the user and + // safely usable for Encapsulation without allowing a user to possibly unbind from/close + // the port, which could potentially impact the traffic of the next user who binds to that + // socket. + @NonNull + public UdpEncapsulationSocket openUdpEncapsulationSocket() + throws IOException, ResourceUnavailableException { + try { + return new UdpEncapsulationSocket(mService, 0); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } + } + + /** + * This class represents an IpSecTunnelInterface + * + *

IpSecTunnelInterface objects track tunnel interfaces that serve as + * local endpoints for IPsec tunnels. + * + *

Creating an IpSecTunnelInterface creates a device to which IpSecTransforms may be + * applied to provide IPsec security to packets sent through the tunnel. While a tunnel + * cannot be used in standalone mode within Android, the higher layers may use the tunnel + * to create Network objects which are accessible to the Android system. + * @hide + */ + @SystemApi + public static final class IpSecTunnelInterface implements AutoCloseable { + private final String mOpPackageName; + private final IIpSecService mService; + private final InetAddress mRemoteAddress; + private final InetAddress mLocalAddress; + private final Network mUnderlyingNetwork; + private final CloseGuard mCloseGuard = CloseGuard.get(); + private String mInterfaceName; + private int mResourceId = INVALID_RESOURCE_ID; + + /** Get the underlying SPI held by this object. */ + @NonNull + public String getInterfaceName() { + return mInterfaceName; + } + + /** + * Add an address to the IpSecTunnelInterface + * + *

Add an address which may be used as the local inner address for + * tunneled traffic. + * + * @param address the local address for traffic inside the tunnel + * @param prefixLen length of the InetAddress prefix + * @hide + */ + @SystemApi + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) + public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException { + try { + mService.addAddressToTunnelInterface( + mResourceId, new LinkAddress(address, prefixLen), mOpPackageName); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove an address from the IpSecTunnelInterface + * + *

Remove an address which was previously added to the IpSecTunnelInterface + * + * @param address to be removed + * @param prefixLen length of the InetAddress prefix + * @hide + */ + @SystemApi + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) + public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException { + try { + mService.removeAddressFromTunnelInterface( + mResourceId, new LinkAddress(address, prefixLen), mOpPackageName); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Update the underlying network for this IpSecTunnelInterface. + * + *

This new underlying network will be used for all transforms applied AFTER this call is + * complete. Before new {@link IpSecTransform}(s) with matching addresses are applied to + * this tunnel interface, traffic will still use the old SA, and be routed on the old + * underlying network. + * + *

To migrate IPsec tunnel mode traffic, a caller should: + * + *

    + *
  1. Update the IpSecTunnelInterface’s underlying network. + *
  2. Apply {@link IpSecTransform}(s) with matching addresses to this + * IpSecTunnelInterface. + *
+ * + * @param underlyingNetwork the new {@link Network} that will carry traffic for this tunnel. + * This network MUST never be the network exposing this IpSecTunnelInterface, otherwise + * this method will throw an {@link IllegalArgumentException}. If the + * IpSecTunnelInterface is later added to this network, all outbound traffic will be + * blackholed. + */ + // TODO: b/169171001 Update the documentation when transform migration is supported. + // The purpose of making updating network and applying transforms separate is to leave open + // the possibility to support lossless migration procedures. To do that, Android platform + // will need to support multiple inbound tunnel mode transforms, just like it can support + // multiple transport mode transforms. + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) + public void setUnderlyingNetwork(@NonNull Network underlyingNetwork) throws IOException { + try { + mService.setNetworkForTunnelInterface( + mResourceId, underlyingNetwork, mOpPackageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private IpSecTunnelInterface(@NonNull Context ctx, @NonNull IIpSecService service, + @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress, + @NonNull Network underlyingNetwork) + throws ResourceUnavailableException, IOException { + mOpPackageName = ctx.getOpPackageName(); + mService = service; + mLocalAddress = localAddress; + mRemoteAddress = remoteAddress; + mUnderlyingNetwork = underlyingNetwork; + + try { + IpSecTunnelInterfaceResponse result = + mService.createTunnelInterface( + localAddress.getHostAddress(), + remoteAddress.getHostAddress(), + underlyingNetwork, + new Binder(), + mOpPackageName); + switch (result.status) { + case Status.OK: + break; + case Status.RESOURCE_UNAVAILABLE: + throw new ResourceUnavailableException( + "No more tunnel interfaces may be allocated by this requester."); + default: + throw new RuntimeException( + "Unknown status returned by IpSecService: " + result.status); + } + mResourceId = result.resourceId; + mInterfaceName = result.interfaceName; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mCloseGuard.open("constructor"); + } + + /** + * Delete an IpSecTunnelInterface + * + *

Calling close will deallocate the IpSecTunnelInterface and all of its system + * resources. Any packets bound for this interface either inbound or outbound will + * all be lost. + */ + @Override + public void close() { + try { + mService.deleteTunnelInterface(mResourceId, mOpPackageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (Exception e) { + // On close we swallow all random exceptions since failure to close is not + // actionable by the user. + Log.e(TAG, "Failed to close " + this + ", Exception=" + e); + } finally { + mResourceId = INVALID_RESOURCE_ID; + mCloseGuard.close(); + } + } + + /** Check that the Interface was closed properly. */ + @Override + protected void finalize() throws Throwable { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } + + /** @hide */ + @VisibleForTesting + public int getResourceId() { + return mResourceId; + } + + @NonNull + @Override + public String toString() { + return new StringBuilder() + .append("IpSecTunnelInterface{ifname=") + .append(mInterfaceName) + .append(",resourceId=") + .append(mResourceId) + .append("}") + .toString(); + } + } + + /** + * Create a new IpSecTunnelInterface as a local endpoint for tunneled IPsec traffic. + * + *

An application that creates tunnels is responsible for cleaning up the tunnel when the + * underlying network goes away, and the onLost() callback is received. + * + * @param localAddress The local addres of the tunnel + * @param remoteAddress The local addres of the tunnel + * @param underlyingNetwork the {@link Network} that will carry traffic for this tunnel. + * This network should almost certainly be a network such as WiFi with an L2 address. + * @return a new {@link IpSecManager#IpSecTunnelInterface} with the specified properties + * @throws IOException indicating that the socket could not be opened or bound + * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open + * @hide + */ + @SystemApi + @NonNull + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) + public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress, + @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork) + throws ResourceUnavailableException, IOException { + try { + return new IpSecTunnelInterface( + mContext, mService, localAddress, remoteAddress, underlyingNetwork); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } + } + + /** + * Apply an active Tunnel Mode IPsec Transform to a {@link IpSecTunnelInterface}, which will + * tunnel all traffic for the given direction through the underlying network's interface with + * IPsec (applies an outer IP header and IPsec Header to all traffic, and expects an additional + * IP header and IPsec Header on all inbound traffic). + *

Applications should probably not use this API directly. + * + * + * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied + * transform. + * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which + * the transform will be used. + * @param transform an {@link IpSecTransform} created in tunnel mode + * @throws IOException indicating that the transform could not be applied due to a lower + * layer failure. + * @hide + */ + @SystemApi + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) + public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel, + @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { + try { + mService.applyTunnelModeTransform( + tunnel.getResourceId(), direction, + transform.getResourceId(), mContext.getOpPackageName()); + } catch (ServiceSpecificException e) { + throw rethrowCheckedExceptionFromServiceSpecificException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public IpSecTransformResponse createTransform(IpSecConfig config, IBinder binder, + String callingPackage) { + try { + return mService.createTransform(config, binder, callingPackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + */ + public void deleteTransform(int resourceId) { + try { + mService.deleteTransform(resourceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Construct an instance of IpSecManager within an application context. + * + * @param context the application context for this manager + * @hide + */ + public IpSecManager(Context ctx, IIpSecService service) { + mContext = ctx; + mService = Objects.requireNonNull(service, "missing service"); + } + + private static void maybeHandleServiceSpecificException(ServiceSpecificException sse) { + // OsConstants are late binding, so switch statements can't be used. + if (sse.errorCode == OsConstants.EINVAL) { + throw new IllegalArgumentException(sse); + } else if (sse.errorCode == OsConstants.EAGAIN) { + throw new IllegalStateException(sse); + } else if (sse.errorCode == OsConstants.EOPNOTSUPP + || sse.errorCode == OsConstants.EPROTONOSUPPORT) { + throw new UnsupportedOperationException(sse); + } + } + + /** + * Convert an Errno SSE to the correct Unchecked exception type. + * + * This method never actually returns. + */ + // package + static RuntimeException + rethrowUncheckedExceptionFromServiceSpecificException(ServiceSpecificException sse) { + maybeHandleServiceSpecificException(sse); + throw new RuntimeException(sse); + } + + /** + * Convert an Errno SSE to the correct Checked or Unchecked exception type. + * + * This method may throw IOException, or it may throw an unchecked exception; it will never + * actually return. + */ + // package + static IOException rethrowCheckedExceptionFromServiceSpecificException( + ServiceSpecificException sse) throws IOException { + // First see if this is an unchecked exception of a type we know. + // If so, then we prefer the unchecked (specific) type of exception. + maybeHandleServiceSpecificException(sse); + // If not, then all we can do is provide the SSE in the form of an IOException. + throw new ErrnoException( + "IpSec encountered errno=" + sse.errorCode, sse.errorCode).rethrowAsIOException(); + } +} diff --git a/framework-t/src/android/net/IpSecSpiResponse.aidl b/framework-t/src/android/net/IpSecSpiResponse.aidl new file mode 100644 index 0000000000..6484a0013c --- /dev/null +++ b/framework-t/src/android/net/IpSecSpiResponse.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** @hide */ +parcelable IpSecSpiResponse; diff --git a/framework-t/src/android/net/IpSecSpiResponse.java b/framework-t/src/android/net/IpSecSpiResponse.java new file mode 100644 index 0000000000..f99e570fb7 --- /dev/null +++ b/framework-t/src/android/net/IpSecSpiResponse.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class is used to return an SPI and corresponding status from the IpSecService to an + * IpSecManager.SecurityParameterIndex. + * + * @hide + */ +public final class IpSecSpiResponse implements Parcelable { + private static final String TAG = "IpSecSpiResponse"; + + public final int resourceId; + public final int status; + public final int spi; + // Parcelable Methods + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(status); + out.writeInt(resourceId); + out.writeInt(spi); + } + + public IpSecSpiResponse(int inStatus, int inResourceId, int inSpi) { + status = inStatus; + resourceId = inResourceId; + spi = inSpi; + } + + public IpSecSpiResponse(int inStatus) { + if (inStatus == IpSecManager.Status.OK) { + throw new IllegalArgumentException("Valid status implies other args must be provided"); + } + status = inStatus; + resourceId = IpSecManager.INVALID_RESOURCE_ID; + spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; + } + + private IpSecSpiResponse(Parcel in) { + status = in.readInt(); + resourceId = in.readInt(); + spi = in.readInt(); + } + + public static final @android.annotation.NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public IpSecSpiResponse createFromParcel(Parcel in) { + return new IpSecSpiResponse(in); + } + + public IpSecSpiResponse[] newArray(int size) { + return new IpSecSpiResponse[size]; + } + }; +} diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java new file mode 100644 index 0000000000..68ae5de4ee --- /dev/null +++ b/framework-t/src/android/net/IpSecTransform.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import static android.net.IpSecManager.INVALID_RESOURCE_ID; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresFeature; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import dalvik.system.CloseGuard; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.util.Objects; + +/** + * This class represents a transform, which roughly corresponds to an IPsec Security Association. + * + *

Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform} + * object encapsulates the properties and state of an IPsec security association. That includes, + * but is not limited to, algorithm choice, key material, and allocated system resources. + * + * @see RFC 4301, Security Architecture for the + * Internet Protocol + */ +public final class IpSecTransform implements AutoCloseable { + private static final String TAG = "IpSecTransform"; + + /** @hide */ + public static final int MODE_TRANSPORT = 0; + + /** @hide */ + public static final int MODE_TUNNEL = 1; + + /** @hide */ + public static final int ENCAP_NONE = 0; + + /** + * IPsec traffic will be encapsulated within UDP, but with 8 zero-value bytes between the UDP + * header and payload. This prevents traffic from being interpreted as ESP or IKEv2. + * + * @hide + */ + public static final int ENCAP_ESPINUDP_NON_IKE = 1; + + /** + * IPsec traffic will be encapsulated within UDP as per + * RFC 3498. + * + * @hide + */ + public static final int ENCAP_ESPINUDP = 2; + + /** @hide */ + @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE}) + @Retention(RetentionPolicy.SOURCE) + public @interface EncapType {} + + /** @hide */ + @VisibleForTesting + public IpSecTransform(Context context, IpSecConfig config) { + mContext = context; + mConfig = new IpSecConfig(config); + mResourceId = INVALID_RESOURCE_ID; + } + + private IpSecManager getIpSecManager(Context context) { + return context.getSystemService(IpSecManager.class); + } + /** + * Checks the result status and throws an appropriate exception if the status is not Status.OK. + */ + private void checkResultStatus(int status) + throws IOException, IpSecManager.ResourceUnavailableException, + IpSecManager.SpiUnavailableException { + switch (status) { + case IpSecManager.Status.OK: + return; + // TODO: Pass Error string back from bundle so that errors can be more specific + case IpSecManager.Status.RESOURCE_UNAVAILABLE: + throw new IpSecManager.ResourceUnavailableException( + "Failed to allocate a new IpSecTransform"); + case IpSecManager.Status.SPI_UNAVAILABLE: + Log.wtf(TAG, "Attempting to use an SPI that was somehow not reserved"); + // Fall through + default: + throw new IllegalStateException( + "Failed to Create a Transform with status code " + status); + } + } + + private IpSecTransform activate() + throws IOException, IpSecManager.ResourceUnavailableException, + IpSecManager.SpiUnavailableException { + synchronized (this) { + try { + IpSecTransformResponse result = getIpSecManager(mContext).createTransform( + mConfig, new Binder(), mContext.getOpPackageName()); + int status = result.status; + checkResultStatus(status); + mResourceId = result.resourceId; + Log.d(TAG, "Added Transform with Id " + mResourceId); + mCloseGuard.open("build"); + } catch (ServiceSpecificException e) { + throw IpSecManager.rethrowUncheckedExceptionFromServiceSpecificException(e); + } + } + + return this; + } + + /** + * Standard equals. + */ + public boolean equals(@Nullable Object other) { + if (this == other) return true; + if (!(other instanceof IpSecTransform)) return false; + final IpSecTransform rhs = (IpSecTransform) other; + return getConfig().equals(rhs.getConfig()) && mResourceId == rhs.mResourceId; + } + + /** + * Deactivate this {@code IpSecTransform} and free allocated resources. + * + *

Deactivating a transform while it is still applied to a socket will result in errors on + * that socket. Make sure to remove transforms by calling {@link + * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a + * socket will not deactivate it (because one transform may be applied to multiple sockets). + * + *

It is safe to call this method on a transform that has already been deactivated. + */ + public void close() { + Log.d(TAG, "Removing Transform with Id " + mResourceId); + + // Always safe to attempt cleanup + if (mResourceId == INVALID_RESOURCE_ID) { + mCloseGuard.close(); + return; + } + try { + getIpSecManager(mContext).deleteTransform(mResourceId); + } catch (Exception e) { + // On close we swallow all random exceptions since failure to close is not + // actionable by the user. + Log.e(TAG, "Failed to close " + this + ", Exception=" + e); + } finally { + mResourceId = INVALID_RESOURCE_ID; + mCloseGuard.close(); + } + } + + /** Check that the transform was closed properly. */ + @Override + protected void finalize() throws Throwable { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } + + /* Package */ + IpSecConfig getConfig() { + return mConfig; + } + + private final IpSecConfig mConfig; + private int mResourceId; + private final Context mContext; + private final CloseGuard mCloseGuard = CloseGuard.get(); + + /** @hide */ + @VisibleForTesting + public int getResourceId() { + return mResourceId; + } + + /** + * A callback class to provide status information regarding a NAT-T keepalive session + * + *

Use this callback to receive status information regarding a NAT-T keepalive session + * by registering it when calling {@link #startNattKeepalive}. + * + * @hide + */ + public static class NattKeepaliveCallback { + /** The specified {@code Network} is not connected. */ + public static final int ERROR_INVALID_NETWORK = 1; + /** The hardware does not support this request. */ + public static final int ERROR_HARDWARE_UNSUPPORTED = 2; + /** The hardware returned an error. */ + public static final int ERROR_HARDWARE_ERROR = 3; + + /** The requested keepalive was successfully started. */ + public void onStarted() {} + /** The keepalive was successfully stopped. */ + public void onStopped() {} + /** An error occurred. */ + public void onError(int error) {} + } + + /** This class is used to build {@link IpSecTransform} objects. */ + public static class Builder { + private Context mContext; + private IpSecConfig mConfig; + + /** + * Set the encryption algorithm. + * + *

Encryption is mutually exclusive with authenticated encryption. + * + * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied. + */ + @NonNull + public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) { + // TODO: throw IllegalArgumentException if algo is not an encryption algorithm. + Objects.requireNonNull(algo); + mConfig.setEncryption(algo); + return this; + } + + /** + * Set the authentication (integrity) algorithm. + * + *

Authentication is mutually exclusive with authenticated encryption. + * + * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied. + */ + @NonNull + public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) { + // TODO: throw IllegalArgumentException if algo is not an authentication algorithm. + Objects.requireNonNull(algo); + mConfig.setAuthentication(algo); + return this; + } + + /** + * Set the authenticated encryption algorithm. + * + *

The Authenticated Encryption (AE) class of algorithms are also known as + * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode + * algorithms (as referred to in + * RFC 4301). + * + *

Authenticated encryption is mutually exclusive with encryption and authentication. + * + * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to + * be applied. + */ + @NonNull + public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) { + Objects.requireNonNull(algo); + mConfig.setAuthenticatedEncryption(algo); + return this; + } + + /** + * Add UDP encapsulation to an IPv4 transform. + * + *

This allows IPsec traffic to pass through a NAT. + * + * @see RFC 3948, UDP Encapsulation of IPsec + * ESP Packets + * @see RFC 7296 section 2.23, + * NAT Traversal of IKEv2 + * @param localSocket a socket for sending and receiving encapsulated traffic + * @param remotePort the UDP port number of the remote host that will send and receive + * encapsulated traffic. In the case of IKEv2, this should be port 4500. + */ + @NonNull + public IpSecTransform.Builder setIpv4Encapsulation( + @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) { + Objects.requireNonNull(localSocket); + mConfig.setEncapType(ENCAP_ESPINUDP); + if (localSocket.getResourceId() == INVALID_RESOURCE_ID) { + throw new IllegalArgumentException("Invalid UdpEncapsulationSocket"); + } + mConfig.setEncapSocketResourceId(localSocket.getResourceId()); + mConfig.setEncapRemotePort(remotePort); + return this; + } + + /** + * Build a transport mode {@link IpSecTransform}. + * + *

This builds and activates a transport mode transform. Note that an active transform + * will not affect any network traffic until it has been applied to one or more sockets. + * + * @see IpSecManager#applyTransportModeTransform + * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use + * this transform; this address must belong to the Network used by all sockets that + * utilize this transform; if provided, then only traffic originating from the + * specified source address will be processed. + * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed + * traffic + * @throws IllegalArgumentException indicating that a particular combination of transform + * properties is invalid + * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms + * are active + * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI + * collides with an existing transform + * @throws IOException indicating other errors + */ + @NonNull + public IpSecTransform buildTransportModeTransform( + @NonNull InetAddress sourceAddress, + @NonNull IpSecManager.SecurityParameterIndex spi) + throws IpSecManager.ResourceUnavailableException, + IpSecManager.SpiUnavailableException, IOException { + Objects.requireNonNull(sourceAddress); + Objects.requireNonNull(spi); + if (spi.getResourceId() == INVALID_RESOURCE_ID) { + throw new IllegalArgumentException("Invalid SecurityParameterIndex"); + } + mConfig.setMode(MODE_TRANSPORT); + mConfig.setSourceAddress(sourceAddress.getHostAddress()); + mConfig.setSpiResourceId(spi.getResourceId()); + // FIXME: modifying a builder after calling build can change the built transform. + return new IpSecTransform(mContext, mConfig).activate(); + } + + /** + * Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some + * parameters have interdependencies that are checked at build time. + * + * @param sourceAddress the {@link InetAddress} that provides the source address for this + * IPsec tunnel. This is almost certainly an address belonging to the {@link Network} + * that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}. + * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed + * traffic + * @throws IllegalArgumentException indicating that a particular combination of transform + * properties is invalid. + * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms + * are active + * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI + * collides with an existing transform + * @throws IOException indicating other errors + * @hide + */ + @SystemApi + @NonNull + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) + @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) + public IpSecTransform buildTunnelModeTransform( + @NonNull InetAddress sourceAddress, + @NonNull IpSecManager.SecurityParameterIndex spi) + throws IpSecManager.ResourceUnavailableException, + IpSecManager.SpiUnavailableException, IOException { + Objects.requireNonNull(sourceAddress); + Objects.requireNonNull(spi); + if (spi.getResourceId() == INVALID_RESOURCE_ID) { + throw new IllegalArgumentException("Invalid SecurityParameterIndex"); + } + mConfig.setMode(MODE_TUNNEL); + mConfig.setSourceAddress(sourceAddress.getHostAddress()); + mConfig.setSpiResourceId(spi.getResourceId()); + return new IpSecTransform(mContext, mConfig).activate(); + } + + /** + * Create a new IpSecTransform.Builder. + * + * @param context current context + */ + public Builder(@NonNull Context context) { + Objects.requireNonNull(context); + mContext = context; + mConfig = new IpSecConfig(); + } + } + + @Override + public String toString() { + return new StringBuilder() + .append("IpSecTransform{resourceId=") + .append(mResourceId) + .append("}") + .toString(); + } +} diff --git a/framework-t/src/android/net/IpSecTransformResponse.aidl b/framework-t/src/android/net/IpSecTransformResponse.aidl new file mode 100644 index 0000000000..546230d5b8 --- /dev/null +++ b/framework-t/src/android/net/IpSecTransformResponse.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** @hide */ +parcelable IpSecTransformResponse; diff --git a/framework-t/src/android/net/IpSecTransformResponse.java b/framework-t/src/android/net/IpSecTransformResponse.java new file mode 100644 index 0000000000..363f3165ee --- /dev/null +++ b/framework-t/src/android/net/IpSecTransformResponse.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class is used to return an IpSecTransform resource Id and and corresponding status from the + * IpSecService to an IpSecTransform object. + * + * @hide + */ +public final class IpSecTransformResponse implements Parcelable { + private static final String TAG = "IpSecTransformResponse"; + + public final int resourceId; + public final int status; + // Parcelable Methods + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(status); + out.writeInt(resourceId); + } + + public IpSecTransformResponse(int inStatus) { + if (inStatus == IpSecManager.Status.OK) { + throw new IllegalArgumentException("Valid status implies other args must be provided"); + } + status = inStatus; + resourceId = IpSecManager.INVALID_RESOURCE_ID; + } + + public IpSecTransformResponse(int inStatus, int inResourceId) { + status = inStatus; + resourceId = inResourceId; + } + + private IpSecTransformResponse(Parcel in) { + status = in.readInt(); + resourceId = in.readInt(); + } + + @android.annotation.NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public IpSecTransformResponse createFromParcel(Parcel in) { + return new IpSecTransformResponse(in); + } + + public IpSecTransformResponse[] newArray(int size) { + return new IpSecTransformResponse[size]; + } + }; +} diff --git a/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl new file mode 100644 index 0000000000..7239221415 --- /dev/null +++ b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** @hide */ +parcelable IpSecTunnelInterfaceResponse; diff --git a/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java new file mode 100644 index 0000000000..127e30a693 --- /dev/null +++ b/framework-t/src/android/net/IpSecTunnelInterfaceResponse.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class is used to return an IpSecTunnelInterface resource Id and and corresponding status + * from the IpSecService to an IpSecTunnelInterface object. + * + * @hide + */ +public final class IpSecTunnelInterfaceResponse implements Parcelable { + private static final String TAG = "IpSecTunnelInterfaceResponse"; + + public final int resourceId; + public final String interfaceName; + public final int status; + // Parcelable Methods + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(status); + out.writeInt(resourceId); + out.writeString(interfaceName); + } + + public IpSecTunnelInterfaceResponse(int inStatus) { + if (inStatus == IpSecManager.Status.OK) { + throw new IllegalArgumentException("Valid status implies other args must be provided"); + } + status = inStatus; + resourceId = IpSecManager.INVALID_RESOURCE_ID; + interfaceName = ""; + } + + public IpSecTunnelInterfaceResponse(int inStatus, int inResourceId, String inInterfaceName) { + status = inStatus; + resourceId = inResourceId; + interfaceName = inInterfaceName; + } + + private IpSecTunnelInterfaceResponse(Parcel in) { + status = in.readInt(); + resourceId = in.readInt(); + interfaceName = in.readString(); + } + + @android.annotation.NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public IpSecTunnelInterfaceResponse createFromParcel(Parcel in) { + return new IpSecTunnelInterfaceResponse(in); + } + + public IpSecTunnelInterfaceResponse[] newArray(int size) { + return new IpSecTunnelInterfaceResponse[size]; + } + }; +} diff --git a/framework-t/src/android/net/IpSecUdpEncapResponse.aidl b/framework-t/src/android/net/IpSecUdpEncapResponse.aidl new file mode 100644 index 0000000000..5e451f3651 --- /dev/null +++ b/framework-t/src/android/net/IpSecUdpEncapResponse.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** @hide */ +parcelable IpSecUdpEncapResponse; diff --git a/framework-t/src/android/net/IpSecUdpEncapResponse.java b/framework-t/src/android/net/IpSecUdpEncapResponse.java new file mode 100644 index 0000000000..732cf198a9 --- /dev/null +++ b/framework-t/src/android/net/IpSecUdpEncapResponse.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * This class is used to return a UDP Socket and corresponding status from the IpSecService to an + * IpSecManager.UdpEncapsulationSocket. + * + * @hide + */ +public final class IpSecUdpEncapResponse implements Parcelable { + private static final String TAG = "IpSecUdpEncapResponse"; + + public final int resourceId; + public final int port; + public final int status; + // There is a weird asymmetry with FileDescriptor: you can write a FileDescriptor + // but you read a ParcelFileDescriptor. To circumvent this, when we receive a FD + // from the user, we immediately create a ParcelFileDescriptor DUP, which we invalidate + // on writeParcel() by setting the flag to do close-on-write. + // TODO: tests to ensure this doesn't leak + public final ParcelFileDescriptor fileDescriptor; + + // Parcelable Methods + + @Override + public int describeContents() { + return (fileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(status); + out.writeInt(resourceId); + out.writeInt(port); + out.writeParcelable(fileDescriptor, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } + + public IpSecUdpEncapResponse(int inStatus) { + if (inStatus == IpSecManager.Status.OK) { + throw new IllegalArgumentException("Valid status implies other args must be provided"); + } + status = inStatus; + resourceId = IpSecManager.INVALID_RESOURCE_ID; + port = -1; + fileDescriptor = null; // yes I know it's redundant, but readability + } + + public IpSecUdpEncapResponse(int inStatus, int inResourceId, int inPort, FileDescriptor inFd) + throws IOException { + if (inStatus == IpSecManager.Status.OK && inFd == null) { + throw new IllegalArgumentException("Valid status implies FD must be non-null"); + } + status = inStatus; + resourceId = inResourceId; + port = inPort; + fileDescriptor = (status == IpSecManager.Status.OK) ? ParcelFileDescriptor.dup(inFd) : null; + } + + private IpSecUdpEncapResponse(Parcel in) { + status = in.readInt(); + resourceId = in.readInt(); + port = in.readInt(); + fileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); + } + + @android.annotation.NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public IpSecUdpEncapResponse createFromParcel(Parcel in) { + return new IpSecUdpEncapResponse(in); + } + + public IpSecUdpEncapResponse[] newArray(int size) { + return new IpSecUdpEncapResponse[size]; + } + }; +} diff --git a/framework-t/src/android/net/NetworkIdentity.java b/framework-t/src/android/net/NetworkIdentity.java new file mode 100644 index 0000000000..da5f88dc3b --- /dev/null +++ b/framework-t/src/android/net/NetworkIdentity.java @@ -0,0 +1,594 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.NetworkTemplate.NETWORK_TYPE_ALL; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.net.wifi.WifiInfo; +import android.service.NetworkIdentityProto; +import android.telephony.TelephonyManager; +import android.util.proto.ProtoOutputStream; + +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.NetworkCapabilitiesUtils; +import com.android.net.module.util.NetworkIdentityUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Objects; + +/** + * Network definition that includes strong identity. Analogous to combining + * {@link NetworkCapabilities} and an IMSI. + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public class NetworkIdentity { + private static final String TAG = "NetworkIdentity"; + + /** @hide */ + // TODO: Remove this after migrating all callers to use + // {@link NetworkTemplate#NETWORK_TYPE_ALL} instead. + public static final int SUBTYPE_COMBINED = -1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "OEM_MANAGED_" }, flag = true, value = { + NetworkTemplate.OEM_MANAGED_NO, + NetworkTemplate.OEM_MANAGED_PAID, + NetworkTemplate.OEM_MANAGED_PRIVATE + }) + public @interface OemManaged{} + + /** + * Network has no {@code NetworkCapabilities#NET_CAPABILITY_OEM_*}. + * @hide + */ + public static final int OEM_NONE = 0x0; + /** + * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PAID}. + * @hide + */ + public static final int OEM_PAID = 1 << 0; + /** + * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PRIVATE}. + * @hide + */ + public static final int OEM_PRIVATE = 1 << 1; + + private static final long SUPPORTED_OEM_MANAGED_TYPES = OEM_PAID | OEM_PRIVATE; + + final int mType; + final int mRatType; + final int mSubId; + final String mSubscriberId; + final String mWifiNetworkKey; + final boolean mRoaming; + final boolean mMetered; + final boolean mDefaultNetwork; + final int mOemManaged; + + /** @hide */ + public NetworkIdentity( + int type, int ratType, @Nullable String subscriberId, @Nullable String wifiNetworkKey, + boolean roaming, boolean metered, boolean defaultNetwork, int oemManaged, int subId) { + mType = type; + mRatType = ratType; + mSubscriberId = subscriberId; + mWifiNetworkKey = wifiNetworkKey; + mRoaming = roaming; + mMetered = metered; + mDefaultNetwork = defaultNetwork; + mOemManaged = oemManaged; + mSubId = subId; + } + + @Override + public int hashCode() { + return Objects.hash(mType, mRatType, mSubscriberId, mWifiNetworkKey, mRoaming, mMetered, + mDefaultNetwork, mOemManaged, mSubId); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof NetworkIdentity) { + final NetworkIdentity ident = (NetworkIdentity) obj; + return mType == ident.mType && mRatType == ident.mRatType && mRoaming == ident.mRoaming + && Objects.equals(mSubscriberId, ident.mSubscriberId) + && Objects.equals(mWifiNetworkKey, ident.mWifiNetworkKey) + && mMetered == ident.mMetered + && mDefaultNetwork == ident.mDefaultNetwork + && mOemManaged == ident.mOemManaged + && mSubId == ident.mSubId; + } + return false; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("{"); + builder.append("type=").append(mType); + builder.append(", ratType="); + if (mRatType == NETWORK_TYPE_ALL) { + builder.append("COMBINED"); + } else { + builder.append(mRatType); + } + if (mSubscriberId != null) { + builder.append(", subscriberId=") + .append(NetworkIdentityUtils.scrubSubscriberId(mSubscriberId)); + } + if (mWifiNetworkKey != null) { + builder.append(", wifiNetworkKey=").append(mWifiNetworkKey); + } + if (mRoaming) { + builder.append(", ROAMING"); + } + builder.append(", metered=").append(mMetered); + builder.append(", defaultNetwork=").append(mDefaultNetwork); + builder.append(", oemManaged=").append(getOemManagedNames(mOemManaged)); + builder.append(", subId=").append(mSubId); + return builder.append("}").toString(); + } + + /** + * Get the human readable representation of a bitfield representing the OEM managed state of a + * network. + */ + static String getOemManagedNames(int oemManaged) { + if (oemManaged == OEM_NONE) { + return "OEM_NONE"; + } + final int[] bitPositions = NetworkCapabilitiesUtils.unpackBits(oemManaged); + final ArrayList oemManagedNames = new ArrayList(); + for (int position : bitPositions) { + oemManagedNames.add(nameOfOemManaged(1 << position)); + } + return String.join(",", oemManagedNames); + } + + private static String nameOfOemManaged(int oemManagedBit) { + switch (oemManagedBit) { + case OEM_PAID: + return "OEM_PAID"; + case OEM_PRIVATE: + return "OEM_PRIVATE"; + default: + return "Invalid(" + oemManagedBit + ")"; + } + } + + /** @hide */ + public void dumpDebug(ProtoOutputStream proto, long tag) { + final long start = proto.start(tag); + + proto.write(NetworkIdentityProto.TYPE, mType); + + // TODO: dump mRatType as well. + + proto.write(NetworkIdentityProto.ROAMING, mRoaming); + proto.write(NetworkIdentityProto.METERED, mMetered); + proto.write(NetworkIdentityProto.DEFAULT_NETWORK, mDefaultNetwork); + proto.write(NetworkIdentityProto.OEM_MANAGED_NETWORK, mOemManaged); + + proto.end(start); + } + + /** Get the network type of this instance. */ + public int getType() { + return mType; + } + + /** Get the Radio Access Technology(RAT) type of this instance. */ + public int getRatType() { + return mRatType; + } + + /** Get the Subscriber Id of this instance. */ + @Nullable + public String getSubscriberId() { + return mSubscriberId; + } + + /** Get the Wifi Network Key of this instance. See {@link WifiInfo#getNetworkKey()}. */ + @Nullable + public String getWifiNetworkKey() { + return mWifiNetworkKey; + } + + /** @hide */ + // TODO: Remove this function after all callers are removed. + public boolean getRoaming() { + return mRoaming; + } + + /** Return whether this network is roaming. */ + public boolean isRoaming() { + return mRoaming; + } + + /** @hide */ + // TODO: Remove this function after all callers are removed. + public boolean getMetered() { + return mMetered; + } + + /** Return whether this network is metered. */ + public boolean isMetered() { + return mMetered; + } + + /** @hide */ + // TODO: Remove this function after all callers are removed. + public boolean getDefaultNetwork() { + return mDefaultNetwork; + } + + /** Return whether this network is the default network. */ + public boolean isDefaultNetwork() { + return mDefaultNetwork; + } + + /** Get the OEM managed type of this instance. */ + public int getOemManaged() { + return mOemManaged; + } + + /** Get the SubId of this instance. */ + public int getSubId() { + return mSubId; + } + + /** + * Assemble a {@link NetworkIdentity} from the passed arguments. + * + * This methods builds an identity based on the capabilities of the network in the + * snapshot and other passed arguments. The identity is used as a key to record data usage. + * + * @param snapshot the snapshot of network state. See {@link NetworkStateSnapshot}. + * @param defaultNetwork whether the network is a default network. + * @param ratType the Radio Access Technology(RAT) type of the network. Or + * {@link TelephonyManager#NETWORK_TYPE_UNKNOWN} if not applicable. + * See {@code TelephonyManager.NETWORK_TYPE_*}. + * @hide + * @deprecated See {@link NetworkIdentity.Builder}. + */ + // TODO: Remove this after all callers are migrated to use new Api. + @Deprecated + @NonNull + public static NetworkIdentity buildNetworkIdentity(Context context, + @NonNull NetworkStateSnapshot snapshot, boolean defaultNetwork, int ratType) { + final NetworkIdentity.Builder builder = new NetworkIdentity.Builder() + .setNetworkStateSnapshot(snapshot).setDefaultNetwork(defaultNetwork) + .setSubId(snapshot.getSubId()); + if (snapshot.getLegacyType() == TYPE_MOBILE && ratType != NETWORK_TYPE_ALL) { + builder.setRatType(ratType); + } + return builder.build(); + } + + /** + * Builds a bitfield of {@code NetworkIdentity.OEM_*} based on {@link NetworkCapabilities}. + * @hide + */ + public static int getOemBitfield(@NonNull NetworkCapabilities nc) { + int oemManaged = OEM_NONE; + + if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID)) { + oemManaged |= OEM_PAID; + } + if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)) { + oemManaged |= OEM_PRIVATE; + } + + return oemManaged; + } + + /** @hide */ + public static int compare(@NonNull NetworkIdentity left, @NonNull NetworkIdentity right) { + Objects.requireNonNull(right); + int res = Integer.compare(left.mType, right.mType); + if (res == 0) { + res = Integer.compare(left.mRatType, right.mRatType); + } + if (res == 0 && left.mSubscriberId != null && right.mSubscriberId != null) { + res = left.mSubscriberId.compareTo(right.mSubscriberId); + } + if (res == 0 && left.mWifiNetworkKey != null && right.mWifiNetworkKey != null) { + res = left.mWifiNetworkKey.compareTo(right.mWifiNetworkKey); + } + if (res == 0) { + res = Boolean.compare(left.mRoaming, right.mRoaming); + } + if (res == 0) { + res = Boolean.compare(left.mMetered, right.mMetered); + } + if (res == 0) { + res = Boolean.compare(left.mDefaultNetwork, right.mDefaultNetwork); + } + if (res == 0) { + res = Integer.compare(left.mOemManaged, right.mOemManaged); + } + if (res == 0) { + res = Integer.compare(left.mSubId, right.mSubId); + } + return res; + } + + /** + * Builder class for {@link NetworkIdentity}. + */ + public static final class Builder { + // Need to be synchronized with ConnectivityManager. + // TODO: Use {@link ConnectivityManager#MAX_NETWORK_TYPE} when this file is in the module. + private static final int MAX_NETWORK_TYPE = 18; // TYPE_TEST + private static final int MIN_NETWORK_TYPE = TYPE_MOBILE; + + private int mType; + private int mRatType; + private String mSubscriberId; + private String mWifiNetworkKey; + private boolean mRoaming; + private boolean mMetered; + private boolean mDefaultNetwork; + private int mOemManaged; + private int mSubId; + + /** + * Creates a new Builder. + */ + public Builder() { + // Initialize with default values. Will be overwritten by setters. + mType = ConnectivityManager.TYPE_NONE; + mRatType = NetworkTemplate.NETWORK_TYPE_ALL; + mSubscriberId = null; + mWifiNetworkKey = null; + mRoaming = false; + mMetered = false; + mDefaultNetwork = false; + mOemManaged = NetworkTemplate.OEM_MANAGED_NO; + mSubId = INVALID_SUBSCRIPTION_ID; + } + + /** + * Add an {@link NetworkStateSnapshot} into the {@link NetworkIdentity} instance. + * This is a useful shorthand that will read from the snapshot and set the + * following fields, if they are set in the snapshot : + * - type + * - subscriberId + * - roaming + * - metered + * - oemManaged + * - wifiNetworkKey + * + * @param snapshot The target {@link NetworkStateSnapshot} object. + * @return The builder object. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder setNetworkStateSnapshot(@NonNull NetworkStateSnapshot snapshot) { + setType(snapshot.getLegacyType()); + + setSubscriberId(snapshot.getSubscriberId()); + setRoaming(!snapshot.getNetworkCapabilities().hasCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)); + setMetered(!(snapshot.getNetworkCapabilities().hasCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + || snapshot.getNetworkCapabilities().hasCapability( + NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED))); + + setOemManaged(getOemBitfield(snapshot.getNetworkCapabilities())); + + if (mType == TYPE_WIFI) { + final TransportInfo transportInfo = snapshot.getNetworkCapabilities() + .getTransportInfo(); + if (transportInfo instanceof WifiInfo) { + final WifiInfo info = (WifiInfo) transportInfo; + setWifiNetworkKey(info.getNetworkKey()); + } + } + return this; + } + + /** + * Set the network type of the network. + * + * @param type the network type. See {@link ConnectivityManager#TYPE_*}. + * + * @return this builder. + */ + @NonNull + public Builder setType(int type) { + // Include TYPE_NONE for compatibility, type field might not be filled by some + // networks such as test networks. + if ((type < MIN_NETWORK_TYPE || MAX_NETWORK_TYPE < type) + && type != ConnectivityManager.TYPE_NONE) { + throw new IllegalArgumentException("Invalid network type: " + type); + } + mType = type; + return this; + } + + /** + * Set the Radio Access Technology(RAT) type of the network. + * + * No RAT type is specified by default. Call clearRatType to reset. + * + * @param ratType the Radio Access Technology(RAT) type if applicable. See + * {@code TelephonyManager.NETWORK_TYPE_*}. + * + * @return this builder. + */ + @NonNull + public Builder setRatType(int ratType) { + if (!CollectionUtils.contains(TelephonyManager.getAllNetworkTypes(), ratType) + && ratType != TelephonyManager.NETWORK_TYPE_UNKNOWN + && ratType != NetworkStatsManager.NETWORK_TYPE_5G_NSA) { + throw new IllegalArgumentException("Invalid ratType " + ratType); + } + mRatType = ratType; + return this; + } + + /** + * Clear the Radio Access Technology(RAT) type of the network. + * + * @return this builder. + */ + @NonNull + public Builder clearRatType() { + mRatType = NetworkTemplate.NETWORK_TYPE_ALL; + return this; + } + + /** + * Set the Subscriber Id. + * + * @param subscriberId the Subscriber Id of the network. Or null if not applicable. + * @return this builder. + */ + @NonNull + public Builder setSubscriberId(@Nullable String subscriberId) { + mSubscriberId = subscriberId; + return this; + } + + /** + * Set the Wifi Network Key. + * + * @param wifiNetworkKey Wifi Network Key of the network, + * see {@link WifiInfo#getNetworkKey()}. + * Or null if not applicable. + * @return this builder. + */ + @NonNull + public Builder setWifiNetworkKey(@Nullable String wifiNetworkKey) { + mWifiNetworkKey = wifiNetworkKey; + return this; + } + + /** + * Set whether this network is roaming. + * + * This field is false by default. Call with false to reset. + * + * @param roaming the roaming status of the network. + * @return this builder. + */ + @NonNull + public Builder setRoaming(boolean roaming) { + mRoaming = roaming; + return this; + } + + /** + * Set whether this network is metered. + * + * This field is false by default. Call with false to reset. + * + * @param metered the meteredness of the network. + * @return this builder. + */ + @NonNull + public Builder setMetered(boolean metered) { + mMetered = metered; + return this; + } + + /** + * Set whether this network is the default network. + * + * This field is false by default. Call with false to reset. + * + * @param defaultNetwork the default network status of the network. + * @return this builder. + */ + @NonNull + public Builder setDefaultNetwork(boolean defaultNetwork) { + mDefaultNetwork = defaultNetwork; + return this; + } + + /** + * Set the OEM managed type. + * + * @param oemManaged Type of OEM managed network or unmanaged networks. + * See {@code NetworkTemplate#OEM_MANAGED_*}. + * @return this builder. + */ + @NonNull + public Builder setOemManaged(@OemManaged int oemManaged) { + // Assert input does not contain illegal oemManage bits. + if ((~SUPPORTED_OEM_MANAGED_TYPES & oemManaged) != 0) { + throw new IllegalArgumentException("Invalid value for OemManaged : " + oemManaged); + } + mOemManaged = oemManaged; + return this; + } + + /** + * Set the Subscription Id. + * + * @param subId the Subscription Id of the network. Or INVALID_SUBSCRIPTION_ID if not + * applicable. + * @return this builder. + */ + @NonNull + public Builder setSubId(int subId) { + mSubId = subId; + return this; + } + + private void ensureValidParameters() { + // Assert non-mobile network cannot have a ratType. + if (mType != TYPE_MOBILE && mRatType != NetworkTemplate.NETWORK_TYPE_ALL) { + throw new IllegalArgumentException( + "Invalid ratType " + mRatType + " for type " + mType); + } + + // Assert non-wifi network cannot have a wifi network key. + if (mType != TYPE_WIFI && mWifiNetworkKey != null) { + throw new IllegalArgumentException("Invalid wifi network key for type " + mType); + } + } + + /** + * Builds the instance of the {@link NetworkIdentity}. + * + * @return the built instance of {@link NetworkIdentity}. + */ + @NonNull + public NetworkIdentity build() { + ensureValidParameters(); + return new NetworkIdentity(mType, mRatType, mSubscriberId, mWifiNetworkKey, + mRoaming, mMetered, mDefaultNetwork, mOemManaged, mSubId); + } + } +} diff --git a/framework-t/src/android/net/NetworkIdentitySet.java b/framework-t/src/android/net/NetworkIdentitySet.java new file mode 100644 index 0000000000..ad3a958a68 --- /dev/null +++ b/framework-t/src/android/net/NetworkIdentitySet.java @@ -0,0 +1,230 @@ +/* + * 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 android.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import android.annotation.NonNull; +import android.service.NetworkIdentitySetProto; +import android.util.proto.ProtoOutputStream; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Identity of a {@code iface}, defined by the set of {@link NetworkIdentity} + * active on that interface. + * + * @hide + */ +public class NetworkIdentitySet extends HashSet { + private static final int VERSION_INIT = 1; + private static final int VERSION_ADD_ROAMING = 2; + private static final int VERSION_ADD_NETWORK_ID = 3; + private static final int VERSION_ADD_METERED = 4; + private static final int VERSION_ADD_DEFAULT_NETWORK = 5; + private static final int VERSION_ADD_OEM_MANAGED_NETWORK = 6; + private static final int VERSION_ADD_SUB_ID = 7; + + /** + * Construct a {@link NetworkIdentitySet} object. + */ + public NetworkIdentitySet() { + super(); + } + + /** @hide */ + public NetworkIdentitySet(@NonNull Set ident) { + super(ident); + } + + /** @hide */ + public NetworkIdentitySet(DataInput in) throws IOException { + final int version = in.readInt(); + final int size = in.readInt(); + for (int i = 0; i < size; i++) { + if (version <= VERSION_INIT) { + final int ignored = in.readInt(); + } + final int type = in.readInt(); + final int ratType = in.readInt(); + final String subscriberId = readOptionalString(in); + final String networkId; + if (version >= VERSION_ADD_NETWORK_ID) { + networkId = readOptionalString(in); + } else { + networkId = null; + } + final boolean roaming; + if (version >= VERSION_ADD_ROAMING) { + roaming = in.readBoolean(); + } else { + roaming = false; + } + + final boolean metered; + if (version >= VERSION_ADD_METERED) { + metered = in.readBoolean(); + } else { + // If this is the old data and the type is mobile, treat it as metered. (Note that + // if this is a mobile network, TYPE_MOBILE is the only possible type that could be + // used.) + metered = (type == TYPE_MOBILE); + } + + final boolean defaultNetwork; + if (version >= VERSION_ADD_DEFAULT_NETWORK) { + defaultNetwork = in.readBoolean(); + } else { + defaultNetwork = true; + } + + final int oemNetCapabilities; + if (version >= VERSION_ADD_OEM_MANAGED_NETWORK) { + oemNetCapabilities = in.readInt(); + } else { + oemNetCapabilities = NetworkIdentity.OEM_NONE; + } + + final int subId; + if (version >= VERSION_ADD_SUB_ID) { + subId = in.readInt(); + } else { + subId = INVALID_SUBSCRIPTION_ID; + } + + add(new NetworkIdentity(type, ratType, subscriberId, networkId, roaming, metered, + defaultNetwork, oemNetCapabilities, subId)); + } + } + + /** + * Method to serialize this object into a {@code DataOutput}. + * @hide + */ + public void writeToStream(DataOutput out) throws IOException { + out.writeInt(VERSION_ADD_SUB_ID); + out.writeInt(size()); + for (NetworkIdentity ident : this) { + out.writeInt(ident.getType()); + out.writeInt(ident.getRatType()); + writeOptionalString(out, ident.getSubscriberId()); + writeOptionalString(out, ident.getWifiNetworkKey()); + out.writeBoolean(ident.isRoaming()); + out.writeBoolean(ident.isMetered()); + out.writeBoolean(ident.isDefaultNetwork()); + out.writeInt(ident.getOemManaged()); + out.writeInt(ident.getSubId()); + } + } + + /** + * @return whether any {@link NetworkIdentity} in this set is considered metered. + * @hide + */ + public boolean isAnyMemberMetered() { + if (isEmpty()) { + return false; + } + for (NetworkIdentity ident : this) { + if (ident.isMetered()) { + return true; + } + } + return false; + } + + /** + * @return whether any {@link NetworkIdentity} in this set is considered roaming. + * @hide + */ + public boolean isAnyMemberRoaming() { + if (isEmpty()) { + return false; + } + for (NetworkIdentity ident : this) { + if (ident.isRoaming()) { + return true; + } + } + return false; + } + + /** + * @return whether any {@link NetworkIdentity} in this set is considered on the default + * network. + * @hide + */ + public boolean areAllMembersOnDefaultNetwork() { + if (isEmpty()) { + return true; + } + for (NetworkIdentity ident : this) { + if (!ident.isDefaultNetwork()) { + return false; + } + } + return true; + } + + private static void writeOptionalString(DataOutput out, String value) throws IOException { + if (value != null) { + out.writeByte(1); + out.writeUTF(value); + } else { + out.writeByte(0); + } + } + + private static String readOptionalString(DataInput in) throws IOException { + if (in.readByte() != 0) { + return in.readUTF(); + } else { + return null; + } + } + + public static int compare(@NonNull NetworkIdentitySet left, @NonNull NetworkIdentitySet right) { + Objects.requireNonNull(left); + Objects.requireNonNull(right); + if (left.isEmpty()) return -1; + if (right.isEmpty()) return 1; + + final NetworkIdentity leftIdent = left.iterator().next(); + final NetworkIdentity rightIdent = right.iterator().next(); + return NetworkIdentity.compare(leftIdent, rightIdent); + } + + /** + * Method to dump this object into proto debug file. + * @hide + */ + public void dumpDebug(ProtoOutputStream proto, long tag) { + final long start = proto.start(tag); + + for (NetworkIdentity ident : this) { + ident.dumpDebug(proto, NetworkIdentitySetProto.IDENTITIES); + } + + proto.end(start); + } +} diff --git a/framework-t/src/android/net/NetworkStateSnapshot.java b/framework-t/src/android/net/NetworkStateSnapshot.java new file mode 100644 index 0000000000..d3f785a8f9 --- /dev/null +++ b/framework-t/src/android/net/NetworkStateSnapshot.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.net.module.util.NetworkIdentityUtils; + +import java.util.Objects; + +/** + * Snapshot of network state. + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class NetworkStateSnapshot implements Parcelable { + /** The network associated with this snapshot. */ + @NonNull + private final Network mNetwork; + + /** The {@link NetworkCapabilities} of the network associated with this snapshot. */ + @NonNull + private final NetworkCapabilities mNetworkCapabilities; + + /** The {@link LinkProperties} of the network associated with this snapshot. */ + @NonNull + private final LinkProperties mLinkProperties; + + /** + * The Subscriber Id of the network associated with this snapshot. See + * {@link android.telephony.TelephonyManager#getSubscriberId()}. + */ + @Nullable + private final String mSubscriberId; + + /** + * The legacy type of the network associated with this snapshot. See + * {@code ConnectivityManager#TYPE_*}. + */ + private final int mLegacyType; + + public NetworkStateSnapshot(@NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties, + @Nullable String subscriberId, int legacyType) { + mNetwork = Objects.requireNonNull(network); + mNetworkCapabilities = Objects.requireNonNull(networkCapabilities); + mLinkProperties = Objects.requireNonNull(linkProperties); + mSubscriberId = subscriberId; + mLegacyType = legacyType; + } + + /** @hide */ + public NetworkStateSnapshot(@NonNull Parcel in) { + mNetwork = in.readParcelable(null); + mNetworkCapabilities = in.readParcelable(null); + mLinkProperties = in.readParcelable(null); + mSubscriberId = in.readString(); + mLegacyType = in.readInt(); + } + + /** Get the network associated with this snapshot */ + @NonNull + public Network getNetwork() { + return mNetwork; + } + + /** Get {@link NetworkCapabilities} of the network associated with this snapshot. */ + @NonNull + public NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities; + } + + /** Get the {@link LinkProperties} of the network associated with this snapshot. */ + @NonNull + public LinkProperties getLinkProperties() { + return mLinkProperties; + } + + /** + * Get the Subscriber Id of the network associated with this snapshot. + * @deprecated Please use #getSubId, which doesn't return personally identifiable + * information. + */ + @Deprecated + @Nullable + public String getSubscriberId() { + return mSubscriberId; + } + + /** Get the subId of the network associated with this snapshot. */ + public int getSubId() { + if (mNetworkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { + final NetworkSpecifier spec = mNetworkCapabilities.getNetworkSpecifier(); + if (spec instanceof TelephonyNetworkSpecifier) { + return ((TelephonyNetworkSpecifier) spec).getSubscriptionId(); + } + } + return INVALID_SUBSCRIPTION_ID; + } + + + /** + * Get the legacy type of the network associated with this snapshot. + * @return the legacy network type. See {@code ConnectivityManager#TYPE_*}. + */ + public int getLegacyType() { + return mLegacyType; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeParcelable(mNetwork, flags); + out.writeParcelable(mNetworkCapabilities, flags); + out.writeParcelable(mLinkProperties, flags); + out.writeString(mSubscriberId); + out.writeInt(mLegacyType); + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + @NonNull + @Override + public NetworkStateSnapshot createFromParcel(@NonNull Parcel in) { + return new NetworkStateSnapshot(in); + } + + @NonNull + @Override + public NetworkStateSnapshot[] newArray(int size) { + return new NetworkStateSnapshot[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NetworkStateSnapshot)) return false; + NetworkStateSnapshot that = (NetworkStateSnapshot) o; + return mLegacyType == that.mLegacyType + && Objects.equals(mNetwork, that.mNetwork) + && Objects.equals(mNetworkCapabilities, that.mNetworkCapabilities) + && Objects.equals(mLinkProperties, that.mLinkProperties) + && Objects.equals(mSubscriberId, that.mSubscriberId); + } + + @Override + public int hashCode() { + return Objects.hash(mNetwork, + mNetworkCapabilities, mLinkProperties, mSubscriberId, mLegacyType); + } + + @Override + public String toString() { + return "NetworkStateSnapshot{" + + "network=" + mNetwork + + ", networkCapabilities=" + mNetworkCapabilities + + ", linkProperties=" + mLinkProperties + + ", subscriberId='" + NetworkIdentityUtils.scrubSubscriberId(mSubscriberId) + '\'' + + ", legacyType=" + mLegacyType + + '}'; + } +} diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java new file mode 100644 index 0000000000..06f2a621bc --- /dev/null +++ b/framework-t/src/android/net/NetworkStats.java @@ -0,0 +1,1834 @@ +/* + * 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 android.net; + +import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.CollectionUtils; + +import libcore.util.EmptyArray; + +import java.io.CharArrayWriter; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Collection of active network statistics. Can contain summary details across + * all interfaces, or details with per-UID granularity. Internally stores data + * as a large table, closely matching {@code /proc/} data format. This structure + * optimizes for rapid in-memory comparison, but consider using + * {@link NetworkStatsHistory} when persisting. + * + * @hide + */ +// @NotThreadSafe +@SystemApi +public final class NetworkStats implements Parcelable, Iterable { + private static final String TAG = "NetworkStats"; + + /** + * {@link #iface} value when interface details unavailable. + * @hide + */ + @Nullable public static final String IFACE_ALL = null; + + /** + * Virtual network interface for video telephony. This is for VT data usage counting + * purpose. + */ + public static final String IFACE_VT = "vt_data0"; + + /** {@link #uid} value when UID details unavailable. */ + public static final int UID_ALL = -1; + /** Special UID value for data usage by tethering. */ + public static final int UID_TETHERING = -5; + + /** + * {@link #tag} value matching any tag. + * @hide + */ + // TODO: Rename TAG_ALL to TAG_ANY. + public static final int TAG_ALL = -1; + /** {@link #set} value for all sets combined, not including debug sets. */ + public static final int SET_ALL = -1; + /** {@link #set} value where background data is accounted. */ + public static final int SET_DEFAULT = 0; + /** {@link #set} value where foreground data is accounted. */ + public static final int SET_FOREGROUND = 1; + /** + * All {@link #set} value greater than SET_DEBUG_START are debug {@link #set} values. + * @hide + */ + public static final int SET_DEBUG_START = 1000; + /** + * Debug {@link #set} value when the VPN stats are moved in. + * @hide + */ + public static final int SET_DBG_VPN_IN = 1001; + /** + * Debug {@link #set} value when the VPN stats are moved out of a vpn UID. + * @hide + */ + public static final int SET_DBG_VPN_OUT = 1002; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "SET_" }, value = { + SET_ALL, + SET_DEFAULT, + SET_FOREGROUND, + }) + public @interface State { + } + + /** + * Include all interfaces when filtering + * @hide + */ + public @Nullable static final String[] INTERFACES_ALL = null; + + /** {@link #tag} value for total data across all tags. */ + // TODO: Rename TAG_NONE to TAG_ALL. + public static final int TAG_NONE = 0; + + /** {@link #metered} value to account for all metered states. */ + public static final int METERED_ALL = -1; + /** {@link #metered} value where native, unmetered data is accounted. */ + public static final int METERED_NO = 0; + /** {@link #metered} value where metered data is accounted. */ + public static final int METERED_YES = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "METERED_" }, value = { + METERED_ALL, + METERED_NO, + METERED_YES + }) + public @interface Meteredness { + } + + + /** {@link #roaming} value to account for all roaming states. */ + public static final int ROAMING_ALL = -1; + /** {@link #roaming} value where native, non-roaming data is accounted. */ + public static final int ROAMING_NO = 0; + /** {@link #roaming} value where roaming data is accounted. */ + public static final int ROAMING_YES = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ROAMING_" }, value = { + ROAMING_ALL, + ROAMING_NO, + ROAMING_YES + }) + public @interface Roaming { + } + + /** {@link #onDefaultNetwork} value to account for all default network states. */ + public static final int DEFAULT_NETWORK_ALL = -1; + /** {@link #onDefaultNetwork} value to account for usage while not the default network. */ + public static final int DEFAULT_NETWORK_NO = 0; + /** {@link #onDefaultNetwork} value to account for usage while the default network. */ + public static final int DEFAULT_NETWORK_YES = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = { + DEFAULT_NETWORK_ALL, + DEFAULT_NETWORK_NO, + DEFAULT_NETWORK_YES + }) + public @interface DefaultNetwork { + } + + /** + * Denotes a request for stats at the interface level. + * @hide + */ + public static final int STATS_PER_IFACE = 0; + /** + * Denotes a request for stats at the interface and UID level. + * @hide + */ + public static final int STATS_PER_UID = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "STATS_PER_" }, value = { + STATS_PER_IFACE, + STATS_PER_UID + }) + public @interface StatsType { + } + + private static final String CLATD_INTERFACE_PREFIX = "v4-"; + // Delta between IPv4 header (20b) and IPv6 header (40b). + // Used for correct stats accounting on clatd interfaces. + private static final int IPV4V6_HEADER_DELTA = 20; + + // TODO: move fields to "mVariable" notation + + /** + * {@link SystemClock#elapsedRealtime()} timestamp in milliseconds when this data was + * generated. + * It's a timestamps delta when {@link #subtract()}, + * {@code INetworkStatsSession#getSummaryForAllUid()} methods are used. + */ + private long elapsedRealtime; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private int size; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private int capacity; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private String[] iface; + @UnsupportedAppUsage + private int[] uid; + @UnsupportedAppUsage + private int[] set; + @UnsupportedAppUsage + private int[] tag; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private int[] metered; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private int[] roaming; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private int[] defaultNetwork; + @UnsupportedAppUsage + private long[] rxBytes; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private long[] rxPackets; + @UnsupportedAppUsage + private long[] txBytes; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private long[] txPackets; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private long[] operations; + + /** + * Basic element of network statistics. Contains the number of packets and number of bytes + * transferred on both directions in a given set of conditions. See + * {@link Entry#Entry(String, int, int, int, int, int, int, long, long, long, long, long)}. + * + * @hide + */ + @SystemApi + public static class Entry { + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public String iface; + /** @hide */ + @UnsupportedAppUsage + public int uid; + /** @hide */ + @UnsupportedAppUsage + public int set; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public int tag; + /** + * Note that this is only populated w/ the default value when read from /proc or written + * to disk. We merge in the correct value when reporting this value to clients of + * getSummary(). + * @hide + */ + public int metered; + /** + * Note that this is only populated w/ the default value when read from /proc or written + * to disk. We merge in the correct value when reporting this value to clients of + * getSummary(). + * @hide + */ + public int roaming; + /** + * Note that this is only populated w/ the default value when read from /proc or written + * to disk. We merge in the correct value when reporting this value to clients of + * getSummary(). + * @hide + */ + public int defaultNetwork; + /** @hide */ + @UnsupportedAppUsage + public long rxBytes; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long rxPackets; + /** @hide */ + @UnsupportedAppUsage + public long txBytes; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long txPackets; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long operations; + + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public Entry() { + this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L); + } + + /** @hide */ + public Entry(long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { + this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, + operations); + } + + /** @hide */ + public Entry(String iface, int uid, int set, int tag, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + this(iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + /** + * Construct a {@link Entry} object by giving statistics of packet and byte transferred on + * both direction, and associated with a set of given conditions. + * + * @param iface interface name of this {@link Entry}. Or null if not specified. + * @param uid uid of this {@link Entry}. {@link #UID_TETHERING} if this {@link Entry} is + * for tethering. Or {@link #UID_ALL} if this {@link NetworkStats} is only + * counting iface stats. + * @param set usage state of this {@link Entry}. + * @param tag tag of this {@link Entry}. + * @param metered metered state of this {@link Entry}. + * @param roaming roaming state of this {@link Entry}. + * @param defaultNetwork default network status of this {@link Entry}. + * @param rxBytes Number of bytes received for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param rxPackets Number of packets received for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param txBytes Number of bytes transmitted for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param txPackets Number of bytes transmitted for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param operations count of network operations performed for this {@link Entry}. This can + * be used to derive bytes-per-operation. + */ + public Entry(@Nullable String iface, int uid, @State int set, int tag, + @Meteredness int metered, @Roaming int roaming, @DefaultNetwork int defaultNetwork, + long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { + this.iface = iface; + this.uid = uid; + this.set = set; + this.tag = tag; + this.metered = metered; + this.roaming = roaming; + this.defaultNetwork = defaultNetwork; + this.rxBytes = rxBytes; + this.rxPackets = rxPackets; + this.txBytes = txBytes; + this.txPackets = txPackets; + this.operations = operations; + } + + /** @hide */ + public boolean isNegative() { + return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0; + } + + /** @hide */ + public boolean isEmpty() { + return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0 + && operations == 0; + } + + /** @hide */ + public void add(Entry another) { + this.rxBytes += another.rxBytes; + this.rxPackets += another.rxPackets; + this.txBytes += another.txBytes; + this.txPackets += another.txPackets; + this.operations += another.operations; + } + + /** + * @return interface name of this entry. + * @hide + */ + @Nullable public String getIface() { + return iface; + } + + /** + * @return the uid of this entry. + */ + public int getUid() { + return uid; + } + + /** + * @return the set state of this entry. + */ + @State public int getSet() { + return set; + } + + /** + * @return the tag value of this entry. + */ + public int getTag() { + return tag; + } + + /** + * @return the metered state. + */ + @Meteredness public int getMetered() { + return metered; + } + + /** + * @return the roaming state. + */ + @Roaming public int getRoaming() { + return roaming; + } + + /** + * @return the default network state. + */ + @DefaultNetwork public int getDefaultNetwork() { + return defaultNetwork; + } + + /** + * @return the number of received bytes. + */ + public long getRxBytes() { + return rxBytes; + } + + /** + * @return the number of received packets. + */ + public long getRxPackets() { + return rxPackets; + } + + /** + * @return the number of transmitted bytes. + */ + public long getTxBytes() { + return txBytes; + } + + /** + * @return the number of transmitted packets. + */ + public long getTxPackets() { + return txPackets; + } + + /** + * @return the count of network operations performed for this entry. + */ + public long getOperations() { + return operations; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("iface=").append(iface); + builder.append(" uid=").append(uid); + builder.append(" set=").append(setToString(set)); + builder.append(" tag=").append(tagToString(tag)); + builder.append(" metered=").append(meteredToString(metered)); + builder.append(" roaming=").append(roamingToString(roaming)); + builder.append(" defaultNetwork=").append(defaultNetworkToString(defaultNetwork)); + builder.append(" rxBytes=").append(rxBytes); + builder.append(" rxPackets=").append(rxPackets); + builder.append(" txBytes=").append(txBytes); + builder.append(" txPackets=").append(txPackets); + builder.append(" operations=").append(operations); + return builder.toString(); + } + + /** @hide */ + @Override + public boolean equals(@Nullable Object o) { + if (o instanceof Entry) { + final Entry e = (Entry) o; + return uid == e.uid && set == e.set && tag == e.tag && metered == e.metered + && roaming == e.roaming && defaultNetwork == e.defaultNetwork + && rxBytes == e.rxBytes && rxPackets == e.rxPackets + && txBytes == e.txBytes && txPackets == e.txPackets + && operations == e.operations && TextUtils.equals(iface, e.iface); + } + return false; + } + + /** @hide */ + @Override + public int hashCode() { + return Objects.hash(uid, set, tag, metered, roaming, defaultNetwork, iface); + } + } + + public NetworkStats(long elapsedRealtime, int initialSize) { + this.elapsedRealtime = elapsedRealtime; + this.size = 0; + if (initialSize > 0) { + this.capacity = initialSize; + this.iface = new String[initialSize]; + this.uid = new int[initialSize]; + this.set = new int[initialSize]; + this.tag = new int[initialSize]; + this.metered = new int[initialSize]; + this.roaming = new int[initialSize]; + this.defaultNetwork = new int[initialSize]; + this.rxBytes = new long[initialSize]; + this.rxPackets = new long[initialSize]; + this.txBytes = new long[initialSize]; + this.txPackets = new long[initialSize]; + this.operations = new long[initialSize]; + } else { + // Special case for use by NetworkStatsFactory to start out *really* empty. + clear(); + } + } + + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public NetworkStats(Parcel parcel) { + elapsedRealtime = parcel.readLong(); + size = parcel.readInt(); + capacity = parcel.readInt(); + iface = parcel.createStringArray(); + uid = parcel.createIntArray(); + set = parcel.createIntArray(); + tag = parcel.createIntArray(); + metered = parcel.createIntArray(); + roaming = parcel.createIntArray(); + defaultNetwork = parcel.createIntArray(); + rxBytes = parcel.createLongArray(); + rxPackets = parcel.createLongArray(); + txBytes = parcel.createLongArray(); + txPackets = parcel.createLongArray(); + operations = parcel.createLongArray(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(elapsedRealtime); + dest.writeInt(size); + dest.writeInt(capacity); + dest.writeStringArray(iface); + dest.writeIntArray(uid); + dest.writeIntArray(set); + dest.writeIntArray(tag); + dest.writeIntArray(metered); + dest.writeIntArray(roaming); + dest.writeIntArray(defaultNetwork); + dest.writeLongArray(rxBytes); + dest.writeLongArray(rxPackets); + dest.writeLongArray(txBytes); + dest.writeLongArray(txPackets); + dest.writeLongArray(operations); + } + + /** + * @hide + */ + @Override + public NetworkStats clone() { + final NetworkStats clone = new NetworkStats(elapsedRealtime, size); + NetworkStats.Entry entry = null; + for (int i = 0; i < size; i++) { + entry = getValues(i, entry); + clone.insertEntry(entry); + } + return clone; + } + + /** + * Clear all data stored in this object. + * @hide + */ + public void clear() { + this.capacity = 0; + this.iface = EmptyArray.STRING; + this.uid = EmptyArray.INT; + this.set = EmptyArray.INT; + this.tag = EmptyArray.INT; + this.metered = EmptyArray.INT; + this.roaming = EmptyArray.INT; + this.defaultNetwork = EmptyArray.INT; + this.rxBytes = EmptyArray.LONG; + this.rxPackets = EmptyArray.LONG; + this.txBytes = EmptyArray.LONG; + this.txPackets = EmptyArray.LONG; + this.operations = EmptyArray.LONG; + } + + /** @hide */ + @VisibleForTesting + public NetworkStats insertEntry( + String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) { + return insertEntry( + iface, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, 0L); + } + + /** @hide */ + @VisibleForTesting + public NetworkStats insertEntry(String iface, int uid, int set, int tag, long rxBytes, + long rxPackets, long txBytes, long txPackets, long operations) { + return insertEntry(new Entry( + iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations)); + } + + /** @hide */ + @VisibleForTesting + public NetworkStats insertEntry(String iface, int uid, int set, int tag, int metered, + int roaming, int defaultNetwork, long rxBytes, long rxPackets, long txBytes, + long txPackets, long operations) { + return insertEntry(new Entry( + iface, uid, set, tag, metered, roaming, defaultNetwork, rxBytes, rxPackets, + txBytes, txPackets, operations)); + } + + /** + * Add new stats entry, copying from given {@link Entry}. The {@link Entry} + * object can be recycled across multiple calls. + * @hide + */ + public NetworkStats insertEntry(Entry entry) { + if (size >= capacity) { + final int newLength = Math.max(size, 10) * 3 / 2; + iface = Arrays.copyOf(iface, newLength); + uid = Arrays.copyOf(uid, newLength); + set = Arrays.copyOf(set, newLength); + tag = Arrays.copyOf(tag, newLength); + metered = Arrays.copyOf(metered, newLength); + roaming = Arrays.copyOf(roaming, newLength); + defaultNetwork = Arrays.copyOf(defaultNetwork, newLength); + rxBytes = Arrays.copyOf(rxBytes, newLength); + rxPackets = Arrays.copyOf(rxPackets, newLength); + txBytes = Arrays.copyOf(txBytes, newLength); + txPackets = Arrays.copyOf(txPackets, newLength); + operations = Arrays.copyOf(operations, newLength); + capacity = newLength; + } + + setValues(size, entry); + size++; + + return this; + } + + private void setValues(int i, Entry entry) { + iface[i] = entry.iface; + uid[i] = entry.uid; + set[i] = entry.set; + tag[i] = entry.tag; + metered[i] = entry.metered; + roaming[i] = entry.roaming; + defaultNetwork[i] = entry.defaultNetwork; + rxBytes[i] = entry.rxBytes; + rxPackets[i] = entry.rxPackets; + txBytes[i] = entry.txBytes; + txPackets[i] = entry.txPackets; + operations[i] = entry.operations; + } + + /** + * Iterate over Entry objects. + * + * Return an iterator of this object that will iterate through all contained Entry objects. + * + * This iterator does not support concurrent modification and makes no guarantee of fail-fast + * behavior. If any method that can mutate the contents of this object is called while + * iteration is in progress, either inside the loop or in another thread, then behavior is + * undefined. + * The remove() method is not implemented and will throw UnsupportedOperationException. + * @hide + */ + @SystemApi + @NonNull public Iterator iterator() { + return new Iterator() { + int mIndex = 0; + + @Override + public boolean hasNext() { + return mIndex < size; + } + + @Override + public Entry next() { + return getValues(mIndex++, null); + } + }; + } + + /** + * Return specific stats entry. + * @hide + */ + @UnsupportedAppUsage + public Entry getValues(int i, @Nullable Entry recycle) { + final Entry entry = recycle != null ? recycle : new Entry(); + entry.iface = iface[i]; + entry.uid = uid[i]; + entry.set = set[i]; + entry.tag = tag[i]; + entry.metered = metered[i]; + entry.roaming = roaming[i]; + entry.defaultNetwork = defaultNetwork[i]; + entry.rxBytes = rxBytes[i]; + entry.rxPackets = rxPackets[i]; + entry.txBytes = txBytes[i]; + entry.txPackets = txPackets[i]; + entry.operations = operations[i]; + return entry; + } + + /** + * If @{code dest} is not equal to @{code src}, copy entry from index @{code src} to index + * @{code dest}. + */ + private void maybeCopyEntry(int dest, int src) { + if (dest == src) return; + iface[dest] = iface[src]; + uid[dest] = uid[src]; + set[dest] = set[src]; + tag[dest] = tag[src]; + metered[dest] = metered[src]; + roaming[dest] = roaming[src]; + defaultNetwork[dest] = defaultNetwork[src]; + rxBytes[dest] = rxBytes[src]; + rxPackets[dest] = rxPackets[src]; + txBytes[dest] = txBytes[src]; + txPackets[dest] = txPackets[src]; + operations[dest] = operations[src]; + } + + /** @hide */ + public long getElapsedRealtime() { + return elapsedRealtime; + } + + /** @hide */ + public void setElapsedRealtime(long time) { + elapsedRealtime = time; + } + + /** + * Return age of this {@link NetworkStats} object with respect to + * {@link SystemClock#elapsedRealtime()}. + * @hide + */ + public long getElapsedRealtimeAge() { + return SystemClock.elapsedRealtime() - elapsedRealtime; + } + + /** @hide */ + @UnsupportedAppUsage + public int size() { + return size; + } + + /** @hide */ + @VisibleForTesting + public int internalSize() { + return capacity; + } + + /** @hide */ + @Deprecated + public NetworkStats combineValues(String iface, int uid, int tag, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + return combineValues( + iface, uid, SET_DEFAULT, tag, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + /** @hide */ + public NetworkStats combineValues(String iface, int uid, int set, int tag, + long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { + return combineValues(new Entry( + iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations)); + } + + /** + * Combine given values with an existing row, or create a new row if + * {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can + * also be used to subtract values from existing rows. This method mutates the referencing + * {@link NetworkStats} object. + * + * @param entry the {@link Entry} to combine. + * @return a reference to this mutated {@link NetworkStats} object. + * @hide + */ + public @NonNull NetworkStats combineValues(@NonNull Entry entry) { + final int i = findIndex(entry.iface, entry.uid, entry.set, entry.tag, entry.metered, + entry.roaming, entry.defaultNetwork); + if (i == -1) { + // only create new entry when positive contribution + insertEntry(entry); + } else { + rxBytes[i] += entry.rxBytes; + rxPackets[i] += entry.rxPackets; + txBytes[i] += entry.txBytes; + txPackets[i] += entry.txPackets; + operations[i] += entry.operations; + } + return this; + } + + /** + * Add given values with an existing row, or create a new row if + * {@link #findIndex(String, int, int, int, int, int, int)} is unable to find match. Can + * also be used to subtract values from existing rows. + * + * @param entry the {@link Entry} to add. + * @return a new constructed {@link NetworkStats} object that contains the result. + */ + public @NonNull NetworkStats addEntry(@NonNull Entry entry) { + return this.clone().combineValues(entry); + } + + /** + * Add the given {@link NetworkStats} objects. + * + * @return the sum of two objects. + */ + public @NonNull NetworkStats add(@NonNull NetworkStats another) { + final NetworkStats ret = this.clone(); + ret.combineAllValues(another); + return ret; + } + + /** + * Combine all values from another {@link NetworkStats} into this object. + * @hide + */ + public void combineAllValues(@NonNull NetworkStats another) { + NetworkStats.Entry entry = null; + for (int i = 0; i < another.size; i++) { + entry = another.getValues(i, entry); + combineValues(entry); + } + } + + /** + * Find first stats index that matches the requested parameters. + * @hide + */ + public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming, + int defaultNetwork) { + for (int i = 0; i < size; i++) { + if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i] + && metered == this.metered[i] && roaming == this.roaming[i] + && defaultNetwork == this.defaultNetwork[i] + && Objects.equals(iface, this.iface[i])) { + return i; + } + } + return -1; + } + + /** + * Find first stats index that matches the requested parameters, starting + * search around the hinted index as an optimization. + * @hide + */ + @VisibleForTesting + public int findIndexHinted(String iface, int uid, int set, int tag, int metered, int roaming, + int defaultNetwork, int hintIndex) { + for (int offset = 0; offset < size; offset++) { + final int halfOffset = offset / 2; + + // search outwards from hint index, alternating forward and backward + final int i; + if (offset % 2 == 0) { + i = (hintIndex + halfOffset) % size; + } else { + i = (size + hintIndex - halfOffset - 1) % size; + } + + if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i] + && metered == this.metered[i] && roaming == this.roaming[i] + && defaultNetwork == this.defaultNetwork[i] + && Objects.equals(iface, this.iface[i])) { + return i; + } + } + return -1; + } + + /** + * Splice in {@link #operations} from the given {@link NetworkStats} based + * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface}, + * since operation counts are at data layer. + * @hide + */ + public void spliceOperationsFrom(NetworkStats stats) { + for (int i = 0; i < size; i++) { + final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i], + defaultNetwork[i]); + if (j == -1) { + operations[i] = 0; + } else { + operations[i] = stats.operations[j]; + } + } + } + + /** + * Return list of unique interfaces known by this data structure. + * @hide + */ + public String[] getUniqueIfaces() { + final HashSet ifaces = new HashSet(); + for (String iface : this.iface) { + if (iface != IFACE_ALL) { + ifaces.add(iface); + } + } + return ifaces.toArray(new String[ifaces.size()]); + } + + /** + * Return list of unique UIDs known by this data structure. + * @hide + */ + @UnsupportedAppUsage + public int[] getUniqueUids() { + final SparseBooleanArray uids = new SparseBooleanArray(); + for (int uid : this.uid) { + uids.put(uid, true); + } + + final int size = uids.size(); + final int[] result = new int[size]; + for (int i = 0; i < size; i++) { + result[i] = uids.keyAt(i); + } + return result; + } + + /** + * Return total bytes represented by this snapshot object, usually used when + * checking if a {@link #subtract(NetworkStats)} delta passes a threshold. + * @hide + */ + @UnsupportedAppUsage + public long getTotalBytes() { + final Entry entry = getTotal(null); + return entry.rxBytes + entry.txBytes; + } + + /** + * Return total of all fields represented by this snapshot object. + * @hide + */ + @UnsupportedAppUsage + public Entry getTotal(Entry recycle) { + return getTotal(recycle, null, UID_ALL, false); + } + + /** + * Return total of all fields represented by this snapshot object matching + * the requested {@link #uid}. + * @hide + */ + @UnsupportedAppUsage + public Entry getTotal(Entry recycle, int limitUid) { + return getTotal(recycle, null, limitUid, false); + } + + /** + * Return total of all fields represented by this snapshot object matching + * the requested {@link #iface}. + * @hide + */ + public Entry getTotal(Entry recycle, HashSet limitIface) { + return getTotal(recycle, limitIface, UID_ALL, false); + } + + /** @hide */ + @UnsupportedAppUsage + public Entry getTotalIncludingTags(Entry recycle) { + return getTotal(recycle, null, UID_ALL, true); + } + + /** + * Return total of all fields represented by this snapshot object matching + * the requested {@link #iface} and {@link #uid}. + * + * @param limitIface Set of {@link #iface} to include in total; or {@code + * null} to include all ifaces. + */ + private Entry getTotal( + Entry recycle, HashSet limitIface, int limitUid, boolean includeTags) { + final Entry entry = recycle != null ? recycle : new Entry(); + + entry.iface = IFACE_ALL; + entry.uid = limitUid; + entry.set = SET_ALL; + entry.tag = TAG_NONE; + entry.metered = METERED_ALL; + entry.roaming = ROAMING_ALL; + entry.defaultNetwork = DEFAULT_NETWORK_ALL; + entry.rxBytes = 0; + entry.rxPackets = 0; + entry.txBytes = 0; + entry.txPackets = 0; + entry.operations = 0; + + for (int i = 0; i < size; i++) { + final boolean matchesUid = (limitUid == UID_ALL) || (limitUid == uid[i]); + final boolean matchesIface = (limitIface == null) || (limitIface.contains(iface[i])); + + if (matchesUid && matchesIface) { + // skip specific tags, since already counted in TAG_NONE + if (tag[i] != TAG_NONE && !includeTags) continue; + + entry.rxBytes += rxBytes[i]; + entry.rxPackets += rxPackets[i]; + entry.txBytes += txBytes[i]; + entry.txPackets += txPackets[i]; + entry.operations += operations[i]; + } + } + return entry; + } + + /** + * Fast path for battery stats. + * @hide + */ + public long getTotalPackets() { + long total = 0; + for (int i = size-1; i >= 0; i--) { + total += rxPackets[i] + txPackets[i]; + } + return total; + } + + /** + * Subtract the given {@link NetworkStats}, effectively leaving the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. This method does not mutate + * the referencing object. + * + * @return the delta between two objects. + */ + public @NonNull NetworkStats subtract(@NonNull NetworkStats right) { + return subtract(this, right, null, null); + } + + /** + * Subtract the two given {@link NetworkStats} objects, returning the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. + *

+ * If counters have rolled backwards, they are clamped to {@code 0} and + * reported to the given {@link NonMonotonicObserver}. + * @hide + */ + public static NetworkStats subtract(NetworkStats left, NetworkStats right, + NonMonotonicObserver observer, C cookie) { + return subtract(left, right, observer, cookie, null); + } + + /** + * Subtract the two given {@link NetworkStats} objects, returning the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. + *

+ * If counters have rolled backwards, they are clamped to {@code 0} and + * reported to the given {@link NonMonotonicObserver}. + *

+ * If recycle is supplied, this NetworkStats object will be + * reused (and returned) as the result if it is large enough to contain + * the data. + * @hide + */ + public static NetworkStats subtract(NetworkStats left, NetworkStats right, + NonMonotonicObserver observer, C cookie, NetworkStats recycle) { + long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime; + if (deltaRealtime < 0) { + if (observer != null) { + observer.foundNonMonotonic(left, -1, right, -1, cookie); + } + deltaRealtime = 0; + } + + // result will have our rows, and elapsed time between snapshots + final Entry entry = new Entry(); + final NetworkStats result; + if (recycle != null && recycle.capacity >= left.size) { + result = recycle; + result.size = 0; + result.elapsedRealtime = deltaRealtime; + } else { + result = new NetworkStats(deltaRealtime, left.size); + } + for (int i = 0; i < left.size; i++) { + entry.iface = left.iface[i]; + entry.uid = left.uid[i]; + entry.set = left.set[i]; + entry.tag = left.tag[i]; + entry.metered = left.metered[i]; + entry.roaming = left.roaming[i]; + entry.defaultNetwork = left.defaultNetwork[i]; + entry.rxBytes = left.rxBytes[i]; + entry.rxPackets = left.rxPackets[i]; + entry.txBytes = left.txBytes[i]; + entry.txPackets = left.txPackets[i]; + entry.operations = left.operations[i]; + + // find remote row that matches, and subtract + final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, + entry.metered, entry.roaming, entry.defaultNetwork, i); + if (j != -1) { + // Found matching row, subtract remote value. + entry.rxBytes -= right.rxBytes[j]; + entry.rxPackets -= right.rxPackets[j]; + entry.txBytes -= right.txBytes[j]; + entry.txPackets -= right.txPackets[j]; + entry.operations -= right.operations[j]; + } + + if (entry.isNegative()) { + if (observer != null) { + observer.foundNonMonotonic(left, i, right, j, cookie); + } + 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); + } + + result.insertEntry(entry); + } + + return result; + } + + /** + * Calculate and apply adjustments to captured statistics for 464xlat traffic. + * + *

This mutates stacked traffic stats, to account for IPv4/IPv6 header size difference. + * + *

UID stats, which are only accounted on the stacked interface, need to be increased + * by 20 bytes/packet to account for translation overhead. + * + *

The potential additional overhead of 8 bytes/packet for ip fragments is ignored. + * + *

Interface stats need to sum traffic on both stacked and base interface because: + * - eBPF offloaded packets appear only on the stacked interface + * - Non-offloaded ingress packets appear only on the stacked interface + * (due to iptables raw PREROUTING drop rules) + * - Non-offloaded egress packets appear only on the stacked interface + * (due to ignoring traffic from clat daemon by uid match) + * (and of course the 20 bytes/packet overhead needs to be applied to stacked interface stats) + * + *

This method will behave fine if {@code stackedIfaces} is an non-synchronized but add-only + * {@code ConcurrentHashMap} + * @param baseTraffic Traffic on the base interfaces. Will be mutated. + * @param stackedTraffic Stats with traffic stacked on top of our ifaces. Will also be mutated. + * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both. + * @hide + */ + public static void apply464xlatAdjustments(NetworkStats baseTraffic, + NetworkStats stackedTraffic, Map stackedIfaces) { + // For recycling + Entry entry = null; + for (int i = 0; i < stackedTraffic.size; i++) { + entry = stackedTraffic.getValues(i, entry); + if (entry == null) continue; + if (entry.iface == null) continue; + if (!entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) continue; + + // For 464xlat traffic, per uid stats only counts the bytes of the native IPv4 packet + // sent on the stacked interface with prefix "v4-" and drops the IPv6 header size after + // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes + // difference for all packets (http://b/12249687, http:/b/33681750). + // + // Note: this doesn't account for LRO/GRO/GSO/TSO (ie. >mtu) traffic correctly, nor + // does it correctly account for the 8 extra bytes in the IPv6 fragmentation header. + // + // While the ebpf code path does try to simulate proper post segmentation packet + // counts, we have nothing of the sort of xt_qtaguid stats. + entry.rxBytes += entry.rxPackets * IPV4V6_HEADER_DELTA; + entry.txBytes += entry.txPackets * IPV4V6_HEADER_DELTA; + stackedTraffic.setValues(i, entry); + } + } + + /** + * Calculate and apply adjustments to captured statistics for 464xlat traffic counted twice. + * + *

This mutates the object this method is called on. Equivalent to calling + * {@link #apply464xlatAdjustments(NetworkStats, NetworkStats, Map)} with {@code this} as + * base and stacked traffic. + * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both. + * @hide + */ + public void apply464xlatAdjustments(Map stackedIfaces) { + apply464xlatAdjustments(this, this, stackedIfaces); + } + + /** + * Return total statistics grouped by {@link #iface}; doesn't mutate the + * original structure. + * @hide + */ + public NetworkStats groupedByIface() { + final NetworkStats stats = new NetworkStats(elapsedRealtime, 10); + + final Entry entry = new Entry(); + entry.uid = UID_ALL; + entry.set = SET_ALL; + entry.tag = TAG_NONE; + entry.metered = METERED_ALL; + entry.roaming = ROAMING_ALL; + entry.defaultNetwork = DEFAULT_NETWORK_ALL; + entry.operations = 0L; + + for (int i = 0; i < size; i++) { + // skip specific tags, since already counted in TAG_NONE + if (tag[i] != TAG_NONE) continue; + + entry.iface = iface[i]; + entry.rxBytes = rxBytes[i]; + entry.rxPackets = rxPackets[i]; + entry.txBytes = txBytes[i]; + entry.txPackets = txPackets[i]; + stats.combineValues(entry); + } + + return stats; + } + + /** + * Return total statistics grouped by {@link #uid}; doesn't mutate the + * original structure. + * @hide + */ + public NetworkStats groupedByUid() { + final NetworkStats stats = new NetworkStats(elapsedRealtime, 10); + + final Entry entry = new Entry(); + entry.iface = IFACE_ALL; + entry.set = SET_ALL; + entry.tag = TAG_NONE; + entry.metered = METERED_ALL; + entry.roaming = ROAMING_ALL; + entry.defaultNetwork = DEFAULT_NETWORK_ALL; + + for (int i = 0; i < size; i++) { + // skip specific tags, since already counted in TAG_NONE + if (tag[i] != TAG_NONE) continue; + + entry.uid = uid[i]; + entry.rxBytes = rxBytes[i]; + entry.rxPackets = rxPackets[i]; + entry.txBytes = txBytes[i]; + entry.txPackets = txPackets[i]; + entry.operations = operations[i]; + stats.combineValues(entry); + } + + return stats; + } + + /** + * Remove all rows that match one of specified UIDs. + * This mutates the original structure in place. + * @hide + */ + public void removeUids(int[] uids) { + filter(e -> !CollectionUtils.contains(uids, e.uid)); + } + + /** + * Remove all rows that match one of specified UIDs. + * @return the result object. + * @hide + */ + @NonNull + public NetworkStats removeEmptyEntries() { + final NetworkStats ret = this.clone(); + ret.filter(e -> e.rxBytes != 0 || e.rxPackets != 0 || e.txBytes != 0 || e.txPackets != 0 + || e.operations != 0); + return ret; + } + + /** + * Only keep entries that match all specified filters. + * + *

This mutates the original structure in place. After this method is called, + * size is the number of matching entries, and capacity is the previous capacity. + * @param limitUid UID to filter for, or {@link #UID_ALL}. + * @param limitIfaces Interfaces to filter for, or {@link #INTERFACES_ALL}. + * @param limitTag Tag to filter for, or {@link #TAG_ALL}. + * @hide + */ + public void filter(int limitUid, String[] limitIfaces, int limitTag) { + if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) { + return; + } + filter(e -> (limitUid == UID_ALL || limitUid == e.uid) + && (limitTag == TAG_ALL || limitTag == e.tag) + && (limitIfaces == INTERFACES_ALL + || CollectionUtils.contains(limitIfaces, e.iface))); + } + + /** + * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}. + * + *

This mutates the original structure in place. + * @hide + */ + public void filterDebugEntries() { + filter(e -> e.set < SET_DEBUG_START); + } + + private void filter(Predicate predicate) { + Entry entry = new Entry(); + int nextOutputEntry = 0; + for (int i = 0; i < size; i++) { + entry = getValues(i, entry); + if (predicate.test(entry)) { + if (nextOutputEntry != i) { + setValues(nextOutputEntry, entry); + } + nextOutputEntry++; + } + } + size = nextOutputEntry; + } + + /** @hide */ + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); + pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime); + for (int i = 0; i < size; i++) { + pw.print(prefix); + pw.print(" ["); pw.print(i); pw.print("]"); + pw.print(" iface="); pw.print(iface[i]); + pw.print(" uid="); pw.print(uid[i]); + pw.print(" set="); pw.print(setToString(set[i])); + pw.print(" tag="); pw.print(tagToString(tag[i])); + pw.print(" metered="); pw.print(meteredToString(metered[i])); + pw.print(" roaming="); pw.print(roamingToString(roaming[i])); + pw.print(" defaultNetwork="); pw.print(defaultNetworkToString(defaultNetwork[i])); + pw.print(" rxBytes="); pw.print(rxBytes[i]); + pw.print(" rxPackets="); pw.print(rxPackets[i]); + pw.print(" txBytes="); pw.print(txBytes[i]); + pw.print(" txPackets="); pw.print(txPackets[i]); + pw.print(" operations="); pw.println(operations[i]); + } + } + + /** + * Return text description of {@link #set} value. + * @hide + */ + public static String setToString(int set) { + switch (set) { + case SET_ALL: + return "ALL"; + case SET_DEFAULT: + return "DEFAULT"; + case SET_FOREGROUND: + return "FOREGROUND"; + case SET_DBG_VPN_IN: + return "DBG_VPN_IN"; + case SET_DBG_VPN_OUT: + return "DBG_VPN_OUT"; + default: + return "UNKNOWN"; + } + } + + /** + * Return text description of {@link #set} value. + * @hide + */ + public static String setToCheckinString(int set) { + switch (set) { + case SET_ALL: + return "all"; + case SET_DEFAULT: + return "def"; + case SET_FOREGROUND: + return "fg"; + case SET_DBG_VPN_IN: + return "vpnin"; + case SET_DBG_VPN_OUT: + return "vpnout"; + default: + return "unk"; + } + } + + /** + * @return true if the querySet matches the dataSet. + * @hide + */ + public static boolean setMatches(int querySet, int dataSet) { + if (querySet == dataSet) { + return true; + } + // SET_ALL matches all non-debugging sets. + return querySet == SET_ALL && dataSet < SET_DEBUG_START; + } + + /** + * Return text description of {@link #tag} value. + * @hide + */ + public static String tagToString(int tag) { + return "0x" + Integer.toHexString(tag); + } + + /** + * Return text description of {@link #metered} value. + * @hide + */ + public static String meteredToString(int metered) { + switch (metered) { + case METERED_ALL: + return "ALL"; + case METERED_NO: + return "NO"; + case METERED_YES: + return "YES"; + default: + return "UNKNOWN"; + } + } + + /** + * Return text description of {@link #roaming} value. + * @hide + */ + public static String roamingToString(int roaming) { + switch (roaming) { + case ROAMING_ALL: + return "ALL"; + case ROAMING_NO: + return "NO"; + case ROAMING_YES: + return "YES"; + default: + return "UNKNOWN"; + } + } + + /** + * Return text description of {@link #defaultNetwork} value. + * @hide + */ + public static String defaultNetworkToString(int defaultNetwork) { + switch (defaultNetwork) { + case DEFAULT_NETWORK_ALL: + return "ALL"; + case DEFAULT_NETWORK_NO: + return "NO"; + case DEFAULT_NETWORK_YES: + return "YES"; + default: + return "UNKNOWN"; + } + } + + /** @hide */ + @Override + public String toString() { + final CharArrayWriter writer = new CharArrayWriter(); + dump("", new PrintWriter(writer)); + return writer.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator CREATOR = new Creator() { + @Override + public NetworkStats createFromParcel(Parcel in) { + return new NetworkStats(in); + } + + @Override + public NetworkStats[] newArray(int size) { + return new NetworkStats[size]; + } + }; + + /** @hide */ + public interface NonMonotonicObserver { + public void foundNonMonotonic( + NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie); + public void foundNonMonotonic( + NetworkStats stats, int statsIndex, C cookie); + } + + /** + * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface. + * + *

This method should only be called on delta NetworkStats. Do not call this method on a + * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may change + * over time. + * + *

This method performs adjustments for one active VPN package and one VPN iface at a time. + * + * @param tunUid uid of the VPN application + * @param tunIface iface of the vpn tunnel + * @param underlyingIfaces underlying network ifaces used by the VPN application + * @hide + */ + public void migrateTun(int tunUid, @NonNull String tunIface, + @NonNull List underlyingIfaces) { + // Combined usage by all apps using VPN. + final Entry tunIfaceTotal = new Entry(); + // Usage by VPN, grouped by its {@code underlyingIfaces}. + final Entry[] perInterfaceTotal = new Entry[underlyingIfaces.size()]; + // Usage by VPN, summed across all its {@code underlyingIfaces}. + final Entry underlyingIfacesTotal = new Entry(); + + for (int i = 0; i < perInterfaceTotal.length; i++) { + perInterfaceTotal[i] = new Entry(); + } + + tunAdjustmentInit(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, perInterfaceTotal, + underlyingIfacesTotal); + + // If tunIface < underlyingIfacesTotal, it leaves the overhead traffic in the VPN app. + // If tunIface > underlyingIfacesTotal, the VPN app doesn't get credit for data compression. + // Negative stats should be avoided. + final Entry[] moved = + addTrafficToApplications(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, + perInterfaceTotal, underlyingIfacesTotal); + deductTrafficFromVpnApp(tunUid, underlyingIfaces, moved); + } + + /** + * Initializes the data used by the migrateTun() method. + * + *

This is the first pass iteration which does the following work: + * + *

    + *
  • Adds up all the traffic through the tunUid's underlyingIfaces (both foreground and + * background). + *
  • Adds up all the traffic through tun0 excluding traffic from the vpn app itself. + *
+ * + * @param tunUid uid of the VPN application + * @param tunIface iface of the vpn tunnel + * @param underlyingIfaces underlying network ifaces used by the VPN application + * @param tunIfaceTotal output parameter; combined data usage by all apps using VPN + * @param perInterfaceTotal output parameter; data usage by VPN app, grouped by its {@code + * underlyingIfaces} + * @param underlyingIfacesTotal output parameter; data usage by VPN, summed across all of its + * {@code underlyingIfaces} + */ + private void tunAdjustmentInit(int tunUid, @NonNull String tunIface, + @NonNull List underlyingIfaces, @NonNull Entry tunIfaceTotal, + @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) { + final Entry recycle = new Entry(); + for (int i = 0; i < size; i++) { + getValues(i, recycle); + if (recycle.uid == UID_ALL) { + throw new IllegalStateException( + "Cannot adjust VPN accounting on an iface aggregated NetworkStats."); + } + if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) { + throw new IllegalStateException( + "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*"); + } + if (recycle.tag != TAG_NONE) { + // TODO(b/123666283): Take all tags for tunUid into account. + continue; + } + + if (tunUid == Process.SYSTEM_UID) { + // Kernel-based VPN or VCN, traffic sent by apps on the VPN/VCN network + // + // Since the data is not UID-accounted on underlying networks, just use VPN/VCN + // network usage as ground truth. Encrypted traffic on the underlying networks will + // never be processed here because encrypted traffic on the underlying interfaces + // is not present in UID stats, and this method is only called on UID stats. + if (tunIface.equals(recycle.iface)) { + tunIfaceTotal.add(recycle); + underlyingIfacesTotal.add(recycle); + + // In steady state, there should always be one network, but edge cases may + // result in the network being null (network lost), and thus no underlying + // ifaces is possible. + if (perInterfaceTotal.length > 0) { + // While platform VPNs and VCNs have exactly one underlying network, that + // network may have multiple interfaces (eg for 464xlat). This layer does + // not have the required information to identify which of the interfaces + // were used. Select "any" of the interfaces. Since overhead is already + // lost, this number is an approximation anyways. + perInterfaceTotal[0].add(recycle); + } + } + } else if (recycle.uid == tunUid) { + // VpnService VPN, traffic sent by the VPN app over underlying networks + for (int j = 0; j < underlyingIfaces.size(); j++) { + if (Objects.equals(underlyingIfaces.get(j), recycle.iface)) { + perInterfaceTotal[j].add(recycle); + underlyingIfacesTotal.add(recycle); + break; + } + } + } else if (tunIface.equals(recycle.iface)) { + // VpnService VPN; traffic sent by apps on the VPN network + tunIfaceTotal.add(recycle); + } + } + } + + /** + * Distributes traffic across apps that are using given {@code tunIface}, and returns the total + * traffic that should be moved off of {@code tunUid} grouped by {@code underlyingIfaces}. + * + * @param tunUid uid of the VPN application + * @param tunIface iface of the vpn tunnel + * @param underlyingIfaces underlying network ifaces used by the VPN application + * @param tunIfaceTotal combined data usage across all apps using {@code tunIface} + * @param perInterfaceTotal data usage by VPN app, grouped by its {@code underlyingIfaces} + * @param underlyingIfacesTotal data usage by VPN, summed across all of its {@code + * underlyingIfaces} + */ + private Entry[] addTrafficToApplications(int tunUid, @NonNull String tunIface, + @NonNull List underlyingIfaces, @NonNull Entry tunIfaceTotal, + @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) { + // Traffic that should be moved off of each underlying interface for tunUid (see + // deductTrafficFromVpnApp below). + final Entry[] moved = new Entry[underlyingIfaces.size()]; + for (int i = 0; i < underlyingIfaces.size(); i++) { + moved[i] = new Entry(); + } + + final Entry tmpEntry = new Entry(); + final int origSize = size; + for (int i = 0; i < origSize; i++) { + if (!Objects.equals(iface[i], tunIface)) { + // Consider only entries that go onto the VPN interface. + continue; + } + + if (uid[i] == tunUid && tunUid != Process.SYSTEM_UID) { + // Exclude VPN app from the redistribution, as it can choose to create packet + // streams by writing to itself. + // + // However, for platform VPNs, do not exclude the system's usage of the VPN network, + // since it is never local-only, and never double counted + continue; + } + tmpEntry.uid = uid[i]; + tmpEntry.tag = tag[i]; + tmpEntry.metered = metered[i]; + tmpEntry.roaming = roaming[i]; + tmpEntry.defaultNetwork = defaultNetwork[i]; + + // In a first pass, compute this entry's total share of data across all + // underlyingIfaces. This is computed on the basis of the share of this entry's usage + // over tunIface. + // TODO: Consider refactoring first pass into a separate helper method. + long totalRxBytes = 0; + if (tunIfaceTotal.rxBytes > 0) { + // Note - The multiplication below should not overflow since NetworkStatsService + // processes this every time device has transmitted/received amount equivalent to + // global threshold alert (~ 2MB) across all interfaces. + final long rxBytesAcrossUnderlyingIfaces = + multiplySafeByRational(underlyingIfacesTotal.rxBytes, + rxBytes[i], tunIfaceTotal.rxBytes); + // app must not be blamed for more than it consumed on tunIface + totalRxBytes = Math.min(rxBytes[i], rxBytesAcrossUnderlyingIfaces); + } + long totalRxPackets = 0; + if (tunIfaceTotal.rxPackets > 0) { + final long rxPacketsAcrossUnderlyingIfaces = + multiplySafeByRational(underlyingIfacesTotal.rxPackets, + rxPackets[i], tunIfaceTotal.rxPackets); + totalRxPackets = Math.min(rxPackets[i], rxPacketsAcrossUnderlyingIfaces); + } + long totalTxBytes = 0; + if (tunIfaceTotal.txBytes > 0) { + final long txBytesAcrossUnderlyingIfaces = + multiplySafeByRational(underlyingIfacesTotal.txBytes, + txBytes[i], tunIfaceTotal.txBytes); + totalTxBytes = Math.min(txBytes[i], txBytesAcrossUnderlyingIfaces); + } + long totalTxPackets = 0; + if (tunIfaceTotal.txPackets > 0) { + final long txPacketsAcrossUnderlyingIfaces = + multiplySafeByRational(underlyingIfacesTotal.txPackets, + txPackets[i], tunIfaceTotal.txPackets); + totalTxPackets = Math.min(txPackets[i], txPacketsAcrossUnderlyingIfaces); + } + long totalOperations = 0; + if (tunIfaceTotal.operations > 0) { + final long operationsAcrossUnderlyingIfaces = + multiplySafeByRational(underlyingIfacesTotal.operations, + operations[i], tunIfaceTotal.operations); + totalOperations = Math.min(operations[i], operationsAcrossUnderlyingIfaces); + } + // In a second pass, distribute these values across interfaces in the proportion that + // each interface represents of the total traffic of the underlying interfaces. + for (int j = 0; j < underlyingIfaces.size(); j++) { + tmpEntry.iface = underlyingIfaces.get(j); + tmpEntry.rxBytes = 0; + // Reset 'set' to correct value since it gets updated when adding debug info below. + tmpEntry.set = set[i]; + if (underlyingIfacesTotal.rxBytes > 0) { + tmpEntry.rxBytes = + multiplySafeByRational(totalRxBytes, + perInterfaceTotal[j].rxBytes, + underlyingIfacesTotal.rxBytes); + } + tmpEntry.rxPackets = 0; + if (underlyingIfacesTotal.rxPackets > 0) { + tmpEntry.rxPackets = + multiplySafeByRational(totalRxPackets, + perInterfaceTotal[j].rxPackets, + underlyingIfacesTotal.rxPackets); + } + tmpEntry.txBytes = 0; + if (underlyingIfacesTotal.txBytes > 0) { + tmpEntry.txBytes = + multiplySafeByRational(totalTxBytes, + perInterfaceTotal[j].txBytes, + underlyingIfacesTotal.txBytes); + } + tmpEntry.txPackets = 0; + if (underlyingIfacesTotal.txPackets > 0) { + tmpEntry.txPackets = + multiplySafeByRational(totalTxPackets, + perInterfaceTotal[j].txPackets, + underlyingIfacesTotal.txPackets); + } + tmpEntry.operations = 0; + if (underlyingIfacesTotal.operations > 0) { + tmpEntry.operations = + multiplySafeByRational(totalOperations, + perInterfaceTotal[j].operations, + underlyingIfacesTotal.operations); + } + // tmpEntry now contains the migrated data of the i-th entry for the j-th underlying + // interface. Add that data usage to this object. + combineValues(tmpEntry); + if (tag[i] == TAG_NONE) { + // Add the migrated data to moved so it is deducted from the VPN app later. + moved[j].add(tmpEntry); + // Add debug info + tmpEntry.set = SET_DBG_VPN_IN; + combineValues(tmpEntry); + } + } + } + return moved; + } + + private void deductTrafficFromVpnApp( + int tunUid, + @NonNull List underlyingIfaces, + @NonNull Entry[] moved) { + if (tunUid == Process.SYSTEM_UID) { + // No traffic recorded on a per-UID basis for in-kernel VPN/VCNs over underlying + // networks; thus no traffic to deduct. + return; + } + + for (int i = 0; i < underlyingIfaces.size(); i++) { + moved[i].uid = tunUid; + // Add debug info + moved[i].set = SET_DBG_VPN_OUT; + moved[i].tag = TAG_NONE; + moved[i].iface = underlyingIfaces.get(i); + moved[i].metered = METERED_ALL; + moved[i].roaming = ROAMING_ALL; + moved[i].defaultNetwork = DEFAULT_NETWORK_ALL; + combineValues(moved[i]); + + // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than + // the TAG_NONE traffic. + // + // Relies on the fact that the underlying traffic only has state ROAMING_NO and + // METERED_NO, which should be the case as it comes directly from the /proc file. + // We only blend in the roaming data after applying these adjustments, by checking the + // NetworkIdentity of the underlying iface. + final int idxVpnBackground = findIndex(underlyingIfaces.get(i), tunUid, SET_DEFAULT, + TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); + if (idxVpnBackground != -1) { + // Note - tunSubtract also updates moved[i]; whatever traffic that's left is removed + // from foreground usage. + tunSubtract(idxVpnBackground, this, moved[i]); + } + + final int idxVpnForeground = findIndex(underlyingIfaces.get(i), tunUid, SET_FOREGROUND, + TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); + if (idxVpnForeground != -1) { + tunSubtract(idxVpnForeground, this, moved[i]); + } + } + } + + private static void tunSubtract(int i, @NonNull NetworkStats left, @NonNull Entry right) { + long rxBytes = Math.min(left.rxBytes[i], right.rxBytes); + left.rxBytes[i] -= rxBytes; + right.rxBytes -= rxBytes; + + long rxPackets = Math.min(left.rxPackets[i], right.rxPackets); + left.rxPackets[i] -= rxPackets; + right.rxPackets -= rxPackets; + + long txBytes = Math.min(left.txBytes[i], right.txBytes); + left.txBytes[i] -= txBytes; + right.txBytes -= txBytes; + + long txPackets = Math.min(left.txPackets[i], right.txPackets); + left.txPackets[i] -= txPackets; + right.txPackets -= txPackets; + } +} diff --git a/framework-t/src/android/net/NetworkStatsAccess.java b/framework-t/src/android/net/NetworkStatsAccess.java new file mode 100644 index 0000000000..b64fbdba9a --- /dev/null +++ b/framework-t/src/android/net/NetworkStatsAccess.java @@ -0,0 +1,208 @@ +/* + * 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 android.net; + +import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.NetworkStats.UID_ALL; +import static android.net.TrafficStats.UID_REMOVED; +import static android.net.TrafficStats.UID_TETHERING; + +import android.Manifest; +import android.annotation.IntDef; +import android.app.AppOpsManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.Process; +import android.os.UserHandle; +import android.telephony.TelephonyManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Utility methods for controlling access to network stats APIs. + * + * @hide + */ +public final class NetworkStatsAccess { + private NetworkStatsAccess() {} + + /** + * Represents an access level for the network usage history and statistics APIs. + * + *

Access levels are in increasing order; that is, it is reasonable to check access by + * verifying that the caller's access level is at least the minimum required level. + */ + @IntDef({ + Level.DEFAULT, + Level.USER, + Level.DEVICESUMMARY, + Level.DEVICE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Level { + /** + * Default, unprivileged access level. + * + *

Can only access usage for one's own UID. + * + *

Every app will have at least this access level. + */ + int DEFAULT = 0; + + /** + * Access level for apps which can access usage for any app running in the same user. + * + *

Granted to: + *

    + *
  • Profile owners. + *
+ */ + int USER = 1; + + /** + * Access level for apps which can access usage summary of device. Device summary includes + * usage by apps running in any profiles/users, however this access level does not + * allow querying usage of individual apps running in other profiles/users. + * + *

Granted to: + *

    + *
  • Apps with the PACKAGE_USAGE_STATS permission granted. Note that this is an AppOps bit + * so it is not necessarily sufficient to declare this in the manifest. + *
  • Apps with the (signature/privileged) READ_NETWORK_USAGE_HISTORY permission. + *
+ */ + int DEVICESUMMARY = 2; + + /** + * Access level for apps which can access usage for any app on the device, including apps + * running on other users/profiles. + * + *

Granted to: + *

    + *
  • Device owners. + *
  • Carrier-privileged applications. + *
  • The system UID. + *
+ */ + int DEVICE = 3; + } + + /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */ + public static @NetworkStatsAccess.Level int checkAccessLevel( + Context context, int callingPid, int callingUid, String callingPackage) { + final DevicePolicyManager mDpm = context.getSystemService(DevicePolicyManager.class); + final TelephonyManager tm = (TelephonyManager) + context.getSystemService(Context.TELEPHONY_SERVICE); + boolean hasCarrierPrivileges; + final long token = Binder.clearCallingIdentity(); + try { + hasCarrierPrivileges = tm != null + && tm.checkCarrierPrivilegesForPackageAnyPhone(callingPackage) + == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; + } finally { + Binder.restoreCallingIdentity(token); + } + + final boolean isDeviceOwner = mDpm != null && mDpm.isDeviceOwnerApp(callingPackage); + final int appId = UserHandle.getAppId(callingUid); + + final boolean isNetworkStack = context.checkPermission( + android.Manifest.permission.NETWORK_STACK, callingPid, callingUid) + == PERMISSION_GRANTED; + + if (hasCarrierPrivileges || isDeviceOwner + || appId == Process.SYSTEM_UID || isNetworkStack) { + // Carrier-privileged apps and device owners, and the system (including the + // network stack) can access data usage for all apps on the device. + return NetworkStatsAccess.Level.DEVICE; + } + + boolean hasAppOpsPermission = hasAppOpsPermission(context, callingUid, callingPackage); + if (hasAppOpsPermission || context.checkCallingOrSelfPermission( + READ_NETWORK_USAGE_HISTORY) == PackageManager.PERMISSION_GRANTED) { + return NetworkStatsAccess.Level.DEVICESUMMARY; + } + + //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode. + boolean isProfileOwner = mDpm != null && (mDpm.isProfileOwnerApp(callingPackage) + || mDpm.isDeviceOwnerApp(callingPackage)); + if (isProfileOwner) { + // Apps with the AppOps permission, profile owners, and apps with the privileged + // permission can access data usage for all apps in this user/profile. + return NetworkStatsAccess.Level.USER; + } + + // Everyone else gets default access (only to their own UID). + return NetworkStatsAccess.Level.DEFAULT; + } + + /** + * Returns whether the given caller should be able to access the given UID when the caller has + * the given {@link NetworkStatsAccess.Level}. + */ + public static boolean isAccessibleToUser(int uid, int callerUid, + @NetworkStatsAccess.Level int accessLevel) { + final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); + final int callerUserId = UserHandle.getUserHandleForUid(callerUid).getIdentifier(); + switch (accessLevel) { + case NetworkStatsAccess.Level.DEVICE: + // Device-level access - can access usage for any uid. + return true; + case NetworkStatsAccess.Level.DEVICESUMMARY: + // Can access usage for any app running in the same user, along + // with some special uids (system, removed, or tethering) and + // anonymized uids + return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED + || uid == UID_TETHERING || uid == UID_ALL + || userId == callerUserId; + case NetworkStatsAccess.Level.USER: + // User-level access - can access usage for any app running in the same user, along + // with some special uids (system, removed, or tethering). + return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED + || uid == UID_TETHERING + || userId == callerUserId; + case NetworkStatsAccess.Level.DEFAULT: + default: + // Default access level - can only access one's own usage. + return uid == callerUid; + } + } + + private static boolean hasAppOpsPermission( + Context context, int callingUid, String callingPackage) { + if (callingPackage != null) { + AppOpsManager appOps = (AppOpsManager) context.getSystemService( + Context.APP_OPS_SERVICE); + + final int mode = appOps.noteOp(AppOpsManager.OPSTR_GET_USAGE_STATS, + callingUid, callingPackage, null /* attributionTag */, null /* message */); + if (mode == AppOpsManager.MODE_DEFAULT) { + // The default behavior here is to check if PackageManager has given the app + // permission. + final int permissionCheck = context.checkCallingPermission( + Manifest.permission.PACKAGE_USAGE_STATS); + return permissionCheck == PackageManager.PERMISSION_GRANTED; + } + return (mode == AppOpsManager.MODE_ALLOWED); + } + return false; + } +} diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java new file mode 100644 index 0000000000..e385b33447 --- /dev/null +++ b/framework-t/src/android/net/NetworkStatsCollection.java @@ -0,0 +1,956 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.TrafficStats.UID_REMOVED; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; + +import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.net.NetworkStats.State; +import android.net.NetworkStatsHistory.Entry; +import android.os.Binder; +import android.service.NetworkStatsCollectionKeyProto; +import android.service.NetworkStatsCollectionProto; +import android.service.NetworkStatsCollectionStatsProto; +import android.telephony.SubscriptionPlan; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.IndentingPrintWriter; +import android.util.Log; +import android.util.Range; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FileRotator; +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.NetworkStatsUtils; + +import libcore.io.IoUtils; + +import java.io.BufferedInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.ProtocolException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Collection of {@link NetworkStatsHistory}, stored based on combined key of + * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself. + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer { + private static final String TAG = NetworkStatsCollection.class.getSimpleName(); + /** File header magic number: "ANET" */ + private static final int FILE_MAGIC = 0x414E4554; + + private static final int VERSION_NETWORK_INIT = 1; + + private static final int VERSION_UID_INIT = 1; + private static final int VERSION_UID_WITH_IDENT = 2; + private static final int VERSION_UID_WITH_TAG = 3; + private static final int VERSION_UID_WITH_SET = 4; + + private static final int VERSION_UNIFIED_INIT = 16; + + private ArrayMap mStats = new ArrayMap<>(); + + private final long mBucketDurationMillis; + + private long mStartMillis; + private long mEndMillis; + private long mTotalBytes; + private boolean mDirty; + + /** + * Construct a {@link NetworkStatsCollection} object. + * + * @param bucketDuration duration of the buckets in this object, in milliseconds. + * @hide + */ + public NetworkStatsCollection(long bucketDurationMillis) { + mBucketDurationMillis = bucketDurationMillis; + reset(); + } + + /** @hide */ + public void clear() { + reset(); + } + + /** @hide */ + public void reset() { + mStats.clear(); + mStartMillis = Long.MAX_VALUE; + mEndMillis = Long.MIN_VALUE; + mTotalBytes = 0; + mDirty = false; + } + + /** @hide */ + public long getStartMillis() { + return mStartMillis; + } + + /** + * Return first atomic bucket in this collection, which is more conservative + * than {@link #mStartMillis}. + * @hide + */ + public long getFirstAtomicBucketMillis() { + if (mStartMillis == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } else { + return mStartMillis + mBucketDurationMillis; + } + } + + /** @hide */ + public long getEndMillis() { + return mEndMillis; + } + + /** @hide */ + public long getTotalBytes() { + return mTotalBytes; + } + + /** @hide */ + public boolean isDirty() { + return mDirty; + } + + /** @hide */ + public void clearDirty() { + mDirty = false; + } + + /** @hide */ + public boolean isEmpty() { + return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE; + } + + /** @hide */ + @VisibleForTesting + public long roundUp(long time) { + if (time == Long.MIN_VALUE || time == Long.MAX_VALUE + || time == SubscriptionPlan.TIME_UNKNOWN) { + return time; + } else { + final long mod = time % mBucketDurationMillis; + if (mod > 0) { + time -= mod; + time += mBucketDurationMillis; + } + return time; + } + } + + /** @hide */ + @VisibleForTesting + public long roundDown(long time) { + if (time == Long.MIN_VALUE || time == Long.MAX_VALUE + || time == SubscriptionPlan.TIME_UNKNOWN) { + return time; + } else { + final long mod = time % mBucketDurationMillis; + if (mod > 0) { + time -= mod; + } + return time; + } + } + + /** @hide */ + public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) { + return getRelevantUids(accessLevel, Binder.getCallingUid()); + } + + /** @hide */ + public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel, + final int callerUid) { + final ArrayList uids = new ArrayList<>(); + for (int i = 0; i < mStats.size(); i++) { + final Key key = mStats.keyAt(i); + if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) { + int j = Collections.binarySearch(uids, new Integer(key.uid)); + + if (j < 0) { + j = ~j; + uids.add(j, key.uid); + } + } + } + return CollectionUtils.toIntArray(uids); + } + + /** + * Combine all {@link NetworkStatsHistory} in this collection which match + * the requested parameters. + * @hide + */ + public NetworkStatsHistory getHistory(NetworkTemplate template, SubscriptionPlan augmentPlan, + int uid, int set, int tag, int fields, long start, long end, + @NetworkStatsAccess.Level int accessLevel, int callerUid) { + if (!NetworkStatsAccess.isAccessibleToUser(uid, callerUid, accessLevel)) { + throw new SecurityException("Network stats history of uid " + uid + + " is forbidden for caller " + callerUid); + } + + // 180 days of history should be enough for anyone; if we end up needing + // more, we'll dynamically grow the history object. + final int bucketEstimate = (int) NetworkStatsUtils.constrain( + ((end - start) / mBucketDurationMillis), 0, + (180 * DateUtils.DAY_IN_MILLIS) / mBucketDurationMillis); + final NetworkStatsHistory combined = new NetworkStatsHistory( + mBucketDurationMillis, bucketEstimate, fields); + + // shortcut when we know stats will be empty + if (start == end) return combined; + + // Figure out the window of time that we should be augmenting (if any) + long augmentStart = SubscriptionPlan.TIME_UNKNOWN; + long augmentEnd = (augmentPlan != null) ? augmentPlan.getDataUsageTime() + : SubscriptionPlan.TIME_UNKNOWN; + // And if augmenting, we might need to collect more data to adjust with + long collectStart = start; + long collectEnd = end; + + if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) { + final Iterator> it = augmentPlan.cycleIterator(); + while (it.hasNext()) { + final Range cycle = it.next(); + final long cycleStart = cycle.getLower().toInstant().toEpochMilli(); + final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli(); + if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) { + augmentStart = cycleStart; + collectStart = Long.min(collectStart, augmentStart); + collectEnd = Long.max(collectEnd, augmentEnd); + break; + } + } + } + + if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) { + // Shrink augmentation window so we don't risk undercounting. + augmentStart = roundUp(augmentStart); + augmentEnd = roundDown(augmentEnd); + // Grow collection window so we get all the stats needed. + collectStart = roundDown(collectStart); + collectEnd = roundUp(collectEnd); + } + + for (int i = 0; i < mStats.size(); i++) { + final Key key = mStats.keyAt(i); + if (key.uid == uid && NetworkStats.setMatches(set, key.set) && key.tag == tag + && templateMatches(template, key.ident)) { + final NetworkStatsHistory value = mStats.valueAt(i); + combined.recordHistory(value, collectStart, collectEnd); + } + } + + if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) { + final NetworkStatsHistory.Entry entry = combined.getValues( + augmentStart, augmentEnd, null); + + // If we don't have any recorded data for this time period, give + // ourselves something to scale with. + if (entry.rxBytes == 0 || entry.txBytes == 0) { + combined.recordData(augmentStart, augmentEnd, + new NetworkStats.Entry(1, 0, 1, 0, 0)); + combined.getValues(augmentStart, augmentEnd, entry); + } + + final long rawBytes = (entry.rxBytes + entry.txBytes) == 0 ? 1 : + (entry.rxBytes + entry.txBytes); + final long rawRxBytes = entry.rxBytes == 0 ? 1 : entry.rxBytes; + final long rawTxBytes = entry.txBytes == 0 ? 1 : entry.txBytes; + final long targetBytes = augmentPlan.getDataUsageBytes(); + + final long targetRxBytes = multiplySafeByRational(targetBytes, rawRxBytes, rawBytes); + final long targetTxBytes = multiplySafeByRational(targetBytes, rawTxBytes, rawBytes); + + + // Scale all matching buckets to reach anchor target + final long beforeTotal = combined.getTotalBytes(); + for (int i = 0; i < combined.size(); i++) { + combined.getValues(i, entry); + if (entry.bucketStart >= augmentStart + && entry.bucketStart + entry.bucketDuration <= augmentEnd) { + entry.rxBytes = multiplySafeByRational( + targetRxBytes, entry.rxBytes, rawRxBytes); + entry.txBytes = multiplySafeByRational( + targetTxBytes, entry.txBytes, rawTxBytes); + // We purposefully clear out packet counters to indicate + // that this data has been augmented. + entry.rxPackets = 0; + entry.txPackets = 0; + combined.setValues(i, entry); + } + } + + final long deltaTotal = combined.getTotalBytes() - beforeTotal; + if (deltaTotal != 0) { + Log.d(TAG, "Augmented network usage by " + deltaTotal + " bytes"); + } + + // Finally we can slice data as originally requested + final NetworkStatsHistory sliced = new NetworkStatsHistory( + mBucketDurationMillis, bucketEstimate, fields); + sliced.recordHistory(combined, start, end); + return sliced; + } else { + return combined; + } + } + + /** + * Summarize all {@link NetworkStatsHistory} in this collection which match + * the requested parameters across the requested range. + * + * @param template - a predicate for filtering netstats. + * @param start - start of the range, timestamp in milliseconds since the epoch. + * @param end - end of the range, timestamp in milliseconds since the epoch. + * @param accessLevel - caller access level. + * @param callerUid - caller UID. + * @hide + */ + public NetworkStats getSummary(NetworkTemplate template, long start, long end, + @NetworkStatsAccess.Level int accessLevel, int callerUid) { + final long now = System.currentTimeMillis(); + + final NetworkStats stats = new NetworkStats(end - start, 24); + + // shortcut when we know stats will be empty + if (start == end) return stats; + + final NetworkStats.Entry entry = new NetworkStats.Entry(); + NetworkStatsHistory.Entry historyEntry = null; + + for (int i = 0; i < mStats.size(); i++) { + final Key key = mStats.keyAt(i); + if (templateMatches(template, key.ident) + && NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel) + && key.set < NetworkStats.SET_DEBUG_START) { + final NetworkStatsHistory value = mStats.valueAt(i); + historyEntry = value.getValues(start, end, now, historyEntry); + + entry.iface = IFACE_ALL; + entry.uid = key.uid; + entry.set = key.set; + entry.tag = key.tag; + entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork() + ? DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO; + entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO; + entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO; + entry.rxBytes = historyEntry.rxBytes; + entry.rxPackets = historyEntry.rxPackets; + entry.txBytes = historyEntry.txBytes; + entry.txPackets = historyEntry.txPackets; + entry.operations = historyEntry.operations; + + if (!entry.isEmpty()) { + stats.combineValues(entry); + } + } + } + + return stats; + } + + /** + * Record given {@link android.net.NetworkStats.Entry} into this collection. + * @hide + */ + public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start, + long end, NetworkStats.Entry entry) { + final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag); + history.recordData(start, end, entry); + noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes); + } + + /** + * Record given {@link NetworkStatsHistory} into this collection. + * + * @hide + */ + public void recordHistory(@NonNull Key key, @NonNull NetworkStatsHistory history) { + Objects.requireNonNull(key); + Objects.requireNonNull(history); + if (history.size() == 0) return; + noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes()); + + NetworkStatsHistory target = mStats.get(key); + if (target == null) { + target = new NetworkStatsHistory(history.getBucketDuration()); + mStats.put(key, target); + } + target.recordEntireHistory(history); + } + + /** + * Record all {@link NetworkStatsHistory} contained in the given collection + * into this collection. + * + * @hide + */ + public void recordCollection(@NonNull NetworkStatsCollection another) { + Objects.requireNonNull(another); + for (int i = 0; i < another.mStats.size(); i++) { + final Key key = another.mStats.keyAt(i); + final NetworkStatsHistory value = another.mStats.valueAt(i); + recordHistory(key, value); + } + } + + private NetworkStatsHistory findOrCreateHistory( + NetworkIdentitySet ident, int uid, int set, int tag) { + final Key key = new Key(ident, uid, set, tag); + final NetworkStatsHistory existing = mStats.get(key); + + // update when no existing, or when bucket duration changed + NetworkStatsHistory updated = null; + if (existing == null) { + updated = new NetworkStatsHistory(mBucketDurationMillis, 10); + } else if (existing.getBucketDuration() != mBucketDurationMillis) { + updated = new NetworkStatsHistory(existing, mBucketDurationMillis); + } + + if (updated != null) { + mStats.put(key, updated); + return updated; + } else { + return existing; + } + } + + /** @hide */ + @Override + public void read(InputStream in) throws IOException { + read((DataInput) new DataInputStream(in)); + } + + private void read(DataInput in) throws IOException { + // verify file magic header intact + final int magic = in.readInt(); + if (magic != FILE_MAGIC) { + throw new ProtocolException("unexpected magic: " + magic); + } + + final int version = in.readInt(); + switch (version) { + case VERSION_UNIFIED_INIT: { + // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory)) + final int identSize = in.readInt(); + for (int i = 0; i < identSize; i++) { + final NetworkIdentitySet ident = new NetworkIdentitySet(in); + + final int size = in.readInt(); + for (int j = 0; j < size; j++) { + final int uid = in.readInt(); + final int set = in.readInt(); + final int tag = in.readInt(); + + final Key key = new Key(ident, uid, set, tag); + final NetworkStatsHistory history = new NetworkStatsHistory(in); + recordHistory(key, history); + } + } + break; + } + default: { + throw new ProtocolException("unexpected version: " + version); + } + } + } + + /** @hide */ + @Override + public void write(OutputStream out) throws IOException { + write((DataOutput) new DataOutputStream(out)); + out.flush(); + } + + private void write(DataOutput out) throws IOException { + // cluster key lists grouped by ident + final HashMap> keysByIdent = new HashMap<>(); + for (Key key : mStats.keySet()) { + ArrayList keys = keysByIdent.get(key.ident); + if (keys == null) { + keys = new ArrayList<>(); + keysByIdent.put(key.ident, keys); + } + keys.add(key); + } + + out.writeInt(FILE_MAGIC); + out.writeInt(VERSION_UNIFIED_INIT); + + out.writeInt(keysByIdent.size()); + for (NetworkIdentitySet ident : keysByIdent.keySet()) { + final ArrayList keys = keysByIdent.get(ident); + ident.writeToStream(out); + + out.writeInt(keys.size()); + for (Key key : keys) { + final NetworkStatsHistory history = mStats.get(key); + out.writeInt(key.uid); + out.writeInt(key.set); + out.writeInt(key.tag); + history.writeToStream(out); + } + } + } + + /** + * Read legacy network summary statistics file format into the collection, + * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}. + * + * @deprecated + * @hide + */ + @Deprecated + public void readLegacyNetwork(File file) throws IOException { + final AtomicFile inputFile = new AtomicFile(file); + + DataInputStream in = null; + try { + in = new DataInputStream(new BufferedInputStream(inputFile.openRead())); + + // verify file magic header intact + final int magic = in.readInt(); + if (magic != FILE_MAGIC) { + throw new ProtocolException("unexpected magic: " + magic); + } + + final int version = in.readInt(); + switch (version) { + case VERSION_NETWORK_INIT: { + // network := size *(NetworkIdentitySet NetworkStatsHistory) + final int size = in.readInt(); + for (int i = 0; i < size; i++) { + final NetworkIdentitySet ident = new NetworkIdentitySet(in); + final NetworkStatsHistory history = new NetworkStatsHistory(in); + + final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE); + recordHistory(key, history); + } + break; + } + default: { + throw new ProtocolException("unexpected version: " + version); + } + } + } catch (FileNotFoundException e) { + // missing stats is okay, probably first boot + } finally { + IoUtils.closeQuietly(in); + } + } + + /** + * Read legacy Uid statistics file format into the collection, + * See {@code NetworkStatsService#maybeUpgradeLegacyStatsLocked}. + * + * @deprecated + * @hide + */ + @Deprecated + public void readLegacyUid(File file, boolean onlyTags) throws IOException { + final AtomicFile inputFile = new AtomicFile(file); + + DataInputStream in = null; + try { + in = new DataInputStream(new BufferedInputStream(inputFile.openRead())); + + // verify file magic header intact + final int magic = in.readInt(); + if (magic != FILE_MAGIC) { + throw new ProtocolException("unexpected magic: " + magic); + } + + final int version = in.readInt(); + switch (version) { + case VERSION_UID_INIT: { + // uid := size *(UID NetworkStatsHistory) + + // drop this data version, since we don't have a good + // mapping into NetworkIdentitySet. + break; + } + case VERSION_UID_WITH_IDENT: { + // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory)) + + // drop this data version, since this version only existed + // for a short time. + break; + } + case VERSION_UID_WITH_TAG: + case VERSION_UID_WITH_SET: { + // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory)) + final int identSize = in.readInt(); + for (int i = 0; i < identSize; i++) { + final NetworkIdentitySet ident = new NetworkIdentitySet(in); + + final int size = in.readInt(); + for (int j = 0; j < size; j++) { + final int uid = in.readInt(); + final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt() + : SET_DEFAULT; + final int tag = in.readInt(); + + final Key key = new Key(ident, uid, set, tag); + final NetworkStatsHistory history = new NetworkStatsHistory(in); + + if ((tag == TAG_NONE) != onlyTags) { + recordHistory(key, history); + } + } + } + break; + } + default: { + throw new ProtocolException("unexpected version: " + version); + } + } + } catch (FileNotFoundException e) { + // missing stats is okay, probably first boot + } finally { + IoUtils.closeQuietly(in); + } + } + + /** + * Remove any {@link NetworkStatsHistory} attributed to the requested UID, + * moving any {@link NetworkStats#TAG_NONE} series to + * {@link TrafficStats#UID_REMOVED}. + * @hide + */ + public void removeUids(int[] uids) { + final ArrayList knownKeys = new ArrayList<>(); + knownKeys.addAll(mStats.keySet()); + + // migrate all UID stats into special "removed" bucket + for (Key key : knownKeys) { + if (CollectionUtils.contains(uids, key.uid)) { + // only migrate combined TAG_NONE history + if (key.tag == TAG_NONE) { + final NetworkStatsHistory uidHistory = mStats.get(key); + final NetworkStatsHistory removedHistory = findOrCreateHistory( + key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE); + removedHistory.recordEntireHistory(uidHistory); + } + mStats.remove(key); + mDirty = true; + } + } + } + + private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) { + if (startMillis < mStartMillis) mStartMillis = startMillis; + if (endMillis > mEndMillis) mEndMillis = endMillis; + mTotalBytes += totalBytes; + mDirty = true; + } + + private int estimateBuckets() { + return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5) + / mBucketDurationMillis); + } + + private ArrayList getSortedKeys() { + final ArrayList keys = new ArrayList<>(); + keys.addAll(mStats.keySet()); + Collections.sort(keys, (left, right) -> Key.compare(left, right)); + return keys; + } + + /** @hide */ + public void dump(IndentingPrintWriter pw) { + for (Key key : getSortedKeys()) { + pw.print("ident="); pw.print(key.ident.toString()); + pw.print(" uid="); pw.print(key.uid); + pw.print(" set="); pw.print(NetworkStats.setToString(key.set)); + pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag)); + + final NetworkStatsHistory history = mStats.get(key); + pw.increaseIndent(); + history.dump(pw, true); + pw.decreaseIndent(); + } + } + + /** @hide */ + public void dumpDebug(ProtoOutputStream proto, long tag) { + final long start = proto.start(tag); + + for (Key key : getSortedKeys()) { + final long startStats = proto.start(NetworkStatsCollectionProto.STATS); + + // Key + final long startKey = proto.start(NetworkStatsCollectionStatsProto.KEY); + key.ident.dumpDebug(proto, NetworkStatsCollectionKeyProto.IDENTITY); + proto.write(NetworkStatsCollectionKeyProto.UID, key.uid); + proto.write(NetworkStatsCollectionKeyProto.SET, key.set); + proto.write(NetworkStatsCollectionKeyProto.TAG, key.tag); + proto.end(startKey); + + // Value + final NetworkStatsHistory history = mStats.get(key); + history.dumpDebug(proto, NetworkStatsCollectionStatsProto.HISTORY); + proto.end(startStats); + } + + proto.end(start); + } + + /** @hide */ + public void dumpCheckin(PrintWriter pw, long start, long end) { + dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell"); + dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi"); + dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth"); + dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt"); + } + + /** + * Dump all contained stats that match requested parameters, but group + * together all matching {@link NetworkTemplate} under a single prefix. + */ + private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate, + String groupPrefix) { + final ArrayMap grouped = new ArrayMap<>(); + + // Walk through all history, grouping by matching network templates + for (int i = 0; i < mStats.size(); i++) { + final Key key = mStats.keyAt(i); + final NetworkStatsHistory value = mStats.valueAt(i); + + if (!templateMatches(groupTemplate, key.ident)) continue; + if (key.set >= NetworkStats.SET_DEBUG_START) continue; + + final Key groupKey = new Key(null, key.uid, key.set, key.tag); + NetworkStatsHistory groupHistory = grouped.get(groupKey); + if (groupHistory == null) { + groupHistory = new NetworkStatsHistory(value.getBucketDuration()); + grouped.put(groupKey, groupHistory); + } + groupHistory.recordHistory(value, start, end); + } + + for (int i = 0; i < grouped.size(); i++) { + final Key key = grouped.keyAt(i); + final NetworkStatsHistory value = grouped.valueAt(i); + + if (value.size() == 0) continue; + + pw.print("c,"); + pw.print(groupPrefix); pw.print(','); + pw.print(key.uid); pw.print(','); + pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(','); + pw.print(key.tag); + pw.println(); + + value.dumpCheckin(pw); + } + } + + /** + * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity} + * in the given {@link NetworkIdentitySet}. + */ + private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) { + for (NetworkIdentity ident : identSet) { + if (template.matches(ident)) { + return true; + } + } + return false; + } + + /** + * Get the all historical stats of the collection {@link NetworkStatsCollection}. + * + * @return All {@link NetworkStatsHistory} in this collection. + */ + @NonNull + public Map getEntries() { + return new ArrayMap(mStats); + } + + /** + * Builder class for {@link NetworkStatsCollection}. + */ + public static final class Builder { + private final long mBucketDurationMillis; + private final ArrayMap mEntries = new ArrayMap<>(); + + /** + * Creates a new Builder with given bucket duration. + * + * @param bucketDuration Duration of the buckets of the object, in milliseconds. + */ + public Builder(long bucketDurationMillis) { + mBucketDurationMillis = bucketDurationMillis; + } + + /** + * Add association of the history with the specified key in this map. + * + * @param key The object used to identify a network, see {@link Key}. + * @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}. + * @return The builder object. + */ + @NonNull + public NetworkStatsCollection.Builder addEntry(@NonNull Key key, + @NonNull NetworkStatsHistory history) { + Objects.requireNonNull(key); + Objects.requireNonNull(history); + final List historyEntries = history.getEntries(); + + final NetworkStatsHistory.Builder historyBuilder = + new NetworkStatsHistory.Builder(mBucketDurationMillis, historyEntries.size()); + for (Entry entry : historyEntries) { + historyBuilder.addEntry(entry); + } + + mEntries.put(key, historyBuilder.build()); + return this; + } + + /** + * Builds the instance of the {@link NetworkStatsCollection}. + * + * @return the built instance of {@link NetworkStatsCollection}. + */ + @NonNull + public NetworkStatsCollection build() { + final NetworkStatsCollection collection = + new NetworkStatsCollection(mBucketDurationMillis); + for (int i = 0; i < mEntries.size(); i++) { + collection.recordHistory(mEntries.keyAt(i), mEntries.valueAt(i)); + } + return collection; + } + } + + /** + * the identifier that associate with the {@link NetworkStatsHistory} object to identify + * a certain record in the {@link NetworkStatsCollection} object. + */ + public static final class Key { + /** @hide */ + public final NetworkIdentitySet ident; + /** @hide */ + public final int uid; + /** @hide */ + public final int set; + /** @hide */ + public final int tag; + + private final int mHashCode; + + /** + * Construct a {@link Key} object. + * + * @param ident a Set of {@link NetworkIdentity} that associated with the record. + * @param uid Uid of the record. + * @param set Set of the record, see {@code NetworkStats#SET_*}. + * @param tag Tag of the record, see {@link TrafficStats#setThreadStatsTag(int)}. + */ + public Key(@NonNull Set ident, int uid, @State int set, int tag) { + this(new NetworkIdentitySet(Objects.requireNonNull(ident)), uid, set, tag); + } + + /** @hide */ + public Key(@NonNull NetworkIdentitySet ident, int uid, int set, int tag) { + this.ident = Objects.requireNonNull(ident); + this.uid = uid; + this.set = set; + this.tag = tag; + mHashCode = Objects.hash(ident, uid, set, tag); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof Key) { + final Key key = (Key) obj; + return uid == key.uid && set == key.set && tag == key.tag + && Objects.equals(ident, key.ident); + } + return false; + } + + /** @hide */ + public static int compare(@NonNull Key left, @NonNull Key right) { + Objects.requireNonNull(left); + Objects.requireNonNull(right); + int res = 0; + if (left.ident != null && right.ident != null) { + res = NetworkIdentitySet.compare(left.ident, right.ident); + } + if (res == 0) { + res = Integer.compare(left.uid, right.uid); + } + if (res == 0) { + res = Integer.compare(left.set, right.set); + } + if (res == 0) { + res = Integer.compare(left.tag, right.tag); + } + return res; + } + } +} diff --git a/framework-t/src/android/net/NetworkStatsHistory.aidl b/framework-t/src/android/net/NetworkStatsHistory.aidl new file mode 100644 index 0000000000..8b9069f8fa --- /dev/null +++ b/framework-t/src/android/net/NetworkStatsHistory.aidl @@ -0,0 +1,19 @@ +/** + * 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 android.net; + +parcelable NetworkStatsHistory; diff --git a/framework-t/src/android/net/NetworkStatsHistory.java b/framework-t/src/android/net/NetworkStatsHistory.java new file mode 100644 index 0000000000..301fef9441 --- /dev/null +++ b/framework-t/src/android/net/NetworkStatsHistory.java @@ -0,0 +1,1162 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStatsHistory.DataStreamUtils.readFullLongArray; +import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLongArray; +import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray; +import static android.net.NetworkStatsHistory.Entry.UNKNOWN; +import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray; +import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; + +import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.NetworkStatsHistoryBucketProto; +import android.service.NetworkStatsHistoryProto; +import android.util.IndentingPrintWriter; +import android.util.proto.ProtoOutputStream; + +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.NetworkStatsUtils; + +import libcore.util.EmptyArray; + +import java.io.CharArrayWriter; +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.ProtocolException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * Collection of historical network statistics, recorded into equally-sized + * "buckets" in time. Internally it stores data in {@code long} series for more + * efficient persistence. + *

+ * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for + * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is + * sorted at all times. + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class NetworkStatsHistory implements Parcelable { + private static final int VERSION_INIT = 1; + private static final int VERSION_ADD_PACKETS = 2; + private static final int VERSION_ADD_ACTIVE = 3; + + /** @hide */ + public static final int FIELD_ACTIVE_TIME = 0x01; + /** @hide */ + public static final int FIELD_RX_BYTES = 0x02; + /** @hide */ + public static final int FIELD_RX_PACKETS = 0x04; + /** @hide */ + public static final int FIELD_TX_BYTES = 0x08; + /** @hide */ + public static final int FIELD_TX_PACKETS = 0x10; + /** @hide */ + public static final int FIELD_OPERATIONS = 0x20; + /** @hide */ + public static final int FIELD_ALL = 0xFFFFFFFF; + + private long bucketDuration; + private int bucketCount; + private long[] bucketStart; + private long[] activeTime; + private long[] rxBytes; + private long[] rxPackets; + private long[] txBytes; + private long[] txPackets; + private long[] operations; + private long totalBytes; + + /** @hide */ + public NetworkStatsHistory(long bucketDuration, long[] bucketStart, long[] activeTime, + long[] rxBytes, long[] rxPackets, long[] txBytes, long[] txPackets, + long[] operations, int bucketCount, long totalBytes) { + this.bucketDuration = bucketDuration; + this.bucketStart = bucketStart; + this.activeTime = activeTime; + this.rxBytes = rxBytes; + this.rxPackets = rxPackets; + this.txBytes = txBytes; + this.txPackets = txPackets; + this.operations = operations; + this.bucketCount = bucketCount; + this.totalBytes = totalBytes; + } + + /** + * An instance to represent a single record in a {@link NetworkStatsHistory} object. + */ + public static final class Entry { + /** @hide */ + public static final long UNKNOWN = -1; + + /** @hide */ + // TODO: Migrate all callers to get duration from the history object and remove this field. + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long bucketDuration; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long bucketStart; + /** @hide */ + public long activeTime; + /** @hide */ + @UnsupportedAppUsage + public long rxBytes; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long rxPackets; + /** @hide */ + @UnsupportedAppUsage + public long txBytes; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public long txPackets; + /** @hide */ + public long operations; + /** @hide */ + Entry() {} + + /** + * Construct a {@link Entry} instance to represent a single record in a + * {@link NetworkStatsHistory} object. + * + * @param bucketStart Start of period for this {@link Entry}, in milliseconds since the + * Unix epoch, see {@link java.lang.System#currentTimeMillis}. + * @param activeTime Active time for this {@link Entry}, in milliseconds. + * @param rxBytes Number of bytes received for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param rxPackets Number of packets received for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param txBytes Number of bytes transmitted for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param txPackets Number of bytes transmitted for this {@link Entry}. Statistics should + * represent the contents of IP packets, including IP headers. + * @param operations count of network operations performed for this {@link Entry}. This can + * be used to derive bytes-per-operation. + */ + public Entry(long bucketStart, long activeTime, long rxBytes, + long rxPackets, long txBytes, long txPackets, long operations) { + this.bucketStart = bucketStart; + this.activeTime = activeTime; + this.rxBytes = rxBytes; + this.rxPackets = rxPackets; + this.txBytes = txBytes; + this.txPackets = txPackets; + this.operations = operations; + } + + /** + * Get start timestamp of the bucket's time interval, in milliseconds since the Unix epoch. + */ + public long getBucketStart() { + return bucketStart; + } + + /** + * Get active time of the bucket's time interval, in milliseconds. + */ + public long getActiveTime() { + return activeTime; + } + + /** Get number of bytes received for this {@link Entry}. */ + public long getRxBytes() { + return rxBytes; + } + + /** Get number of packets received for this {@link Entry}. */ + public long getRxPackets() { + return rxPackets; + } + + /** Get number of bytes transmitted for this {@link Entry}. */ + public long getTxBytes() { + return txBytes; + } + + /** Get number of packets transmitted for this {@link Entry}. */ + public long getTxPackets() { + return txPackets; + } + + /** Get count of network operations performed for this {@link Entry}. */ + public long getOperations() { + return operations; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o.getClass() != getClass()) return false; + Entry entry = (Entry) o; + return bucketStart == entry.bucketStart + && activeTime == entry.activeTime && rxBytes == entry.rxBytes + && rxPackets == entry.rxPackets && txBytes == entry.txBytes + && txPackets == entry.txPackets && operations == entry.operations; + } + + @Override + public int hashCode() { + return (int) (bucketStart * 2 + + activeTime * 3 + + rxBytes * 5 + + rxPackets * 7 + + txBytes * 11 + + txPackets * 13 + + operations * 17); + } + + @Override + public String toString() { + return "Entry{" + + "bucketStart=" + bucketStart + + ", activeTime=" + activeTime + + ", rxBytes=" + rxBytes + + ", rxPackets=" + rxPackets + + ", txBytes=" + txBytes + + ", txPackets=" + txPackets + + ", operations=" + operations + + "}"; + } + } + + /** @hide */ + @UnsupportedAppUsage + public NetworkStatsHistory(long bucketDuration) { + this(bucketDuration, 10, FIELD_ALL); + } + + /** @hide */ + public NetworkStatsHistory(long bucketDuration, int initialSize) { + this(bucketDuration, initialSize, FIELD_ALL); + } + + /** @hide */ + public NetworkStatsHistory(long bucketDuration, int initialSize, int fields) { + this.bucketDuration = bucketDuration; + bucketStart = new long[initialSize]; + if ((fields & FIELD_ACTIVE_TIME) != 0) activeTime = new long[initialSize]; + if ((fields & FIELD_RX_BYTES) != 0) rxBytes = new long[initialSize]; + if ((fields & FIELD_RX_PACKETS) != 0) rxPackets = new long[initialSize]; + if ((fields & FIELD_TX_BYTES) != 0) txBytes = new long[initialSize]; + if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize]; + if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize]; + bucketCount = 0; + totalBytes = 0; + } + + /** @hide */ + public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) { + this(bucketDuration, existing.estimateResizeBuckets(bucketDuration)); + recordEntireHistory(existing); + } + + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public NetworkStatsHistory(Parcel in) { + bucketDuration = in.readLong(); + bucketStart = readLongArray(in); + activeTime = readLongArray(in); + rxBytes = readLongArray(in); + rxPackets = readLongArray(in); + txBytes = readLongArray(in); + txPackets = readLongArray(in); + operations = readLongArray(in); + bucketCount = bucketStart.length; + totalBytes = in.readLong(); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeLong(bucketDuration); + writeLongArray(out, bucketStart, bucketCount); + writeLongArray(out, activeTime, bucketCount); + writeLongArray(out, rxBytes, bucketCount); + writeLongArray(out, rxPackets, bucketCount); + writeLongArray(out, txBytes, bucketCount); + writeLongArray(out, txPackets, bucketCount); + writeLongArray(out, operations, bucketCount); + out.writeLong(totalBytes); + } + + /** @hide */ + public NetworkStatsHistory(DataInput in) throws IOException { + final int version = in.readInt(); + switch (version) { + case VERSION_INIT: { + bucketDuration = in.readLong(); + bucketStart = readFullLongArray(in); + rxBytes = readFullLongArray(in); + rxPackets = new long[bucketStart.length]; + txBytes = readFullLongArray(in); + txPackets = new long[bucketStart.length]; + operations = new long[bucketStart.length]; + bucketCount = bucketStart.length; + totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes); + break; + } + case VERSION_ADD_PACKETS: + case VERSION_ADD_ACTIVE: { + bucketDuration = in.readLong(); + bucketStart = readVarLongArray(in); + activeTime = (version >= VERSION_ADD_ACTIVE) ? readVarLongArray(in) + : new long[bucketStart.length]; + rxBytes = readVarLongArray(in); + rxPackets = readVarLongArray(in); + txBytes = readVarLongArray(in); + txPackets = readVarLongArray(in); + operations = readVarLongArray(in); + bucketCount = bucketStart.length; + totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes); + break; + } + default: { + throw new ProtocolException("unexpected version: " + version); + } + } + + if (bucketStart.length != bucketCount || rxBytes.length != bucketCount + || rxPackets.length != bucketCount || txBytes.length != bucketCount + || txPackets.length != bucketCount || operations.length != bucketCount) { + throw new ProtocolException("Mismatched history lengths"); + } + } + + /** @hide */ + public void writeToStream(DataOutput out) throws IOException { + out.writeInt(VERSION_ADD_ACTIVE); + out.writeLong(bucketDuration); + writeVarLongArray(out, bucketStart, bucketCount); + writeVarLongArray(out, activeTime, bucketCount); + writeVarLongArray(out, rxBytes, bucketCount); + writeVarLongArray(out, rxPackets, bucketCount); + writeVarLongArray(out, txBytes, bucketCount); + writeVarLongArray(out, txPackets, bucketCount); + writeVarLongArray(out, operations, bucketCount); + } + + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public int size() { + return bucketCount; + } + + /** @hide */ + public long getBucketDuration() { + return bucketDuration; + } + + /** @hide */ + @UnsupportedAppUsage + public long getStart() { + if (bucketCount > 0) { + return bucketStart[0]; + } else { + return Long.MAX_VALUE; + } + } + + /** @hide */ + @UnsupportedAppUsage + public long getEnd() { + if (bucketCount > 0) { + return bucketStart[bucketCount - 1] + bucketDuration; + } else { + return Long.MIN_VALUE; + } + } + + /** + * Return total bytes represented by this history. + * @hide + */ + public long getTotalBytes() { + return totalBytes; + } + + /** + * Return index of bucket that contains or is immediately before the + * requested time. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public int getIndexBefore(long time) { + int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time); + if (index < 0) { + index = (~index) - 1; + } else { + index -= 1; + } + return NetworkStatsUtils.constrain(index, 0, bucketCount - 1); + } + + /** + * Return index of bucket that contains or is immediately after the + * requested time. + * @hide + */ + public int getIndexAfter(long time) { + int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time); + if (index < 0) { + index = ~index; + } else { + index += 1; + } + return NetworkStatsUtils.constrain(index, 0, bucketCount - 1); + } + + /** + * Return specific stats entry. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public Entry getValues(int i, Entry recycle) { + final Entry entry = recycle != null ? recycle : new Entry(); + entry.bucketStart = bucketStart[i]; + entry.bucketDuration = bucketDuration; + entry.activeTime = getLong(activeTime, i, UNKNOWN); + entry.rxBytes = getLong(rxBytes, i, UNKNOWN); + entry.rxPackets = getLong(rxPackets, i, UNKNOWN); + entry.txBytes = getLong(txBytes, i, UNKNOWN); + entry.txPackets = getLong(txPackets, i, UNKNOWN); + entry.operations = getLong(operations, i, UNKNOWN); + return entry; + } + + /** + * Get List of {@link Entry} of the {@link NetworkStatsHistory} instance. + * + * @return + */ + @NonNull + public List getEntries() { + // TODO: Return a wrapper that uses this list instead, to prevent the returned result + // from being changed. + final ArrayList ret = new ArrayList<>(size()); + for (int i = 0; i < size(); i++) { + ret.add(getValues(i, null /* recycle */)); + } + return ret; + } + + /** @hide */ + public void setValues(int i, Entry entry) { + // Unwind old values + if (rxBytes != null) totalBytes -= rxBytes[i]; + if (txBytes != null) totalBytes -= txBytes[i]; + + bucketStart[i] = entry.bucketStart; + setLong(activeTime, i, entry.activeTime); + setLong(rxBytes, i, entry.rxBytes); + setLong(rxPackets, i, entry.rxPackets); + setLong(txBytes, i, entry.txBytes); + setLong(txPackets, i, entry.txPackets); + setLong(operations, i, entry.operations); + + // Apply new values + if (rxBytes != null) totalBytes += rxBytes[i]; + if (txBytes != null) totalBytes += txBytes[i]; + } + + /** + * Record that data traffic occurred in the given time range. Will + * distribute across internal buckets, creating new buckets as needed. + * @hide + */ + @Deprecated + public void recordData(long start, long end, long rxBytes, long txBytes) { + recordData(start, end, new NetworkStats.Entry( + IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, 0L, txBytes, 0L, 0L)); + } + + /** + * Record that data traffic occurred in the given time range. Will + * distribute across internal buckets, creating new buckets as needed. + * @hide + */ + public void recordData(long start, long end, NetworkStats.Entry entry) { + long rxBytes = entry.rxBytes; + long rxPackets = entry.rxPackets; + long txBytes = entry.txBytes; + long txPackets = entry.txPackets; + long operations = entry.operations; + + if (entry.isNegative()) { + throw new IllegalArgumentException("tried recording negative data"); + } + if (entry.isEmpty()) { + return; + } + + // create any buckets needed by this range + ensureBuckets(start, end); + // Return fast if there is still no entry. This would typically happen when the start, + // end or duration are not valid values, e.g. start > end, negative duration value, etc. + if (bucketCount == 0) return; + + // distribute data usage into buckets + long duration = end - start; + final int startIndex = getIndexAfter(end); + for (int i = startIndex; i >= 0; i--) { + final long curStart = bucketStart[i]; + final long curEnd = curStart + bucketDuration; + + // bucket is older than record; we're finished + if (curEnd < start) break; + // bucket is newer than record; keep looking + if (curStart > end) continue; + + final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); + if (overlap <= 0) continue; + + // integer math each time is faster than floating point + final long fracRxBytes = multiplySafeByRational(rxBytes, overlap, duration); + final long fracRxPackets = multiplySafeByRational(rxPackets, overlap, duration); + final long fracTxBytes = multiplySafeByRational(txBytes, overlap, duration); + final long fracTxPackets = multiplySafeByRational(txPackets, overlap, duration); + final long fracOperations = multiplySafeByRational(operations, overlap, duration); + + + addLong(activeTime, i, overlap); + addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes; + addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets; + addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes; + addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets; + addLong(this.operations, i, fracOperations); operations -= fracOperations; + + duration -= overlap; + } + + totalBytes += entry.rxBytes + entry.txBytes; + } + + /** + * Record an entire {@link NetworkStatsHistory} into this history. Usually + * for combining together stats for external reporting. + * @hide + */ + @UnsupportedAppUsage + public void recordEntireHistory(NetworkStatsHistory input) { + recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE); + } + + /** + * Record given {@link NetworkStatsHistory} into this history, copying only + * buckets that atomically occur in the inclusive time range. Doesn't + * interpolate across partial buckets. + * @hide + */ + public void recordHistory(NetworkStatsHistory input, long start, long end) { + final NetworkStats.Entry entry = new NetworkStats.Entry( + IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L); + for (int i = 0; i < input.bucketCount; i++) { + final long bucketStart = input.bucketStart[i]; + final long bucketEnd = bucketStart + input.bucketDuration; + + // skip when bucket is outside requested range + if (bucketStart < start || bucketEnd > end) continue; + + entry.rxBytes = getLong(input.rxBytes, i, 0L); + entry.rxPackets = getLong(input.rxPackets, i, 0L); + entry.txBytes = getLong(input.txBytes, i, 0L); + entry.txPackets = getLong(input.txPackets, i, 0L); + entry.operations = getLong(input.operations, i, 0L); + + recordData(bucketStart, bucketEnd, entry); + } + } + + /** + * Ensure that buckets exist for given time range, creating as needed. + */ + private void ensureBuckets(long start, long end) { + // normalize incoming range to bucket boundaries + start -= start % bucketDuration; + end += (bucketDuration - (end % bucketDuration)) % bucketDuration; + + for (long now = start; now < end; now += bucketDuration) { + // try finding existing bucket + final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now); + if (index < 0) { + // bucket missing, create and insert + insertBucket(~index, now); + } + } + } + + /** + * Insert new bucket at requested index and starting time. + */ + private void insertBucket(int index, long start) { + // create more buckets when needed + if (bucketCount >= bucketStart.length) { + final int newLength = Math.max(bucketStart.length, 10) * 3 / 2; + bucketStart = Arrays.copyOf(bucketStart, newLength); + if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength); + if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength); + if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength); + if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength); + if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength); + if (operations != null) operations = Arrays.copyOf(operations, newLength); + } + + // create gap when inserting bucket in middle + if (index < bucketCount) { + final int dstPos = index + 1; + final int length = bucketCount - index; + + System.arraycopy(bucketStart, index, bucketStart, dstPos, length); + if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length); + if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length); + if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length); + if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length); + if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length); + if (operations != null) System.arraycopy(operations, index, operations, dstPos, length); + } + + bucketStart[index] = start; + setLong(activeTime, index, 0L); + setLong(rxBytes, index, 0L); + setLong(rxPackets, index, 0L); + setLong(txBytes, index, 0L); + setLong(txPackets, index, 0L); + setLong(operations, index, 0L); + bucketCount++; + } + + /** + * Clear all data stored in this object. + * @hide + */ + public void clear() { + bucketStart = EmptyArray.LONG; + if (activeTime != null) activeTime = EmptyArray.LONG; + if (rxBytes != null) rxBytes = EmptyArray.LONG; + if (rxPackets != null) rxPackets = EmptyArray.LONG; + if (txBytes != null) txBytes = EmptyArray.LONG; + if (txPackets != null) txPackets = EmptyArray.LONG; + if (operations != null) operations = EmptyArray.LONG; + bucketCount = 0; + totalBytes = 0; + } + + /** + * Remove buckets older than requested cutoff. + * @hide + */ + public void removeBucketsBefore(long cutoff) { + // TODO: Consider use getIndexBefore. + int i; + for (i = 0; i < bucketCount; i++) { + final long curStart = bucketStart[i]; + final long curEnd = curStart + bucketDuration; + + // cutoff happens before or during this bucket; everything before + // this bucket should be removed. + if (curEnd > cutoff) break; + } + + if (i > 0) { + final int length = bucketStart.length; + bucketStart = Arrays.copyOfRange(bucketStart, i, length); + if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length); + if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length); + if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length); + if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length); + if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length); + if (operations != null) operations = Arrays.copyOfRange(operations, i, length); + bucketCount -= i; + + totalBytes = 0; + if (rxBytes != null) totalBytes += CollectionUtils.total(rxBytes); + if (txBytes != null) totalBytes += CollectionUtils.total(txBytes); + } + } + + /** + * Return interpolated data usage across the requested range. Interpolates + * across buckets, so values may be rounded slightly. + * + *

If the active bucket is not completed yet, it returns the proportional value of it + * based on its duration and the {@code end} param. + * + * @param start - start of the range, timestamp in milliseconds since the epoch. + * @param end - end of the range, timestamp in milliseconds since the epoch. + * @param recycle - entry instance for performance, could be null. + * @hide + */ + @UnsupportedAppUsage + public Entry getValues(long start, long end, Entry recycle) { + return getValues(start, end, Long.MAX_VALUE, recycle); + } + + /** + * Return interpolated data usage across the requested range. Interpolates + * across buckets, so values may be rounded slightly. + * + * @param start - start of the range, timestamp in milliseconds since the epoch. + * @param end - end of the range, timestamp in milliseconds since the epoch. + * @param now - current timestamp in milliseconds since the epoch (wall clock). + * @param recycle - entry instance for performance, could be null. + * @hide + */ + @UnsupportedAppUsage + public Entry getValues(long start, long end, long now, Entry recycle) { + final Entry entry = recycle != null ? recycle : new Entry(); + entry.bucketDuration = end - start; + entry.bucketStart = start; + entry.activeTime = activeTime != null ? 0 : UNKNOWN; + entry.rxBytes = rxBytes != null ? 0 : UNKNOWN; + entry.rxPackets = rxPackets != null ? 0 : UNKNOWN; + entry.txBytes = txBytes != null ? 0 : UNKNOWN; + entry.txPackets = txPackets != null ? 0 : UNKNOWN; + entry.operations = operations != null ? 0 : UNKNOWN; + + // Return fast if there is no entry. + if (bucketCount == 0) return entry; + + final int startIndex = getIndexAfter(end); + for (int i = startIndex; i >= 0; i--) { + final long curStart = bucketStart[i]; + long curEnd = curStart + bucketDuration; + + // bucket is older than request; we're finished + if (curEnd <= start) break; + // bucket is newer than request; keep looking + if (curStart >= end) continue; + + // the active bucket is shorter then a normal completed bucket + if (curEnd > now) curEnd = now; + // usually this is simply bucketDuration + final long bucketSpan = curEnd - curStart; + // prevent division by zero + if (bucketSpan <= 0) continue; + + final long overlapEnd = curEnd < end ? curEnd : end; + final long overlapStart = curStart > start ? curStart : start; + final long overlap = overlapEnd - overlapStart; + if (overlap <= 0) continue; + + // integer math each time is faster than floating point + if (activeTime != null) { + entry.activeTime += multiplySafeByRational(activeTime[i], overlap, bucketSpan); + } + if (rxBytes != null) { + entry.rxBytes += multiplySafeByRational(rxBytes[i], overlap, bucketSpan); + } + if (rxPackets != null) { + entry.rxPackets += multiplySafeByRational(rxPackets[i], overlap, bucketSpan); + } + if (txBytes != null) { + entry.txBytes += multiplySafeByRational(txBytes[i], overlap, bucketSpan); + } + if (txPackets != null) { + entry.txPackets += multiplySafeByRational(txPackets[i], overlap, bucketSpan); + } + if (operations != null) { + entry.operations += multiplySafeByRational(operations[i], overlap, bucketSpan); + } + } + return entry; + } + + /** + * @deprecated only for temporary testing + * @hide + */ + @Deprecated + public void generateRandom(long start, long end, long bytes) { + final Random r = new Random(); + + final float fractionRx = r.nextFloat(); + final long rxBytes = (long) (bytes * fractionRx); + final long txBytes = (long) (bytes * (1 - fractionRx)); + + final long rxPackets = rxBytes / 1024; + final long txPackets = txBytes / 1024; + final long operations = rxBytes / 2048; + + generateRandom(start, end, rxBytes, rxPackets, txBytes, txPackets, operations, r); + } + + /** + * @deprecated only for temporary testing + * @hide + */ + @Deprecated + public void generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes, + long txPackets, long operations, Random r) { + ensureBuckets(start, end); + + final NetworkStats.Entry entry = new NetworkStats.Entry( + IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L); + while (rxBytes > 1024 || rxPackets > 128 || txBytes > 1024 || txPackets > 128 + || operations > 32) { + final long curStart = randomLong(r, start, end); + final long curEnd = curStart + randomLong(r, 0, (end - curStart) / 2); + + entry.rxBytes = randomLong(r, 0, rxBytes); + entry.rxPackets = randomLong(r, 0, rxPackets); + entry.txBytes = randomLong(r, 0, txBytes); + entry.txPackets = randomLong(r, 0, txPackets); + entry.operations = randomLong(r, 0, operations); + + rxBytes -= entry.rxBytes; + rxPackets -= entry.rxPackets; + txBytes -= entry.txBytes; + txPackets -= entry.txPackets; + operations -= entry.operations; + + recordData(curStart, curEnd, entry); + } + } + + /** @hide */ + public static long randomLong(Random r, long start, long end) { + return (long) (start + (r.nextFloat() * (end - start))); + } + + /** + * Quickly determine if this history intersects with given window. + * @hide + */ + public boolean intersects(long start, long end) { + final long dataStart = getStart(); + final long dataEnd = getEnd(); + if (start >= dataStart && start <= dataEnd) return true; + if (end >= dataStart && end <= dataEnd) return true; + if (dataStart >= start && dataStart <= end) return true; + if (dataEnd >= start && dataEnd <= end) return true; + return false; + } + + /** @hide */ + public void dump(IndentingPrintWriter pw, boolean fullHistory) { + pw.print("NetworkStatsHistory: bucketDuration="); + pw.println(bucketDuration / SECOND_IN_MILLIS); + pw.increaseIndent(); + + final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32); + if (start > 0) { + pw.print("(omitting "); pw.print(start); pw.println(" buckets)"); + } + + for (int i = start; i < bucketCount; i++) { + pw.print("st="); pw.print(bucketStart[i] / SECOND_IN_MILLIS); + if (rxBytes != null) { pw.print(" rb="); pw.print(rxBytes[i]); } + if (rxPackets != null) { pw.print(" rp="); pw.print(rxPackets[i]); } + if (txBytes != null) { pw.print(" tb="); pw.print(txBytes[i]); } + if (txPackets != null) { pw.print(" tp="); pw.print(txPackets[i]); } + if (operations != null) { pw.print(" op="); pw.print(operations[i]); } + pw.println(); + } + + pw.decreaseIndent(); + } + + /** @hide */ + public void dumpCheckin(PrintWriter pw) { + pw.print("d,"); + pw.print(bucketDuration / SECOND_IN_MILLIS); + pw.println(); + + for (int i = 0; i < bucketCount; i++) { + pw.print("b,"); + pw.print(bucketStart[i] / SECOND_IN_MILLIS); pw.print(','); + if (rxBytes != null) { pw.print(rxBytes[i]); } else { pw.print("*"); } pw.print(','); + if (rxPackets != null) { pw.print(rxPackets[i]); } else { pw.print("*"); } pw.print(','); + if (txBytes != null) { pw.print(txBytes[i]); } else { pw.print("*"); } pw.print(','); + if (txPackets != null) { pw.print(txPackets[i]); } else { pw.print("*"); } pw.print(','); + if (operations != null) { pw.print(operations[i]); } else { pw.print("*"); } + pw.println(); + } + } + + /** @hide */ + public void dumpDebug(ProtoOutputStream proto, long tag) { + final long start = proto.start(tag); + + proto.write(NetworkStatsHistoryProto.BUCKET_DURATION_MS, bucketDuration); + + for (int i = 0; i < bucketCount; i++) { + final long startBucket = proto.start(NetworkStatsHistoryProto.BUCKETS); + + proto.write(NetworkStatsHistoryBucketProto.BUCKET_START_MS, + bucketStart[i]); + dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_BYTES, rxBytes, i); + dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_PACKETS, rxPackets, i); + dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_BYTES, txBytes, i); + dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_PACKETS, txPackets, i); + dumpDebug(proto, NetworkStatsHistoryBucketProto.OPERATIONS, operations, i); + + proto.end(startBucket); + } + + proto.end(start); + } + + private static void dumpDebug(ProtoOutputStream proto, long tag, long[] array, int index) { + if (array != null) { + proto.write(tag, array[index]); + } + } + + @Override + public String toString() { + final CharArrayWriter writer = new CharArrayWriter(); + dump(new IndentingPrintWriter(writer, " "), false); + return writer.toString(); + } + + @UnsupportedAppUsage + public static final @android.annotation.NonNull Creator CREATOR = new Creator() { + @Override + public NetworkStatsHistory createFromParcel(Parcel in) { + return new NetworkStatsHistory(in); + } + + @Override + public NetworkStatsHistory[] newArray(int size) { + return new NetworkStatsHistory[size]; + } + }; + + private static long getLong(long[] array, int i, long value) { + return array != null ? array[i] : value; + } + + private static void setLong(long[] array, int i, long value) { + if (array != null) array[i] = value; + } + + private static void addLong(long[] array, int i, long value) { + if (array != null) array[i] += value; + } + + /** @hide */ + public int estimateResizeBuckets(long newBucketDuration) { + return (int) (size() * getBucketDuration() / newBucketDuration); + } + + /** + * Utility methods for interacting with {@link DataInputStream} and + * {@link DataOutputStream}, mostly dealing with writing partial arrays. + * @hide + */ + public static class DataStreamUtils { + @Deprecated + public static long[] readFullLongArray(DataInput in) throws IOException { + final int size = in.readInt(); + if (size < 0) throw new ProtocolException("negative array size"); + final long[] values = new long[size]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readLong(); + } + return values; + } + + /** + * Read variable-length {@link Long} using protobuf-style approach. + */ + public static long readVarLong(DataInput in) throws IOException { + int shift = 0; + long result = 0; + while (shift < 64) { + byte b = in.readByte(); + result |= (long) (b & 0x7F) << shift; + if ((b & 0x80) == 0) + return result; + shift += 7; + } + throw new ProtocolException("malformed long"); + } + + /** + * Write variable-length {@link Long} using protobuf-style approach. + */ + public static void writeVarLong(DataOutput out, long value) throws IOException { + while (true) { + if ((value & ~0x7FL) == 0) { + out.writeByte((int) value); + return; + } else { + out.writeByte(((int) value & 0x7F) | 0x80); + value >>>= 7; + } + } + } + + public static long[] readVarLongArray(DataInput in) throws IOException { + final int size = in.readInt(); + if (size == -1) return null; + if (size < 0) throw new ProtocolException("negative array size"); + final long[] values = new long[size]; + for (int i = 0; i < values.length; i++) { + values[i] = readVarLong(in); + } + return values; + } + + public static void writeVarLongArray(DataOutput out, long[] values, int size) + throws IOException { + if (values == null) { + out.writeInt(-1); + return; + } + if (size > values.length) { + throw new IllegalArgumentException("size larger than length"); + } + out.writeInt(size); + for (int i = 0; i < size; i++) { + writeVarLong(out, values[i]); + } + } + } + + /** + * Utility methods for interacting with {@link Parcel} structures, mostly + * dealing with writing partial arrays. + * @hide + */ + public static class ParcelUtils { + public static long[] readLongArray(Parcel in) { + final int size = in.readInt(); + if (size == -1) return null; + final long[] values = new long[size]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readLong(); + } + return values; + } + + public static void writeLongArray(Parcel out, long[] values, int size) { + if (values == null) { + out.writeInt(-1); + return; + } + if (size > values.length) { + throw new IllegalArgumentException("size larger than length"); + } + out.writeInt(size); + for (int i = 0; i < size; i++) { + out.writeLong(values[i]); + } + } + } + + /** + * Builder class for {@link NetworkStatsHistory}. + */ + public static final class Builder { + private final long mBucketDuration; + private final List mBucketStart; + private final List mActiveTime; + private final List mRxBytes; + private final List mRxPackets; + private final List mTxBytes; + private final List mTxPackets; + private final List mOperations; + + /** + * Creates a new Builder with given bucket duration and initial capacity to construct + * {@link NetworkStatsHistory} objects. + * + * @param bucketDuration Duration of the buckets of the object, in milliseconds. + * @param initialCapacity Estimated number of records. + */ + public Builder(long bucketDuration, int initialCapacity) { + mBucketDuration = bucketDuration; + mBucketStart = new ArrayList<>(initialCapacity); + mActiveTime = new ArrayList<>(initialCapacity); + mRxBytes = new ArrayList<>(initialCapacity); + mRxPackets = new ArrayList<>(initialCapacity); + mTxBytes = new ArrayList<>(initialCapacity); + mTxPackets = new ArrayList<>(initialCapacity); + mOperations = new ArrayList<>(initialCapacity); + } + + /** + * Add an {@link Entry} into the {@link NetworkStatsHistory} instance. + * + * @param entry The target {@link Entry} object. + * @return The builder object. + */ + @NonNull + public Builder addEntry(@NonNull Entry entry) { + mBucketStart.add(entry.bucketStart); + mActiveTime.add(entry.activeTime); + mRxBytes.add(entry.rxBytes); + mRxPackets.add(entry.rxPackets); + mTxBytes.add(entry.txBytes); + mTxPackets.add(entry.txPackets); + mOperations.add(entry.operations); + return this; + } + + private static long sum(@NonNull List list) { + long sum = 0; + for (long entry : list) { + sum += entry; + } + return sum; + } + + /** + * Builds the instance of the {@link NetworkStatsHistory}. + * + * @return the built instance of {@link NetworkStatsHistory}. + */ + @NonNull + public NetworkStatsHistory build() { + return new NetworkStatsHistory(mBucketDuration, + CollectionUtils.toLongArray(mBucketStart), + CollectionUtils.toLongArray(mActiveTime), + CollectionUtils.toLongArray(mRxBytes), + CollectionUtils.toLongArray(mRxPackets), + CollectionUtils.toLongArray(mTxBytes), + CollectionUtils.toLongArray(mTxPackets), + CollectionUtils.toLongArray(mOperations), + mBucketStart.size(), + sum(mRxBytes) + sum(mTxBytes)); + } + } +} diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java new file mode 100644 index 0000000000..7b5afd7200 --- /dev/null +++ b/framework-t/src/android/net/NetworkTemplate.java @@ -0,0 +1,1119 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_PROXY; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIFI_P2P; +import static android.net.ConnectivityManager.TYPE_WIMAX; +import static android.net.NetworkIdentity.OEM_NONE; +import static android.net.NetworkIdentity.OEM_PAID; +import static android.net.NetworkIdentity.OEM_PRIVATE; +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.usage.NetworkStatsManager; +import android.compat.annotation.UnsupportedAppUsage; +import android.net.wifi.WifiInfo; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.ArraySet; + +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.NetworkIdentityUtils; +import com.android.net.module.util.NetworkStatsUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Predicate used to match {@link NetworkIdentity}, usually when collecting + * statistics. (It should probably have been named {@code NetworkPredicate}.) + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class NetworkTemplate implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "MATCH_" }, value = { + MATCH_MOBILE, + MATCH_WIFI, + MATCH_ETHERNET, + MATCH_BLUETOOTH, + MATCH_PROXY, + MATCH_CARRIER, + }) + public @interface TemplateMatchRule{} + + /** Match rule to match cellular networks with given Subscriber Ids. */ + public static final int MATCH_MOBILE = 1; + /** Match rule to match wifi networks. */ + public static final int MATCH_WIFI = 4; + /** Match rule to match ethernet networks. */ + public static final int MATCH_ETHERNET = 5; + /** + * Match rule to match all cellular networks. + * + * @hide + */ + public static final int MATCH_MOBILE_WILDCARD = 6; + /** + * Match rule to match all wifi networks. + * + * @hide + */ + public static final int MATCH_WIFI_WILDCARD = 7; + /** Match rule to match bluetooth networks. */ + public static final int MATCH_BLUETOOTH = 8; + /** + * Match rule to match networks with {@link ConnectivityManager#TYPE_PROXY} as the legacy + * network type. + */ + public static final int MATCH_PROXY = 9; + /** + * Match rule to match all networks with subscriberId inside the template. Some carriers + * may offer non-cellular networks like WiFi, which will be matched by this rule. + */ + public static final int MATCH_CARRIER = 10; + + // TODO: Remove this and replace all callers with WIFI_NETWORK_KEY_ALL. + /** @hide */ + public static final String WIFI_NETWORKID_ALL = null; + + /** + * Wi-Fi Network Key is never supposed to be null (if it is, it is a bug that + * should be fixed), so it's not possible to want to match null vs + * non-null. Therefore it's fine to use null as a sentinel for Wifi Network Key. + * + * @hide + */ + public static final String WIFI_NETWORK_KEY_ALL = WIFI_NETWORKID_ALL; + + /** + * Include all network types when filtering. This is meant to merge in with the + * {@code TelephonyManager.NETWORK_TYPE_*} constants, and thus needs to stay in sync. + */ + public static final int NETWORK_TYPE_ALL = -1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "OEM_MANAGED_" }, value = { + OEM_MANAGED_ALL, + OEM_MANAGED_NO, + OEM_MANAGED_YES, + OEM_MANAGED_PAID, + OEM_MANAGED_PRIVATE + }) + public @interface OemManaged{} + + /** + * Value to match both OEM managed and unmanaged networks (all networks). + */ + public static final int OEM_MANAGED_ALL = -1; + /** + * Value to match networks which are not OEM managed. + */ + public static final int OEM_MANAGED_NO = OEM_NONE; + /** + * Value to match any OEM managed network. + */ + public static final int OEM_MANAGED_YES = -2; + /** + * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PAID}. + */ + public static final int OEM_MANAGED_PAID = OEM_PAID; + /** + * Network has {@link NetworkCapabilities#NET_CAPABILITY_OEM_PRIVATE}. + */ + public static final int OEM_MANAGED_PRIVATE = OEM_PRIVATE; + + private static boolean isKnownMatchRule(final int rule) { + switch (rule) { + case MATCH_MOBILE: + case MATCH_WIFI: + case MATCH_ETHERNET: + case MATCH_MOBILE_WILDCARD: + case MATCH_WIFI_WILDCARD: + case MATCH_BLUETOOTH: + case MATCH_PROXY: + case MATCH_CARRIER: + return true; + + default: + return false; + } + } + + /** + * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks with + * the given IMSI. + * + * @hide + */ + @UnsupportedAppUsage + public static NetworkTemplate buildTemplateMobileAll(String subscriberId) { + return new NetworkTemplate(MATCH_MOBILE, subscriberId, null); + } + + /** + * Template to match cellular networks with the given IMSI, {@code ratType} and + * {@code metered}. Use {@link #NETWORK_TYPE_ALL} to include all network types when + * filtering. See {@code TelephonyManager.NETWORK_TYPE_*}. + * + * @hide + */ + public static NetworkTemplate buildTemplateMobileWithRatType(@Nullable String subscriberId, + int ratType, int metered) { + if (TextUtils.isEmpty(subscriberId)) { + return new NetworkTemplate(MATCH_MOBILE_WILDCARD, null /* subscriberId */, + null /* matchSubscriberIds */, + new String[0] /* matchWifiNetworkKeys */, metered, ROAMING_ALL, + DEFAULT_NETWORK_ALL, ratType, OEM_MANAGED_ALL, + NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT); + } + return new NetworkTemplate(MATCH_MOBILE, subscriberId, new String[] { subscriberId }, + new String[0] /* matchWifiNetworkKeys */, + metered, ROAMING_ALL, DEFAULT_NETWORK_ALL, ratType, OEM_MANAGED_ALL, + NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT); + } + + /** + * Template to match metered {@link ConnectivityManager#TYPE_MOBILE} networks, + * regardless of IMSI. + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static NetworkTemplate buildTemplateMobileWildcard() { + return new NetworkTemplate(MATCH_MOBILE_WILDCARD, null, null); + } + + /** + * Template to match all metered {@link ConnectivityManager#TYPE_WIFI} networks, + * regardless of key of the wifi network. + * + * @hide + */ + @UnsupportedAppUsage + public static NetworkTemplate buildTemplateWifiWildcard() { + // TODO: Consider replace this with MATCH_WIFI with NETWORK_ID_ALL + // and SUBSCRIBER_ID_MATCH_RULE_ALL. + return new NetworkTemplate(MATCH_WIFI_WILDCARD, null, null); + } + + /** @hide */ + @Deprecated + @UnsupportedAppUsage + public static NetworkTemplate buildTemplateWifi() { + return buildTemplateWifiWildcard(); + } + + /** + * Template to match {@link ConnectivityManager#TYPE_WIFI} networks with the + * given key of the wifi network. + * + * @param wifiNetworkKey key of the wifi network. see {@link WifiInfo#getNetworkKey()} + * to know details about the key. + * @hide + */ + public static NetworkTemplate buildTemplateWifi(@NonNull String wifiNetworkKey) { + Objects.requireNonNull(wifiNetworkKey); + return new NetworkTemplate(MATCH_WIFI, null /* subscriberId */, + new String[] { null } /* matchSubscriberIds */, + new String[] { wifiNetworkKey }, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_ALL, + NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL); + } + + /** + * Template to match all {@link ConnectivityManager#TYPE_WIFI} networks with the given + * key of the wifi network and IMSI. + * + * Call with {@link #WIFI_NETWORK_KEY_ALL} for {@code wifiNetworkKey} to get result regardless + * of key of the wifi network. + * + * @param wifiNetworkKey key of the wifi network. see {@link WifiInfo#getNetworkKey()} + * to know details about the key. + * @param subscriberId the IMSI associated to this wifi network. + * + * @hide + */ + public static NetworkTemplate buildTemplateWifi(@Nullable String wifiNetworkKey, + @Nullable String subscriberId) { + return new NetworkTemplate(MATCH_WIFI, subscriberId, new String[] { subscriberId }, + wifiNetworkKey != null + ? new String[] { wifiNetworkKey } : new String[0], + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_ALL, + NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT); + } + + /** + * Template to combine all {@link ConnectivityManager#TYPE_ETHERNET} style + * networks together. + * + * @hide + */ + @UnsupportedAppUsage + public static NetworkTemplate buildTemplateEthernet() { + return new NetworkTemplate(MATCH_ETHERNET, null, null); + } + + /** + * Template to combine all {@link ConnectivityManager#TYPE_BLUETOOTH} style + * networks together. + * + * @hide + */ + public static NetworkTemplate buildTemplateBluetooth() { + return new NetworkTemplate(MATCH_BLUETOOTH, null, null); + } + + /** + * Template to combine all {@link ConnectivityManager#TYPE_PROXY} style + * networks together. + * + * @hide + */ + public static NetworkTemplate buildTemplateProxy() { + return new NetworkTemplate(MATCH_PROXY, null, null); + } + + /** + * Template to match all metered carrier networks with the given IMSI. + * + * @hide + */ + public static NetworkTemplate buildTemplateCarrierMetered(@NonNull String subscriberId) { + Objects.requireNonNull(subscriberId); + return new NetworkTemplate(MATCH_CARRIER, subscriberId, + new String[] { subscriberId }, + new String[0] /* matchWifiNetworkKeys */, + METERED_YES, ROAMING_ALL, + DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_ALL, + NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT); + } + + private final int mMatchRule; + private final String mSubscriberId; + + /** + * Ugh, templates are designed to target a single subscriber, but we might + * need to match several "merged" subscribers. These are the subscribers + * that should be considered to match this template. + *

+ * Since the merge set is dynamic, it should not be persisted or + * used for determining equality. + */ + private final String[] mMatchSubscriberIds; + + @NonNull + private final String[] mMatchWifiNetworkKeys; + + // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*. + private final int mMetered; + private final int mRoaming; + private final int mDefaultNetwork; + private final int mRatType; + /** + * The subscriber Id match rule defines how the template should match networks with + * specific subscriberId(s). See NetworkTemplate#SUBSCRIBER_ID_MATCH_RULE_* for more detail. + */ + private final int mSubscriberIdMatchRule; + + // Bitfield containing OEM network properties{@code NetworkIdentity#OEM_*}. + private final int mOemManaged; + + private static void checkValidSubscriberIdMatchRule(int matchRule, int subscriberIdMatchRule) { + switch (matchRule) { + case MATCH_MOBILE: + case MATCH_CARRIER: + // MOBILE and CARRIER templates must always specify a subscriber ID. + if (subscriberIdMatchRule == NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL) { + throw new IllegalArgumentException("Invalid SubscriberIdMatchRule " + + "on match rule: " + getMatchRuleName(matchRule)); + } + return; + default: + return; + } + } + + /** @hide */ + // TODO: Deprecate this constructor, mark it @UnsupportedAppUsage(maxTargetSdk = S) + @UnsupportedAppUsage + public NetworkTemplate(int matchRule, String subscriberId, String wifiNetworkKey) { + this(matchRule, subscriberId, new String[] { subscriberId }, wifiNetworkKey); + } + + /** @hide */ + public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds, + String wifiNetworkKey) { + // Older versions used to only match MATCH_MOBILE and MATCH_MOBILE_WILDCARD templates + // to metered networks. It is now possible to match mobile with any meteredness, but + // in order to preserve backward compatibility of @UnsupportedAppUsage methods, this + //constructor passes METERED_YES for these types. + this(matchRule, subscriberId, matchSubscriberIds, + wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0], + (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD) ? METERED_YES + : METERED_ALL , ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_MANAGED_ALL, NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT); + } + + /** @hide */ + // TODO: Remove it after updating all of the caller. + public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds, + String wifiNetworkKey, int metered, int roaming, int defaultNetwork, int ratType, + int oemManaged) { + this(matchRule, subscriberId, matchSubscriberIds, + wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0], + metered, roaming, defaultNetwork, ratType, oemManaged, + NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT); + } + + /** @hide */ + public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds, + String[] matchWifiNetworkKeys, int metered, int roaming, + int defaultNetwork, int ratType, int oemManaged, int subscriberIdMatchRule) { + Objects.requireNonNull(matchWifiNetworkKeys); + mMatchRule = matchRule; + mSubscriberId = subscriberId; + // TODO: Check whether mMatchSubscriberIds = null or mMatchSubscriberIds = {null} when + // mSubscriberId is null + mMatchSubscriberIds = matchSubscriberIds; + mMatchWifiNetworkKeys = matchWifiNetworkKeys; + mMetered = metered; + mRoaming = roaming; + mDefaultNetwork = defaultNetwork; + mRatType = ratType; + mOemManaged = oemManaged; + mSubscriberIdMatchRule = subscriberIdMatchRule; + checkValidSubscriberIdMatchRule(matchRule, subscriberIdMatchRule); + if (!isKnownMatchRule(matchRule)) { + throw new IllegalArgumentException("Unknown network template rule " + matchRule + + " will not match any identity."); + } + } + + private NetworkTemplate(Parcel in) { + mMatchRule = in.readInt(); + mSubscriberId = in.readString(); + mMatchSubscriberIds = in.createStringArray(); + mMatchWifiNetworkKeys = in.createStringArray(); + mMetered = in.readInt(); + mRoaming = in.readInt(); + mDefaultNetwork = in.readInt(); + mRatType = in.readInt(); + mOemManaged = in.readInt(); + mSubscriberIdMatchRule = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mMatchRule); + dest.writeString(mSubscriberId); + dest.writeStringArray(mMatchSubscriberIds); + dest.writeStringArray(mMatchWifiNetworkKeys); + dest.writeInt(mMetered); + dest.writeInt(mRoaming); + dest.writeInt(mDefaultNetwork); + dest.writeInt(mRatType); + dest.writeInt(mOemManaged); + dest.writeInt(mSubscriberIdMatchRule); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("NetworkTemplate: "); + builder.append("matchRule=").append(getMatchRuleName(mMatchRule)); + if (mSubscriberId != null) { + builder.append(", subscriberId=").append( + NetworkIdentityUtils.scrubSubscriberId(mSubscriberId)); + } + if (mMatchSubscriberIds != null) { + builder.append(", matchSubscriberIds=").append( + Arrays.toString(NetworkIdentityUtils.scrubSubscriberIds(mMatchSubscriberIds))); + } + builder.append(", matchWifiNetworkKeys=").append(Arrays.toString(mMatchWifiNetworkKeys)); + if (mMetered != METERED_ALL) { + builder.append(", metered=").append(NetworkStats.meteredToString(mMetered)); + } + if (mRoaming != ROAMING_ALL) { + builder.append(", roaming=").append(NetworkStats.roamingToString(mRoaming)); + } + if (mDefaultNetwork != DEFAULT_NETWORK_ALL) { + builder.append(", defaultNetwork=").append(NetworkStats.defaultNetworkToString( + mDefaultNetwork)); + } + if (mRatType != NETWORK_TYPE_ALL) { + builder.append(", ratType=").append(mRatType); + } + if (mOemManaged != OEM_MANAGED_ALL) { + builder.append(", oemManaged=").append(getOemManagedNames(mOemManaged)); + } + builder.append(", subscriberIdMatchRule=") + .append(subscriberIdMatchRuleToString(mSubscriberIdMatchRule)); + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(mMatchRule, mSubscriberId, Arrays.hashCode(mMatchWifiNetworkKeys), + mMetered, mRoaming, mDefaultNetwork, mRatType, mOemManaged, mSubscriberIdMatchRule); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof NetworkTemplate) { + final NetworkTemplate other = (NetworkTemplate) obj; + return mMatchRule == other.mMatchRule + && Objects.equals(mSubscriberId, other.mSubscriberId) + && mMetered == other.mMetered + && mRoaming == other.mRoaming + && mDefaultNetwork == other.mDefaultNetwork + && mRatType == other.mRatType + && mOemManaged == other.mOemManaged + && mSubscriberIdMatchRule == other.mSubscriberIdMatchRule + && Arrays.equals(mMatchWifiNetworkKeys, other.mMatchWifiNetworkKeys); + } + return false; + } + + private static String subscriberIdMatchRuleToString(int rule) { + switch (rule) { + case NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT: + return "EXACT_MATCH"; + case NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL: + return "ALL"; + default: + return "Unknown rule " + rule; + } + } + + /** @hide */ + public boolean isMatchRuleMobile() { + switch (mMatchRule) { + case MATCH_MOBILE: + case MATCH_MOBILE_WILDCARD: + return true; + default: + return false; + } + } + + /** + * Get match rule of the template. See {@code MATCH_*}. + */ + @UnsupportedAppUsage + public int getMatchRule() { + // Wildcard rules are not exposed. For external callers, convert wildcard rules to + // exposed rules before returning. + switch (mMatchRule) { + case MATCH_MOBILE_WILDCARD: + return MATCH_MOBILE; + case MATCH_WIFI_WILDCARD: + return MATCH_WIFI; + default: + return mMatchRule; + } + } + + /** + * Get subscriber Id of the template. + * @hide + */ + @Nullable + @UnsupportedAppUsage + public String getSubscriberId() { + return mSubscriberId; + } + + /** + * Get set of subscriber Ids of the template. + */ + @NonNull + public Set getSubscriberIds() { + return new ArraySet<>(Arrays.asList(mMatchSubscriberIds)); + } + + /** + * Get the set of Wifi Network Keys of the template. + * See {@link WifiInfo#getNetworkKey()}. + */ + @NonNull + public Set getWifiNetworkKeys() { + return new ArraySet<>(Arrays.asList(mMatchWifiNetworkKeys)); + } + + /** @hide */ + // TODO: Remove this and replace all callers with {@link #getWifiNetworkKeys()}. + @Nullable + public String getNetworkId() { + return getWifiNetworkKeys().isEmpty() ? null : getWifiNetworkKeys().iterator().next(); + } + + /** + * Get meteredness filter of the template. + */ + @NetworkStats.Meteredness + public int getMeteredness() { + return mMetered; + } + + /** + * Get roaming filter of the template. + */ + @NetworkStats.Roaming + public int getRoaming() { + return mRoaming; + } + + /** + * Get the default network status filter of the template. + */ + @NetworkStats.DefaultNetwork + public int getDefaultNetworkStatus() { + return mDefaultNetwork; + } + + /** + * Get the Radio Access Technology(RAT) type filter of the template. + */ + public int getRatType() { + return mRatType; + } + + /** + * Get the OEM managed filter of the template. See {@code OEM_MANAGED_*} or + * {@code android.net.NetworkIdentity#OEM_*}. + */ + @OemManaged + public int getOemManaged() { + return mOemManaged; + } + + /** + * Test if given {@link NetworkIdentity} matches this template. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public boolean matches(@NonNull NetworkIdentity ident) { + Objects.requireNonNull(ident); + if (!matchesMetered(ident)) return false; + if (!matchesRoaming(ident)) return false; + if (!matchesDefaultNetwork(ident)) return false; + if (!matchesOemNetwork(ident)) return false; + + switch (mMatchRule) { + case MATCH_MOBILE: + return matchesMobile(ident); + case MATCH_WIFI: + return matchesWifi(ident); + case MATCH_ETHERNET: + return matchesEthernet(ident); + case MATCH_MOBILE_WILDCARD: + return matchesMobileWildcard(ident); + case MATCH_WIFI_WILDCARD: + return matchesWifiWildcard(ident); + case MATCH_BLUETOOTH: + return matchesBluetooth(ident); + case MATCH_PROXY: + return matchesProxy(ident); + case MATCH_CARRIER: + return matchesCarrier(ident); + default: + // We have no idea what kind of network template we are, so we + // just claim not to match anything. + return false; + } + } + + private boolean matchesMetered(NetworkIdentity ident) { + return (mMetered == METERED_ALL) + || (mMetered == METERED_YES && ident.mMetered) + || (mMetered == METERED_NO && !ident.mMetered); + } + + private boolean matchesRoaming(NetworkIdentity ident) { + return (mRoaming == ROAMING_ALL) + || (mRoaming == ROAMING_YES && ident.mRoaming) + || (mRoaming == ROAMING_NO && !ident.mRoaming); + } + + private boolean matchesDefaultNetwork(NetworkIdentity ident) { + return (mDefaultNetwork == DEFAULT_NETWORK_ALL) + || (mDefaultNetwork == DEFAULT_NETWORK_YES && ident.mDefaultNetwork) + || (mDefaultNetwork == DEFAULT_NETWORK_NO && !ident.mDefaultNetwork); + } + + private boolean matchesOemNetwork(NetworkIdentity ident) { + return (mOemManaged == OEM_MANAGED_ALL) + || (mOemManaged == OEM_MANAGED_YES + && ident.mOemManaged != OEM_NONE) + || (mOemManaged == ident.mOemManaged); + } + + private boolean matchesCollapsedRatType(NetworkIdentity ident) { + return mRatType == NETWORK_TYPE_ALL + || NetworkStatsManager.getCollapsedRatType(mRatType) + == NetworkStatsManager.getCollapsedRatType(ident.mRatType); + } + + /** + * Check if this template matches {@code subscriberId}. Returns true if this + * template was created with {@code SUBSCRIBER_ID_MATCH_RULE_ALL}, or with a + * {@code mMatchSubscriberIds} array that contains {@code subscriberId}. + * + * @hide + */ + public boolean matchesSubscriberId(@Nullable String subscriberId) { + return mSubscriberIdMatchRule == NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL + || CollectionUtils.contains(mMatchSubscriberIds, subscriberId); + } + + /** + * Check if network matches key of the wifi network. + * Returns true when the key matches, or when {@code mMatchWifiNetworkKeys} is + * empty. + * + * @param wifiNetworkKey key of the wifi network. see {@link WifiInfo#getNetworkKey()} + * to know details about the key. + */ + private boolean matchesWifiNetworkKey(@NonNull String wifiNetworkKey) { + Objects.requireNonNull(wifiNetworkKey); + return CollectionUtils.isEmpty(mMatchWifiNetworkKeys) + || CollectionUtils.contains(mMatchWifiNetworkKeys, wifiNetworkKey); + } + + /** + * Check if mobile network matches IMSI. + */ + private boolean matchesMobile(NetworkIdentity ident) { + if (ident.mType == TYPE_WIMAX) { + // TODO: consider matching against WiMAX subscriber identity + return true; + } else { + return ident.mType == TYPE_MOBILE && !CollectionUtils.isEmpty(mMatchSubscriberIds) + && CollectionUtils.contains(mMatchSubscriberIds, ident.mSubscriberId) + && matchesCollapsedRatType(ident); + } + } + + /** + * Check if matches Wi-Fi network template. + */ + private boolean matchesWifi(NetworkIdentity ident) { + switch (ident.mType) { + case TYPE_WIFI: + return matchesSubscriberId(ident.mSubscriberId) + && matchesWifiNetworkKey(ident.mWifiNetworkKey); + default: + return false; + } + } + + /** + * Check if matches Ethernet network template. + */ + private boolean matchesEthernet(NetworkIdentity ident) { + if (ident.mType == TYPE_ETHERNET) { + return true; + } + return false; + } + + /** + * Check if matches carrier network. The carrier networks means it includes the subscriberId. + */ + private boolean matchesCarrier(NetworkIdentity ident) { + return ident.mSubscriberId != null + && !CollectionUtils.isEmpty(mMatchSubscriberIds) + && CollectionUtils.contains(mMatchSubscriberIds, ident.mSubscriberId); + } + + private boolean matchesMobileWildcard(NetworkIdentity ident) { + if (ident.mType == TYPE_WIMAX) { + return true; + } else { + return ident.mType == TYPE_MOBILE && matchesCollapsedRatType(ident); + } + } + + private boolean matchesWifiWildcard(NetworkIdentity ident) { + switch (ident.mType) { + case TYPE_WIFI: + case TYPE_WIFI_P2P: + return true; + default: + return false; + } + } + + /** + * Check if matches Bluetooth network template. + */ + private boolean matchesBluetooth(NetworkIdentity ident) { + if (ident.mType == TYPE_BLUETOOTH) { + return true; + } + return false; + } + + /** + * Check if matches Proxy network template. + */ + private boolean matchesProxy(NetworkIdentity ident) { + return ident.mType == TYPE_PROXY; + } + + private static String getMatchRuleName(int matchRule) { + switch (matchRule) { + case MATCH_MOBILE: + return "MOBILE"; + case MATCH_WIFI: + return "WIFI"; + case MATCH_ETHERNET: + return "ETHERNET"; + case MATCH_MOBILE_WILDCARD: + return "MOBILE_WILDCARD"; + case MATCH_WIFI_WILDCARD: + return "WIFI_WILDCARD"; + case MATCH_BLUETOOTH: + return "BLUETOOTH"; + case MATCH_PROXY: + return "PROXY"; + case MATCH_CARRIER: + return "CARRIER"; + default: + return "UNKNOWN(" + matchRule + ")"; + } + } + + private static String getOemManagedNames(int oemManaged) { + switch (oemManaged) { + case OEM_MANAGED_ALL: + return "OEM_MANAGED_ALL"; + case OEM_MANAGED_NO: + return "OEM_MANAGED_NO"; + case OEM_MANAGED_YES: + return "OEM_MANAGED_YES"; + default: + return NetworkIdentity.getOemManagedNames(oemManaged); + } + } + + /** + * Examine the given template and normalize it. + * We pick the "lowest" merged subscriber as the primary + * for key purposes, and expand the template to match all other merged + * subscribers. + *

+ * For example, given an incoming template matching B, and the currently + * active merge set [A,B], we'd return a new template that primarily matches + * A, but also matches B. + * TODO: remove and use {@link #normalize(NetworkTemplate, List)}. + * + * @hide + */ + @UnsupportedAppUsage + public static NetworkTemplate normalize(NetworkTemplate template, String[] merged) { + return normalize(template, Arrays.asList(merged)); + } + + /** + * Examine the given template and normalize it. + * We pick the "lowest" merged subscriber as the primary + * for key purposes, and expand the template to match all other merged + * subscribers. + * + * There can be multiple merged subscriberIds for multi-SIM devices. + * + *

+ * For example, given an incoming template matching B, and the currently + * active merge set [A,B], we'd return a new template that primarily matches + * A, but also matches B. + * + * @hide + */ + // TODO: @SystemApi when ready. + public static NetworkTemplate normalize(NetworkTemplate template, List mergedList) { + // Now there are several types of network which uses SubscriberId to store network + // information. For instances: + // The TYPE_WIFI with subscriberId means that it is a merged carrier wifi network. + // The TYPE_CARRIER means that the network associate to specific carrier network. + + if (template.mSubscriberId == null) return template; + + for (String[] merged : mergedList) { + if (CollectionUtils.contains(merged, template.mSubscriberId)) { + // Requested template subscriber is part of the merge group; return + // a template that matches all merged subscribers. + final String[] matchWifiNetworkKeys = template.mMatchWifiNetworkKeys; + return new NetworkTemplate(template.mMatchRule, merged[0], merged, + CollectionUtils.isEmpty(matchWifiNetworkKeys) + ? null : matchWifiNetworkKeys[0]); + } + } + + return template; + } + + @UnsupportedAppUsage + public static final @android.annotation.NonNull Creator CREATOR = new Creator() { + @Override + public NetworkTemplate createFromParcel(Parcel in) { + return new NetworkTemplate(in); + } + + @Override + public NetworkTemplate[] newArray(int size) { + return new NetworkTemplate[size]; + } + }; + + /** + * Builder class for NetworkTemplate. + */ + public static final class Builder { + private final int mMatchRule; + // Use a SortedSet to provide a deterministic order when fetching the first one. + @NonNull + private final SortedSet mMatchSubscriberIds = + new TreeSet<>(Comparator.nullsFirst(Comparator.naturalOrder())); + @NonNull + private final SortedSet mMatchWifiNetworkKeys = new TreeSet<>(); + + // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*. + private int mMetered; + private int mRoaming; + private int mDefaultNetwork; + private int mRatType; + + // Bitfield containing OEM network properties {@code NetworkIdentity#OEM_*}. + private int mOemManaged; + + /** + * Creates a new Builder with given match rule to construct NetworkTemplate objects. + * + * @param matchRule the match rule of the template, see {@code MATCH_*}. + */ + public Builder(@TemplateMatchRule final int matchRule) { + assertRequestableMatchRule(matchRule); + // Initialize members with default values. + mMatchRule = matchRule; + mMetered = METERED_ALL; + mRoaming = ROAMING_ALL; + mDefaultNetwork = DEFAULT_NETWORK_ALL; + mRatType = NETWORK_TYPE_ALL; + mOemManaged = OEM_MANAGED_ALL; + } + + /** + * Set the Subscriber Ids. Calling this function with an empty set represents + * the intention of matching any Subscriber Ids. + * + * @param subscriberIds the list of Subscriber Ids. + * @return this builder. + */ + @NonNull + public Builder setSubscriberIds(@NonNull Set subscriberIds) { + Objects.requireNonNull(subscriberIds); + mMatchSubscriberIds.clear(); + mMatchSubscriberIds.addAll(subscriberIds); + return this; + } + + /** + * Set the Wifi Network Keys. Calling this function with an empty set represents + * the intention of matching any Wifi Network Key. + * + * @param wifiNetworkKeys the list of Wifi Network Key, + * see {@link WifiInfo#getNetworkKey()}. + * Or an empty list to match all networks. + * Note that {@code getNetworkKey()} might get null key + * when wifi disconnects. However, the caller should never invoke + * this function with a null Wifi Network Key since such statistics + * never exists. + * @return this builder. + */ + @NonNull + public Builder setWifiNetworkKeys(@NonNull Set wifiNetworkKeys) { + Objects.requireNonNull(wifiNetworkKeys); + for (String key : wifiNetworkKeys) { + if (key == null) { + throw new IllegalArgumentException("Null is not a valid key"); + } + } + mMatchWifiNetworkKeys.clear(); + mMatchWifiNetworkKeys.addAll(wifiNetworkKeys); + return this; + } + + /** + * Set the meteredness filter. + * + * @param metered the meteredness filter. + * @return this builder. + */ + @NonNull + public Builder setMeteredness(@NetworkStats.Meteredness int metered) { + mMetered = metered; + return this; + } + + /** + * Set the roaming filter. + * + * @param roaming the roaming filter. + * @return this builder. + */ + @NonNull + public Builder setRoaming(@NetworkStats.Roaming int roaming) { + mRoaming = roaming; + return this; + } + + /** + * Set the default network status filter. + * + * @param defaultNetwork the default network status filter. + * @return this builder. + */ + @NonNull + public Builder setDefaultNetworkStatus(@NetworkStats.DefaultNetwork int defaultNetwork) { + mDefaultNetwork = defaultNetwork; + return this; + } + + /** + * Set the Radio Access Technology(RAT) type filter. + * + * @param ratType the Radio Access Technology(RAT) type filter. Use + * {@link #NETWORK_TYPE_ALL} to include all network types when filtering. + * See {@code TelephonyManager.NETWORK_TYPE_*}. + * @return this builder. + */ + @NonNull + public Builder setRatType(int ratType) { + // Input will be validated with the match rule when building the template. + mRatType = ratType; + return this; + } + + /** + * Set the OEM managed filter. + * + * @param oemManaged the match rule to match different type of OEM managed network or + * unmanaged networks. See {@code OEM_MANAGED_*}. + * @return this builder. + */ + @NonNull + public Builder setOemManaged(@OemManaged int oemManaged) { + mOemManaged = oemManaged; + return this; + } + + /** + * Check whether the match rule is requestable. + * + * @param matchRule the target match rule to be checked. + */ + private static void assertRequestableMatchRule(final int matchRule) { + if (!isKnownMatchRule(matchRule) + || matchRule == MATCH_PROXY + || matchRule == MATCH_MOBILE_WILDCARD + || matchRule == MATCH_WIFI_WILDCARD) { + throw new IllegalArgumentException("Invalid match rule: " + + getMatchRuleName(matchRule)); + } + } + + private void assertRequestableParameters() { + validateWifiNetworkKeys(); + // TODO: Check all the input are legitimate. + } + + private void validateWifiNetworkKeys() { + if (mMatchRule != MATCH_WIFI && !mMatchWifiNetworkKeys.isEmpty()) { + throw new IllegalArgumentException("Trying to build non wifi match rule: " + + mMatchRule + " with wifi network keys"); + } + } + + /** + * For backward compatibility, deduce match rule to a wildcard match rule + * if the Subscriber Ids are empty. + */ + private int getWildcardDeducedMatchRule() { + if (mMatchRule == MATCH_MOBILE && mMatchSubscriberIds.isEmpty()) { + return MATCH_MOBILE_WILDCARD; + } else if (mMatchRule == MATCH_WIFI && mMatchSubscriberIds.isEmpty() + && mMatchWifiNetworkKeys.isEmpty()) { + return MATCH_WIFI_WILDCARD; + } + return mMatchRule; + } + + /** + * Builds the instance of the NetworkTemplate. + * + * @return the built instance of NetworkTemplate. + */ + @NonNull + public NetworkTemplate build() { + assertRequestableParameters(); + final int subscriberIdMatchRule = mMatchSubscriberIds.isEmpty() + ? NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL + : NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT; + return new NetworkTemplate(getWildcardDeducedMatchRule(), + mMatchSubscriberIds.isEmpty() ? null : mMatchSubscriberIds.iterator().next(), + mMatchSubscriberIds.toArray(new String[0]), + mMatchWifiNetworkKeys.toArray(new String[0]), mMetered, mRoaming, + mDefaultNetwork, mRatType, mOemManaged, subscriberIdMatchRule); + } + } +} diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java new file mode 100644 index 0000000000..bc836d857e --- /dev/null +++ b/framework-t/src/android/net/TrafficStats.java @@ -0,0 +1,1148 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.app.DownloadManager; +import android.app.backup.BackupManager; +import android.app.usage.NetworkStatsManager; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.media.MediaPlayer; +import android.os.Binder; +import android.os.Build; +import android.os.RemoteException; +import android.os.StrictMode; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.Socket; +import java.net.SocketException; + +/** + * Class that provides network traffic statistics. These statistics include + * bytes transmitted and received and network packets transmitted and received, + * over all interfaces, over the mobile interface, and on a per-UID basis. + *

+ * These statistics may not be available on all platforms. If the statistics are + * not supported by this device, {@link #UNSUPPORTED} will be returned. + *

+ * Note that the statistics returned by this class reset and start from zero + * after every reboot. To access more robust historical network statistics data, + * use {@link NetworkStatsManager} instead. + */ +public class TrafficStats { + static { + System.loadLibrary("framework-connectivity-tiramisu-jni"); + } + + private static final String TAG = TrafficStats.class.getSimpleName(); + /** + * The return value to indicate that the device does not support the statistic. + */ + public final static int UNSUPPORTED = -1; + + /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */ + @Deprecated + public static final long KB_IN_BYTES = 1024; + /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */ + @Deprecated + public static final long MB_IN_BYTES = KB_IN_BYTES * 1024; + /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */ + @Deprecated + public static final long GB_IN_BYTES = MB_IN_BYTES * 1024; + /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */ + @Deprecated + public static final long TB_IN_BYTES = GB_IN_BYTES * 1024; + /** @hide @deprecated use {@code DataUnit} instead to clarify SI-vs-IEC */ + @Deprecated + public static final long PB_IN_BYTES = TB_IN_BYTES * 1024; + + /** + * Special UID value used when collecting {@link NetworkStatsHistory} for + * removed applications. + * + * @hide + */ + public static final int UID_REMOVED = -4; + + /** + * Special UID value used when collecting {@link NetworkStatsHistory} for + * tethering traffic. + * + * @hide + */ + public static final int UID_TETHERING = NetworkStats.UID_TETHERING; + + /** + * Tag values in this range are reserved for the network stack. The network stack is + * running as UID {@link android.os.Process.NETWORK_STACK_UID} when in the mainline + * module separate process, and as the system UID otherwise. + */ + /** @hide */ + @SystemApi + public static final int TAG_NETWORK_STACK_RANGE_START = 0xFFFFFD00; + /** @hide */ + @SystemApi + public static final int TAG_NETWORK_STACK_RANGE_END = 0xFFFFFEFF; + + /** + * Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used internally by system services + * like DownloadManager when performing traffic on behalf of an application. + */ + // Please note there is no enforcement of these constants, so do not rely on them to + // determine that the caller is a system caller. + /** @hide */ + @SystemApi + public static final int TAG_SYSTEM_IMPERSONATION_RANGE_START = 0xFFFFFF00; + /** @hide */ + @SystemApi + public static final int TAG_SYSTEM_IMPERSONATION_RANGE_END = 0xFFFFFF0F; + + /** + * Tag values between these ranges are reserved for the network stack to do traffic + * on behalf of applications. It is a subrange of the range above. + */ + /** @hide */ + @SystemApi + public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = 0xFFFFFF80; + /** @hide */ + @SystemApi + public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = 0xFFFFFF8F; + + /** + * Default tag value for {@link DownloadManager} traffic. + * + * @hide + */ + public static final int TAG_SYSTEM_DOWNLOAD = 0xFFFFFF01; + + /** + * Default tag value for {@link MediaPlayer} traffic. + * + * @hide + */ + public static final int TAG_SYSTEM_MEDIA = 0xFFFFFF02; + + /** + * Default tag value for {@link BackupManager} backup traffic; that is, + * traffic from the device to the storage backend. + * + * @hide + */ + public static final int TAG_SYSTEM_BACKUP = 0xFFFFFF03; + + /** + * Default tag value for {@link BackupManager} restore traffic; that is, + * app data retrieved from the storage backend at install time. + * + * @hide + */ + public static final int TAG_SYSTEM_RESTORE = 0xFFFFFF04; + + /** + * Default tag value for code (typically APKs) downloaded by an app store on + * behalf of the app, such as updates. + * + * @hide + */ + public static final int TAG_SYSTEM_APP = 0xFFFFFF05; + + // TODO : remove this constant when Wifi code is updated + /** @hide */ + public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42; + + private static INetworkStatsService sStatsService; + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + private synchronized static INetworkStatsService getStatsService() { + if (sStatsService == null) { + throw new IllegalStateException("TrafficStats not initialized, uid=" + + Binder.getCallingUid()); + } + return sStatsService; + } + + /** + * Snapshot of {@link NetworkStats} when the currently active profiling + * session started, or {@code null} if no session active. + * + * @see #startDataProfiling(Context) + * @see #stopDataProfiling(Context) + */ + private static NetworkStats sActiveProfilingStart; + + private static Object sProfilingLock = new Object(); + + private static final String LOOPBACK_IFACE = "lo"; + + /** + * Initialization {@link TrafficStats} with the context, to + * allow {@link TrafficStats} to fetch the needed binder. + * + * @param context a long-lived context, such as the application context or system + * server context. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SuppressLint("VisiblySynchronized") + public static synchronized void init(@NonNull final Context context) { + if (sStatsService != null) { + throw new IllegalStateException("TrafficStats is already initialized, uid=" + + Binder.getCallingUid()); + } + final NetworkStatsManager statsManager = + context.getSystemService(NetworkStatsManager.class); + if (statsManager == null) { + // TODO: Currently Process.isSupplemental is not working yet, because it depends on + // process to run in a certain UID range, which is not true for now. Change this + // to Log.wtf once Process.isSupplemental is ready. + Log.e(TAG, "TrafficStats not initialized, uid=" + Binder.getCallingUid()); + return; + } + sStatsService = statsManager.getBinder(); + } + + /** + * Attach the socket tagger implementation to the current process, to + * get notified when a socket's {@link FileDescriptor} is assigned to + * a thread. See {@link SocketTagger#set(SocketTagger)}. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static void attachSocketTagger() { + dalvik.system.SocketTagger.set(new SocketTagger()); + } + + private static class SocketTagger extends dalvik.system.SocketTagger { + + // TODO: set to false + private static final boolean LOGD = true; + + SocketTagger() { + } + + @Override + public void tag(FileDescriptor fd) throws SocketException { + final UidTag tagInfo = sThreadUidTag.get(); + if (LOGD) { + Log.d(TAG, "tagSocket(" + fd.getInt$() + ") with statsTag=0x" + + Integer.toHexString(tagInfo.tag) + ", statsUid=" + tagInfo.uid); + } + if (tagInfo.tag == -1) { + StrictMode.noteUntaggedSocket(); + } + + if (tagInfo.tag == -1 && tagInfo.uid == -1) return; + final int errno = native_tagSocketFd(fd, tagInfo.tag, tagInfo.uid); + if (errno < 0) { + Log.i(TAG, "tagSocketFd(" + fd.getInt$() + ", " + + tagInfo.tag + ", " + + tagInfo.uid + ") failed with errno" + errno); + } + } + + @Override + public void untag(FileDescriptor fd) throws SocketException { + if (LOGD) { + Log.i(TAG, "untagSocket(" + fd.getInt$() + ")"); + } + + final UidTag tagInfo = sThreadUidTag.get(); + if (tagInfo.tag == -1 && tagInfo.uid == -1) return; + + final int errno = native_untagSocketFd(fd); + if (errno < 0) { + Log.w(TAG, "untagSocket(" + fd.getInt$() + ") failed with errno " + errno); + } + } + } + + private static native int native_tagSocketFd(FileDescriptor fd, int tag, int uid); + private static native int native_untagSocketFd(FileDescriptor fd); + + private static class UidTag { + public int tag = -1; + public int uid = -1; + } + + private static ThreadLocal sThreadUidTag = new ThreadLocal() { + @Override + protected UidTag initialValue() { + return new UidTag(); + } + }; + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + *

+ * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + *

+ * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and + * used internally by system services like {@link DownloadManager} when + * performing traffic on behalf of an application. + * + * @see #clearThreadStatsTag() + */ + public static void setThreadStatsTag(int tag) { + getAndSetThreadStatsTag(tag); + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + *

+ * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + *

+ * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and + * used internally by system services like {@link DownloadManager} when + * performing traffic on behalf of an application. + * + * @return the current tag for the calling thread, which can be used to + * restore any existing values after a nested operation is finished + */ + public static int getAndSetThreadStatsTag(int tag) { + final int old = sThreadUidTag.get().tag; + sThreadUidTag.get().tag = tag; + return old; + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. The tag used internally is well-defined to + * distinguish all backup-related traffic. + * + * @hide + */ + @SystemApi + public static void setThreadStatsTagBackup() { + setThreadStatsTag(TAG_SYSTEM_BACKUP); + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. The tag used internally is well-defined to + * distinguish all restore-related traffic. + * + * @hide + */ + @SystemApi + public static void setThreadStatsTagRestore() { + setThreadStatsTag(TAG_SYSTEM_RESTORE); + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. The tag used internally is well-defined to + * distinguish all code (typically APKs) downloaded by an app store on + * behalf of the app, such as updates. + * + * @hide + */ + @SystemApi + public static void setThreadStatsTagApp() { + setThreadStatsTag(TAG_SYSTEM_APP); + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. The tag used internally is well-defined to + * distinguish all download provider traffic. + * + * @hide + */ + @SystemApi + public static void setThreadStatsTagDownload() { + setThreadStatsTag(TAG_SYSTEM_DOWNLOAD); + } + + /** + * Get the active tag used when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + * {@link #tagSocket(Socket)}. + * + * @see #setThreadStatsTag(int) + */ + public static int getThreadStatsTag() { + return sThreadUidTag.get().tag; + } + + /** + * Clear any active tag set to account {@link Socket} traffic originating + * from the current thread. + * + * @see #setThreadStatsTag(int) + */ + public static void clearThreadStatsTag() { + sThreadUidTag.get().tag = -1; + } + + /** + * Set specific UID to use when accounting {@link Socket} traffic + * originating from the current thread. Designed for use when performing an + * operation on behalf of another application, or when another application + * is performing operations on your behalf. + *

+ * Any app can accept blame for traffic performed on a socket + * originally created by another app by calling this method with the + * {@link android.system.Os#getuid()} value. However, only apps holding the + * {@code android.Manifest.permission#UPDATE_DEVICE_STATS} permission may + * assign blame to another UIDs. + *

+ * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + */ + @SuppressLint("RequiresPermission") + public static void setThreadStatsUid(int uid) { + sThreadUidTag.get().uid = uid; + } + + /** + * Get the active UID used when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + * {@link #tagSocket(Socket)}. + * + * @see #setThreadStatsUid(int) + */ + public static int getThreadStatsUid() { + return sThreadUidTag.get().uid; + } + + /** + * Set specific UID to use when accounting {@link Socket} traffic + * originating from the current thread as the calling UID. Designed for use + * when another application is performing operations on your behalf. + *

+ * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + * + * @removed + * @deprecated use {@link #setThreadStatsUid(int)} instead. + */ + @Deprecated + public static void setThreadStatsUidSelf() { + setThreadStatsUid(android.os.Process.myUid()); + } + + /** + * Clear any active UID set to account {@link Socket} traffic originating + * from the current thread. + * + * @see #setThreadStatsUid(int) + */ + @SuppressLint("RequiresPermission") + public static void clearThreadStatsUid() { + setThreadStatsUid(-1); + } + + /** + * Tag the given {@link Socket} with any statistics parameters active for + * the current thread. Subsequent calls always replace any existing + * parameters. When finished, call {@link #untagSocket(Socket)} to remove + * statistics parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagSocket(Socket socket) throws SocketException { + SocketTagger.get().tag(socket); + } + + /** + * Remove any statistics parameters from the given {@link Socket}. + *

+ * In Android 8.1 (API level 27) and lower, a socket is automatically + * untagged when it's sent to another process using binder IPC with a + * {@code ParcelFileDescriptor} container. In Android 9.0 (API level 28) + * and higher, the socket tag is kept when the socket is sent to another + * process using binder IPC. You can mimic the previous behavior by + * calling {@code untagSocket()} before sending the socket to another + * process. + */ + public static void untagSocket(Socket socket) throws SocketException { + SocketTagger.get().untag(socket); + } + + /** + * Tag the given {@link DatagramSocket} with any statistics parameters + * active for the current thread. Subsequent calls always replace any + * existing parameters. When finished, call + * {@link #untagDatagramSocket(DatagramSocket)} to remove statistics + * parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagDatagramSocket(DatagramSocket socket) throws SocketException { + SocketTagger.get().tag(socket); + } + + /** + * Remove any statistics parameters from the given {@link DatagramSocket}. + */ + public static void untagDatagramSocket(DatagramSocket socket) throws SocketException { + SocketTagger.get().untag(socket); + } + + /** + * Tag the given {@link FileDescriptor} socket with any statistics + * parameters active for the current thread. Subsequent calls always replace + * any existing parameters. When finished, call + * {@link #untagFileDescriptor(FileDescriptor)} to remove statistics + * parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagFileDescriptor(FileDescriptor fd) throws IOException { + SocketTagger.get().tag(fd); + } + + /** + * Remove any statistics parameters from the given {@link FileDescriptor} + * socket. + */ + public static void untagFileDescriptor(FileDescriptor fd) throws IOException { + SocketTagger.get().untag(fd); + } + + /** + * Start profiling data usage for current UID. Only one profiling session + * can be active at a time. + * + * @hide + */ + public static void startDataProfiling(Context context) { + synchronized (sProfilingLock) { + if (sActiveProfilingStart != null) { + throw new IllegalStateException("already profiling data"); + } + + // take snapshot in time; we calculate delta later + sActiveProfilingStart = getDataLayerSnapshotForUid(context); + } + } + + /** + * Stop profiling data usage for current UID. + * + * @return Detailed {@link NetworkStats} of data that occurred since last + * {@link #startDataProfiling(Context)} call. + * @hide + */ + public static NetworkStats stopDataProfiling(Context context) { + synchronized (sProfilingLock) { + if (sActiveProfilingStart == null) { + throw new IllegalStateException("not profiling data"); + } + + // subtract starting values and return delta + final NetworkStats profilingStop = getDataLayerSnapshotForUid(context); + final NetworkStats profilingDelta = NetworkStats.subtract( + profilingStop, sActiveProfilingStart, null, null); + sActiveProfilingStart = null; + return profilingDelta; + } + } + + /** + * Increment count of network operations performed under the accounting tag + * currently active on the calling thread. This can be used to derive + * bytes-per-operation. + * + * @param operationCount Number of operations to increment count by. + */ + public static void incrementOperationCount(int operationCount) { + final int tag = getThreadStatsTag(); + incrementOperationCount(tag, operationCount); + } + + /** + * Increment count of network operations performed under the given + * accounting tag. This can be used to derive bytes-per-operation. + * + * @param tag Accounting tag used in {@link #setThreadStatsTag(int)}. + * @param operationCount Number of operations to increment count by. + */ + public static void incrementOperationCount(int tag, int operationCount) { + final int uid = android.os.Process.myUid(); + try { + getStatsService().incrementOperationCount(uid, tag, operationCount); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + public static void closeQuietly(INetworkStatsSession session) { + // TODO: move to NetworkStatsService once it exists + if (session != null) { + try { + session.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + private static long addIfSupported(long stat) { + return (stat == UNSUPPORTED) ? 0 : stat; + } + + /** + * Return number of packets transmitted across mobile networks since device + * boot. Counts packets across all mobile network interfaces, and always + * increases monotonically since device boot. Statistics are measured at the + * network layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getMobileTxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += addIfSupported(getTxPackets(iface)); + } + return total; + } + + /** + * Return number of packets received across mobile networks since device + * boot. Counts packets across all mobile network interfaces, and always + * increases monotonically since device boot. Statistics are measured at the + * network layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getMobileRxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += addIfSupported(getRxPackets(iface)); + } + return total; + } + + /** + * Return number of bytes transmitted across mobile networks since device + * boot. Counts packets across all mobile network interfaces, and always + * increases monotonically since device boot. Statistics are measured at the + * network layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getMobileTxBytes() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += addIfSupported(getTxBytes(iface)); + } + return total; + } + + /** + * Return number of bytes received across mobile networks since device boot. + * Counts packets across all mobile network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getMobileRxBytes() { + long total = 0; + for (String iface : getMobileIfaces()) { + total += addIfSupported(getRxBytes(iface)); + } + return total; + } + + /** {@hide} */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static long getMobileTcpRxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + long stat = UNSUPPORTED; + try { + stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + total += addIfSupported(stat); + } + return total; + } + + /** {@hide} */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static long getMobileTcpTxPackets() { + long total = 0; + for (String iface : getMobileIfaces()) { + long stat = UNSUPPORTED; + try { + stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + total += addIfSupported(stat); + } + return total; + } + + /** + * Return the number of packets transmitted on the specified interface since the interface + * was created. Statistics are measured at the network layer, so both TCP and + * UDP usage are included. + * + * Note that the returned values are partial statistics that do not count data from several + * sources and do not apply several adjustments that are necessary for correctness, such + * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to + * determine whether traffic is being transferred on the specific interface but are not a + * substitute for the more accurate statistics provided by the {@link NetworkStatsManager} + * APIs. + * + * @param iface The name of the interface. + * @return The number of transmitted packets. + */ + public static long getTxPackets(@NonNull String iface) { + try { + return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the number of packets received on the specified interface since the interface was + * created. Statistics are measured at the network layer, so both TCP + * and UDP usage are included. + * + * Note that the returned values are partial statistics that do not count data from several + * sources and do not apply several adjustments that are necessary for correctness, such + * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to + * determine whether traffic is being transferred on the specific interface but are not a + * substitute for the more accurate statistics provided by the {@link NetworkStatsManager} + * APIs. + * + * @param iface The name of the interface. + * @return The number of received packets. + */ + public static long getRxPackets(@NonNull String iface) { + try { + return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the number of bytes transmitted on the specified interface since the interface + * was created. Statistics are measured at the network layer, so both TCP and + * UDP usage are included. + * + * Note that the returned values are partial statistics that do not count data from several + * sources and do not apply several adjustments that are necessary for correctness, such + * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to + * determine whether traffic is being transferred on the specific interface but are not a + * substitute for the more accurate statistics provided by the {@link NetworkStatsManager} + * APIs. + * + * @param iface The name of the interface. + * @return The number of transmitted bytes. + */ + public static long getTxBytes(@NonNull String iface) { + try { + return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return the number of bytes received on the specified interface since the interface + * was created. Statistics are measured at the network layer, so both TCP + * and UDP usage are included. + * + * Note that the returned values are partial statistics that do not count data from several + * sources and do not apply several adjustments that are necessary for correctness, such + * as adjusting for VPN apps, IPv6-in-IPv4 translation, etc. These values can be used to + * determine whether traffic is being transferred on the specific interface but are not a + * substitute for the more accurate statistics provided by the {@link NetworkStatsManager} + * APIs. + * + * @param iface The name of the interface. + * @return The number of received bytes. + */ + public static long getRxBytes(@NonNull String iface) { + try { + return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackTxPackets() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackRxPackets() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackTxBytes() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** {@hide} */ + @TestApi + public static long getLoopbackRxBytes() { + try { + return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of packets transmitted since device boot. Counts packets + * across all network interfaces, and always increases monotonically since + * device boot. Statistics are measured at the network layer, so they + * include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getTotalTxPackets() { + try { + return getStatsService().getTotalStats(TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of packets received since device boot. Counts packets + * across all network interfaces, and always increases monotonically since + * device boot. Statistics are measured at the network layer, so they + * include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getTotalRxPackets() { + try { + return getStatsService().getTotalStats(TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of bytes transmitted since device boot. Counts packets + * across all network interfaces, and always increases monotonically since + * device boot. Statistics are measured at the network layer, so they + * include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getTotalTxBytes() { + try { + return getStatsService().getTotalStats(TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of bytes received since device boot. Counts packets across + * all network interfaces, and always increases monotonically since device + * boot. Statistics are measured at the network layer, so they include both + * TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + */ + public static long getTotalRxBytes() { + try { + return getStatsService().getTotalStats(TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of bytes transmitted by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#N} this will only + * report traffic statistics for the calling UID. It will return + * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access + * historical network statistics belonging to other UIDs, use + * {@link NetworkStatsManager}. + * + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid + */ + public static long getUidTxBytes(int uid) { + try { + return getStatsService().getUidStats(uid, TYPE_TX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of bytes received by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#N} this will only + * report traffic statistics for the calling UID. It will return + * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access + * historical network statistics belonging to other UIDs, use + * {@link NetworkStatsManager}. + * + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid + */ + public static long getUidRxBytes(int uid) { + try { + return getStatsService().getUidStats(uid, TYPE_RX_BYTES); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of packets transmitted by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#N} this will only + * report traffic statistics for the calling UID. It will return + * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access + * historical network statistics belonging to other UIDs, use + * {@link NetworkStatsManager}. + * + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid + */ + public static long getUidTxPackets(int uid) { + try { + return getStatsService().getUidStats(uid, TYPE_TX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return number of packets received by the given UID since device boot. + * Counts packets across all network interfaces, and always increases + * monotonically since device boot. Statistics are measured at the network + * layer, so they include both TCP and UDP usage. + *

+ * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#N} this will only + * report traffic statistics for the calling UID. It will return + * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access + * historical network statistics belonging to other UIDs, use + * {@link NetworkStatsManager}. + * + * @see android.os.Process#myUid() + * @see android.content.pm.ApplicationInfo#uid + */ + public static long getUidRxPackets(int uid) { + try { + return getStatsService().getUidStats(uid, TYPE_RX_PACKETS); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxBytes(int) + */ + @Deprecated + public static long getUidTcpTxBytes(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxBytes(int) + */ + @Deprecated + public static long getUidTcpRxBytes(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxBytes(int) + */ + @Deprecated + public static long getUidUdpTxBytes(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxBytes(int) + */ + @Deprecated + public static long getUidUdpRxBytes(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxPackets(int) + */ + @Deprecated + public static long getUidTcpTxSegments(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxPackets(int) + */ + @Deprecated + public static long getUidTcpRxSegments(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidTxPackets(int) + */ + @Deprecated + public static long getUidUdpTxPackets(int uid) { + return UNSUPPORTED; + } + + /** + * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, + * transport layer statistics are no longer available, and will + * always return {@link #UNSUPPORTED}. + * @see #getUidRxPackets(int) + */ + @Deprecated + public static long getUidUdpRxPackets(int uid) { + return UNSUPPORTED; + } + + /** + * Return detailed {@link NetworkStats} for the current UID. Requires no + * special permission. + */ + private static NetworkStats getDataLayerSnapshotForUid(Context context) { + // TODO: take snapshot locally, since proc file is now visible + final int uid = android.os.Process.myUid(); + try { + return getStatsService().getDataLayerSnapshotForUid(uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return set of any ifaces associated with mobile networks since boot. + * Interfaces are never removed from this list, so counters should always be + * monotonic. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + private static String[] getMobileIfaces() { + try { + return getStatsService().getMobileIfaces(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + // NOTE: keep these in sync with {@code com_android_server_net_NetworkStatsService.cpp}. + /** {@hide} */ + public static final int TYPE_RX_BYTES = 0; + /** {@hide} */ + public static final int TYPE_RX_PACKETS = 1; + /** {@hide} */ + public static final int TYPE_TX_BYTES = 2; + /** {@hide} */ + public static final int TYPE_TX_PACKETS = 3; + /** {@hide} */ + public static final int TYPE_TCP_RX_PACKETS = 4; + /** {@hide} */ + public static final int TYPE_TCP_TX_PACKETS = 5; +} diff --git a/framework-t/src/android/net/UnderlyingNetworkInfo.aidl b/framework-t/src/android/net/UnderlyingNetworkInfo.aidl new file mode 100644 index 0000000000..a56f2f4058 --- /dev/null +++ b/framework-t/src/android/net/UnderlyingNetworkInfo.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.net; + +parcelable UnderlyingNetworkInfo; diff --git a/framework-t/src/android/net/UnderlyingNetworkInfo.java b/framework-t/src/android/net/UnderlyingNetworkInfo.java new file mode 100644 index 0000000000..33f9375c03 --- /dev/null +++ b/framework-t/src/android/net/UnderlyingNetworkInfo.java @@ -0,0 +1,135 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A lightweight container used to carry information on the networks that underly a given + * virtual network. + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class UnderlyingNetworkInfo implements Parcelable { + /** The owner of this network. */ + private final int mOwnerUid; + + /** The interface name of this network. */ + @NonNull + private final String mIface; + + /** The names of the interfaces underlying this network. */ + @NonNull + private final List mUnderlyingIfaces; + + public UnderlyingNetworkInfo(int ownerUid, @NonNull String iface, + @NonNull List underlyingIfaces) { + Objects.requireNonNull(iface); + Objects.requireNonNull(underlyingIfaces); + mOwnerUid = ownerUid; + mIface = iface; + mUnderlyingIfaces = Collections.unmodifiableList(new ArrayList<>(underlyingIfaces)); + } + + private UnderlyingNetworkInfo(@NonNull Parcel in) { + mOwnerUid = in.readInt(); + mIface = in.readString(); + List underlyingIfaces = new ArrayList<>(); + in.readList(underlyingIfaces, null /*classLoader*/); + mUnderlyingIfaces = Collections.unmodifiableList(underlyingIfaces); + } + + /** Get the owner of this network. */ + public int getOwnerUid() { + return mOwnerUid; + } + + /** Get the interface name of this network. */ + @NonNull + public String getInterface() { + return mIface; + } + + /** Get the names of the interfaces underlying this network. */ + @NonNull + public List getUnderlyingInterfaces() { + return mUnderlyingIfaces; + } + + @Override + public String toString() { + return "UnderlyingNetworkInfo{" + + "ownerUid=" + mOwnerUid + + ", iface='" + mIface + '\'' + + ", underlyingIfaces='" + mUnderlyingIfaces.toString() + '\'' + + '}'; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mOwnerUid); + dest.writeString(mIface); + dest.writeList(mUnderlyingIfaces); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @NonNull + @Override + public UnderlyingNetworkInfo createFromParcel(@NonNull Parcel in) { + return new UnderlyingNetworkInfo(in); + } + + @NonNull + @Override + public UnderlyingNetworkInfo[] newArray(int size) { + return new UnderlyingNetworkInfo[size]; + } + }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UnderlyingNetworkInfo)) return false; + final UnderlyingNetworkInfo that = (UnderlyingNetworkInfo) o; + return mOwnerUid == that.getOwnerUid() + && Objects.equals(mIface, that.getInterface()) + && Objects.equals(mUnderlyingIfaces, that.getUnderlyingInterfaces()); + } + + @Override + public int hashCode() { + return Objects.hash(mOwnerUid, mIface, mUnderlyingIfaces); + } +} diff --git a/framework-t/src/android/net/netstats/IUsageCallback.aidl b/framework-t/src/android/net/netstats/IUsageCallback.aidl new file mode 100644 index 0000000000..4e8a5b2309 --- /dev/null +++ b/framework-t/src/android/net/netstats/IUsageCallback.aidl @@ -0,0 +1,29 @@ +/* + * 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 android.net.netstats; + +import android.net.DataUsageRequest; + +/** + * Interface for NetworkStatsService to notify events to the callers of registerUsageCallback. + * + * @hide + */ +oneway interface IUsageCallback { + void onThresholdReached(in DataUsageRequest request); + void onCallbackReleased(in DataUsageRequest request); +} diff --git a/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl b/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl new file mode 100644 index 0000000000..74c3ba44b6 --- /dev/null +++ b/framework-t/src/android/net/netstats/provider/INetworkStatsProvider.aidl @@ -0,0 +1,28 @@ +/* + * 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 android.net.netstats.provider; + +/** + * Interface for NetworkStatsService to query network statistics and set data limits. + * + * @hide + */ +oneway interface INetworkStatsProvider { + void onRequestStatsUpdate(int token); + void onSetAlert(long quotaBytes); + void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes); +} diff --git a/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl b/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl new file mode 100644 index 0000000000..01ff02dfc5 --- /dev/null +++ b/framework-t/src/android/net/netstats/provider/INetworkStatsProviderCallback.aidl @@ -0,0 +1,32 @@ +/* + * 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 android.net.netstats.provider; + +import android.net.NetworkStats; + +/** + * Interface for implementor of {@link INetworkStatsProviderCallback} to push events + * such as network statistics update or notify limit reached. + * @hide + */ +oneway interface INetworkStatsProviderCallback { + void notifyStatsUpdated(int token, in NetworkStats ifaceStats, in NetworkStats uidStats); + void notifyAlertReached(); + void notifyWarningReached(); + void notifyLimitReached(); + void unregister(); +} diff --git a/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java b/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java new file mode 100644 index 0000000000..d37a53dbf1 --- /dev/null +++ b/framework-t/src/android/net/netstats/provider/NetworkStatsProvider.java @@ -0,0 +1,232 @@ +/* + * 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 android.net.netstats.provider; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.net.NetworkStats; +import android.os.RemoteException; + +/** + * A base class that allows external modules to implement a custom network statistics provider. + * @hide + */ +@SystemApi +public abstract class NetworkStatsProvider { + /** + * A value used by {@link #onSetLimit}, {@link #onSetAlert} and {@link #onSetWarningAndLimit} + * indicates there is no limit. + */ + public static final int QUOTA_UNLIMITED = -1; + + @NonNull private final INetworkStatsProvider mProviderBinder = + new INetworkStatsProvider.Stub() { + + @Override + public void onRequestStatsUpdate(int token) { + NetworkStatsProvider.this.onRequestStatsUpdate(token); + } + + @Override + public void onSetAlert(long quotaBytes) { + NetworkStatsProvider.this.onSetAlert(quotaBytes); + } + + @Override + public void onSetWarningAndLimit(String iface, long warningBytes, long limitBytes) { + NetworkStatsProvider.this.onSetWarningAndLimit(iface, warningBytes, limitBytes); + } + }; + + // The binder given by the service when successfully registering. Only null before registering, + // never null once non-null. + @Nullable + private INetworkStatsProviderCallback mProviderCbBinder; + + /** + * Return the binder invoked by the service and redirect function calls to the overridden + * methods. + * @hide + */ + @NonNull + public INetworkStatsProvider getProviderBinder() { + return mProviderBinder; + } + + /** + * Store the binder that was returned by the service when successfully registering. Note that + * the provider cannot be re-registered. Hence this method can only be called once per provider. + * + * @hide + */ + public void setProviderCallbackBinder(@NonNull INetworkStatsProviderCallback binder) { + if (mProviderCbBinder != null) { + throw new IllegalArgumentException("provider is already registered"); + } + mProviderCbBinder = binder; + } + + /** + * Get the binder that was returned by the service when successfully registering. Or null if the + * provider was never registered. + * + * @hide + */ + @Nullable + public INetworkStatsProviderCallback getProviderCallbackBinder() { + return mProviderCbBinder; + } + + /** + * Get the binder that was returned by the service when successfully registering. Throw an + * {@link IllegalStateException} if the provider is not registered. + * + * @hide + */ + @NonNull + public INetworkStatsProviderCallback getProviderCallbackBinderOrThrow() { + if (mProviderCbBinder == null) { + throw new IllegalStateException("the provider is not registered"); + } + return mProviderCbBinder; + } + + /** + * Notify the system of new network statistics. + * + * Send the network statistics recorded since the last call to {@link #notifyStatsUpdated}. Must + * be called as soon as possible after {@link NetworkStatsProvider#onRequestStatsUpdate(int)} + * being called. Responding later increases the probability stats will be dropped. The + * provider can also call this whenever it wants to reports new stats for any reason. + * Note that the system will not necessarily immediately propagate the statistics to + * reflect the update. + * + * @param token the token under which these stats were gathered. Providers can call this method + * with the current token as often as they want, until the token changes. + * {@see NetworkStatsProvider#onRequestStatsUpdate()} + * @param ifaceStats the {@link NetworkStats} per interface to be reported. + * The provider should not include any traffic that is already counted by + * kernel interface counters. + * @param uidStats the same stats as above, but counts {@link NetworkStats} + * per uid. + */ + public void notifyStatsUpdated(int token, @NonNull NetworkStats ifaceStats, + @NonNull NetworkStats uidStats) { + try { + getProviderCallbackBinderOrThrow().notifyStatsUpdated(token, ifaceStats, uidStats); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Notify system that the quota set by {@code onSetAlert} has been reached. + */ + public void notifyAlertReached() { + try { + getProviderCallbackBinderOrThrow().notifyAlertReached(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Notify system that the warning set by {@link #onSetWarningAndLimit} has been reached. + */ + public void notifyWarningReached() { + try { + // Reuse the code path to notify warning reached with limit reached + // since framework handles them in the same way. + getProviderCallbackBinderOrThrow().notifyWarningReached(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Notify system that the limit set by {@link #onSetLimit} or limit set by + * {@link #onSetWarningAndLimit} has been reached. + */ + public void notifyLimitReached() { + try { + getProviderCallbackBinderOrThrow().notifyLimitReached(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + /** + * Called by {@code NetworkStatsService} when it requires to know updated stats. + * The provider MUST respond by calling {@link #notifyStatsUpdated} as soon as possible. + * Responding later increases the probability stats will be dropped. Memory allowing, the + * system will try to take stats into account up to one minute after calling + * {@link #onRequestStatsUpdate}. + * + * @param token a positive number identifying the new state of the system under which + * {@link NetworkStats} have to be gathered from now on. When this is called, + * custom implementations of providers MUST tally and report the latest stats with + * the previous token, under which stats were being gathered so far. + */ + public abstract void onRequestStatsUpdate(int token); + + /** + * Called by {@code NetworkStatsService} when setting the interface quota for the specified + * upstream interface. When this is called, the custom implementation should block all egress + * packets on the {@code iface} associated with the provider when {@code quotaBytes} bytes have + * been reached, and MUST respond to it by calling + * {@link NetworkStatsProvider#notifyLimitReached()}. + * + * @param iface the interface requiring the operation. + * @param quotaBytes the quota defined as the number of bytes, starting from zero and counting + * from now. A value of {@link #QUOTA_UNLIMITED} indicates there is no limit. + */ + public abstract void onSetLimit(@NonNull String iface, long quotaBytes); + + /** + * Called by {@code NetworkStatsService} when setting the interface quotas for the specified + * upstream interface. If a provider implements {@link #onSetWarningAndLimit}, the system + * will not call {@link #onSetLimit}. When this method is called, the implementation + * should behave as follows: + * 1. If {@code warningBytes} is reached on {@code iface}, block all further traffic on + * {@code iface} and call {@link NetworkStatsProvider@notifyWarningReached()}. + * 2. If {@code limitBytes} is reached on {@code iface}, block all further traffic on + * {@code iface} and call {@link NetworkStatsProvider#notifyLimitReached()}. + * + * @param iface the interface requiring the operation. + * @param warningBytes the warning defined as the number of bytes, starting from zero and + * counting from now. A value of {@link #QUOTA_UNLIMITED} indicates + * there is no warning. + * @param limitBytes the limit defined as the number of bytes, starting from zero and counting + * from now. A value of {@link #QUOTA_UNLIMITED} indicates there is no limit. + */ + public void onSetWarningAndLimit(@NonNull String iface, long warningBytes, long limitBytes) { + // Backward compatibility for those who didn't override this function. + onSetLimit(iface, limitBytes); + } + + /** + * Called by {@code NetworkStatsService} when setting the alert bytes. Custom implementations + * MUST call {@link NetworkStatsProvider#notifyAlertReached()} when {@code quotaBytes} bytes + * have been reached. Unlike {@link #onSetLimit(String, long)}, the custom implementation should + * not block all egress packets. + * + * @param quotaBytes the quota defined as the number of bytes, starting from zero and counting + * from now. A value of {@link #QUOTA_UNLIMITED} indicates there is no alert. + */ + public abstract void onSetAlert(long quotaBytes); +} diff --git a/framework-t/src/android/net/nsd/INsdManager.aidl b/framework-t/src/android/net/nsd/INsdManager.aidl new file mode 100644 index 0000000000..89e9cdbd44 --- /dev/null +++ b/framework-t/src/android/net/nsd/INsdManager.aidl @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.nsd; + +import android.net.nsd.INsdManagerCallback; +import android.net.nsd.INsdServiceConnector; +import android.os.Messenger; + +/** + * Interface that NsdService implements to connect NsdManager clients. + * + * {@hide} + */ +interface INsdManager { + INsdServiceConnector connect(INsdManagerCallback cb); +} diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl new file mode 100644 index 0000000000..1a262ec0e9 --- /dev/null +++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.nsd; + +import android.os.Messenger; +import android.net.nsd.NsdServiceInfo; + +/** + * Callbacks from NsdService to NsdManager + * @hide + */ +oneway interface INsdManagerCallback { + void onDiscoverServicesStarted(int listenerKey, in NsdServiceInfo info); + void onDiscoverServicesFailed(int listenerKey, int error); + void onServiceFound(int listenerKey, in NsdServiceInfo info); + void onServiceLost(int listenerKey, in NsdServiceInfo info); + void onStopDiscoveryFailed(int listenerKey, int error); + void onStopDiscoverySucceeded(int listenerKey); + void onRegisterServiceFailed(int listenerKey, int error); + void onRegisterServiceSucceeded(int listenerKey, in NsdServiceInfo info); + void onUnregisterServiceFailed(int listenerKey, int error); + void onUnregisterServiceSucceeded(int listenerKey); + void onResolveServiceFailed(int listenerKey, int error); + void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info); +} diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl new file mode 100644 index 0000000000..b06ae55b15 --- /dev/null +++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.nsd; + +import android.net.nsd.INsdManagerCallback; +import android.net.nsd.NsdServiceInfo; +import android.os.Messenger; + +/** + * Interface that NsdService implements for each NsdManager client. + * + * {@hide} + */ +interface INsdServiceConnector { + void registerService(int listenerKey, in NsdServiceInfo serviceInfo); + void unregisterService(int listenerKey); + void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo); + void stopDiscovery(int listenerKey); + void resolveService(int listenerKey, in NsdServiceInfo serviceInfo); + void startDaemon(); +} \ No newline at end of file diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java new file mode 100644 index 0000000000..209f372fd3 --- /dev/null +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -0,0 +1,1083 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.nsd; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemService; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * The Network Service Discovery Manager class provides the API to discover services + * on a network. As an example, if device A and device B are connected over a Wi-Fi + * network, a game registered on device A can be discovered by a game on device + * B. Another example use case is an application discovering printers on the network. + * + *

The API currently supports DNS based service discovery and discovery is currently + * limited to a local network over Multicast DNS. DNS service discovery is described at + * http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt + * + *

The API is asynchronous, and responses to requests from an application are on listener + * callbacks on a separate internal thread. + * + *

There are three main operations the API supports - registration, discovery and resolution. + *

+ *                          Application start
+ *                                 |
+ *                                 |
+ *                                 |                  onServiceRegistered()
+ *                     Register any local services  /
+ *                      to be advertised with       \
+ *                       registerService()            onRegistrationFailed()
+ *                                 |
+ *                                 |
+ *                          discoverServices()
+ *                                 |
+ *                      Maintain a list to track
+ *                        discovered services
+ *                                 |
+ *                                 |--------->
+ *                                 |          |
+ *                                 |      onServiceFound()
+ *                                 |          |
+ *                                 |     add service to list
+ *                                 |          |
+ *                                 |<----------
+ *                                 |
+ *                                 |--------->
+ *                                 |          |
+ *                                 |      onServiceLost()
+ *                                 |          |
+ *                                 |   remove service from list
+ *                                 |          |
+ *                                 |<----------
+ *                                 |
+ *                                 |
+ *                                 | Connect to a service
+ *                                 | from list ?
+ *                                 |
+ *                          resolveService()
+ *                                 |
+ *                         onServiceResolved()
+ *                                 |
+ *                     Establish connection to service
+ *                     with the host and port information
+ *
+ * 
+ * An application that needs to advertise itself over a network for other applications to + * discover it can do so with a call to {@link #registerService}. If Example is a http based + * application that can provide HTML data to peer services, it can register a name "Example" + * with service type "_http._tcp". A successful registration is notified with a callback to + * {@link RegistrationListener#onServiceRegistered} and a failure to register is notified + * over {@link RegistrationListener#onRegistrationFailed} + * + *

A peer application looking for http services can initiate a discovery for "_http._tcp" + * with a call to {@link #discoverServices}. A service found is notified with a callback + * to {@link DiscoveryListener#onServiceFound} and a service lost is notified on + * {@link DiscoveryListener#onServiceLost}. + * + *

Once the peer application discovers the "Example" http service, and either needs to read the + * attributes of the service or wants to receive data from the "Example" application, it can + * initiate a resolve with {@link #resolveService} to resolve the attributes, host, and port + * details. A successful resolve is notified on {@link ResolveListener#onServiceResolved} and a + * failure is notified on {@link ResolveListener#onResolveFailed}. + * + * Applications can reserve for a service type at + * http://www.iana.org/form/ports-service. Existing services can be found at + * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml + * + * {@see NsdServiceInfo} + */ +@SystemService(Context.NSD_SERVICE) +public final class NsdManager { + private static final String TAG = NsdManager.class.getSimpleName(); + private static final boolean DBG = false; + + /** + * When enabled, apps targeting < Android 12 are considered legacy for + * the NSD native daemon. + * The platform will only keep the daemon running as long as there are + * any legacy apps connected. + * + * After Android 12, directly communicate with native daemon might not + * work since the native damon won't always stay alive. + * Use the NSD APIs from NsdManager as the replacement is recommended. + * An another alternative could be bundling your own mdns solutions instead of + * depending on the system mdns native daemon. + * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S) + public static final long RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS = 191844585L; + + /** + * Broadcast intent action to indicate whether network service discovery is + * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state + * information as int. + * + * @see #EXTRA_NSD_STATE + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED"; + + /** + * The lookup key for an int that indicates whether network service discovery is enabled + * or disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}. + * + * @see #NSD_STATE_DISABLED + * @see #NSD_STATE_ENABLED + */ + public static final String EXTRA_NSD_STATE = "nsd_state"; + + /** + * Network service discovery is disabled + * + * @see #ACTION_NSD_STATE_CHANGED + */ + public static final int NSD_STATE_DISABLED = 1; + + /** + * Network service discovery is enabled + * + * @see #ACTION_NSD_STATE_CHANGED + */ + public static final int NSD_STATE_ENABLED = 2; + + /** @hide */ + public static final int DISCOVER_SERVICES = 1; + /** @hide */ + public static final int DISCOVER_SERVICES_STARTED = 2; + /** @hide */ + public static final int DISCOVER_SERVICES_FAILED = 3; + /** @hide */ + public static final int SERVICE_FOUND = 4; + /** @hide */ + public static final int SERVICE_LOST = 5; + + /** @hide */ + public static final int STOP_DISCOVERY = 6; + /** @hide */ + public static final int STOP_DISCOVERY_FAILED = 7; + /** @hide */ + public static final int STOP_DISCOVERY_SUCCEEDED = 8; + + /** @hide */ + public static final int REGISTER_SERVICE = 9; + /** @hide */ + public static final int REGISTER_SERVICE_FAILED = 10; + /** @hide */ + public static final int REGISTER_SERVICE_SUCCEEDED = 11; + + /** @hide */ + public static final int UNREGISTER_SERVICE = 12; + /** @hide */ + public static final int UNREGISTER_SERVICE_FAILED = 13; + /** @hide */ + public static final int UNREGISTER_SERVICE_SUCCEEDED = 14; + + /** @hide */ + public static final int RESOLVE_SERVICE = 15; + /** @hide */ + public static final int RESOLVE_SERVICE_FAILED = 16; + /** @hide */ + public static final int RESOLVE_SERVICE_SUCCEEDED = 17; + + /** @hide */ + public static final int DAEMON_CLEANUP = 18; + + /** @hide */ + public static final int DAEMON_STARTUP = 19; + + /** @hide */ + public static final int ENABLE = 20; + /** @hide */ + public static final int DISABLE = 21; + + /** @hide */ + public static final int NATIVE_DAEMON_EVENT = 22; + + /** @hide */ + public static final int REGISTER_CLIENT = 23; + /** @hide */ + public static final int UNREGISTER_CLIENT = 24; + + /** Dns based service discovery protocol */ + public static final int PROTOCOL_DNS_SD = 0x0001; + + private static final SparseArray EVENT_NAMES = new SparseArray<>(); + static { + EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES"); + EVENT_NAMES.put(DISCOVER_SERVICES_STARTED, "DISCOVER_SERVICES_STARTED"); + EVENT_NAMES.put(DISCOVER_SERVICES_FAILED, "DISCOVER_SERVICES_FAILED"); + EVENT_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND"); + EVENT_NAMES.put(SERVICE_LOST, "SERVICE_LOST"); + EVENT_NAMES.put(STOP_DISCOVERY, "STOP_DISCOVERY"); + EVENT_NAMES.put(STOP_DISCOVERY_FAILED, "STOP_DISCOVERY_FAILED"); + EVENT_NAMES.put(STOP_DISCOVERY_SUCCEEDED, "STOP_DISCOVERY_SUCCEEDED"); + EVENT_NAMES.put(REGISTER_SERVICE, "REGISTER_SERVICE"); + EVENT_NAMES.put(REGISTER_SERVICE_FAILED, "REGISTER_SERVICE_FAILED"); + EVENT_NAMES.put(REGISTER_SERVICE_SUCCEEDED, "REGISTER_SERVICE_SUCCEEDED"); + EVENT_NAMES.put(UNREGISTER_SERVICE, "UNREGISTER_SERVICE"); + EVENT_NAMES.put(UNREGISTER_SERVICE_FAILED, "UNREGISTER_SERVICE_FAILED"); + EVENT_NAMES.put(UNREGISTER_SERVICE_SUCCEEDED, "UNREGISTER_SERVICE_SUCCEEDED"); + EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE"); + EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED"); + EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED"); + EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP"); + EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP"); + EVENT_NAMES.put(ENABLE, "ENABLE"); + EVENT_NAMES.put(DISABLE, "DISABLE"); + EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT"); + } + + /** @hide */ + public static String nameOf(int event) { + String name = EVENT_NAMES.get(event); + if (name == null) { + return Integer.toString(event); + } + return name; + } + + private static final int FIRST_LISTENER_KEY = 1; + + private final INsdServiceConnector mService; + private final Context mContext; + + private int mListenerKey = FIRST_LISTENER_KEY; + @GuardedBy("mMapLock") + private final SparseArray mListenerMap = new SparseArray(); + @GuardedBy("mMapLock") + private final SparseArray mServiceMap = new SparseArray<>(); + @GuardedBy("mMapLock") + private final SparseArray mExecutorMap = new SparseArray<>(); + private final Object mMapLock = new Object(); + // Map of listener key sent by client -> per-network discovery tracker + @GuardedBy("mPerNetworkDiscoveryMap") + private final ArrayMap + mPerNetworkDiscoveryMap = new ArrayMap<>(); + + private final ServiceHandler mHandler; + + private class PerNetworkDiscoveryTracker { + final String mServiceType; + final int mProtocolType; + final DiscoveryListener mBaseListener; + final Executor mBaseExecutor; + final ArrayMap mPerNetworkListeners = + new ArrayMap<>(); + + final NetworkCallback mNetworkCb = new NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + final DelegatingDiscoveryListener wrappedListener = new DelegatingDiscoveryListener( + network, mBaseListener); + mPerNetworkListeners.put(network, wrappedListener); + discoverServices(mServiceType, mProtocolType, network, mBaseExecutor, + wrappedListener); + } + + @Override + public void onLost(@NonNull Network network) { + final DelegatingDiscoveryListener listener = mPerNetworkListeners.get(network); + if (listener == null) return; + listener.notifyAllServicesLost(); + // Listener will be removed from map in discovery stopped callback + stopServiceDiscovery(listener); + } + }; + + // Accessed from mHandler + private boolean mStopRequested; + + public void start(@NonNull NetworkRequest request) { + final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); + cm.registerNetworkCallback(request, mNetworkCb, mHandler); + mHandler.post(() -> mBaseListener.onDiscoveryStarted(mServiceType)); + } + + /** + * Stop discovery on all networks tracked by this class. + * + * This will request all underlying listeners to stop, and the last one to stop will call + * onDiscoveryStopped or onStopDiscoveryFailed. + * + * Must be called on the handler thread. + */ + public void requestStop() { + mHandler.post(() -> { + mStopRequested = true; + final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); + cm.unregisterNetworkCallback(mNetworkCb); + if (mPerNetworkListeners.size() == 0) { + mBaseListener.onDiscoveryStopped(mServiceType); + return; + } + for (int i = 0; i < mPerNetworkListeners.size(); i++) { + final DelegatingDiscoveryListener listener = mPerNetworkListeners.valueAt(i); + stopServiceDiscovery(listener); + } + }); + } + + private PerNetworkDiscoveryTracker(String serviceType, int protocolType, + Executor baseExecutor, DiscoveryListener baseListener) { + mServiceType = serviceType; + mProtocolType = protocolType; + mBaseExecutor = baseExecutor; + mBaseListener = baseListener; + } + + /** + * Subset of NsdServiceInfo that is tracked to generate service lost notifications when a + * network is lost. + * + * Service lost notifications only contain service name, type and network, so only track + * that information (Network is known from the listener). This also implements + * equals/hashCode for usage in maps. + */ + private class TrackedNsdInfo { + private final String mServiceName; + private final String mServiceType; + TrackedNsdInfo(NsdServiceInfo info) { + mServiceName = info.getServiceName(); + mServiceType = info.getServiceType(); + } + + @Override + public int hashCode() { + return Objects.hash(mServiceName, mServiceType); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TrackedNsdInfo)) return false; + final TrackedNsdInfo other = (TrackedNsdInfo) obj; + return Objects.equals(mServiceName, other.mServiceName) + && Objects.equals(mServiceType, other.mServiceType); + } + } + + private class DelegatingDiscoveryListener implements DiscoveryListener { + private final Network mNetwork; + private final DiscoveryListener mWrapped; + private final ArraySet mFoundInfo = new ArraySet<>(); + + private DelegatingDiscoveryListener(Network network, DiscoveryListener listener) { + mNetwork = network; + mWrapped = listener; + } + + void notifyAllServicesLost() { + for (int i = 0; i < mFoundInfo.size(); i++) { + final TrackedNsdInfo trackedInfo = mFoundInfo.valueAt(i); + final NsdServiceInfo serviceInfo = new NsdServiceInfo( + trackedInfo.mServiceName, trackedInfo.mServiceType); + serviceInfo.setNetwork(mNetwork); + mWrapped.onServiceLost(serviceInfo); + } + } + + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + // The delegated listener is used when NsdManager takes care of starting/stopping + // discovery on multiple networks. Failure to start on one network is not a global + // failure to be reported up, as other networks may succeed: just log. + Log.e(TAG, "Failed to start discovery for " + serviceType + " on " + mNetwork + + " with code " + errorCode); + mPerNetworkListeners.remove(mNetwork); + } + + @Override + public void onDiscoveryStarted(String serviceType) { + // Wrapped listener was called upon registration, it is not called for discovery + // on each network + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + Log.e(TAG, "Failed to stop discovery for " + serviceType + " on " + mNetwork + + " with code " + errorCode); + mPerNetworkListeners.remove(mNetwork); + if (mStopRequested && mPerNetworkListeners.size() == 0) { + // Do not report onStopDiscoveryFailed when some underlying listeners failed: + // this does not mean that all listeners did, and onStopDiscoveryFailed is not + // actionable anyway. Just report that discovery stopped. + mWrapped.onDiscoveryStopped(serviceType); + } + } + + @Override + public void onDiscoveryStopped(String serviceType) { + mPerNetworkListeners.remove(mNetwork); + if (mStopRequested && mPerNetworkListeners.size() == 0) { + mWrapped.onDiscoveryStopped(serviceType); + } + } + + @Override + public void onServiceFound(NsdServiceInfo serviceInfo) { + mFoundInfo.add(new TrackedNsdInfo(serviceInfo)); + mWrapped.onServiceFound(serviceInfo); + } + + @Override + public void onServiceLost(NsdServiceInfo serviceInfo) { + mFoundInfo.remove(new TrackedNsdInfo(serviceInfo)); + mWrapped.onServiceLost(serviceInfo); + } + } + } + + /** + * Create a new Nsd instance. Applications use + * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve + * {@link android.content.Context#NSD_SERVICE Context.NSD_SERVICE}. + * @param service the Binder interface + * @hide - hide this because it takes in a parameter of type INsdManager, which + * is a system private class. + */ + public NsdManager(Context context, INsdManager service) { + mContext = context; + + HandlerThread t = new HandlerThread("NsdManager"); + t.start(); + mHandler = new ServiceHandler(t.getLooper()); + + try { + mService = service.connect(new NsdCallbackImpl(mHandler)); + } catch (RemoteException e) { + throw new RuntimeException("Failed to connect to NsdService"); + } + + // Only proactively start the daemon if the target SDK < S, otherwise the internal service + // would automatically start/stop the native daemon as needed. + if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)) { + try { + mService.startDaemon(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to proactively start daemon"); + // Continue: the daemon can still be started on-demand later + } + } + } + + private static class NsdCallbackImpl extends INsdManagerCallback.Stub { + private final Handler mServHandler; + + NsdCallbackImpl(Handler serviceHandler) { + mServHandler = serviceHandler; + } + + private void sendInfo(int message, int listenerKey, NsdServiceInfo info) { + mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey, info)); + } + + private void sendError(int message, int listenerKey, int error) { + mServHandler.sendMessage(mServHandler.obtainMessage(message, error, listenerKey)); + } + + private void sendNoArg(int message, int listenerKey) { + mServHandler.sendMessage(mServHandler.obtainMessage(message, 0, listenerKey)); + } + + @Override + public void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) { + sendInfo(DISCOVER_SERVICES_STARTED, listenerKey, info); + } + + @Override + public void onDiscoverServicesFailed(int listenerKey, int error) { + sendError(DISCOVER_SERVICES_FAILED, listenerKey, error); + } + + @Override + public void onServiceFound(int listenerKey, NsdServiceInfo info) { + sendInfo(SERVICE_FOUND, listenerKey, info); + } + + @Override + public void onServiceLost(int listenerKey, NsdServiceInfo info) { + sendInfo(SERVICE_LOST, listenerKey, info); + } + + @Override + public void onStopDiscoveryFailed(int listenerKey, int error) { + sendError(STOP_DISCOVERY_FAILED, listenerKey, error); + } + + @Override + public void onStopDiscoverySucceeded(int listenerKey) { + sendNoArg(STOP_DISCOVERY_SUCCEEDED, listenerKey); + } + + @Override + public void onRegisterServiceFailed(int listenerKey, int error) { + sendError(REGISTER_SERVICE_FAILED, listenerKey, error); + } + + @Override + public void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) { + sendInfo(REGISTER_SERVICE_SUCCEEDED, listenerKey, info); + } + + @Override + public void onUnregisterServiceFailed(int listenerKey, int error) { + sendError(UNREGISTER_SERVICE_FAILED, listenerKey, error); + } + + @Override + public void onUnregisterServiceSucceeded(int listenerKey) { + sendNoArg(UNREGISTER_SERVICE_SUCCEEDED, listenerKey); + } + + @Override + public void onResolveServiceFailed(int listenerKey, int error) { + sendError(RESOLVE_SERVICE_FAILED, listenerKey, error); + } + + @Override + public void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) { + sendInfo(RESOLVE_SERVICE_SUCCEEDED, listenerKey, info); + } + } + + /** + * Failures are passed with {@link RegistrationListener#onRegistrationFailed}, + * {@link RegistrationListener#onUnregistrationFailed}, + * {@link DiscoveryListener#onStartDiscoveryFailed}, + * {@link DiscoveryListener#onStopDiscoveryFailed} or {@link ResolveListener#onResolveFailed}. + * + * Indicates that the operation failed due to an internal error. + */ + public static final int FAILURE_INTERNAL_ERROR = 0; + + /** + * Indicates that the operation failed because it is already active. + */ + public static final int FAILURE_ALREADY_ACTIVE = 3; + + /** + * Indicates that the operation failed because the maximum outstanding + * requests from the applications have reached. + */ + public static final int FAILURE_MAX_LIMIT = 4; + + /** Interface for callback invocation for service discovery */ + public interface DiscoveryListener { + + public void onStartDiscoveryFailed(String serviceType, int errorCode); + + public void onStopDiscoveryFailed(String serviceType, int errorCode); + + public void onDiscoveryStarted(String serviceType); + + public void onDiscoveryStopped(String serviceType); + + public void onServiceFound(NsdServiceInfo serviceInfo); + + public void onServiceLost(NsdServiceInfo serviceInfo); + } + + /** Interface for callback invocation for service registration */ + public interface RegistrationListener { + + public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode); + + public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode); + + public void onServiceRegistered(NsdServiceInfo serviceInfo); + + public void onServiceUnregistered(NsdServiceInfo serviceInfo); + } + + /** Interface for callback invocation for service resolution */ + public interface ResolveListener { + + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode); + + public void onServiceResolved(NsdServiceInfo serviceInfo); + } + + @VisibleForTesting + class ServiceHandler extends Handler { + ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message message) { + final int what = message.what; + final int key = message.arg2; + final Object listener; + final NsdServiceInfo ns; + final Executor executor; + synchronized (mMapLock) { + listener = mListenerMap.get(key); + ns = mServiceMap.get(key); + executor = mExecutorMap.get(key); + } + if (listener == null) { + Log.d(TAG, "Stale key " + message.arg2); + return; + } + if (DBG) { + Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns); + } + switch (what) { + case DISCOVER_SERVICES_STARTED: + final String s = getNsdServiceInfoType((NsdServiceInfo) message.obj); + executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s)); + break; + case DISCOVER_SERVICES_FAILED: + removeListener(key); + executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed( + getNsdServiceInfoType(ns), message.arg1)); + break; + case SERVICE_FOUND: + executor.execute(() -> ((DiscoveryListener) listener).onServiceFound( + (NsdServiceInfo) message.obj)); + break; + case SERVICE_LOST: + executor.execute(() -> ((DiscoveryListener) listener).onServiceLost( + (NsdServiceInfo) message.obj)); + break; + case STOP_DISCOVERY_FAILED: + // TODO: failure to stop discovery should be internal and retried internally, as + // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED + removeListener(key); + executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed( + getNsdServiceInfoType(ns), message.arg1)); + break; + case STOP_DISCOVERY_SUCCEEDED: + removeListener(key); + executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStopped( + getNsdServiceInfoType(ns))); + break; + case REGISTER_SERVICE_FAILED: + removeListener(key); + executor.execute(() -> ((RegistrationListener) listener).onRegistrationFailed( + ns, message.arg1)); + break; + case REGISTER_SERVICE_SUCCEEDED: + executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered( + (NsdServiceInfo) message.obj)); + break; + case UNREGISTER_SERVICE_FAILED: + removeListener(key); + executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed( + ns, message.arg1)); + break; + case UNREGISTER_SERVICE_SUCCEEDED: + // TODO: do not unregister listener until service is unregistered, or provide + // alternative way for unregistering ? + removeListener(message.arg2); + executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered( + ns)); + break; + case RESOLVE_SERVICE_FAILED: + removeListener(key); + executor.execute(() -> ((ResolveListener) listener).onResolveFailed( + ns, message.arg1)); + break; + case RESOLVE_SERVICE_SUCCEEDED: + removeListener(key); + executor.execute(() -> ((ResolveListener) listener).onServiceResolved( + (NsdServiceInfo) message.obj)); + break; + default: + Log.d(TAG, "Ignored " + message); + break; + } + } + } + + private int nextListenerKey() { + // Ensure mListenerKey >= FIRST_LISTENER_KEY; + mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1); + return mListenerKey; + } + + // Assert that the listener is not in the map, then add it and returns its key + private int putListener(Object listener, Executor e, NsdServiceInfo s) { + checkListener(listener); + final int key; + synchronized (mMapLock) { + int valueIndex = mListenerMap.indexOfValue(listener); + if (valueIndex != -1) { + throw new IllegalArgumentException("listener already in use"); + } + key = nextListenerKey(); + mListenerMap.put(key, listener); + mServiceMap.put(key, s); + mExecutorMap.put(key, e); + } + return key; + } + + private void removeListener(int key) { + synchronized (mMapLock) { + mListenerMap.remove(key); + mServiceMap.remove(key); + mExecutorMap.remove(key); + } + } + + private int getListenerKey(Object listener) { + checkListener(listener); + synchronized (mMapLock) { + int valueIndex = mListenerMap.indexOfValue(listener); + if (valueIndex == -1) { + throw new IllegalArgumentException("listener not registered"); + } + return mListenerMap.keyAt(valueIndex); + } + } + + private static String getNsdServiceInfoType(NsdServiceInfo s) { + if (s == null) return "?"; + return s.getServiceType(); + } + + /** + * Register a service to be discovered by other services. + * + *

The function call immediately returns after sending a request to register service + * to the framework. The application is notified of a successful registration + * through the callback {@link RegistrationListener#onServiceRegistered} or a failure + * through {@link RegistrationListener#onRegistrationFailed}. + * + *

The application should call {@link #unregisterService} when the service + * registration is no longer required, and/or whenever the application is stopped. + * + * @param serviceInfo The service being registered + * @param protocolType The service discovery protocol + * @param listener The listener notifies of a successful registration and is used to + * unregister this service through a call on {@link #unregisterService}. Cannot be null. + * Cannot be in use for an active service registration. + */ + public void registerService(NsdServiceInfo serviceInfo, int protocolType, + RegistrationListener listener) { + registerService(serviceInfo, protocolType, Runnable::run, listener); + } + + /** + * Register a service to be discovered by other services. + * + *

The function call immediately returns after sending a request to register service + * to the framework. The application is notified of a successful registration + * through the callback {@link RegistrationListener#onServiceRegistered} or a failure + * through {@link RegistrationListener#onRegistrationFailed}. + * + *

The application should call {@link #unregisterService} when the service + * registration is no longer required, and/or whenever the application is stopped. + * @param serviceInfo The service being registered + * @param protocolType The service discovery protocol + * @param executor Executor to run listener callbacks with + * @param listener The listener notifies of a successful registration and is used to + * unregister this service through a call on {@link #unregisterService}. Cannot be null. + */ + public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType, + @NonNull Executor executor, @NonNull RegistrationListener listener) { + if (serviceInfo.getPort() <= 0) { + throw new IllegalArgumentException("Invalid port number"); + } + checkServiceInfo(serviceInfo); + checkProtocol(protocolType); + int key = putListener(listener, executor, serviceInfo); + try { + mService.registerService(key, serviceInfo); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Unregister a service registered through {@link #registerService}. A successful + * unregister is notified to the application with a call to + * {@link RegistrationListener#onServiceUnregistered}. + * + * @param listener This should be the listener object that was passed to + * {@link #registerService}. It identifies the service that should be unregistered + * and notifies of a successful or unsuccessful unregistration via the listener + * callbacks. In API versions 20 and above, the listener object may be used for + * another service registration once the callback has been called. In API versions <= 19, + * there is no entirely reliable way to know when a listener may be re-used, and a new + * listener should be created for each service registration request. + */ + public void unregisterService(RegistrationListener listener) { + int id = getListenerKey(listener); + try { + mService.unregisterService(id); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Initiate service discovery to browse for instances of a service type. Service discovery + * consumes network bandwidth and will continue until the application calls + * {@link #stopServiceDiscovery}. + * + *

The function call immediately returns after sending a request to start service + * discovery to the framework. The application is notified of a success to initiate + * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure + * through {@link DiscoveryListener#onStartDiscoveryFailed}. + * + *

Upon successful start, application is notified when a service is found with + * {@link DiscoveryListener#onServiceFound} or when a service is lost with + * {@link DiscoveryListener#onServiceLost}. + * + *

Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + *

The application should call {@link #stopServiceDiscovery} when discovery of this + * service type is no longer required, and/or whenever the application is paused or + * stopped. + * + * @param serviceType The service type being discovered. Examples include "_http._tcp" for + * http services or "_ipp._tcp" for printers + * @param protocolType The service discovery protocol + * @param listener The listener notifies of a successful discovery and is used + * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. + * Cannot be null. Cannot be in use for an active service discovery. + */ + public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) { + discoverServices(serviceType, protocolType, (Network) null, Runnable::run, listener); + } + + /** + * Initiate service discovery to browse for instances of a service type. Service discovery + * consumes network bandwidth and will continue until the application calls + * {@link #stopServiceDiscovery}. + * + *

The function call immediately returns after sending a request to start service + * discovery to the framework. The application is notified of a success to initiate + * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure + * through {@link DiscoveryListener#onStartDiscoveryFailed}. + * + *

Upon successful start, application is notified when a service is found with + * {@link DiscoveryListener#onServiceFound} or when a service is lost with + * {@link DiscoveryListener#onServiceLost}. + * + *

Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + *

The application should call {@link #stopServiceDiscovery} when discovery of this + * service type is no longer required, and/or whenever the application is paused or + * stopped. + * @param serviceType The service type being discovered. Examples include "_http._tcp" for + * http services or "_ipp._tcp" for printers + * @param protocolType The service discovery protocol + * @param network Network to discover services on, or null to discover on all available networks + * @param executor Executor to run listener callbacks with + * @param listener The listener notifies of a successful discovery and is used + * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. + */ + public void discoverServices(@NonNull String serviceType, int protocolType, + @Nullable Network network, @NonNull Executor executor, + @NonNull DiscoveryListener listener) { + if (TextUtils.isEmpty(serviceType)) { + throw new IllegalArgumentException("Service type cannot be empty"); + } + checkProtocol(protocolType); + + NsdServiceInfo s = new NsdServiceInfo(); + s.setServiceType(serviceType); + s.setNetwork(network); + + int key = putListener(listener, executor, s); + try { + mService.discoverServices(key, s); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Initiate service discovery to browse for instances of a service type. Service discovery + * consumes network bandwidth and will continue until the application calls + * {@link #stopServiceDiscovery}. + * + *

The function call immediately returns after sending a request to start service + * discovery to the framework. The application is notified of a success to initiate + * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure + * through {@link DiscoveryListener#onStartDiscoveryFailed}. + * + *

Upon successful start, application is notified when a service is found with + * {@link DiscoveryListener#onServiceFound} or when a service is lost with + * {@link DiscoveryListener#onServiceLost}. + * + *

Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + *

The application should call {@link #stopServiceDiscovery} when discovery of this + * service type is no longer required, and/or whenever the application is paused or + * stopped. + * + *

During discovery, new networks may connect or existing networks may disconnect - for + * example if wifi is reconnected. When a service was found on a network that disconnects, + * {@link DiscoveryListener#onServiceLost} will be called. If a new network connects that + * matches the {@link NetworkRequest}, {@link DiscoveryListener#onServiceFound} will be called + * for services found on that network. Applications that do not want to track networks + * themselves are encouraged to use this method instead of other overloads of + * {@code discoverServices}, as they will receive proper notifications when a service becomes + * available or unavailable due to network changes. + * @param serviceType The service type being discovered. Examples include "_http._tcp" for + * http services or "_ipp._tcp" for printers + * @param protocolType The service discovery protocol + * @param networkRequest Request specifying networks that should be considered when discovering + * @param executor Executor to run listener callbacks with + * @param listener The listener notifies of a successful discovery and is used + * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public void discoverServices(@NonNull String serviceType, int protocolType, + @NonNull NetworkRequest networkRequest, @NonNull Executor executor, + @NonNull DiscoveryListener listener) { + if (TextUtils.isEmpty(serviceType)) { + throw new IllegalArgumentException("Service type cannot be empty"); + } + Objects.requireNonNull(networkRequest, "NetworkRequest cannot be null"); + checkProtocol(protocolType); + + NsdServiceInfo s = new NsdServiceInfo(); + s.setServiceType(serviceType); + + final int baseListenerKey = putListener(listener, executor, s); + + final PerNetworkDiscoveryTracker discoveryInfo = new PerNetworkDiscoveryTracker( + serviceType, protocolType, executor, listener); + + synchronized (mPerNetworkDiscoveryMap) { + mPerNetworkDiscoveryMap.put(baseListenerKey, discoveryInfo); + discoveryInfo.start(networkRequest); + } + } + + /** + * Stop service discovery initiated with {@link #discoverServices}. An active service + * discovery is notified to the application with {@link DiscoveryListener#onDiscoveryStarted} + * and it stays active until the application invokes a stop service discovery. A successful + * stop is notified to with a call to {@link DiscoveryListener#onDiscoveryStopped}. + * + *

Upon failure to stop service discovery, application is notified through + * {@link DiscoveryListener#onStopDiscoveryFailed}. + * + * @param listener This should be the listener object that was passed to {@link #discoverServices}. + * It identifies the discovery that should be stopped and notifies of a successful or + * unsuccessful stop. In API versions 20 and above, the listener object may be used for + * another service discovery once the callback has been called. In API versions <= 19, + * there is no entirely reliable way to know when a listener may be re-used, and a new + * listener should be created for each service discovery request. + */ + public void stopServiceDiscovery(DiscoveryListener listener) { + int id = getListenerKey(listener); + // If this is a PerNetworkDiscovery request, handle it as such + synchronized (mPerNetworkDiscoveryMap) { + final PerNetworkDiscoveryTracker info = mPerNetworkDiscoveryMap.get(id); + if (info != null) { + info.requestStop(); + return; + } + } + try { + mService.stopDiscovery(id); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Resolve a discovered service. An application can resolve a service right before + * establishing a connection to fetch the IP and port details on which to setup + * the connection. + * + * @param serviceInfo service to be resolved + * @param listener to receive callback upon success or failure. Cannot be null. + * Cannot be in use for an active service resolution. + */ + public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) { + resolveService(serviceInfo, Runnable::run, listener); + } + + /** + * Resolve a discovered service. An application can resolve a service right before + * establishing a connection to fetch the IP and port details on which to setup + * the connection. + * @param serviceInfo service to be resolved + * @param executor Executor to run listener callbacks with + * @param listener to receive callback upon success or failure. + */ + public void resolveService(@NonNull NsdServiceInfo serviceInfo, + @NonNull Executor executor, @NonNull ResolveListener listener) { + checkServiceInfo(serviceInfo); + int key = putListener(listener, executor, serviceInfo); + try { + mService.resolveService(key, serviceInfo); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + private static void checkListener(Object listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + } + + private static void checkProtocol(int protocolType) { + if (protocolType != PROTOCOL_DNS_SD) { + throw new IllegalArgumentException("Unsupported protocol"); + } + } + + private static void checkServiceInfo(NsdServiceInfo serviceInfo) { + Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null"); + if (TextUtils.isEmpty(serviceInfo.getServiceName())) { + throw new IllegalArgumentException("Service name cannot be empty"); + } + if (TextUtils.isEmpty(serviceInfo.getServiceType())) { + throw new IllegalArgumentException("Service type cannot be empty"); + } + } +} diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java new file mode 100644 index 0000000000..8506db1fbe --- /dev/null +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -0,0 +1,418 @@ +/* + * 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 android.net.nsd; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.net.Network; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Base64; +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; + +/** + * A class representing service information for network service discovery + * {@see NsdManager} + */ +public final class NsdServiceInfo implements Parcelable { + + private static final String TAG = "NsdServiceInfo"; + + private String mServiceName; + + private String mServiceType; + + private final ArrayMap mTxtRecord = new ArrayMap<>(); + + private InetAddress mHost; + + private int mPort; + + @Nullable + private Network mNetwork; + + public NsdServiceInfo() { + } + + /** @hide */ + public NsdServiceInfo(String sn, String rt) { + mServiceName = sn; + mServiceType = rt; + } + + /** Get the service name */ + public String getServiceName() { + return mServiceName; + } + + /** Set the service name */ + public void setServiceName(String s) { + mServiceName = s; + } + + /** Get the service type */ + public String getServiceType() { + return mServiceType; + } + + /** Set the service type */ + public void setServiceType(String s) { + mServiceType = s; + } + + /** Get the host address. The host address is valid for a resolved service. */ + public InetAddress getHost() { + return mHost; + } + + /** Set the host address */ + public void setHost(InetAddress s) { + mHost = s; + } + + /** Get port number. The port number is valid for a resolved service. */ + public int getPort() { + return mPort; + } + + /** Set port number */ + public void setPort(int p) { + mPort = p; + } + + /** + * Unpack txt information from a base-64 encoded byte array. + * + * @param rawRecords The raw base64 encoded records string read from netd. + * + * @hide + */ + public void setTxtRecords(@NonNull String rawRecords) { + byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT); + + // There can be multiple TXT records after each other. Each record has to following format: + // + // byte type required meaning + // ------------------- ------------------- -------- ---------------------------------- + // 0 unsigned 8 bit yes size of record excluding this byte + // 1 - n ASCII but not '=' yes key + // n + 1 '=' optional separator of key and value + // n + 2 - record size uninterpreted bytes optional value + // + // Example legal records: + // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff] + // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '='] + // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y'] + // + // Example corrupted records + // [3, =, 1, 2] <- key is empty + // [3, 0, =, 2] <- key contains non-ASCII character. We handle this by replacing the + // invalid characters instead of skipping the record. + // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we + // handle this by reducing the length of the record as needed. + int pos = 0; + while (pos < txtRecordsRawBytes.length) { + // recordLen is an unsigned 8 bit value + int recordLen = txtRecordsRawBytes[pos] & 0xff; + pos += 1; + + try { + if (recordLen == 0) { + throw new IllegalArgumentException("Zero sized txt record"); + } else if (pos + recordLen > txtRecordsRawBytes.length) { + Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen); + recordLen = txtRecordsRawBytes.length - pos; + } + + // Decode key-value records + String key = null; + byte[] value = null; + int valueLen = 0; + for (int i = pos; i < pos + recordLen; i++) { + if (key == null) { + if (txtRecordsRawBytes[i] == '=') { + key = new String(txtRecordsRawBytes, pos, i - pos, + StandardCharsets.US_ASCII); + } + } else { + if (value == null) { + value = new byte[recordLen - key.length() - 1]; + } + value[valueLen] = txtRecordsRawBytes[i]; + valueLen++; + } + } + + // If '=' was not found we have a boolean record + if (key == null) { + key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII); + } + + if (TextUtils.isEmpty(key)) { + // Empty keys are not allowed (RFC6763 6.4) + throw new IllegalArgumentException("Invalid txt record (key is empty)"); + } + + if (getAttributes().containsKey(key)) { + // When we have a duplicate record, the later ones are ignored (RFC6763 6.4) + throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")"); + } + + setAttribute(key, value); + } catch (IllegalArgumentException e) { + Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage()); + } + + pos += recordLen; + } + } + + /** @hide */ + @UnsupportedAppUsage + public void setAttribute(String key, byte[] value) { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("Key cannot be empty"); + } + + // Key must be printable US-ASCII, excluding =. + for (int i = 0; i < key.length(); ++i) { + char character = key.charAt(i); + if (character < 0x20 || character > 0x7E) { + throw new IllegalArgumentException("Key strings must be printable US-ASCII"); + } else if (character == 0x3D) { + throw new IllegalArgumentException("Key strings must not include '='"); + } + } + + // Key length + value length must be < 255. + if (key.length() + (value == null ? 0 : value.length) >= 255) { + throw new IllegalArgumentException("Key length + value length must be < 255 bytes"); + } + + // Warn if key is > 9 characters, as recommended by RFC 6763 section 6.4. + if (key.length() > 9) { + Log.w(TAG, "Key lengths > 9 are discouraged: " + key); + } + + // Check against total TXT record size limits. + // Arbitrary 400 / 1300 byte limits taken from RFC 6763 section 6.2. + int txtRecordSize = getTxtRecordSize(); + int futureSize = txtRecordSize + key.length() + (value == null ? 0 : value.length) + 2; + if (futureSize > 1300) { + throw new IllegalArgumentException("Total length of attributes must be < 1300 bytes"); + } else if (futureSize > 400) { + Log.w(TAG, "Total length of all attributes exceeds 400 bytes; truncation may occur"); + } + + mTxtRecord.put(key, value); + } + + /** + * Add a service attribute as a key/value pair. + * + *

Service attributes are included as DNS-SD TXT record pairs. + * + *

The key must be US-ASCII printable characters, excluding the '=' character. Values may + * be UTF-8 strings or null. The total length of key + value must be less than 255 bytes. + * + *

Keys should be short, ideally no more than 9 characters, and unique per instance of + * {@link NsdServiceInfo}. Calling {@link #setAttribute} twice with the same key will overwrite + * first value. + */ + public void setAttribute(String key, String value) { + try { + setAttribute(key, value == null ? (byte []) null : value.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Value must be UTF-8"); + } + } + + /** Remove an attribute by key */ + public void removeAttribute(String key) { + mTxtRecord.remove(key); + } + + /** + * Retrieve attributes as a map of String keys to byte[] values. The attributes map is only + * valid for a resolved service. + * + *

The returned map is unmodifiable; changes must be made through {@link #setAttribute} and + * {@link #removeAttribute}. + */ + public Map getAttributes() { + return Collections.unmodifiableMap(mTxtRecord); + } + + private int getTxtRecordSize() { + int txtRecordSize = 0; + for (Map.Entry entry : mTxtRecord.entrySet()) { + txtRecordSize += 2; // One for the length byte, one for the = between key and value. + txtRecordSize += entry.getKey().length(); + byte[] value = entry.getValue(); + txtRecordSize += value == null ? 0 : value.length; + } + return txtRecordSize; + } + + /** @hide */ + public @NonNull byte[] getTxtRecord() { + int txtRecordSize = getTxtRecordSize(); + if (txtRecordSize == 0) { + return new byte[]{}; + } + + byte[] txtRecord = new byte[txtRecordSize]; + int ptr = 0; + for (Map.Entry entry : mTxtRecord.entrySet()) { + String key = entry.getKey(); + byte[] value = entry.getValue(); + + // One byte to record the length of this key/value pair. + txtRecord[ptr++] = (byte) (key.length() + (value == null ? 0 : value.length) + 1); + + // The key, in US-ASCII. + // Note: use the StandardCharsets const here because it doesn't raise exceptions and we + // already know the key is ASCII at this point. + System.arraycopy(key.getBytes(StandardCharsets.US_ASCII), 0, txtRecord, ptr, + key.length()); + ptr += key.length(); + + // US-ASCII '=' character. + txtRecord[ptr++] = (byte)'='; + + // The value, as any raw bytes. + if (value != null) { + System.arraycopy(value, 0, txtRecord, ptr, value.length); + ptr += value.length; + } + } + return txtRecord; + } + + /** + * Get the network where the service can be found. + * + * This is never null if this {@link NsdServiceInfo} was obtained from + * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}. + */ + @Nullable + public Network getNetwork() { + return mNetwork; + } + + /** + * Set the network where the service can be found. + * @param network The network, or null to search for, or to announce, the service on all + * connected networks. + */ + public void setNetwork(@Nullable Network network) { + mNetwork = network; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("name: ").append(mServiceName) + .append(", type: ").append(mServiceType) + .append(", host: ").append(mHost) + .append(", port: ").append(mPort) + .append(", network: ").append(mNetwork); + + byte[] txtRecord = getTxtRecord(); + sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8)); + return sb.toString(); + } + + /** Implement the Parcelable interface */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mServiceName); + dest.writeString(mServiceType); + if (mHost != null) { + dest.writeInt(1); + dest.writeByteArray(mHost.getAddress()); + } else { + dest.writeInt(0); + } + dest.writeInt(mPort); + + // TXT record key/value pairs. + dest.writeInt(mTxtRecord.size()); + for (String key : mTxtRecord.keySet()) { + byte[] value = mTxtRecord.get(key); + if (value != null) { + dest.writeInt(1); + dest.writeInt(value.length); + dest.writeByteArray(value); + } else { + dest.writeInt(0); + } + dest.writeString(key); + } + + dest.writeParcelable(mNetwork, 0); + } + + /** Implement the Parcelable interface */ + public static final @android.annotation.NonNull Creator CREATOR = + new Creator() { + public NsdServiceInfo createFromParcel(Parcel in) { + NsdServiceInfo info = new NsdServiceInfo(); + info.mServiceName = in.readString(); + info.mServiceType = in.readString(); + + if (in.readInt() == 1) { + try { + info.mHost = InetAddress.getByAddress(in.createByteArray()); + } catch (java.net.UnknownHostException e) {} + } + + info.mPort = in.readInt(); + + // TXT record key/value pairs. + int recordCount = in.readInt(); + for (int i = 0; i < recordCount; ++i) { + byte[] valueArray = null; + if (in.readInt() == 1) { + int valueLength = in.readInt(); + valueArray = new byte[valueLength]; + in.readByteArray(valueArray); + } + info.mTxtRecord.put(in.readString(), valueArray); + } + info.mNetwork = in.readParcelable(null, Network.class); + return info; + } + + public NsdServiceInfo[] newArray(int size) { + return new NsdServiceInfo[size]; + } + }; +} diff --git a/framework/aidl-export/android/net/NetworkStats.aidl b/framework/aidl-export/android/net/NetworkStats.aidl new file mode 100644 index 0000000000..d06ca65a3e --- /dev/null +++ b/framework/aidl-export/android/net/NetworkStats.aidl @@ -0,0 +1,19 @@ +/** + * 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 android.net; + +parcelable NetworkStats; diff --git a/framework/aidl-export/android/net/NetworkTemplate.aidl b/framework/aidl-export/android/net/NetworkTemplate.aidl new file mode 100644 index 0000000000..3d37488d98 --- /dev/null +++ b/framework/aidl-export/android/net/NetworkTemplate.aidl @@ -0,0 +1,19 @@ +/** + * 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 android.net; + +parcelable NetworkTemplate; diff --git a/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl b/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl new file mode 100644 index 0000000000..657bdd1e87 --- /dev/null +++ b/framework/aidl-export/android/net/nsd/NsdServiceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.nsd; + +@JavaOnlyStableParcelable parcelable NsdServiceInfo; \ No newline at end of file diff --git a/service-t/Sources.bp b/service-t/Sources.bp new file mode 100644 index 0000000000..4b799c599b --- /dev/null +++ b/service-t/Sources.bp @@ -0,0 +1,156 @@ +// +// Copyright (C) 2021 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 { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +// NetworkStats related libraries. + +filegroup { + name: "services.connectivity-netstats-sources", + srcs: [ + "src/com/android/server/net/NetworkIdentity*.java", + "src/com/android/server/net/NetworkStats*.java", + "src/com/android/server/net/BpfInterfaceMapUpdater.java", + "src/com/android/server/net/InterfaceMapValue.java", + "src/com/android/server/net/CookieTagMapKey.java", + "src/com/android/server/net/CookieTagMapValue.java", + "src/com/android/server/net/StatsMapKey.java", + "src/com/android/server/net/StatsMapValue.java", + "src/com/android/server/net/UidStatsMapKey.java", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +// For test code only. +filegroup { + name: "lib_networkStatsFactory_native", + srcs: [ + "jni/com_android_server_net_NetworkStatsFactory.cpp", + ], + path: "jni", + visibility: [ + "//packages/modules/Connectivity:__subpackages__", + ], +} + +filegroup { + name: "services.connectivity-netstats-jni-sources", + srcs: [ + "jni/com_android_server_net_NetworkStatsFactory.cpp", + "jni/com_android_server_net_NetworkStatsService.cpp", + ], + path: "jni", + visibility: [ + "//packages/modules/Connectivity:__subpackages__", + ], +} + +// Nsd related libraries. + +filegroup { + name: "services.connectivity-nsd-sources", + srcs: [ + "src/com/android/server/INativeDaemon*.java", + "src/com/android/server/NativeDaemon*.java", + "src/com/android/server/Nsd*.java", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +// IpSec related libraries. + +filegroup { + name: "services.connectivity-ipsec-sources", + srcs: [ + "src/com/android/server/IpSecService.java", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +// Ethernet related libraries. + +filegroup { + name: "services.connectivity-ethernet-sources", + srcs: [ + "src/com/android/server/net/DelayedDiskWrite.java", + "src/com/android/server/net/IpConfigStore.java", + ], + path: "src", + visibility: [ + "//frameworks/opt/net/ethernet/tests", + ], +} + +// Connectivity-T common libraries. + +// TODO: remove this empty filegroup. +filegroup { + name: "services.connectivity-tiramisu-sources", + srcs: [], + path: "src", + visibility: ["//frameworks/base/services/core"], +} + +filegroup { + name: "services.connectivity-tiramisu-updatable-sources", + srcs: [ + ":services.connectivity-ethernet-sources", + ":services.connectivity-ipsec-sources", + ":services.connectivity-netstats-sources", + ":services.connectivity-nsd-sources", + ], + path: "src", + visibility: [ + "//packages/modules/Connectivity:__subpackages__", + ], +} + +cc_library_shared { + name: "libcom_android_net_module_util_jni", + min_sdk_version: "30", + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + srcs: [ + "jni/onload.cpp", + ], + stl: "libc++_static", + static_libs: [ + "libnet_utils_device_common_bpfjni", + ], + shared_libs: [ + "liblog", + "libnativehelper", + ], + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp b/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp new file mode 100644 index 0000000000..8b6526ff49 --- /dev/null +++ b/service-t/jni/com_android_server_net_NetworkStatsFactory.cpp @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2013 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. + */ + +#define LOG_TAG "NetworkStats" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include "android-base/unique_fd.h" +#include "bpf/BpfUtils.h" +#include "netdbpf/BpfNetworkStats.h" + +using android::bpf::parseBpfNetworkStatsDetail; +using android::bpf::stats_line; + +namespace android { + +static jclass gStringClass; + +static struct { + jfieldID size; + jfieldID capacity; + jfieldID iface; + jfieldID uid; + jfieldID set; + jfieldID tag; + jfieldID metered; + jfieldID roaming; + jfieldID defaultNetwork; + jfieldID rxBytes; + jfieldID rxPackets; + jfieldID txBytes; + jfieldID txPackets; + jfieldID operations; +} gNetworkStatsClassInfo; + +static jobjectArray get_string_array(JNIEnv* env, jobject obj, jfieldID field, int size, bool grow) +{ + if (!grow) { + jobjectArray array = (jobjectArray)env->GetObjectField(obj, field); + if (array != NULL) { + return array; + } + } + return env->NewObjectArray(size, gStringClass, NULL); +} + +static jintArray get_int_array(JNIEnv* env, jobject obj, jfieldID field, int size, bool grow) +{ + if (!grow) { + jintArray array = (jintArray)env->GetObjectField(obj, field); + if (array != NULL) { + return array; + } + } + return env->NewIntArray(size); +} + +static jlongArray get_long_array(JNIEnv* env, jobject obj, jfieldID field, int size, bool grow) +{ + if (!grow) { + jlongArray array = (jlongArray)env->GetObjectField(obj, field); + if (array != NULL) { + return array; + } + } + return env->NewLongArray(size); +} + +static int legacyReadNetworkStatsDetail(std::vector* lines, + const std::vector& limitIfaces, + int limitTag, int limitUid, const char* path) { + FILE* fp = fopen(path, "re"); + if (fp == NULL) { + return -1; + } + + int lastIdx = 1; + int idx; + char buffer[384]; + while (fgets(buffer, sizeof(buffer), fp) != NULL) { + stats_line s; + int64_t rawTag; + char* pos = buffer; + char* endPos; + // First field is the index. + idx = (int)strtol(pos, &endPos, 10); + //ALOGI("Index #%d: %s", idx, buffer); + if (pos == endPos) { + // Skip lines that don't start with in index. In particular, + // this will skip the initial header line. + continue; + } + if (idx != lastIdx + 1) { + ALOGE("inconsistent idx=%d after lastIdx=%d: %s", idx, lastIdx, buffer); + fclose(fp); + return -1; + } + lastIdx = idx; + pos = endPos; + // Skip whitespace. + while (*pos == ' ') { + pos++; + } + // Next field is iface. + int ifaceIdx = 0; + while (*pos != ' ' && *pos != 0 && ifaceIdx < (int)(sizeof(s.iface)-1)) { + s.iface[ifaceIdx] = *pos; + ifaceIdx++; + pos++; + } + if (*pos != ' ') { + ALOGE("bad iface: %s", buffer); + fclose(fp); + return -1; + } + s.iface[ifaceIdx] = 0; + if (limitIfaces.size() > 0) { + // Is this an iface the caller is interested in? + int i = 0; + while (i < (int)limitIfaces.size()) { + if (limitIfaces[i] == s.iface) { + break; + } + i++; + } + if (i >= (int)limitIfaces.size()) { + // Nothing matched; skip this line. + //ALOGI("skipping due to iface: %s", buffer); + continue; + } + } + + // Ignore whitespace + while (*pos == ' ') pos++; + + // Find end of tag field + endPos = pos; + while (*endPos != ' ') endPos++; + + // Three digit field is always 0x0, otherwise parse + if (endPos - pos == 3) { + rawTag = 0; + } else { + if (sscanf(pos, "%" PRIx64, &rawTag) != 1) { + ALOGE("bad tag: %s", pos); + fclose(fp); + return -1; + } + } + s.tag = rawTag >> 32; + if (limitTag != -1 && s.tag != static_cast(limitTag)) { + //ALOGI("skipping due to tag: %s", buffer); + continue; + } + pos = endPos; + + // Ignore whitespace + while (*pos == ' ') pos++; + + // Parse remaining fields. + if (sscanf(pos, "%u %u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, + &s.uid, &s.set, &s.rxBytes, &s.rxPackets, + &s.txBytes, &s.txPackets) == 6) { + if (limitUid != -1 && static_cast(limitUid) != s.uid) { + //ALOGI("skipping due to uid: %s", buffer); + continue; + } + lines->push_back(s); + } else { + //ALOGI("skipping due to bad remaining fields: %s", pos); + } + } + + if (fclose(fp) != 0) { + ALOGE("Failed to close netstats file"); + return -1; + } + return 0; +} + +static int statsLinesToNetworkStats(JNIEnv* env, jclass clazz, jobject stats, + std::vector& lines) { + int size = lines.size(); + + bool grow = size > env->GetIntField(stats, gNetworkStatsClassInfo.capacity); + + ScopedLocalRef iface(env, get_string_array(env, stats, + gNetworkStatsClassInfo.iface, size, grow)); + if (iface.get() == NULL) return -1; + ScopedIntArrayRW uid(env, get_int_array(env, stats, + gNetworkStatsClassInfo.uid, size, grow)); + if (uid.get() == NULL) return -1; + ScopedIntArrayRW set(env, get_int_array(env, stats, + gNetworkStatsClassInfo.set, size, grow)); + if (set.get() == NULL) return -1; + ScopedIntArrayRW tag(env, get_int_array(env, stats, + gNetworkStatsClassInfo.tag, size, grow)); + if (tag.get() == NULL) return -1; + ScopedIntArrayRW metered(env, get_int_array(env, stats, + gNetworkStatsClassInfo.metered, size, grow)); + if (metered.get() == NULL) return -1; + ScopedIntArrayRW roaming(env, get_int_array(env, stats, + gNetworkStatsClassInfo.roaming, size, grow)); + if (roaming.get() == NULL) return -1; + ScopedIntArrayRW defaultNetwork(env, get_int_array(env, stats, + gNetworkStatsClassInfo.defaultNetwork, size, grow)); + if (defaultNetwork.get() == NULL) return -1; + ScopedLongArrayRW rxBytes(env, get_long_array(env, stats, + gNetworkStatsClassInfo.rxBytes, size, grow)); + if (rxBytes.get() == NULL) return -1; + ScopedLongArrayRW rxPackets(env, get_long_array(env, stats, + gNetworkStatsClassInfo.rxPackets, size, grow)); + if (rxPackets.get() == NULL) return -1; + ScopedLongArrayRW txBytes(env, get_long_array(env, stats, + gNetworkStatsClassInfo.txBytes, size, grow)); + if (txBytes.get() == NULL) return -1; + ScopedLongArrayRW txPackets(env, get_long_array(env, stats, + gNetworkStatsClassInfo.txPackets, size, grow)); + if (txPackets.get() == NULL) return -1; + ScopedLongArrayRW operations(env, get_long_array(env, stats, + gNetworkStatsClassInfo.operations, size, grow)); + if (operations.get() == NULL) return -1; + + for (int i = 0; i < size; i++) { + ScopedLocalRef ifaceString(env, env->NewStringUTF(lines[i].iface)); + env->SetObjectArrayElement(iface.get(), i, ifaceString.get()); + + uid[i] = lines[i].uid; + set[i] = lines[i].set; + tag[i] = lines[i].tag; + // Metered, roaming and defaultNetwork are populated in Java-land. + rxBytes[i] = lines[i].rxBytes; + rxPackets[i] = lines[i].rxPackets; + txBytes[i] = lines[i].txBytes; + txPackets[i] = lines[i].txPackets; + } + + env->SetIntField(stats, gNetworkStatsClassInfo.size, size); + if (grow) { + env->SetIntField(stats, gNetworkStatsClassInfo.capacity, size); + env->SetObjectField(stats, gNetworkStatsClassInfo.iface, iface.get()); + env->SetObjectField(stats, gNetworkStatsClassInfo.uid, uid.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.set, set.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.tag, tag.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.metered, metered.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.roaming, roaming.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.defaultNetwork, + defaultNetwork.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.rxBytes, rxBytes.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.rxPackets, rxPackets.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.txBytes, txBytes.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.txPackets, txPackets.getJavaArray()); + env->SetObjectField(stats, gNetworkStatsClassInfo.operations, operations.getJavaArray()); + } + return 0; +} + +static int readNetworkStatsDetail(JNIEnv* env, jclass clazz, jobject stats, jstring path, + jint limitUid, jobjectArray limitIfacesObj, jint limitTag, + jboolean useBpfStats) { + + std::vector limitIfaces; + if (limitIfacesObj != NULL && env->GetArrayLength(limitIfacesObj) > 0) { + int num = env->GetArrayLength(limitIfacesObj); + for (int i = 0; i < num; i++) { + jstring string = (jstring)env->GetObjectArrayElement(limitIfacesObj, i); + ScopedUtfChars string8(env, string); + if (string8.c_str() != NULL) { + limitIfaces.push_back(std::string(string8.c_str())); + } + } + } + std::vector lines; + + + if (useBpfStats) { + if (parseBpfNetworkStatsDetail(&lines, limitIfaces, limitTag, limitUid) < 0) + return -1; + } else { + ScopedUtfChars path8(env, path); + if (path8.c_str() == NULL) { + ALOGE("the qtaguid legacy path is invalid: %s", path8.c_str()); + return -1; + } + if (legacyReadNetworkStatsDetail(&lines, limitIfaces, limitTag, + limitUid, path8.c_str()) < 0) + return -1; + } + + return statsLinesToNetworkStats(env, clazz, stats, lines); +} + +static int readNetworkStatsDev(JNIEnv* env, jclass clazz, jobject stats) { + std::vector lines; + + if (parseBpfNetworkStatsDev(&lines) < 0) + return -1; + + return statsLinesToNetworkStats(env, clazz, stats, lines); +} + +static const JNINativeMethod gMethods[] = { + { "nativeReadNetworkStatsDetail", + "(Landroid/net/NetworkStats;Ljava/lang/String;I[Ljava/lang/String;IZ)I", + (void*) readNetworkStatsDetail }, + { "nativeReadNetworkStatsDev", "(Landroid/net/NetworkStats;)I", + (void*) readNetworkStatsDev }, +}; + +int register_android_server_net_NetworkStatsFactory(JNIEnv* env) { + int err = jniRegisterNativeMethods(env, "com/android/server/net/NetworkStatsFactory", gMethods, + NELEM(gMethods)); + gStringClass = env->FindClass("java/lang/String"); + gStringClass = static_cast(env->NewGlobalRef(gStringClass)); + + jclass clazz = env->FindClass("android/net/NetworkStats"); + gNetworkStatsClassInfo.size = env->GetFieldID(clazz, "size", "I"); + gNetworkStatsClassInfo.capacity = env->GetFieldID(clazz, "capacity", "I"); + gNetworkStatsClassInfo.iface = env->GetFieldID(clazz, "iface", "[Ljava/lang/String;"); + gNetworkStatsClassInfo.uid = env->GetFieldID(clazz, "uid", "[I"); + gNetworkStatsClassInfo.set = env->GetFieldID(clazz, "set", "[I"); + gNetworkStatsClassInfo.tag = env->GetFieldID(clazz, "tag", "[I"); + gNetworkStatsClassInfo.metered = env->GetFieldID(clazz, "metered", "[I"); + gNetworkStatsClassInfo.roaming = env->GetFieldID(clazz, "roaming", "[I"); + gNetworkStatsClassInfo.defaultNetwork = env->GetFieldID(clazz, "defaultNetwork", "[I"); + gNetworkStatsClassInfo.rxBytes = env->GetFieldID(clazz, "rxBytes", "[J"); + gNetworkStatsClassInfo.rxPackets = env->GetFieldID(clazz, "rxPackets", "[J"); + gNetworkStatsClassInfo.txBytes = env->GetFieldID(clazz, "txBytes", "[J"); + gNetworkStatsClassInfo.txPackets = env->GetFieldID(clazz, "txPackets", "[J"); + gNetworkStatsClassInfo.operations = env->GetFieldID(clazz, "operations", "[J"); + + return err; +} + +} diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp new file mode 100644 index 0000000000..39cbaf716f --- /dev/null +++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2010 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. + */ + +#define LOG_TAG "NetworkStatsNative" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bpf/BpfUtils.h" +#include "netdbpf/BpfNetworkStats.h" + +using android::bpf::bpfGetUidStats; +using android::bpf::bpfGetIfaceStats; + +namespace android { + +// NOTE: keep these in sync with TrafficStats.java +static const uint64_t UNKNOWN = -1; + +enum StatsType { + RX_BYTES = 0, + RX_PACKETS = 1, + TX_BYTES = 2, + TX_PACKETS = 3, + TCP_RX_PACKETS = 4, + TCP_TX_PACKETS = 5 +}; + +static uint64_t getStatsType(Stats* stats, StatsType type) { + switch (type) { + case RX_BYTES: + return stats->rxBytes; + case RX_PACKETS: + return stats->rxPackets; + case TX_BYTES: + return stats->txBytes; + case TX_PACKETS: + return stats->txPackets; + case TCP_RX_PACKETS: + return stats->tcpRxPackets; + case TCP_TX_PACKETS: + return stats->tcpTxPackets; + default: + return UNKNOWN; + } +} + +static jlong getTotalStat(JNIEnv* env, jclass clazz, jint type) { + Stats stats = {}; + + if (bpfGetIfaceStats(NULL, &stats) == 0) { + return getStatsType(&stats, (StatsType) type); + } else { + return UNKNOWN; + } +} + +static jlong getIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) { + ScopedUtfChars iface8(env, iface); + if (iface8.c_str() == NULL) { + return UNKNOWN; + } + + Stats stats = {}; + + if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) { + return getStatsType(&stats, (StatsType) type); + } else { + return UNKNOWN; + } +} + +static jlong getUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) { + Stats stats = {}; + + if (bpfGetUidStats(uid, &stats) == 0) { + return getStatsType(&stats, (StatsType) type); + } else { + return UNKNOWN; + } +} + +static const JNINativeMethod gMethods[] = { + {"nativeGetTotalStat", "(I)J", (void*)getTotalStat}, + {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)getIfaceStat}, + {"nativeGetUidStat", "(II)J", (void*)getUidStat}, +}; + +int register_android_server_net_NetworkStatsService(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/net/NetworkStatsService", gMethods, + NELEM(gMethods)); +} + +} diff --git a/service-t/jni/onload.cpp b/service-t/jni/onload.cpp new file mode 100644 index 0000000000..bca4697560 --- /dev/null +++ b/service-t/jni/onload.cpp @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#include +#include + +namespace android { + +int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name); + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + ALOGE("GetEnv failed"); + return JNI_ERR; + } + + if (register_com_android_net_module_util_BpfMap(env, + "com/android/net/module/util/BpfMap") < 0) return JNI_ERR; + + return JNI_VERSION_1_6; +} + +}; + diff --git a/service-t/src/com/android/server/INativeDaemonConnectorCallbacks.java b/service-t/src/com/android/server/INativeDaemonConnectorCallbacks.java new file mode 100644 index 0000000000..0cf9dcde01 --- /dev/null +++ b/service-t/src/com/android/server/INativeDaemonConnectorCallbacks.java @@ -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); +} diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java new file mode 100644 index 0000000000..4bc40eae44 --- /dev/null +++ b/service-t/src/com/android/server/IpSecService.java @@ -0,0 +1,1878 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static android.Manifest.permission.DUMP; +import static android.net.IpSecManager.INVALID_RESOURCE_ID; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.AF_UNSPEC; +import static android.system.OsConstants.EINVAL; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.IIpSecService; +import android.net.INetd; +import android.net.InetAddresses; +import android.net.IpSecAlgorithm; +import android.net.IpSecConfig; +import android.net.IpSecManager; +import android.net.IpSecSpiResponse; +import android.net.IpSecTransform; +import android.net.IpSecTransformResponse; +import android.net.IpSecTunnelInterfaceResponse; +import android.net.IpSecUdpEncapResponse; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.TrafficStats; +import android.os.Binder; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.text.TextUtils; +import android.util.Log; +import android.util.Range; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.net.module.util.BinderUtils; +import com.android.net.module.util.NetdUtils; +import com.android.net.module.util.PermissionUtils; + +import libcore.io.IoUtils; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A service to manage multiple clients that want to access the IpSec API. The service is + * responsible for maintaining a list of clients and managing the resources (and related quotas) + * that each of them own. + * + *

Synchronization in IpSecService is done on all entrypoints due to potential race conditions at + * the kernel/xfrm level. Further, this allows the simplifying assumption to be made that only one + * thread is ever running at a time. + * + * @hide + */ +public class IpSecService extends IIpSecService.Stub { + private static final String TAG = "IpSecService"; + private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); + private static final int[] ADDRESS_FAMILIES = + new int[] {OsConstants.AF_INET, OsConstants.AF_INET6}; + + private static final int NETD_FETCH_TIMEOUT_MS = 5000; // ms + private static final InetAddress INADDR_ANY; + + @VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10; + + private final INetd mNetd; + + static { + try { + INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0}); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + static final int FREE_PORT_MIN = 1024; // ports 1-1023 are reserved + static final int PORT_MAX = 0xFFFF; // ports are an unsigned 16-bit integer + + /* Binder context for this service */ + private final Context mContext; + private final Dependencies mDeps; + + /** + * The next non-repeating global ID for tracking resources between users, this service, and + * kernel data structures. Accessing this variable is not thread safe, so it is only read or + * modified within blocks synchronized on IpSecService.this. We want to avoid -1 + * (INVALID_RESOURCE_ID) and 0 (we probably forgot to initialize it). + */ + @GuardedBy("IpSecService.this") + private int mNextResourceId = 1; + + /** + * Dependencies of IpSecService, for injection in tests. + */ + @VisibleForTesting + public static class Dependencies { + /** + * Get a reference to INetd. + */ + public INetd getNetdInstance(Context context) throws RemoteException { + final INetd netd = INetd.Stub.asInterface((IBinder) + context.getSystemService(Context.NETD_SERVICE)); + if (netd == null) { + throw new RemoteException("Failed to Get Netd Instance"); + } + return netd; + } + } + + final UidFdTagger mUidFdTagger; + + /** + * Interface for user-reference and kernel-resource cleanup. + * + *

This interface must be implemented for a resource to be reference counted. + */ + @VisibleForTesting + public interface IResource { + /** + * Invalidates a IResource object, ensuring it is invalid for the purposes of allocating new + * objects dependent on it. + * + *

Implementations of this method are expected to remove references to the IResource + * object from the IpSecService's tracking arrays. The removal from the arrays ensures that + * the resource is considered invalid for user access or allocation or use in other + * resources. + * + *

References to the IResource object may be held by other RefcountedResource objects, + * and as such, the underlying resources and quota may not be cleaned up. + */ + void invalidate() throws RemoteException; + + /** + * Releases underlying resources and related quotas. + * + *

Implementations of this method are expected to remove all system resources that are + * tracked by the IResource object. Due to other RefcountedResource objects potentially + * having references to the IResource object, freeUnderlyingResources may not always be + * called from releaseIfUnreferencedRecursively(). + */ + void freeUnderlyingResources() throws RemoteException; + } + + /** + * RefcountedResource manages references and dependencies in an exclusively acyclic graph. + * + *

RefcountedResource implements both explicit and implicit resource management. Creating a + * RefcountedResource object creates an explicit reference that must be freed by calling + * userRelease(). Additionally, adding this object as a child of another RefcountedResource + * object will add an implicit reference. + * + *

Resources are cleaned up when all references, both implicit and explicit, are released + * (ie, when userRelease() is called and when all parents have called releaseReference() on this + * object.) + */ + @VisibleForTesting + public class RefcountedResource implements IBinder.DeathRecipient { + private final T mResource; + private final List mChildren; + int mRefCount = 1; // starts at 1 for user's reference. + IBinder mBinder; + + RefcountedResource(T resource, IBinder binder, RefcountedResource... children) { + synchronized (IpSecService.this) { + this.mResource = resource; + this.mChildren = new ArrayList<>(children.length); + this.mBinder = binder; + + for (RefcountedResource child : children) { + mChildren.add(child); + child.mRefCount++; + } + + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + e.rethrowFromSystemServer(); + } + } + } + + /** + * If the Binder object dies, this function is called to free the system resources that are + * being tracked by this record and to subsequently release this record for garbage + * collection + */ + @Override + public void binderDied() { + synchronized (IpSecService.this) { + try { + userRelease(); + } catch (Exception e) { + Log.e(TAG, "Failed to release resource: " + e); + } + } + } + + public T getResource() { + return mResource; + } + + /** + * Unlinks from binder and performs IpSecService resource cleanup (removes from resource + * arrays) + * + *

If this method has been previously called, the RefcountedResource's binder field will + * be null, and the method will return without performing the cleanup a second time. + * + *

Note that calling this function does not imply that kernel resources will be freed at + * this time, or that the related quota will be returned. Such actions will only be + * performed upon the reference count reaching zero. + */ + @GuardedBy("IpSecService.this") + public void userRelease() throws RemoteException { + // Prevent users from putting reference counts into a bad state by calling + // userRelease() multiple times. + if (mBinder == null) { + return; + } + + mBinder.unlinkToDeath(this, 0); + mBinder = null; + + mResource.invalidate(); + + releaseReference(); + } + + /** + * Removes a reference to this resource. If the resultant reference count is zero, the + * underlying resources are freed, and references to all child resources are also dropped + * recursively (resulting in them freeing their resources and children, etcetera) + * + *

This method also sets the reference count to an invalid value (-1) to signify that it + * has been fully released. Any subsequent calls to this method will result in an + * IllegalStateException being thrown due to resource already having been previously + * released + */ + @VisibleForTesting + @GuardedBy("IpSecService.this") + public void releaseReference() throws RemoteException { + mRefCount--; + + if (mRefCount > 0) { + return; + } else if (mRefCount < 0) { + throw new IllegalStateException( + "Invalid operation - resource has already been released."); + } + + // Cleanup own resources + mResource.freeUnderlyingResources(); + + // Cleanup child resources as needed + for (RefcountedResource child : mChildren) { + child.releaseReference(); + } + + // Enforce that resource cleanup can only be called once + // By decrementing the refcount (from 0 to -1), the next call will throw an + // IllegalStateException - it has already been released fully. + mRefCount--; + } + + @Override + public String toString() { + return new StringBuilder() + .append("{mResource=") + .append(mResource) + .append(", mRefCount=") + .append(mRefCount) + .append(", mChildren=") + .append(mChildren) + .append("}") + .toString(); + } + } + + /** + * Very simple counting class that looks much like a counting semaphore + * + *

This class is not thread-safe, and expects that that users of this class will ensure + * synchronization and thread safety by holding the IpSecService.this instance lock. + */ + @VisibleForTesting + static class ResourceTracker { + private final int mMax; + int mCurrent; + + ResourceTracker(int max) { + mMax = max; + mCurrent = 0; + } + + boolean isAvailable() { + return (mCurrent < mMax); + } + + void take() { + if (!isAvailable()) { + Log.wtf(TAG, "Too many resources allocated!"); + } + mCurrent++; + } + + void give() { + if (mCurrent <= 0) { + Log.wtf(TAG, "We've released this resource too many times"); + } + mCurrent--; + } + + @Override + public String toString() { + return new StringBuilder() + .append("{mCurrent=") + .append(mCurrent) + .append(", mMax=") + .append(mMax) + .append("}") + .toString(); + } + } + + @VisibleForTesting + static final class UserRecord { + /* Maximum number of each type of resource that a single UID may possess */ + + // Up to 4 active VPNs/IWLAN with potential soft handover. + public static final int MAX_NUM_TUNNEL_INTERFACES = 8; + public static final int MAX_NUM_ENCAP_SOCKETS = 16; + + // SPIs and Transforms are both cheap, and are 1:1 correlated. + public static final int MAX_NUM_TRANSFORMS = 64; + public static final int MAX_NUM_SPIS = 64; + + /** + * Store each of the OwnedResource types in an (thinly wrapped) sparse array for indexing + * and explicit (user) reference management. + * + *

These are stored in separate arrays to improve debuggability and dump output clarity. + * + *

Resources are removed from this array when the user releases their explicit reference + * by calling one of the releaseResource() methods. + */ + final RefcountedResourceArray mSpiRecords = + new RefcountedResourceArray<>(SpiRecord.class.getSimpleName()); + final RefcountedResourceArray mTransformRecords = + new RefcountedResourceArray<>(TransformRecord.class.getSimpleName()); + final RefcountedResourceArray mEncapSocketRecords = + new RefcountedResourceArray<>(EncapSocketRecord.class.getSimpleName()); + final RefcountedResourceArray mTunnelInterfaceRecords = + new RefcountedResourceArray<>(TunnelInterfaceRecord.class.getSimpleName()); + + /** + * Trackers for quotas for each of the OwnedResource types. + * + *

These trackers are separate from the resource arrays, since they are incremented and + * decremented at different points in time. Specifically, quota is only returned upon final + * resource deallocation (after all explicit and implicit references are released). Note + * that it is possible that calls to releaseResource() will not return the used quota if + * there are other resources that depend on (are parents of) the resource being released. + */ + final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS); + final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS); + final ResourceTracker mSocketQuotaTracker = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS); + final ResourceTracker mTunnelQuotaTracker = new ResourceTracker(MAX_NUM_TUNNEL_INTERFACES); + + void removeSpiRecord(int resourceId) { + mSpiRecords.remove(resourceId); + } + + void removeTransformRecord(int resourceId) { + mTransformRecords.remove(resourceId); + } + + void removeTunnelInterfaceRecord(int resourceId) { + mTunnelInterfaceRecords.remove(resourceId); + } + + void removeEncapSocketRecord(int resourceId) { + mEncapSocketRecords.remove(resourceId); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{mSpiQuotaTracker=") + .append(mSpiQuotaTracker) + .append(", mTransformQuotaTracker=") + .append(mTransformQuotaTracker) + .append(", mSocketQuotaTracker=") + .append(mSocketQuotaTracker) + .append(", mTunnelQuotaTracker=") + .append(mTunnelQuotaTracker) + .append(", mSpiRecords=") + .append(mSpiRecords) + .append(", mTransformRecords=") + .append(mTransformRecords) + .append(", mEncapSocketRecords=") + .append(mEncapSocketRecords) + .append(", mTunnelInterfaceRecords=") + .append(mTunnelInterfaceRecords) + .append("}") + .toString(); + } + } + + /** + * This class is not thread-safe, and expects that that users of this class will ensure + * synchronization and thread safety by holding the IpSecService.this instance lock. + */ + @VisibleForTesting + static final class UserResourceTracker { + private final SparseArray mUserRecords = new SparseArray<>(); + + /** Lazy-initialization/getter that populates or retrieves the UserRecord as needed */ + public UserRecord getUserRecord(int uid) { + checkCallerUid(uid); + + UserRecord r = mUserRecords.get(uid); + if (r == null) { + r = new UserRecord(); + mUserRecords.put(uid, r); + } + return r; + } + + /** Safety method; guards against access of other user's UserRecords */ + private void checkCallerUid(int uid) { + if (uid != Binder.getCallingUid() && Process.SYSTEM_UID != Binder.getCallingUid()) { + throw new SecurityException("Attempted access of unowned resources"); + } + } + + @Override + public String toString() { + return mUserRecords.toString(); + } + } + + @VisibleForTesting final UserResourceTracker mUserResourceTracker = new UserResourceTracker(); + + /** + * The OwnedResourceRecord class provides a facility to cleanly and reliably track system + * resources. It relies on a provided resourceId that should uniquely identify the kernel + * resource. To use this class, the user should implement the invalidate() and + * freeUnderlyingResources() methods that are responsible for cleaning up IpSecService resource + * tracking arrays and kernel resources, respectively. + * + *

This class associates kernel resources with the UID that owns and controls them. + */ + private abstract class OwnedResourceRecord implements IResource { + final int mPid; + final int mUid; + protected final int mResourceId; + + OwnedResourceRecord(int resourceId) { + super(); + if (resourceId == INVALID_RESOURCE_ID) { + throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID"); + } + mResourceId = resourceId; + mPid = Binder.getCallingPid(); + mUid = Binder.getCallingUid(); + + getResourceTracker().take(); + } + + @Override + public abstract void invalidate() throws RemoteException; + + /** Convenience method; retrieves the user resource record for the stored UID. */ + protected UserRecord getUserRecord() { + return mUserResourceTracker.getUserRecord(mUid); + } + + @Override + public abstract void freeUnderlyingResources() throws RemoteException; + + /** Get the resource tracker for this resource */ + protected abstract ResourceTracker getResourceTracker(); + + @Override + public String toString() { + return new StringBuilder() + .append("{mResourceId=") + .append(mResourceId) + .append(", pid=") + .append(mPid) + .append(", uid=") + .append(mUid) + .append("}") + .toString(); + } + }; + + /** + * Thin wrapper over SparseArray to ensure resources exist, and simplify generic typing. + * + *

RefcountedResourceArray prevents null insertions, and throws an IllegalArgumentException + * if a key is not found during a retrieval process. + */ + static class RefcountedResourceArray { + SparseArray> mArray = new SparseArray<>(); + private final String mTypeName; + + RefcountedResourceArray(String typeName) { + this.mTypeName = typeName; + } + + /** + * Accessor method to get inner resource object. + * + * @throws IllegalArgumentException if no resource with provided key is found. + */ + T getResourceOrThrow(int key) { + return getRefcountedResourceOrThrow(key).getResource(); + } + + /** + * Accessor method to get reference counting wrapper. + * + * @throws IllegalArgumentException if no resource with provided key is found. + */ + RefcountedResource getRefcountedResourceOrThrow(int key) { + RefcountedResource resource = mArray.get(key); + if (resource == null) { + throw new IllegalArgumentException( + String.format("No such %s found for given id: %d", mTypeName, key)); + } + + return resource; + } + + void put(int key, RefcountedResource obj) { + Objects.requireNonNull(obj, "Null resources cannot be added"); + mArray.put(key, obj); + } + + void remove(int key) { + mArray.remove(key); + } + + @Override + public String toString() { + return mArray.toString(); + } + } + + /** + * Tracks an SA in the kernel, and manages cleanup paths. Once a TransformRecord is + * created, the SpiRecord that originally tracked the SAs will reliquish the + * responsibility of freeing the underlying SA to this class via the mOwnedByTransform flag. + */ + private final class TransformRecord extends OwnedResourceRecord { + private final IpSecConfig mConfig; + private final SpiRecord mSpi; + private final EncapSocketRecord mSocket; + + TransformRecord( + int resourceId, IpSecConfig config, SpiRecord spi, EncapSocketRecord socket) { + super(resourceId); + mConfig = config; + mSpi = spi; + mSocket = socket; + + spi.setOwnedByTransform(); + } + + public IpSecConfig getConfig() { + return mConfig; + } + + public SpiRecord getSpiRecord() { + return mSpi; + } + + public EncapSocketRecord getSocketRecord() { + return mSocket; + } + + /** always guarded by IpSecService#this */ + @Override + public void freeUnderlyingResources() { + int spi = mSpi.getSpi(); + try { + mNetd.ipSecDeleteSecurityAssociation( + mUid, + mConfig.getSourceAddress(), + mConfig.getDestinationAddress(), + spi, + mConfig.getMarkValue(), + mConfig.getMarkMask(), + mConfig.getXfrmInterfaceId()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Failed to delete SA with ID: " + mResourceId, e); + } + + getResourceTracker().give(); + } + + @Override + public void invalidate() throws RemoteException { + getUserRecord().removeTransformRecord(mResourceId); + } + + @Override + protected ResourceTracker getResourceTracker() { + return getUserRecord().mTransformQuotaTracker; + } + + @Override + public String toString() { + StringBuilder strBuilder = new StringBuilder(); + strBuilder + .append("{super=") + .append(super.toString()) + .append(", mSocket=") + .append(mSocket) + .append(", mSpi.mResourceId=") + .append(mSpi.mResourceId) + .append(", mConfig=") + .append(mConfig) + .append("}"); + return strBuilder.toString(); + } + } + + /** + * Tracks a single SA in the kernel, and manages cleanup paths. Once used in a Transform, the + * responsibility for cleaning up underlying resources will be passed to the TransformRecord + * object + */ + private final class SpiRecord extends OwnedResourceRecord { + private final String mSourceAddress; + private final String mDestinationAddress; + private int mSpi; + + private boolean mOwnedByTransform = false; + + SpiRecord(int resourceId, String sourceAddress, + String destinationAddress, int spi) { + super(resourceId); + mSourceAddress = sourceAddress; + mDestinationAddress = destinationAddress; + mSpi = spi; + } + + /** always guarded by IpSecService#this */ + @Override + public void freeUnderlyingResources() { + try { + if (!mOwnedByTransform) { + mNetd.ipSecDeleteSecurityAssociation( + mUid, mSourceAddress, mDestinationAddress, mSpi, 0 /* mark */, + 0 /* mask */, 0 /* if_id */); + } + } catch (ServiceSpecificException | RemoteException e) { + Log.e(TAG, "Failed to delete SPI reservation with ID: " + mResourceId, e); + } + + mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; + + getResourceTracker().give(); + } + + public int getSpi() { + return mSpi; + } + + public String getDestinationAddress() { + return mDestinationAddress; + } + + public void setOwnedByTransform() { + if (mOwnedByTransform) { + // Programming error + throw new IllegalStateException("Cannot own an SPI twice!"); + } + + mOwnedByTransform = true; + } + + public boolean getOwnedByTransform() { + return mOwnedByTransform; + } + + @Override + public void invalidate() throws RemoteException { + getUserRecord().removeSpiRecord(mResourceId); + } + + @Override + protected ResourceTracker getResourceTracker() { + return getUserRecord().mSpiQuotaTracker; + } + + @Override + public String toString() { + StringBuilder strBuilder = new StringBuilder(); + strBuilder + .append("{super=") + .append(super.toString()) + .append(", mSpi=") + .append(mSpi) + .append(", mSourceAddress=") + .append(mSourceAddress) + .append(", mDestinationAddress=") + .append(mDestinationAddress) + .append(", mOwnedByTransform=") + .append(mOwnedByTransform) + .append("}"); + return strBuilder.toString(); + } + } + + private final SparseBooleanArray mTunnelNetIds = new SparseBooleanArray(); + final Range mNetIdRange = ConnectivityManager.getIpSecNetIdRange(); + private int mNextTunnelNetId = mNetIdRange.getLower(); + + /** + * Reserves a netId within the range of netIds allocated for IPsec tunnel interfaces + * + *

This method should only be called from Binder threads. Do not call this from within the + * system server as it will crash the system on failure. + * + * @return an integer key within the netId range, if successful + * @throws IllegalStateException if unsuccessful (all netId are currently reserved) + */ + @VisibleForTesting + int reserveNetId() { + final int range = mNetIdRange.getUpper() - mNetIdRange.getLower() + 1; + synchronized (mTunnelNetIds) { + for (int i = 0; i < range; i++) { + final int netId = mNextTunnelNetId; + if (++mNextTunnelNetId > mNetIdRange.getUpper()) { + mNextTunnelNetId = mNetIdRange.getLower(); + } + if (!mTunnelNetIds.get(netId)) { + mTunnelNetIds.put(netId, true); + return netId; + } + } + } + throw new IllegalStateException("No free netIds to allocate"); + } + + @VisibleForTesting + void releaseNetId(int netId) { + synchronized (mTunnelNetIds) { + mTunnelNetIds.delete(netId); + } + } + + /** + * Tracks an tunnel interface, and manages cleanup paths. + * + *

This class is not thread-safe, and expects that that users of this class will ensure + * synchronization and thread safety by holding the IpSecService.this instance lock + */ + @VisibleForTesting + final class TunnelInterfaceRecord extends OwnedResourceRecord { + private final String mInterfaceName; + + // outer addresses + private final String mLocalAddress; + private final String mRemoteAddress; + + private final int mIkey; + private final int mOkey; + + private final int mIfId; + + private Network mUnderlyingNetwork; + + TunnelInterfaceRecord( + int resourceId, + String interfaceName, + Network underlyingNetwork, + String localAddr, + String remoteAddr, + int ikey, + int okey, + int intfId) { + super(resourceId); + + mInterfaceName = interfaceName; + mUnderlyingNetwork = underlyingNetwork; + mLocalAddress = localAddr; + mRemoteAddress = remoteAddr; + mIkey = ikey; + mOkey = okey; + mIfId = intfId; + } + + /** always guarded by IpSecService#this */ + @Override + public void freeUnderlyingResources() { + // Calls to netd + // Teardown VTI + // Delete global policies + try { + mNetd.ipSecRemoveTunnelInterface(mInterfaceName); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + mNetd.ipSecDeleteSecurityPolicy( + mUid, + selAddrFamily, + IpSecManager.DIRECTION_OUT, + mOkey, + 0xffffffff, + mIfId); + mNetd.ipSecDeleteSecurityPolicy( + mUid, + selAddrFamily, + IpSecManager.DIRECTION_IN, + mIkey, + 0xffffffff, + mIfId); + } + } catch (ServiceSpecificException | RemoteException e) { + Log.e( + TAG, + "Failed to delete VTI with interface name: " + + mInterfaceName + + " and id: " + + mResourceId, e); + } + + getResourceTracker().give(); + releaseNetId(mIkey); + releaseNetId(mOkey); + } + + @GuardedBy("IpSecService.this") + public void setUnderlyingNetwork(Network underlyingNetwork) { + // When #applyTunnelModeTransform is called, this new underlying network will be used to + // update the output mark of the input transform. + mUnderlyingNetwork = underlyingNetwork; + } + + @GuardedBy("IpSecService.this") + public Network getUnderlyingNetwork() { + return mUnderlyingNetwork; + } + + public String getInterfaceName() { + return mInterfaceName; + } + + /** Returns the local, outer address for the tunnelInterface */ + public String getLocalAddress() { + return mLocalAddress; + } + + /** Returns the remote, outer address for the tunnelInterface */ + public String getRemoteAddress() { + return mRemoteAddress; + } + + public int getIkey() { + return mIkey; + } + + public int getOkey() { + return mOkey; + } + + public int getIfId() { + return mIfId; + } + + @Override + protected ResourceTracker getResourceTracker() { + return getUserRecord().mTunnelQuotaTracker; + } + + @Override + public void invalidate() { + getUserRecord().removeTunnelInterfaceRecord(mResourceId); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{super=") + .append(super.toString()) + .append(", mInterfaceName=") + .append(mInterfaceName) + .append(", mUnderlyingNetwork=") + .append(mUnderlyingNetwork) + .append(", mLocalAddress=") + .append(mLocalAddress) + .append(", mRemoteAddress=") + .append(mRemoteAddress) + .append(", mIkey=") + .append(mIkey) + .append(", mOkey=") + .append(mOkey) + .append("}") + .toString(); + } + } + + /** + * Tracks a UDP encap socket, and manages cleanup paths + * + *

While this class does not manage non-kernel resources, race conditions around socket + * binding require that the service creates the encap socket, binds it and applies the socket + * policy before handing it to a user. + */ + private final class EncapSocketRecord extends OwnedResourceRecord { + private FileDescriptor mSocket; + private final int mPort; + + EncapSocketRecord(int resourceId, FileDescriptor socket, int port) { + super(resourceId); + mSocket = socket; + mPort = port; + } + + /** always guarded by IpSecService#this */ + @Override + public void freeUnderlyingResources() { + Log.d(TAG, "Closing port " + mPort); + IoUtils.closeQuietly(mSocket); + mSocket = null; + + getResourceTracker().give(); + } + + public int getPort() { + return mPort; + } + + public FileDescriptor getFileDescriptor() { + return mSocket; + } + + @Override + protected ResourceTracker getResourceTracker() { + return getUserRecord().mSocketQuotaTracker; + } + + @Override + public void invalidate() { + getUserRecord().removeEncapSocketRecord(mResourceId); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{super=") + .append(super.toString()) + .append(", mSocket=") + .append(mSocket) + .append(", mPort=") + .append(mPort) + .append("}") + .toString(); + } + } + + /** + * Constructs a new IpSecService instance + * + * @param context Binder context for this service + */ + public IpSecService(Context context) { + this(context, new Dependencies()); + } + + @NonNull + private AppOpsManager getAppOpsManager() { + AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); + if (appOps == null) throw new RuntimeException("System Server couldn't get AppOps"); + return appOps; + } + + /** @hide */ + @VisibleForTesting + public IpSecService(Context context, Dependencies deps) { + this( + context, + deps, + (fd, uid) -> { + try { + TrafficStats.setThreadStatsUid(uid); + TrafficStats.tagFileDescriptor(fd); + } finally { + TrafficStats.clearThreadStatsUid(); + } + }); + } + + /** @hide */ + @VisibleForTesting + public IpSecService(Context context, Dependencies deps, UidFdTagger uidFdTagger) { + mContext = context; + mDeps = Objects.requireNonNull(deps, "Missing dependencies."); + mUidFdTagger = uidFdTagger; + try { + mNetd = mDeps.getNetdInstance(mContext); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Checks that the provided InetAddress is valid for use in an IPsec SA. The address must not be + * a wildcard address and must be in a numeric form such as 1.2.3.4 or 2001::1. + */ + private static void checkInetAddress(String inetAddress) { + if (TextUtils.isEmpty(inetAddress)) { + throw new IllegalArgumentException("Unspecified address"); + } + + InetAddress checkAddr = InetAddresses.parseNumericAddress(inetAddress); + + if (checkAddr.isAnyLocalAddress()) { + throw new IllegalArgumentException("Inappropriate wildcard address: " + inetAddress); + } + } + + /** + * Checks the user-provided direction field and throws an IllegalArgumentException if it is not + * DIRECTION_IN or DIRECTION_OUT + */ + private void checkDirection(int direction) { + switch (direction) { + case IpSecManager.DIRECTION_OUT: + case IpSecManager.DIRECTION_IN: + return; + case IpSecManager.DIRECTION_FWD: + // Only NETWORK_STACK or MAINLINE_NETWORK_STACK allowed to use forward policies + PermissionUtils.enforceNetworkStackPermission(mContext); + return; + } + throw new IllegalArgumentException("Invalid Direction: " + direction); + } + + /** Get a new SPI and maintain the reservation in the system server */ + @Override + public synchronized IpSecSpiResponse allocateSecurityParameterIndex( + String destinationAddress, int requestedSpi, IBinder binder) throws RemoteException { + checkInetAddress(destinationAddress); + // RFC 4303 Section 2.1 - 0=local, 1-255=reserved. + if (requestedSpi > 0 && requestedSpi < 256) { + throw new IllegalArgumentException("ESP SPI must not be in the range of 0-255."); + } + Objects.requireNonNull(binder, "Null Binder passed to allocateSecurityParameterIndex"); + + int callingUid = Binder.getCallingUid(); + UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid); + final int resourceId = mNextResourceId++; + + int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; + try { + if (!userRecord.mSpiQuotaTracker.isAvailable()) { + return new IpSecSpiResponse( + IpSecManager.Status.RESOURCE_UNAVAILABLE, INVALID_RESOURCE_ID, spi); + } + + spi = mNetd.ipSecAllocateSpi(callingUid, "", destinationAddress, requestedSpi); + Log.d(TAG, "Allocated SPI " + spi); + userRecord.mSpiRecords.put( + resourceId, + new RefcountedResource( + new SpiRecord(resourceId, "", + destinationAddress, spi), binder)); + } catch (ServiceSpecificException e) { + if (e.errorCode == OsConstants.ENOENT) { + return new IpSecSpiResponse( + IpSecManager.Status.SPI_UNAVAILABLE, INVALID_RESOURCE_ID, spi); + } + throw e; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return new IpSecSpiResponse(IpSecManager.Status.OK, resourceId, spi); + } + + /* This method should only be called from Binder threads. Do not call this from + * within the system server as it will crash the system on failure. + */ + private void releaseResource(RefcountedResourceArray resArray, int resourceId) + throws RemoteException { + resArray.getRefcountedResourceOrThrow(resourceId).userRelease(); + } + + /** Release a previously allocated SPI that has been registered with the system server */ + @Override + public synchronized void releaseSecurityParameterIndex(int resourceId) throws RemoteException { + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + releaseResource(userRecord.mSpiRecords, resourceId); + } + + /** + * This function finds and forcibly binds to a random system port, ensuring that the port cannot + * be unbound. + * + *

A socket cannot be un-bound from a port if it was bound to that port by number. To select + * a random open port and then bind by number, this function creates a temp socket, binds to a + * random port (specifying 0), gets that port number, and then uses is to bind the user's UDP + * Encapsulation Socket forcibly, so that it cannot be un-bound by the user with the returned + * FileHandle. + * + *

The loop in this function handles the inherent race window between un-binding to a port + * and re-binding, during which the system could *technically* hand that port out to someone + * else. + */ + private int bindToRandomPort(FileDescriptor sockFd) throws IOException { + for (int i = MAX_PORT_BIND_ATTEMPTS; i > 0; i--) { + try { + FileDescriptor probeSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Os.bind(probeSocket, INADDR_ANY, 0); + int port = ((InetSocketAddress) Os.getsockname(probeSocket)).getPort(); + Os.close(probeSocket); + Log.v(TAG, "Binding to port " + port); + Os.bind(sockFd, INADDR_ANY, port); + return port; + } catch (ErrnoException e) { + // Someone miraculously claimed the port just after we closed probeSocket. + if (e.errno == OsConstants.EADDRINUSE) { + continue; + } + throw e.rethrowAsIOException(); + } + } + throw new IOException("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port"); + } + + /** + * Functional interface to do traffic tagging of given sockets to UIDs. + * + *

Specifically used by openUdpEncapsulationSocket to ensure data usage on the UDP encap + * sockets are billed to the UID that the UDP encap socket was created on behalf of. + * + *

Separate class so that the socket tagging logic can be mocked; TrafficStats uses static + * methods that cannot be easily mocked/tested. + */ + @VisibleForTesting + public interface UidFdTagger { + /** + * Sets socket tag to assign all traffic to the provided UID. + * + *

Since the socket is created on behalf of an unprivileged application, all traffic + * should be accounted to the UID of the unprivileged application. + */ + void tag(FileDescriptor fd, int uid) throws IOException; + } + + /** + * Open a socket via the system server and bind it to the specified port (random if port=0). + * This will return a PFD to the user that represent a bound UDP socket. The system server will + * cache the socket and a record of its owner so that it can and must be freed when no longer + * needed. + */ + @Override + public synchronized IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, IBinder binder) + throws RemoteException { + if (port != 0 && (port < FREE_PORT_MIN || port > PORT_MAX)) { + throw new IllegalArgumentException( + "Specified port number must be a valid non-reserved UDP port"); + } + Objects.requireNonNull(binder, "Null Binder passed to openUdpEncapsulationSocket"); + + int callingUid = Binder.getCallingUid(); + UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid); + final int resourceId = mNextResourceId++; + + ParcelFileDescriptor pFd = null; + try { + if (!userRecord.mSocketQuotaTracker.isAvailable()) { + return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); + } + + FileDescriptor sockFd = null; + try { + sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + pFd = ParcelFileDescriptor.dup(sockFd); + } finally { + IoUtils.closeQuietly(sockFd); + } + + mUidFdTagger.tag(pFd.getFileDescriptor(), callingUid); + // This code is common to both the unspecified and specified port cases + Os.setsockoptInt( + pFd.getFileDescriptor(), + OsConstants.IPPROTO_UDP, + OsConstants.UDP_ENCAP, + OsConstants.UDP_ENCAP_ESPINUDP); + + mNetd.ipSecSetEncapSocketOwner(pFd, callingUid); + if (port != 0) { + Log.v(TAG, "Binding to port " + port); + Os.bind(pFd.getFileDescriptor(), INADDR_ANY, port); + } else { + port = bindToRandomPort(pFd.getFileDescriptor()); + } + + userRecord.mEncapSocketRecords.put( + resourceId, + new RefcountedResource( + new EncapSocketRecord(resourceId, pFd.getFileDescriptor(), port), + binder)); + return new IpSecUdpEncapResponse(IpSecManager.Status.OK, resourceId, port, + pFd.getFileDescriptor()); + } catch (IOException | ErrnoException e) { + try { + if (pFd != null) { + pFd.close(); + } + } catch (IOException ex) { + // Nothing can be done at this point + Log.e(TAG, "Failed to close pFd."); + } + } + // If we make it to here, then something has gone wrong and we couldn't open a socket. + // The only reasonable condition that would cause that is resource unavailable. + return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); + } + + /** close a socket that has been been allocated by and registered with the system server */ + @Override + public synchronized void closeUdpEncapsulationSocket(int resourceId) throws RemoteException { + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + releaseResource(userRecord.mEncapSocketRecords, resourceId); + } + + /** + * Create a tunnel interface for use in IPSec tunnel mode. The system server will cache the + * tunnel interface and a record of its owner so that it can and must be freed when no longer + * needed. + */ + @Override + public synchronized IpSecTunnelInterfaceResponse createTunnelInterface( + String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder, + String callingPackage) { + enforceTunnelFeatureAndPermissions(callingPackage); + Objects.requireNonNull(binder, "Null Binder passed to createTunnelInterface"); + Objects.requireNonNull(underlyingNetwork, "No underlying network was specified"); + checkInetAddress(localAddr); + checkInetAddress(remoteAddr); + + // TODO: Check that underlying network exists, and IP addresses not assigned to a different + // network (b/72316676). + + int callerUid = Binder.getCallingUid(); + UserRecord userRecord = mUserResourceTracker.getUserRecord(callerUid); + if (!userRecord.mTunnelQuotaTracker.isAvailable()) { + return new IpSecTunnelInterfaceResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); + } + + final int resourceId = mNextResourceId++; + final int ikey = reserveNetId(); + final int okey = reserveNetId(); + String intfName = String.format("%s%d", INetd.IPSEC_INTERFACE_PREFIX, resourceId); + + try { + // Calls to netd: + // Create VTI + // Add inbound/outbound global policies + // (use reqid = 0) + mNetd.ipSecAddTunnelInterface(intfName, localAddr, remoteAddr, ikey, okey, resourceId); + + BinderUtils.withCleanCallingIdentity(() -> { + NetdUtils.setInterfaceUp(mNetd, intfName); + }); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + // Always send down correct local/remote addresses for template. + mNetd.ipSecAddSecurityPolicy( + callerUid, + selAddrFamily, + IpSecManager.DIRECTION_OUT, + localAddr, + remoteAddr, + 0, + okey, + 0xffffffff, + resourceId); + mNetd.ipSecAddSecurityPolicy( + callerUid, + selAddrFamily, + IpSecManager.DIRECTION_IN, + remoteAddr, + localAddr, + 0, + ikey, + 0xffffffff, + resourceId); + + // Add a forwarding policy on the tunnel interface. In order to support forwarding + // the IpSecTunnelInterface must have a forwarding policy matching the incoming SA. + // + // Unless a IpSecTransform is also applied against this interface in DIRECTION_FWD, + // forwarding will be blocked by default (as would be the case if this policy was + // absent). + // + // This is necessary only on the tunnel interface, and not any the interface to + // which traffic will be forwarded to. + mNetd.ipSecAddSecurityPolicy( + callerUid, + selAddrFamily, + IpSecManager.DIRECTION_FWD, + remoteAddr, + localAddr, + 0, + ikey, + 0xffffffff, + resourceId); + } + + userRecord.mTunnelInterfaceRecords.put( + resourceId, + new RefcountedResource( + new TunnelInterfaceRecord( + resourceId, + intfName, + underlyingNetwork, + localAddr, + remoteAddr, + ikey, + okey, + resourceId), + binder)); + return new IpSecTunnelInterfaceResponse(IpSecManager.Status.OK, resourceId, intfName); + } catch (RemoteException e) { + // Release keys if we got an error. + releaseNetId(ikey); + releaseNetId(okey); + throw e.rethrowFromSystemServer(); + } catch (Throwable t) { + // Release keys if we got an error. + releaseNetId(ikey); + releaseNetId(okey); + throw t; + } + } + + /** + * Adds a new local address to the tunnel interface. This allows packets to be sent and received + * from multiple local IP addresses over the same tunnel. + */ + @Override + public synchronized void addAddressToTunnelInterface( + int tunnelResourceId, LinkAddress localAddr, String callingPackage) { + enforceTunnelFeatureAndPermissions(callingPackage); + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + + // Get tunnelInterface record; if no such interface is found, will throw + // IllegalArgumentException + TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId); + + try { + // We can assume general validity of the IP address, since we get them as a + // LinkAddress, which does some validation. + mNetd.interfaceAddAddress( + tunnelInterfaceInfo.mInterfaceName, + localAddr.getAddress().getHostAddress(), + localAddr.getPrefixLength()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove a new local address from the tunnel interface. After removal, the address will no + * longer be available to send from, or receive on. + */ + @Override + public synchronized void removeAddressFromTunnelInterface( + int tunnelResourceId, LinkAddress localAddr, String callingPackage) { + enforceTunnelFeatureAndPermissions(callingPackage); + + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + // Get tunnelInterface record; if no such interface is found, will throw + // IllegalArgumentException + TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId); + + try { + // We can assume general validity of the IP address, since we get them as a + // LinkAddress, which does some validation. + mNetd.interfaceDelAddress( + tunnelInterfaceInfo.mInterfaceName, + localAddr.getAddress().getHostAddress(), + localAddr.getPrefixLength()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** Set TunnelInterface to use a specific underlying network. */ + @Override + public synchronized void setNetworkForTunnelInterface( + int tunnelResourceId, Network underlyingNetwork, String callingPackage) { + enforceTunnelFeatureAndPermissions(callingPackage); + Objects.requireNonNull(underlyingNetwork, "No underlying network was specified"); + + final UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + + // Get tunnelInterface record; if no such interface is found, will throw + // IllegalArgumentException. userRecord.mTunnelInterfaceRecords is never null + final TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId); + + final ConnectivityManager connectivityManager = + mContext.getSystemService(ConnectivityManager.class); + final LinkProperties lp = connectivityManager.getLinkProperties(underlyingNetwork); + if (tunnelInterfaceInfo.getInterfaceName().equals(lp.getInterfaceName())) { + throw new IllegalArgumentException( + "Underlying network cannot be the network being exposed by this tunnel"); + } + + // It is meaningless to check if the network exists or is valid because the network might + // disconnect at any time after it passes the check. + + tunnelInterfaceInfo.setUnderlyingNetwork(underlyingNetwork); + } + + /** + * Delete a TunnelInterface that has been been allocated by and registered with the system + * server + */ + @Override + public synchronized void deleteTunnelInterface( + int resourceId, String callingPackage) throws RemoteException { + enforceTunnelFeatureAndPermissions(callingPackage); + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + releaseResource(userRecord.mTunnelInterfaceRecords, resourceId); + } + + @VisibleForTesting + void validateAlgorithms(IpSecConfig config) throws IllegalArgumentException { + IpSecAlgorithm auth = config.getAuthentication(); + IpSecAlgorithm crypt = config.getEncryption(); + IpSecAlgorithm aead = config.getAuthenticatedEncryption(); + + // Validate the algorithm set + Preconditions.checkArgument( + aead != null || crypt != null || auth != null, + "No Encryption or Authentication algorithms specified"); + Preconditions.checkArgument( + auth == null || auth.isAuthentication(), + "Unsupported algorithm for Authentication"); + Preconditions.checkArgument( + crypt == null || crypt.isEncryption(), "Unsupported algorithm for Encryption"); + Preconditions.checkArgument( + aead == null || aead.isAead(), + "Unsupported algorithm for Authenticated Encryption"); + Preconditions.checkArgument( + aead == null || (auth == null && crypt == null), + "Authenticated Encryption is mutually exclusive with other Authentication " + + "or Encryption algorithms"); + } + + private int getFamily(String inetAddress) { + int family = AF_UNSPEC; + InetAddress checkAddress = InetAddresses.parseNumericAddress(inetAddress); + if (checkAddress instanceof Inet4Address) { + family = AF_INET; + } else if (checkAddress instanceof Inet6Address) { + family = AF_INET6; + } + return family; + } + + /** + * Checks an IpSecConfig parcel to ensure that the contents are valid and throws an + * IllegalArgumentException if they are not. + */ + private void checkIpSecConfig(IpSecConfig config) { + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + + switch (config.getEncapType()) { + case IpSecTransform.ENCAP_NONE: + break; + case IpSecTransform.ENCAP_ESPINUDP: + case IpSecTransform.ENCAP_ESPINUDP_NON_IKE: + // Retrieve encap socket record; will throw IllegalArgumentException if not found + userRecord.mEncapSocketRecords.getResourceOrThrow( + config.getEncapSocketResourceId()); + + int port = config.getEncapRemotePort(); + if (port <= 0 || port > 0xFFFF) { + throw new IllegalArgumentException("Invalid remote UDP port: " + port); + } + break; + default: + throw new IllegalArgumentException("Invalid Encap Type: " + config.getEncapType()); + } + + validateAlgorithms(config); + + // Retrieve SPI record; will throw IllegalArgumentException if not found + SpiRecord s = userRecord.mSpiRecords.getResourceOrThrow(config.getSpiResourceId()); + + // Check to ensure that SPI has not already been used. + if (s.getOwnedByTransform()) { + throw new IllegalStateException("SPI already in use; cannot be used in new Transforms"); + } + + // If no remote address is supplied, then use one from the SPI. + if (TextUtils.isEmpty(config.getDestinationAddress())) { + config.setDestinationAddress(s.getDestinationAddress()); + } + + // All remote addresses must match + if (!config.getDestinationAddress().equals(s.getDestinationAddress())) { + throw new IllegalArgumentException("Mismatched remote addresseses."); + } + + // This check is technically redundant due to the chain of custody between the SPI and + // the IpSecConfig, but in the future if the dest is allowed to be set explicitly in + // the transform, this will prevent us from messing up. + checkInetAddress(config.getDestinationAddress()); + + // Require a valid source address for all transforms. + checkInetAddress(config.getSourceAddress()); + + // Check to ensure source and destination have the same address family. + String sourceAddress = config.getSourceAddress(); + String destinationAddress = config.getDestinationAddress(); + int sourceFamily = getFamily(sourceAddress); + int destinationFamily = getFamily(destinationAddress); + if (sourceFamily != destinationFamily) { + throw new IllegalArgumentException( + "Source address (" + + sourceAddress + + ") and destination address (" + + destinationAddress + + ") have different address families."); + } + + // Throw an error if UDP Encapsulation is not used in IPv4. + if (config.getEncapType() != IpSecTransform.ENCAP_NONE && sourceFamily != AF_INET) { + throw new IllegalArgumentException( + "UDP Encapsulation is not supported for this address family"); + } + + switch (config.getMode()) { + case IpSecTransform.MODE_TRANSPORT: + break; + case IpSecTransform.MODE_TUNNEL: + break; + default: + throw new IllegalArgumentException( + "Invalid IpSecTransform.mode: " + config.getMode()); + } + + config.setMarkValue(0); + config.setMarkMask(0); + } + + private static final String TUNNEL_OP = AppOpsManager.OPSTR_MANAGE_IPSEC_TUNNELS; + + private void enforceTunnelFeatureAndPermissions(String callingPackage) { + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) { + throw new UnsupportedOperationException( + "IPsec Tunnel Mode requires PackageManager.FEATURE_IPSEC_TUNNELS"); + } + + Objects.requireNonNull(callingPackage, "Null calling package cannot create IpSec tunnels"); + + // OP_MANAGE_IPSEC_TUNNELS will return MODE_ERRORED by default, including for the system + // server. If the appop is not granted, require that the caller has the MANAGE_IPSEC_TUNNELS + // permission or is the System Server. + if (AppOpsManager.MODE_ALLOWED == getAppOpsManager().noteOpNoThrow( + TUNNEL_OP, Binder.getCallingUid(), callingPackage)) { + return; + } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService"); + } + + private void createOrUpdateTransform( + IpSecConfig c, int resourceId, SpiRecord spiRecord, EncapSocketRecord socketRecord) + throws RemoteException { + + int encapType = c.getEncapType(), encapLocalPort = 0, encapRemotePort = 0; + if (encapType != IpSecTransform.ENCAP_NONE) { + encapLocalPort = socketRecord.getPort(); + encapRemotePort = c.getEncapRemotePort(); + } + + IpSecAlgorithm auth = c.getAuthentication(); + IpSecAlgorithm crypt = c.getEncryption(); + IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption(); + + String cryptName; + if (crypt == null) { + cryptName = (authCrypt == null) ? IpSecAlgorithm.CRYPT_NULL : ""; + } else { + cryptName = crypt.getName(); + } + + mNetd.ipSecAddSecurityAssociation( + Binder.getCallingUid(), + c.getMode(), + c.getSourceAddress(), + c.getDestinationAddress(), + (c.getNetwork() != null) ? c.getNetwork().getNetId() : 0, + spiRecord.getSpi(), + c.getMarkValue(), + c.getMarkMask(), + (auth != null) ? auth.getName() : "", + (auth != null) ? auth.getKey() : new byte[] {}, + (auth != null) ? auth.getTruncationLengthBits() : 0, + cryptName, + (crypt != null) ? crypt.getKey() : new byte[] {}, + (crypt != null) ? crypt.getTruncationLengthBits() : 0, + (authCrypt != null) ? authCrypt.getName() : "", + (authCrypt != null) ? authCrypt.getKey() : new byte[] {}, + (authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0, + encapType, + encapLocalPort, + encapRemotePort, + c.getXfrmInterfaceId()); + } + + /** + * Create a IPsec transform, which represents a single security association in the kernel. The + * transform will be cached by the system server and must be freed when no longer needed. It is + * possible to free one, deleting the SA from underneath sockets that are using it, which will + * result in all of those sockets becoming unable to send or receive data. + */ + @Override + public synchronized IpSecTransformResponse createTransform( + IpSecConfig c, IBinder binder, String callingPackage) throws RemoteException { + Objects.requireNonNull(c); + if (c.getMode() == IpSecTransform.MODE_TUNNEL) { + enforceTunnelFeatureAndPermissions(callingPackage); + } + checkIpSecConfig(c); + Objects.requireNonNull(binder, "Null Binder passed to createTransform"); + final int resourceId = mNextResourceId++; + + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + List dependencies = new ArrayList<>(); + + if (!userRecord.mTransformQuotaTracker.isAvailable()) { + return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); + } + + EncapSocketRecord socketRecord = null; + if (c.getEncapType() != IpSecTransform.ENCAP_NONE) { + RefcountedResource refcountedSocketRecord = + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow( + c.getEncapSocketResourceId()); + dependencies.add(refcountedSocketRecord); + socketRecord = refcountedSocketRecord.getResource(); + } + + RefcountedResource refcountedSpiRecord = + userRecord.mSpiRecords.getRefcountedResourceOrThrow(c.getSpiResourceId()); + dependencies.add(refcountedSpiRecord); + SpiRecord spiRecord = refcountedSpiRecord.getResource(); + + createOrUpdateTransform(c, resourceId, spiRecord, socketRecord); + + // SA was created successfully, time to construct a record and lock it away + userRecord.mTransformRecords.put( + resourceId, + new RefcountedResource( + new TransformRecord(resourceId, c, spiRecord, socketRecord), + binder, + dependencies.toArray(new RefcountedResource[dependencies.size()]))); + return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId); + } + + /** + * Delete a transport mode transform that was previously allocated by + registered with the + * system server. If this is called on an inactive (or non-existent) transform, it will not + * return an error. It's safe to de-allocate transforms that may have already been deleted for + * other reasons. + */ + @Override + public synchronized void deleteTransform(int resourceId) throws RemoteException { + UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); + releaseResource(userRecord.mTransformRecords, resourceId); + } + + /** + * Apply an active transport mode transform to a socket, which will apply the IPsec security + * association as a correspondent policy to the provided socket + */ + @Override + public synchronized void applyTransportModeTransform( + ParcelFileDescriptor socket, int direction, int resourceId) throws RemoteException { + int callingUid = Binder.getCallingUid(); + UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid); + checkDirection(direction); + // Get transform record; if no transform is found, will throw IllegalArgumentException + TransformRecord info = userRecord.mTransformRecords.getResourceOrThrow(resourceId); + + // TODO: make this a function. + if (info.mPid != getCallingPid() || info.mUid != callingUid) { + throw new SecurityException("Only the owner of an IpSec Transform may apply it!"); + } + + // Get config and check that to-be-applied transform has the correct mode + IpSecConfig c = info.getConfig(); + Preconditions.checkArgument( + c.getMode() == IpSecTransform.MODE_TRANSPORT, + "Transform mode was not Transport mode; cannot be applied to a socket"); + + mNetd.ipSecApplyTransportModeTransform( + socket, + callingUid, + direction, + c.getSourceAddress(), + c.getDestinationAddress(), + info.getSpiRecord().getSpi()); + } + + /** + * Remove transport mode transforms from a socket, applying the default (empty) policy. This + * ensures that NO IPsec policy is applied to the socket (would be the equivalent of applying a + * policy that performs no IPsec). Today the resourceId parameter is passed but not used: + * reserved for future improved input validation. + */ + @Override + public synchronized void removeTransportModeTransforms(ParcelFileDescriptor socket) + throws RemoteException { + mNetd.ipSecRemoveTransportModeTransform(socket); + } + + /** + * Apply an active tunnel mode transform to a TunnelInterface, which will apply the IPsec + * security association as a correspondent policy to the provided interface + */ + @Override + public synchronized void applyTunnelModeTransform( + int tunnelResourceId, int direction, + int transformResourceId, String callingPackage) throws RemoteException { + enforceTunnelFeatureAndPermissions(callingPackage); + checkDirection(direction); + + int callingUid = Binder.getCallingUid(); + UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid); + + // Get transform record; if no transform is found, will throw IllegalArgumentException + TransformRecord transformInfo = + userRecord.mTransformRecords.getResourceOrThrow(transformResourceId); + + // Get tunnelInterface record; if no such interface is found, will throw + // IllegalArgumentException + TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId); + + // Get config and check that to-be-applied transform has the correct mode + IpSecConfig c = transformInfo.getConfig(); + Preconditions.checkArgument( + c.getMode() == IpSecTransform.MODE_TUNNEL, + "Transform mode was not Tunnel mode; cannot be applied to a tunnel interface"); + + EncapSocketRecord socketRecord = null; + if (c.getEncapType() != IpSecTransform.ENCAP_NONE) { + socketRecord = + userRecord.mEncapSocketRecords.getResourceOrThrow(c.getEncapSocketResourceId()); + } + SpiRecord spiRecord = transformInfo.getSpiRecord(); + + int mark = + (direction == IpSecManager.DIRECTION_OUT) + ? tunnelInterfaceInfo.getOkey() + : tunnelInterfaceInfo.getIkey(); // Ikey also used for FWD policies + + try { + // Default to using the invalid SPI of 0 for inbound SAs. This allows policies to skip + // SPI matching as part of the template resolution. + int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; + c.setXfrmInterfaceId(tunnelInterfaceInfo.getIfId()); + + // TODO: enable this when UPDSA supports updating marks. Adding kernel support upstream + // (and backporting) would allow us to narrow the mark space, and ensure that the SA + // and SPs have matching marks (as VTI are meant to be built). + // Currently update does nothing with marks. Leave empty (defaulting to 0) to ensure the + // config matches the actual allocated resources in the kernel. + // All SAs will have zero marks (from creation time), and any policy that matches the + // same src/dst could match these SAs. Non-IpSecService governed processes that + // establish floating policies with the same src/dst may result in undefined + // behavior. This is generally limited to vendor code due to the permissions + // (CAP_NET_ADMIN) required. + // + // c.setMarkValue(mark); + // c.setMarkMask(0xffffffff); + + if (direction == IpSecManager.DIRECTION_OUT) { + // Set output mark via underlying network (output only) + c.setNetwork(tunnelInterfaceInfo.getUnderlyingNetwork()); + + // Set outbound SPI only. We want inbound to use any valid SA (old, new) on rekeys, + // but want to guarantee outbound packets are sent over the new SA. + spi = spiRecord.getSpi(); + } + + // Always update the policy with the relevant XFRM_IF_ID + for (int selAddrFamily : ADDRESS_FAMILIES) { + mNetd.ipSecUpdateSecurityPolicy( + callingUid, + selAddrFamily, + direction, + transformInfo.getConfig().getSourceAddress(), + transformInfo.getConfig().getDestinationAddress(), + spi, // If outbound, also add SPI to the policy. + mark, // Must always set policy mark; ikey/okey for VTIs + 0xffffffff, + c.getXfrmInterfaceId()); + } + + // Update SA with tunnel mark (ikey or okey based on direction) + createOrUpdateTransform(c, transformResourceId, spiRecord, socketRecord); + } catch (ServiceSpecificException e) { + if (e.errorCode == EINVAL) { + throw new IllegalArgumentException(e.toString()); + } else { + throw e; + } + } + } + + @Override + protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(DUMP, TAG); + + pw.println("IpSecService dump:"); + pw.println(); + + pw.println("mUserResourceTracker:"); + pw.println(mUserResourceTracker); + } +} diff --git a/service-t/src/com/android/server/NativeDaemonConnector.java b/service-t/src/com/android/server/NativeDaemonConnector.java new file mode 100644 index 0000000000..ec8d7798e1 --- /dev/null +++ b/service-t/src/com/android/server/NativeDaemonConnector.java @@ -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 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 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 responses = + new ArrayBlockingQueue(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 mPendingCmds; + private int mMaxCount; + + ResponseQueue(int maxCount) { + mPendingCmds = new LinkedList(); + 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); + } + } + } + } +} diff --git a/service-t/src/com/android/server/NativeDaemonConnectorException.java b/service-t/src/com/android/server/NativeDaemonConnectorException.java new file mode 100644 index 0000000000..4d8881c683 --- /dev/null +++ b/service-t/src/com/android/server/NativeDaemonConnectorException.java @@ -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); + } +} diff --git a/service-t/src/com/android/server/NativeDaemonEvent.java b/service-t/src/com/android/server/NativeDaemonEvent.java new file mode 100644 index 0000000000..5683694346 --- /dev/null +++ b/service-t/src/com/android/server/NativeDaemonEvent.java @@ -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 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 parsed = new ArrayList(); + 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()]); + } +} diff --git a/service-t/src/com/android/server/NativeDaemonTimeoutException.java b/service-t/src/com/android/server/NativeDaemonTimeoutException.java new file mode 100644 index 0000000000..658f7d6264 --- /dev/null +++ b/service-t/src/com/android/server/NativeDaemonTimeoutException.java @@ -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); + } +} + diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java new file mode 100644 index 0000000000..ddf6d2c4ab --- /dev/null +++ b/service-t/src/com/android/server/NsdService.java @@ -0,0 +1,1146 @@ +/* + * Copyright (C) 2021 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.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.nsd.INsdManager; +import android.net.nsd.INsdManagerCallback; +import android.net.nsd.INsdServiceConnector; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Base64; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import com.android.net.module.util.DnsSdTxtRecord; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; + +/** + * Network Service Discovery Service handles remote service discovery operation requests by + * implementing the INsdManager interface. + * + * @hide + */ +public class NsdService extends INsdManager.Stub { + private static final String TAG = "NsdService"; + private static final String MDNS_TAG = "mDnsConnector"; + + private static final boolean DBG = true; + private static final long CLEANUP_DELAY_MS = 10000; + private static final int IFACE_IDX_ANY = 0; + + private final Context mContext; + private final NsdStateMachine mNsdStateMachine; + private final DaemonConnection mDaemon; + private final NativeCallbackReceiver mDaemonCallback; + + /** + * Clients receiving asynchronous messages + */ + private final HashMap mClients = new HashMap<>(); + + /* A map from unique id to client info */ + private final SparseArray mIdToClientInfoMap= new SparseArray<>(); + + private final long mCleanupDelayMs; + + private static final int INVALID_ID = 0; + private int mUniqueId = 1; + // The count of the connected legacy clients. + private int mLegacyClientCount = 0; + + private class NsdStateMachine extends StateMachine { + + private final DefaultState mDefaultState = new DefaultState(); + private final DisabledState mDisabledState = new DisabledState(); + private final EnabledState mEnabledState = new EnabledState(); + + @Override + protected String getWhatToString(int what) { + return NsdManager.nameOf(what); + } + + private void maybeStartDaemon() { + mDaemon.maybeStart(); + maybeScheduleStop(); + } + + private boolean isAnyRequestActive() { + return mIdToClientInfoMap.size() != 0; + } + + private void scheduleStop() { + sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs); + } + private void maybeScheduleStop() { + // The native daemon should stay alive and can't be cleanup + // if any legacy client connected. + if (!isAnyRequestActive() && mLegacyClientCount == 0) { + scheduleStop(); + } + } + + private void cancelStop() { + this.removeMessages(NsdManager.DAEMON_CLEANUP); + } + + NsdStateMachine(String name, Handler handler) { + super(name, handler); + addState(mDefaultState); + addState(mDisabledState, mDefaultState); + addState(mEnabledState, mDefaultState); + State initialState = mEnabledState; + setInitialState(initialState); + setLogRecSize(25); + } + + class DefaultState extends State { + @Override + public boolean processMessage(Message msg) { + final ClientInfo cInfo; + final int clientId = msg.arg2; + switch (msg.what) { + case NsdManager.REGISTER_CLIENT: + final Pair arg = + (Pair) msg.obj; + final INsdManagerCallback cb = arg.second; + try { + cb.asBinder().linkToDeath(arg.first, 0); + cInfo = new ClientInfo(cb); + mClients.put(arg.first, cInfo); + } catch (RemoteException e) { + Log.w(TAG, "Client " + clientId + " has already died"); + } + break; + case NsdManager.UNREGISTER_CLIENT: + final NsdServiceConnector connector = (NsdServiceConnector) msg.obj; + cInfo = mClients.remove(connector); + if (cInfo != null) { + cInfo.expungeAllRequests(); + if (cInfo.isLegacy()) { + mLegacyClientCount -= 1; + } + } + maybeScheduleStop(); + break; + case NsdManager.DISCOVER_SERVICES: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cInfo.onDiscoverServicesFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.STOP_DISCOVERY: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cInfo.onStopDiscoveryFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.REGISTER_SERVICE: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cInfo.onRegisterServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.UNREGISTER_SERVICE: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cInfo.onUnregisterServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.RESOLVE_SERVICE: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.DAEMON_CLEANUP: + mDaemon.maybeStop(); + break; + // This event should be only sent by the legacy (target SDK < S) clients. + // Mark the sending client as legacy. + case NsdManager.DAEMON_STARTUP: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cancelStop(); + cInfo.setLegacy(); + mLegacyClientCount += 1; + maybeStartDaemon(); + } + break; + case NsdManager.NATIVE_DAEMON_EVENT: + default: + Log.e(TAG, "Unhandled " + msg); + return NOT_HANDLED; + } + return HANDLED; + } + + private ClientInfo getClientInfoForReply(Message msg) { + final ListenerArgs args = (ListenerArgs) msg.obj; + return mClients.get(args.connector); + } + } + + class DisabledState extends State { + @Override + public void enter() { + sendNsdStateChangeBroadcast(false); + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case NsdManager.ENABLE: + transitionTo(mEnabledState); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + + class EnabledState extends State { + @Override + public void enter() { + sendNsdStateChangeBroadcast(true); + } + + @Override + public void exit() { + // TODO: it is incorrect to stop the daemon without expunging all requests + // and sending error callbacks to clients. + scheduleStop(); + } + + private boolean requestLimitReached(ClientInfo clientInfo) { + if (clientInfo.mClientIds.size() >= ClientInfo.MAX_LIMIT) { + if (DBG) Log.d(TAG, "Exceeded max outstanding requests " + clientInfo); + return true; + } + return false; + } + + private void storeRequestMap(int clientId, int globalId, ClientInfo clientInfo, int what) { + clientInfo.mClientIds.put(clientId, globalId); + clientInfo.mClientRequests.put(clientId, what); + mIdToClientInfoMap.put(globalId, clientInfo); + // Remove the cleanup event because here comes a new request. + cancelStop(); + } + + private void removeRequestMap(int clientId, int globalId, ClientInfo clientInfo) { + clientInfo.mClientIds.delete(clientId); + clientInfo.mClientRequests.delete(clientId); + mIdToClientInfoMap.remove(globalId); + maybeScheduleStop(); + } + + @Override + public boolean processMessage(Message msg) { + final ClientInfo clientInfo; + final int id; + final int clientId = msg.arg2; + final ListenerArgs args; + switch (msg.what) { + case NsdManager.DISABLE: + //TODO: cleanup clients + transitionTo(mDisabledState); + break; + case NsdManager.DISCOVER_SERVICES: + if (DBG) Log.d(TAG, "Discover services"); + args = (ListenerArgs) msg.obj; + clientInfo = mClients.get(args.connector); + + if (requestLimitReached(clientInfo)) { + clientInfo.onDiscoverServicesFailed( + clientId, NsdManager.FAILURE_MAX_LIMIT); + break; + } + + maybeStartDaemon(); + id = getUniqueId(); + if (discoverServices(id, args.serviceInfo)) { + if (DBG) { + Log.d(TAG, "Discover " + msg.arg2 + " " + id + + args.serviceInfo.getServiceType()); + } + storeRequestMap(clientId, id, clientInfo, msg.what); + clientInfo.onDiscoverServicesStarted(clientId, args.serviceInfo); + } else { + stopServiceDiscovery(id); + clientInfo.onDiscoverServicesFailed(clientId, + NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.STOP_DISCOVERY: + if (DBG) Log.d(TAG, "Stop service discovery"); + args = (ListenerArgs) msg.obj; + clientInfo = mClients.get(args.connector); + + try { + id = clientInfo.mClientIds.get(clientId); + } catch (NullPointerException e) { + clientInfo.onStopDiscoveryFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + break; + } + removeRequestMap(clientId, id, clientInfo); + if (stopServiceDiscovery(id)) { + clientInfo.onStopDiscoverySucceeded(clientId); + } else { + clientInfo.onStopDiscoveryFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.REGISTER_SERVICE: + if (DBG) Log.d(TAG, "Register service"); + args = (ListenerArgs) msg.obj; + clientInfo = mClients.get(args.connector); + if (requestLimitReached(clientInfo)) { + clientInfo.onRegisterServiceFailed( + clientId, NsdManager.FAILURE_MAX_LIMIT); + break; + } + + maybeStartDaemon(); + id = getUniqueId(); + if (registerService(id, args.serviceInfo)) { + if (DBG) Log.d(TAG, "Register " + clientId + " " + id); + storeRequestMap(clientId, id, clientInfo, msg.what); + // Return success after mDns reports success + } else { + unregisterService(id); + clientInfo.onRegisterServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.UNREGISTER_SERVICE: + if (DBG) Log.d(TAG, "unregister service"); + args = (ListenerArgs) msg.obj; + clientInfo = mClients.get(args.connector); + if (clientInfo == null) { + Log.e(TAG, "Unknown connector in unregistration"); + break; + } + id = clientInfo.mClientIds.get(clientId); + removeRequestMap(clientId, id, clientInfo); + if (unregisterService(id)) { + clientInfo.onUnregisterServiceSucceeded(clientId); + } else { + clientInfo.onUnregisterServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.RESOLVE_SERVICE: + if (DBG) Log.d(TAG, "Resolve service"); + args = (ListenerArgs) msg.obj; + clientInfo = mClients.get(args.connector); + + if (clientInfo.mResolvedService != null) { + clientInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_ALREADY_ACTIVE); + break; + } + + maybeStartDaemon(); + id = getUniqueId(); + if (resolveService(id, args.serviceInfo)) { + clientInfo.mResolvedService = new NsdServiceInfo(); + storeRequestMap(clientId, id, clientInfo, msg.what); + } else { + clientInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + break; + case NsdManager.NATIVE_DAEMON_EVENT: + NativeEvent event = (NativeEvent) msg.obj; + if (!handleNativeEvent(event.code, event.raw, event.cooked)) { + return NOT_HANDLED; + } + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + + private boolean handleNativeEvent(int code, String raw, String[] cooked) { + NsdServiceInfo servInfo; + int id = Integer.parseInt(cooked[1]); + ClientInfo clientInfo = mIdToClientInfoMap.get(id); + if (clientInfo == null) { + String name = NativeResponseCode.nameOf(code); + Log.e(TAG, String.format("id %d for %s has no client mapping", id, name)); + return false; + } + + /* This goes in response as msg.arg2 */ + int clientId = clientInfo.getClientId(id); + if (clientId < 0) { + // This can happen because of race conditions. For example, + // SERVICE_FOUND may race with STOP_SERVICE_DISCOVERY, + // and we may get in this situation. + String name = NativeResponseCode.nameOf(code); + Log.d(TAG, String.format( + "Notification %s for listener id %d that is no longer active", + name, id)); + return false; + } + if (DBG) { + String name = NativeResponseCode.nameOf(code); + Log.d(TAG, String.format("Native daemon message %s: %s", name, raw)); + } + switch (code) { + case NativeResponseCode.SERVICE_FOUND: + /* NNN uniqueId serviceName regType domain interfaceIdx netId */ + servInfo = new NsdServiceInfo(cooked[2], cooked[3]); + final int foundNetId; + try { + foundNetId = Integer.parseInt(cooked[6]); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Invalid network received from mdnsd: " + cooked[6]); + break; + } + if (foundNetId == 0L) { + // Ignore services that do not have a Network: they are not usable + // by apps, as they would need privileged permissions to use + // interfaces that do not have an associated Network. + break; + } + servInfo.setNetwork(new Network(foundNetId)); + clientInfo.onServiceFound(clientId, servInfo); + break; + case NativeResponseCode.SERVICE_LOST: + /* NNN uniqueId serviceName regType domain interfaceIdx netId */ + final int lostNetId; + try { + lostNetId = Integer.parseInt(cooked[6]); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Invalid network received from mdnsd: " + cooked[6]); + break; + } + servInfo = new NsdServiceInfo(cooked[2], cooked[3]); + // The network could be null if it was torn down when the service is lost + // TODO: avoid returning null in that case, possibly by remembering found + // services on the same interface index and their network at the time + servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId)); + clientInfo.onServiceLost(clientId, servInfo); + break; + case NativeResponseCode.SERVICE_DISCOVERY_FAILED: + /* NNN uniqueId errorCode */ + clientInfo.onDiscoverServicesFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + break; + case NativeResponseCode.SERVICE_REGISTERED: + /* NNN regId serviceName regType */ + servInfo = new NsdServiceInfo(cooked[2], null); + clientInfo.onRegisterServiceSucceeded(clientId, servInfo); + break; + case NativeResponseCode.SERVICE_REGISTRATION_FAILED: + /* NNN regId errorCode */ + clientInfo.onRegisterServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + break; + case NativeResponseCode.SERVICE_UPDATED: + /* NNN regId */ + break; + case NativeResponseCode.SERVICE_UPDATE_FAILED: + /* NNN regId errorCode */ + break; + case NativeResponseCode.SERVICE_RESOLVED: + /* NNN resolveId fullName hostName port txtlen txtdata interfaceIdx */ + int index = 0; + while (index < cooked[2].length() && cooked[2].charAt(index) != '.') { + if (cooked[2].charAt(index) == '\\') { + ++index; + } + ++index; + } + if (index >= cooked[2].length()) { + Log.e(TAG, "Invalid service found " + raw); + break; + } + + String name = cooked[2].substring(0, index); + String rest = cooked[2].substring(index); + String type = rest.replace(".local.", ""); + + name = unescape(name); + + clientInfo.mResolvedService.setServiceName(name); + clientInfo.mResolvedService.setServiceType(type); + clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4])); + clientInfo.mResolvedService.setTxtRecords(cooked[6]); + // Network will be added after SERVICE_GET_ADDR_SUCCESS + + stopResolveService(id); + removeRequestMap(clientId, id, clientInfo); + + int id2 = getUniqueId(); + if (getAddrInfo(id2, cooked[3], cooked[7] /* interfaceIdx */)) { + storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE); + } else { + clientInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + clientInfo.mResolvedService = null; + } + break; + case NativeResponseCode.SERVICE_RESOLUTION_FAILED: + /* NNN resolveId errorCode */ + stopResolveService(id); + removeRequestMap(clientId, id, clientInfo); + clientInfo.mResolvedService = null; + clientInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + break; + case NativeResponseCode.SERVICE_GET_ADDR_FAILED: + /* NNN resolveId errorCode */ + stopGetAddrInfo(id); + removeRequestMap(clientId, id, clientInfo); + clientInfo.mResolvedService = null; + clientInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + break; + case NativeResponseCode.SERVICE_GET_ADDR_SUCCESS: + /* NNN resolveId hostname ttl addr interfaceIdx netId */ + Network network = null; + try { + final int netId = Integer.parseInt(cooked[6]); + network = netId == 0L ? null : new Network(netId); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Invalid network in GET_ADDR_SUCCESS: " + cooked[6], e); + } + + InetAddress serviceHost = null; + try { + serviceHost = InetAddress.getByName(cooked[4]); + } catch (UnknownHostException e) { + Log.wtf(TAG, "Invalid host in GET_ADDR_SUCCESS", e); + } + + // If the resolved service is on an interface without a network, consider it + // as a failure: it would not be usable by apps as they would need + // privileged permissions. + if (network != null && serviceHost != null) { + clientInfo.mResolvedService.setHost(serviceHost); + clientInfo.mResolvedService.setNetwork(network); + clientInfo.onResolveServiceSucceeded( + clientId, clientInfo.mResolvedService); + } else { + clientInfo.onResolveServiceFailed( + clientId, NsdManager.FAILURE_INTERNAL_ERROR); + } + stopGetAddrInfo(id); + removeRequestMap(clientId, id, clientInfo); + clientInfo.mResolvedService = null; + break; + default: + return false; + } + return true; + } + } + } + + private String unescape(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + if (c == '\\') { + if (++i >= s.length()) { + Log.e(TAG, "Unexpected end of escape sequence in: " + s); + break; + } + c = s.charAt(i); + if (c != '.' && c != '\\') { + if (i + 2 >= s.length()) { + Log.e(TAG, "Unexpected end of escape sequence in: " + s); + break; + } + c = (char) ((c-'0') * 100 + (s.charAt(i+1)-'0') * 10 + (s.charAt(i+2)-'0')); + i += 2; + } + } + sb.append(c); + } + return sb.toString(); + } + + @VisibleForTesting + NsdService(Context ctx, Handler handler, DaemonConnectionSupplier fn, long cleanupDelayMs) { + mCleanupDelayMs = cleanupDelayMs; + mContext = ctx; + mNsdStateMachine = new NsdStateMachine(TAG, handler); + mNsdStateMachine.start(); + mDaemonCallback = new NativeCallbackReceiver(); + mDaemon = fn.get(mDaemonCallback); + } + + public static NsdService create(Context context) throws InterruptedException { + HandlerThread thread = new HandlerThread(TAG); + thread.start(); + Handler handler = new Handler(thread.getLooper()); + NsdService service = + new NsdService(context, handler, DaemonConnection::new, CLEANUP_DELAY_MS); + service.mDaemonCallback.awaitConnection(); + return service; + } + + @Override + public INsdServiceConnector connect(INsdManagerCallback cb) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService"); + final INsdServiceConnector connector = new NsdServiceConnector(); + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.REGISTER_CLIENT, new Pair<>(connector, cb))); + return connector; + } + + private static class ListenerArgs { + public final NsdServiceConnector connector; + public final NsdServiceInfo serviceInfo; + ListenerArgs(NsdServiceConnector connector, NsdServiceInfo serviceInfo) { + this.connector = connector; + this.serviceInfo = serviceInfo; + } + } + + private class NsdServiceConnector extends INsdServiceConnector.Stub + implements IBinder.DeathRecipient { + @Override + public void registerService(int listenerKey, NsdServiceInfo serviceInfo) { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.REGISTER_SERVICE, 0, listenerKey, + new ListenerArgs(this, serviceInfo))); + } + + @Override + public void unregisterService(int listenerKey) { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.UNREGISTER_SERVICE, 0, listenerKey, + new ListenerArgs(this, null))); + } + + @Override + public void discoverServices(int listenerKey, NsdServiceInfo serviceInfo) { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.DISCOVER_SERVICES, 0, listenerKey, + new ListenerArgs(this, serviceInfo))); + } + + @Override + public void stopDiscovery(int listenerKey) { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null))); + } + + @Override + public void resolveService(int listenerKey, NsdServiceInfo serviceInfo) { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.RESOLVE_SERVICE, 0, listenerKey, + new ListenerArgs(this, serviceInfo))); + } + + @Override + public void startDaemon() { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null))); + } + + @Override + public void binderDied() { + mNsdStateMachine.sendMessage( + mNsdStateMachine.obtainMessage(NsdManager.UNREGISTER_CLIENT, this)); + } + } + + private void sendNsdStateChangeBroadcast(boolean isEnabled) { + final Intent intent = new Intent(NsdManager.ACTION_NSD_STATE_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + int nsdState = isEnabled ? NsdManager.NSD_STATE_ENABLED : NsdManager.NSD_STATE_DISABLED; + intent.putExtra(NsdManager.EXTRA_NSD_STATE, nsdState); + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } + + private int getUniqueId() { + if (++mUniqueId == INVALID_ID) return ++mUniqueId; + return mUniqueId; + } + + /* These should be in sync with system/netd/server/ResponseCode.h */ + static final class NativeResponseCode { + public static final int SERVICE_DISCOVERY_FAILED = 602; + public static final int SERVICE_FOUND = 603; + public static final int SERVICE_LOST = 604; + + public static final int SERVICE_REGISTRATION_FAILED = 605; + public static final int SERVICE_REGISTERED = 606; + + public static final int SERVICE_RESOLUTION_FAILED = 607; + public static final int SERVICE_RESOLVED = 608; + + public static final int SERVICE_UPDATED = 609; + public static final int SERVICE_UPDATE_FAILED = 610; + + public static final int SERVICE_GET_ADDR_FAILED = 611; + public static final int SERVICE_GET_ADDR_SUCCESS = 612; + + private static final SparseArray CODE_NAMES = new SparseArray<>(); + static { + CODE_NAMES.put(SERVICE_DISCOVERY_FAILED, "SERVICE_DISCOVERY_FAILED"); + CODE_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND"); + CODE_NAMES.put(SERVICE_LOST, "SERVICE_LOST"); + CODE_NAMES.put(SERVICE_REGISTRATION_FAILED, "SERVICE_REGISTRATION_FAILED"); + CODE_NAMES.put(SERVICE_REGISTERED, "SERVICE_REGISTERED"); + CODE_NAMES.put(SERVICE_RESOLUTION_FAILED, "SERVICE_RESOLUTION_FAILED"); + CODE_NAMES.put(SERVICE_RESOLVED, "SERVICE_RESOLVED"); + CODE_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED"); + CODE_NAMES.put(SERVICE_UPDATE_FAILED, "SERVICE_UPDATE_FAILED"); + CODE_NAMES.put(SERVICE_GET_ADDR_FAILED, "SERVICE_GET_ADDR_FAILED"); + CODE_NAMES.put(SERVICE_GET_ADDR_SUCCESS, "SERVICE_GET_ADDR_SUCCESS"); + } + + static String nameOf(int code) { + String name = CODE_NAMES.get(code); + if (name == null) { + return Integer.toString(code); + } + return name; + } + } + + private class NativeEvent { + final int code; + final String raw; + final String[] cooked; + + NativeEvent(int code, String raw, String[] cooked) { + this.code = code; + this.raw = raw; + this.cooked = cooked; + } + } + + class NativeCallbackReceiver implements INativeDaemonConnectorCallbacks { + private final CountDownLatch connected = new CountDownLatch(1); + + public void awaitConnection() throws InterruptedException { + connected.await(); + } + + @Override + public void onDaemonConnected() { + connected.countDown(); + } + + @Override + public boolean onCheckHoldWakeLock(int code) { + return false; + } + + @Override + public boolean onEvent(int code, String raw, String[] cooked) { + // TODO: NDC translates a message to a callback, we could enhance NDC to + // directly interact with a state machine through messages + NativeEvent event = new NativeEvent(code, raw, cooked); + mNsdStateMachine.sendMessage(NsdManager.NATIVE_DAEMON_EVENT, event); + return true; + } + } + + interface DaemonConnectionSupplier { + DaemonConnection get(NativeCallbackReceiver callback); + } + + @VisibleForTesting + public static class DaemonConnection { + final NativeDaemonConnector mNativeConnector; + boolean mIsStarted = false; + + DaemonConnection(NativeCallbackReceiver callback) { + mNativeConnector = new NativeDaemonConnector(callback, "mdns", 10, MDNS_TAG, 25, null); + new Thread(mNativeConnector, MDNS_TAG).start(); + } + + /** + * Executes the specified cmd on the daemon. + */ + public boolean execute(Object... args) { + if (DBG) { + Log.d(TAG, "mdnssd " + Arrays.toString(args)); + } + try { + mNativeConnector.execute("mdnssd", args); + } catch (NativeDaemonConnectorException e) { + Log.e(TAG, "Failed to execute mdnssd " + Arrays.toString(args), e); + return false; + } + return true; + } + + /** + * Starts the daemon if it is not already started. + */ + public void maybeStart() { + if (mIsStarted) { + return; + } + execute("start-service"); + mIsStarted = true; + } + + /** + * Stops the daemon if it is started. + */ + public void maybeStop() { + if (!mIsStarted) { + return; + } + execute("stop-service"); + mIsStarted = false; + } + } + + private boolean registerService(int regId, NsdServiceInfo service) { + if (DBG) { + Log.d(TAG, "registerService: " + regId + " " + service); + } + String name = service.getServiceName(); + String type = service.getServiceType(); + int port = service.getPort(); + byte[] textRecord = service.getTxtRecord(); + String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", ""); + return mDaemon.execute("register", regId, name, type, port, record); + } + + private boolean unregisterService(int regId) { + return mDaemon.execute("stop-register", regId); + } + + private boolean updateService(int regId, DnsSdTxtRecord t) { + if (t == null) { + return false; + } + return mDaemon.execute("update", regId, t.size(), t.getRawData()); + } + + private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) { + final Network network = serviceInfo.getNetwork(); + final int discoverInterface = getNetworkInterfaceIndex(network); + if (network != null && discoverInterface == IFACE_IDX_ANY) { + Log.e(TAG, "Interface to discover service on not found"); + return false; + } + return mDaemon.execute("discover", discoveryId, serviceInfo.getServiceType(), + discoverInterface); + } + + private boolean stopServiceDiscovery(int discoveryId) { + return mDaemon.execute("stop-discover", discoveryId); + } + + private boolean resolveService(int resolveId, NsdServiceInfo service) { + final String name = service.getServiceName(); + final String type = service.getServiceType(); + final Network network = service.getNetwork(); + final int resolveInterface = getNetworkInterfaceIndex(network); + if (network != null && resolveInterface == IFACE_IDX_ANY) { + Log.e(TAG, "Interface to resolve service on not found"); + return false; + } + return mDaemon.execute("resolve", resolveId, name, type, "local.", resolveInterface); + } + + /** + * Guess the interface to use to resolve or discover a service on a specific network. + * + * This is an imperfect guess, as for example the network may be gone or not yet fully + * registered. This is fine as failing is correct if the network is gone, and a client + * attempting to resolve/discover on a network not yet setup would have a bad time anyway; also + * this is to support the legacy mdnsresponder implementation, which historically resolved + * services on an unspecified network. + */ + private int getNetworkInterfaceIndex(Network network) { + if (network == null) return IFACE_IDX_ANY; + + final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class); + if (cm == null) { + Log.wtf(TAG, "No ConnectivityManager for resolveService"); + return IFACE_IDX_ANY; + } + final LinkProperties lp = cm.getLinkProperties(network); + if (lp == null) return IFACE_IDX_ANY; + + // Only resolve on non-stacked interfaces + final NetworkInterface iface; + try { + iface = NetworkInterface.getByName(lp.getInterfaceName()); + } catch (SocketException e) { + Log.e(TAG, "Error querying interface", e); + return IFACE_IDX_ANY; + } + + if (iface == null) { + Log.e(TAG, "Interface not found: " + lp.getInterfaceName()); + return IFACE_IDX_ANY; + } + + return iface.getIndex(); + } + + private boolean stopResolveService(int resolveId) { + return mDaemon.execute("stop-resolve", resolveId); + } + + private boolean getAddrInfo(int resolveId, String hostname, String interfaceIdx) { + // interfaceIdx is always obtained (as string) from the service resolved callback + return mDaemon.execute("getaddrinfo", resolveId, hostname, interfaceIdx); + } + + private boolean stopGetAddrInfo(int resolveId) { + return mDaemon.execute("stop-getaddrinfo", resolveId); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump " + TAG + + " due to missing android.permission.DUMP permission"); + return; + } + + for (ClientInfo client : mClients.values()) { + pw.println("Client Info"); + pw.println(client); + } + + mNsdStateMachine.dump(fd, pw, args); + } + + /* Information tracked per client */ + private class ClientInfo { + + private static final int MAX_LIMIT = 10; + private final INsdManagerCallback mCb; + /* Remembers a resolved service until getaddrinfo completes */ + private NsdServiceInfo mResolvedService; + + /* A map from client id to unique id sent to mDns */ + private final SparseIntArray mClientIds = new SparseIntArray(); + + /* A map from client id to the type of the request we had received */ + private final SparseIntArray mClientRequests = new SparseIntArray(); + + // The target SDK of this client < Build.VERSION_CODES.S + private boolean mIsLegacy = false; + + private ClientInfo(INsdManagerCallback cb) { + mCb = cb; + if (DBG) Log.d(TAG, "New client"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("mResolvedService ").append(mResolvedService).append("\n"); + sb.append("mIsLegacy ").append(mIsLegacy).append("\n"); + for(int i = 0; i< mClientIds.size(); i++) { + int clientID = mClientIds.keyAt(i); + sb.append("clientId ").append(clientID). + append(" mDnsId ").append(mClientIds.valueAt(i)). + append(" type ").append(mClientRequests.get(clientID)).append("\n"); + } + return sb.toString(); + } + + private boolean isLegacy() { + return mIsLegacy; + } + + private void setLegacy() { + mIsLegacy = true; + } + + // Remove any pending requests from the global map when we get rid of a client, + // and send cancellations to the daemon. + private void expungeAllRequests() { + int globalId, clientId, i; + // TODO: to keep handler responsive, do not clean all requests for that client at once. + for (i = 0; i < mClientIds.size(); i++) { + clientId = mClientIds.keyAt(i); + globalId = mClientIds.valueAt(i); + mIdToClientInfoMap.remove(globalId); + if (DBG) { + Log.d(TAG, "Terminating client-ID " + clientId + + " global-ID " + globalId + " type " + mClientRequests.get(clientId)); + } + switch (mClientRequests.get(clientId)) { + case NsdManager.DISCOVER_SERVICES: + stopServiceDiscovery(globalId); + break; + case NsdManager.RESOLVE_SERVICE: + stopResolveService(globalId); + break; + case NsdManager.REGISTER_SERVICE: + unregisterService(globalId); + break; + default: + break; + } + } + mClientIds.clear(); + mClientRequests.clear(); + } + + // mClientIds is a sparse array of listener id -> mDnsClient id. For a given mDnsClient id, + // return the corresponding listener id. mDnsClient id is also called a global id. + private int getClientId(final int globalId) { + int idx = mClientIds.indexOfValue(globalId); + if (idx < 0) { + return idx; + } + return mClientIds.keyAt(idx); + } + + void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) { + try { + mCb.onDiscoverServicesStarted(listenerKey, info); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onDiscoverServicesStarted", e); + } + } + + void onDiscoverServicesFailed(int listenerKey, int error) { + try { + mCb.onDiscoverServicesFailed(listenerKey, error); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onDiscoverServicesFailed", e); + } + } + + void onServiceFound(int listenerKey, NsdServiceInfo info) { + try { + mCb.onServiceFound(listenerKey, info); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onServiceFound(", e); + } + } + + void onServiceLost(int listenerKey, NsdServiceInfo info) { + try { + mCb.onServiceLost(listenerKey, info); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onServiceLost(", e); + } + } + + void onStopDiscoveryFailed(int listenerKey, int error) { + try { + mCb.onStopDiscoveryFailed(listenerKey, error); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onStopDiscoveryFailed", e); + } + } + + void onStopDiscoverySucceeded(int listenerKey) { + try { + mCb.onStopDiscoverySucceeded(listenerKey); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onStopDiscoverySucceeded", e); + } + } + + void onRegisterServiceFailed(int listenerKey, int error) { + try { + mCb.onRegisterServiceFailed(listenerKey, error); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onRegisterServiceFailed", e); + } + } + + void onRegisterServiceSucceeded(int listenerKey, NsdServiceInfo info) { + try { + mCb.onRegisterServiceSucceeded(listenerKey, info); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onRegisterServiceSucceeded", e); + } + } + + void onUnregisterServiceFailed(int listenerKey, int error) { + try { + mCb.onUnregisterServiceFailed(listenerKey, error); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onUnregisterServiceFailed", e); + } + } + + void onUnregisterServiceSucceeded(int listenerKey) { + try { + mCb.onUnregisterServiceSucceeded(listenerKey); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onUnregisterServiceSucceeded", e); + } + } + + void onResolveServiceFailed(int listenerKey, int error) { + try { + mCb.onResolveServiceFailed(listenerKey, error); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onResolveServiceFailed", e); + } + } + + void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) { + try { + mCb.onResolveServiceSucceeded(listenerKey, info); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onResolveServiceSucceeded", e); + } + } + } +} diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java new file mode 100644 index 0000000000..25c88eb6bd --- /dev/null +++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java @@ -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 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 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)); + } + } +} diff --git a/service-t/src/com/android/server/net/CookieTagMapKey.java b/service-t/src/com/android/server/net/CookieTagMapKey.java new file mode 100644 index 0000000000..443e5b3847 --- /dev/null +++ b/service-t/src/com/android/server/net/CookieTagMapKey.java @@ -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; + } +} diff --git a/service-t/src/com/android/server/net/CookieTagMapValue.java b/service-t/src/com/android/server/net/CookieTagMapValue.java new file mode 100644 index 0000000000..93b9195f92 --- /dev/null +++ b/service-t/src/com/android/server/net/CookieTagMapValue.java @@ -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; + } +} diff --git a/service-t/src/com/android/server/net/DelayedDiskWrite.java b/service-t/src/com/android/server/net/DelayedDiskWrite.java new file mode 100644 index 0000000000..35dc455725 --- /dev/null +++ b/service-t/src/com/android/server/net/DelayedDiskWrite.java @@ -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); + } +} + diff --git a/service-t/src/com/android/server/net/InterfaceMapValue.java b/service-t/src/com/android/server/net/InterfaceMapValue.java new file mode 100644 index 0000000000..42c0044e66 --- /dev/null +++ b/service-t/src/com/android/server/net/InterfaceMapValue.java @@ -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); + } +} diff --git a/service-t/src/com/android/server/net/IpConfigStore.java b/service-t/src/com/android/server/net/IpConfigStore.java new file mode 100644 index 0000000000..3a9a544155 --- /dev/null +++ b/service-t/src/com/android/server/net/IpConfigStore.java @@ -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 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 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 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 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 readIpAndProxyConfigurations( + InputStream inputStream) { + ArrayMap networks = readIpConfigurations(inputStream); + if (networks == null) { + return null; + } + + SparseArray 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 readIpConfigurations( + InputStream inputStream) { + ArrayMap 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 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); + } +} diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java new file mode 100644 index 0000000000..3b93f1a190 --- /dev/null +++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java @@ -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 + * + *

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 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 + * + *

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. + * + *

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 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 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; + } +} diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java new file mode 100644 index 0000000000..fdfc893f57 --- /dev/null +++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java @@ -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 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. + * + *

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. + * + *

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. + * + *

It will update stats asynchronously, so it is safe to call from any thread. + */ + public void updateStats(NetworkStats xtSnapshot, NetworkStats uidSnapshot, + ArrayMap activeIfaces, + ArrayMap 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 mActiveIfaces; + ArrayMap mActiveUidIfaces; + long mCurrentTime; + + StatsContext(NetworkStats xtSnapshot, NetworkStats uidSnapshot, + ArrayMap activeIfaces, + ArrayMap activeUidIfaces, + long currentTime) { + mXtSnapshot = xtSnapshot; + mUidSnapshot = uidSnapshot; + mActiveIfaces = activeIfaces; + mActiveUidIfaces = activeUidIfaces; + mCurrentTime = currentTime; + } + } +} diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java new file mode 100644 index 0000000000..f62765d074 --- /dev/null +++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java @@ -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. + *

+ * 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 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 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 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(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 ifaceIdent, long currentTimeMillis) { + final HashSet 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(); + } +} diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java new file mode 100644 index 0000000000..e3794e441a --- /dev/null +++ b/service-t/src/com/android/server/net/NetworkStatsService.java @@ -0,0 +1,2528 @@ +/* + * 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.Manifest.permission.NETWORK_STATS_PROVIDER; +import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY; +import static android.Manifest.permission.UPDATE_DEVICE_STATS; +import static android.app.usage.NetworkStatsManager.PREFIX_DEV; +import static android.content.Intent.ACTION_SHUTDOWN; +import static android.content.Intent.ACTION_UID_REMOVED; +import static android.content.Intent.ACTION_USER_REMOVED; +import static android.content.Intent.EXTRA_UID; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.IFACE_VT; +import static android.net.NetworkStats.INTERFACES_ALL; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.STATS_PER_IFACE; +import static android.net.NetworkStats.STATS_PER_UID; +import static android.net.NetworkStats.TAG_ALL; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkTemplate.buildTemplateMobileWildcard; +import static android.net.NetworkTemplate.buildTemplateWifiWildcard; +import static android.net.TrafficStats.KB_IN_BYTES; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.net.TrafficStats.UID_TETHERING; +import static android.net.TrafficStats.UNSUPPORTED; +import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID; +import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG; +import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT; +import static android.os.Trace.TRACE_TAG_NETWORK; +import static android.system.OsConstants.ENOENT; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; + +import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport; +import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.usage.NetworkStatsManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.net.DataUsageRequest; +import android.net.INetd; +import android.net.INetworkStatsService; +import android.net.INetworkStatsSession; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkIdentity; +import android.net.NetworkIdentitySet; +import android.net.NetworkPolicyManager; +import android.net.NetworkSpecifier; +import android.net.NetworkStack; +import android.net.NetworkStateSnapshot; +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.TelephonyNetworkSpecifier; +import android.net.TetherStatsParcel; +import android.net.TetheringManager; +import android.net.TrafficStats; +import android.net.UnderlyingNetworkInfo; +import android.net.Uri; +import android.net.netstats.IUsageCallback; +import android.net.netstats.provider.INetworkStatsProvider; +import android.net.netstats.provider.INetworkStatsProviderCallback; +import android.net.netstats.provider.NetworkStatsProvider; +import android.os.Binder; +import android.os.Build; +import android.os.DropBoxManager; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.SystemClock; +import android.os.Trace; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.Settings.Global; +import android.service.NetworkInterfaceProto; +import android.service.NetworkStatsServiceDumpProto; +import android.system.ErrnoException; +import android.telephony.PhoneStateListener; +import android.telephony.SubscriptionPlan; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.EventLog; +import android.util.IndentingPrintWriter; +import android.util.Log; +import android.util.SparseIntArray; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FileRotator; +import com.android.net.module.util.BaseNetdUnsolicitedEventListener; +import com.android.net.module.util.BestClock; +import com.android.net.module.util.BinderUtils; +import com.android.net.module.util.BpfMap; +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.IBpfMap; +import com.android.net.module.util.LocationPermissionChecker; +import com.android.net.module.util.NetworkStatsUtils; +import com.android.net.module.util.PermissionUtils; +import com.android.net.module.util.Struct.U32; +import com.android.net.module.util.Struct.U8; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * Collect and persist detailed network statistics, and provide this data to + * other system services. + */ +@TargetApi(Build.VERSION_CODES.TIRAMISU) +public class NetworkStatsService extends INetworkStatsService.Stub { + static { + System.loadLibrary("service-connectivity"); + } + + static final String TAG = "NetworkStats"; + static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); + static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); + + // Perform polling and persist all (FLAG_PERSIST_ALL). + private static final int MSG_PERFORM_POLL = 1; + // Perform polling, persist network, and register the global alert again. + private static final int MSG_PERFORM_POLL_REGISTER_ALERT = 2; + private static final int MSG_NOTIFY_NETWORK_STATUS = 3; + // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent + // deadlock. + private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4; + + /** Flags to control detail level of poll event. */ + private static final int FLAG_PERSIST_NETWORK = 0x1; + private static final int FLAG_PERSIST_UID = 0x2; + private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID; + private static final int FLAG_PERSIST_FORCE = 0x100; + + /** + * When global alert quota is high, wait for this delay before processing each polling, + * and do not schedule further polls once there is already one queued. + * This avoids firing the global alert too often on devices with high transfer speeds and + * high quota. + */ + private static final int DEFAULT_PERFORM_POLL_DELAY_MS = 1000; + + private static final String TAG_NETSTATS_ERROR = "netstats_error"; + + /** + * EventLog tags used when logging into the event log. Note the values must be sync with + * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct + * name translation. + */ + private static final int LOG_TAG_NETSTATS_MOBILE_SAMPLE = 51100; + private static final int LOG_TAG_NETSTATS_WIFI_SAMPLE = 51101; + + // TODO: Replace the hardcoded string and move it into ConnectivitySettingsManager. + private static final String NETSTATS_COMBINE_SUBTYPE_ENABLED = + "netstats_combine_subtype_enabled"; + + // This is current path but may be changed soon. + private static final String UID_COUNTERSET_MAP_PATH = + "/sys/fs/bpf/map_netd_uid_counterset_map"; + private static final String COOKIE_TAG_MAP_PATH = + "/sys/fs/bpf/map_netd_cookie_tag_map"; + private static final String APP_UID_STATS_MAP_PATH = + "/sys/fs/bpf/map_netd_app_uid_stats_map"; + private static final String STATS_MAP_A_PATH = + "/sys/fs/bpf/map_netd_stats_map_A"; + private static final String STATS_MAP_B_PATH = + "/sys/fs/bpf/map_netd_stats_map_B"; + + private final Context mContext; + private final NetworkStatsFactory mStatsFactory; + private final AlarmManager mAlarmManager; + private final Clock mClock; + private final NetworkStatsSettings mSettings; + private final NetworkStatsObservers mStatsObservers; + + private final File mSystemDir; + private final File mBaseDir; + + private final PowerManager.WakeLock mWakeLock; + + private final ContentObserver mContentObserver; + private final ContentResolver mContentResolver; + + protected INetd mNetd; + private final AlertObserver mAlertObserver = new AlertObserver(); + + @VisibleForTesting + public static final String ACTION_NETWORK_STATS_POLL = + "com.android.server.action.NETWORK_STATS_POLL"; + public static final String ACTION_NETWORK_STATS_UPDATED = + "com.android.server.action.NETWORK_STATS_UPDATED"; + + private PendingIntent mPollIntent; + + /** + * Settings that can be changed externally. + */ + public interface NetworkStatsSettings { + long getPollInterval(); + long getPollDelay(); + boolean getSampleEnabled(); + boolean getAugmentEnabled(); + /** + * When enabled, all mobile data is reported under {@link NetworkTemplate#NETWORK_TYPE_ALL}. + * When disabled, mobile data is broken down by a granular ratType representative of the + * actual ratType. {@see android.app.usage.NetworkStatsManager#getCollapsedRatType}. + * Enabling this decreases the level of detail but saves performance, disk space and + * amount of data logged. + */ + boolean getCombineSubtypeEnabled(); + + class Config { + public final long bucketDuration; + public final long rotateAgeMillis; + public final long deleteAgeMillis; + + public Config(long bucketDuration, long rotateAgeMillis, long deleteAgeMillis) { + this.bucketDuration = bucketDuration; + this.rotateAgeMillis = rotateAgeMillis; + this.deleteAgeMillis = deleteAgeMillis; + } + } + + Config getDevConfig(); + Config getXtConfig(); + Config getUidConfig(); + Config getUidTagConfig(); + + long getGlobalAlertBytes(long def); + long getDevPersistBytes(long def); + long getXtPersistBytes(long def); + long getUidPersistBytes(long def); + long getUidTagPersistBytes(long def); + } + + private final Object mStatsLock = new Object(); + + /** Set of currently active ifaces. */ + @GuardedBy("mStatsLock") + private final ArrayMap mActiveIfaces = new ArrayMap<>(); + + /** Set of currently active ifaces for UID stats. */ + @GuardedBy("mStatsLock") + private final ArrayMap mActiveUidIfaces = new ArrayMap<>(); + + /** Current default active iface. */ + @GuardedBy("mStatsLock") + private String mActiveIface; + + /** Set of any ifaces associated with mobile networks since boot. */ + private volatile String[] mMobileIfaces = new String[0]; + + /** Set of any ifaces associated with wifi networks since boot. */ + private volatile String[] mWifiIfaces = new String[0]; + + /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */ + @GuardedBy("mStatsLock") + private Network[] mDefaultNetworks = new Network[0]; + + /** Last states of all networks sent from ConnectivityService. */ + @GuardedBy("mStatsLock") + @Nullable + private NetworkStateSnapshot[] mLastNetworkStateSnapshots = null; + + private final DropBoxNonMonotonicObserver mNonMonotonicObserver = + new DropBoxNonMonotonicObserver(); + + private static final int MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS = 100; + private final CopyOnWriteArrayList mStatsProviderCbList = + new CopyOnWriteArrayList<>(); + /** Semaphore used to wait for stats provider to respond to request stats update. */ + private final Semaphore mStatsProviderSem = new Semaphore(0, true); + + @GuardedBy("mStatsLock") + private NetworkStatsRecorder mDevRecorder; + @GuardedBy("mStatsLock") + private NetworkStatsRecorder mXtRecorder; + @GuardedBy("mStatsLock") + private NetworkStatsRecorder mUidRecorder; + @GuardedBy("mStatsLock") + private NetworkStatsRecorder mUidTagRecorder; + + /** Cached {@link #mXtRecorder} stats. */ + @GuardedBy("mStatsLock") + private NetworkStatsCollection mXtStatsCached; + + /** + * Current counter sets for each UID. + * TODO: maybe remove mActiveUidCounterSet and read UidCouneterSet value from mUidCounterSetMap + * directly ? But if mActiveUidCounterSet would be accessed very frequently, maybe keep + * mActiveUidCounterSet to avoid accessing kernel too frequently. + */ + private SparseIntArray mActiveUidCounterSet = new SparseIntArray(); + private final IBpfMap mUidCounterSetMap; + private final IBpfMap mCookieTagMap; + private final IBpfMap mStatsMapA; + private final IBpfMap mStatsMapB; + private final IBpfMap mAppUidStatsMap; + + /** Data layer operation counters for splicing into other structures. */ + private NetworkStats mUidOperations = new NetworkStats(0L, 10); + + @NonNull + private final Handler mHandler; + + private volatile boolean mSystemReady; + private long mPersistThreshold = 2 * MB_IN_BYTES; + private long mGlobalAlertBytes; + + private static final long POLL_RATE_LIMIT_MS = 15_000; + + private long mLastStatsSessionPoll; + + /** Map from UID to number of opened sessions */ + @GuardedBy("mOpenSessionCallsPerUid") + private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray(); + + private final static int DUMP_STATS_SESSION_COUNT = 20; + + @NonNull + private final Dependencies mDeps; + + @NonNull + private final NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor; + + @NonNull + private final LocationPermissionChecker mLocationPermissionChecker; + + @NonNull + private final BpfInterfaceMapUpdater mInterfaceMapUpdater; + + private static @NonNull File getDefaultSystemDir() { + return new File(Environment.getDataDirectory(), "system"); + } + + private static @NonNull File getDefaultBaseDir() { + File baseDir = new File(getDefaultSystemDir(), "netstats"); + baseDir.mkdirs(); + return baseDir; + } + + private static @NonNull Clock getDefaultClock() { + return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(), + Clock.systemUTC()); + } + + private final class NetworkStatsHandler extends Handler { + NetworkStatsHandler(@NonNull Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PERFORM_POLL: { + performPoll(FLAG_PERSIST_ALL); + break; + } + case MSG_NOTIFY_NETWORK_STATUS: { + // If no cached states, ignore. + if (mLastNetworkStateSnapshots == null) break; + // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing. + handleNotifyNetworkStatus( + mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface); + break; + } + case MSG_PERFORM_POLL_REGISTER_ALERT: { + performPoll(FLAG_PERSIST_NETWORK); + registerGlobalAlert(); + break; + } + case MSG_BROADCAST_NETWORK_STATS_UPDATED: { + final Intent updatedIntent = new Intent(ACTION_NETWORK_STATS_UPDATED); + updatedIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mContext.sendBroadcastAsUser(updatedIntent, UserHandle.ALL, + READ_NETWORK_USAGE_HISTORY); + break; + } + } + } + } + + /** Creates a new NetworkStatsService */ + public static NetworkStatsService create(Context context) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wakeLock = + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + final INetd netd = INetd.Stub.asInterface( + (IBinder) context.getSystemService(Context.NETD_SERVICE)); + final NetworkStatsService service = new NetworkStatsService(context, + INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)), + alarmManager, wakeLock, getDefaultClock(), + new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context), + new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(), + new Dependencies()); + + return service; + } + + // This must not be called outside of tests, even within the same package, as this constructor + // does not register the local service. Use the create() helper above. + @VisibleForTesting + NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager, + PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings, + NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir, + File baseDir, @NonNull Dependencies deps) { + mContext = Objects.requireNonNull(context, "missing Context"); + mNetd = Objects.requireNonNull(netd, "missing Netd"); + mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager"); + mClock = Objects.requireNonNull(clock, "missing Clock"); + mSettings = Objects.requireNonNull(settings, "missing NetworkStatsSettings"); + mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock"); + mStatsFactory = Objects.requireNonNull(factory, "missing factory"); + mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers"); + mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir"); + mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir"); + mDeps = Objects.requireNonNull(deps, "missing Dependencies"); + + final HandlerThread handlerThread = mDeps.makeHandlerThread(); + handlerThread.start(); + mHandler = new NetworkStatsHandler(handlerThread.getLooper()); + mNetworkStatsSubscriptionsMonitor = deps.makeSubscriptionsMonitor(mContext, + (command) -> mHandler.post(command) , this); + mContentResolver = mContext.getContentResolver(); + mContentObserver = mDeps.makeContentObserver(mHandler, mSettings, + mNetworkStatsSubscriptionsMonitor); + mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext); + mInterfaceMapUpdater = mDeps.makeBpfInterfaceMapUpdater(mContext, mHandler); + mInterfaceMapUpdater.start(); + mUidCounterSetMap = mDeps.getUidCounterSetMap(); + mCookieTagMap = mDeps.getCookieTagMap(); + mStatsMapA = mDeps.getStatsMapA(); + mStatsMapB = mDeps.getStatsMapB(); + mAppUidStatsMap = mDeps.getAppUidStatsMap(); + } + + /** + * Dependencies of NetworkStatsService, for injection in tests. + */ + // TODO: Move more stuff into dependencies object. + @VisibleForTesting + public static class Dependencies { + /** + * Create a HandlerThread to use in NetworkStatsService. + */ + @NonNull + public HandlerThread makeHandlerThread() { + return new HandlerThread(TAG); + } + + /** + * Create a {@link NetworkStatsSubscriptionsMonitor}, can be used to monitor RAT change + * event in NetworkStatsService. + */ + @NonNull + public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(@NonNull Context context, + @NonNull Executor executor, @NonNull NetworkStatsService service) { + // TODO: Update RatType passively in NSS, instead of querying into the monitor + // when notifyNetworkStatus. + return new NetworkStatsSubscriptionsMonitor(context, executor, + (subscriberId, type) -> service.handleOnCollapsedRatTypeChanged()); + } + + /** + * Create a ContentObserver instance which is used to observe settings changes, + * and dispatch onChange events on handler thread. + */ + public @NonNull ContentObserver makeContentObserver(@NonNull Handler handler, + @NonNull NetworkStatsSettings settings, + @NonNull NetworkStatsSubscriptionsMonitor monitor) { + return new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange, @NonNull Uri uri) { + if (!settings.getCombineSubtypeEnabled()) { + monitor.start(); + } else { + monitor.stop(); + } + } + }; + } + + /** + * @see LocationPermissionChecker + */ + public LocationPermissionChecker makeLocationPermissionChecker(final Context context) { + return new LocationPermissionChecker(context); + } + + /** Create BpfInterfaceMapUpdater to update bpf interface map. */ + @NonNull + public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater( + @NonNull Context ctx, @NonNull Handler handler) { + return new BpfInterfaceMapUpdater(ctx, handler); + } + + /** Get counter sets map for each UID. */ + public IBpfMap getUidCounterSetMap() { + try { + return new BpfMap(UID_COUNTERSET_MAP_PATH, BpfMap.BPF_F_RDWR, + U32.class, U8.class); + } catch (ErrnoException e) { + Log.wtf(TAG, "Cannot open uid counter set map: " + e); + return null; + } + } + + /** Gets the cookie tag map */ + public IBpfMap getCookieTagMap() { + try { + return new BpfMap(COOKIE_TAG_MAP_PATH, + BpfMap.BPF_F_RDWR, CookieTagMapKey.class, CookieTagMapValue.class); + } catch (ErrnoException e) { + Log.wtf(TAG, "Cannot open cookie tag map: " + e); + return null; + } + } + + /** Gets stats map A */ + public IBpfMap getStatsMapA() { + try { + return new BpfMap(STATS_MAP_A_PATH, + BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class); + } catch (ErrnoException e) { + Log.wtf(TAG, "Cannot open stats map A: " + e); + return null; + } + } + + /** Gets stats map B */ + public IBpfMap getStatsMapB() { + try { + return new BpfMap(STATS_MAP_B_PATH, + BpfMap.BPF_F_RDWR, StatsMapKey.class, StatsMapValue.class); + } catch (ErrnoException e) { + Log.wtf(TAG, "Cannot open stats map B: " + e); + return null; + } + } + + /** Gets the uid stats map */ + public IBpfMap getAppUidStatsMap() { + try { + return new BpfMap(APP_UID_STATS_MAP_PATH, + BpfMap.BPF_F_RDWR, UidStatsMapKey.class, StatsMapValue.class); + } catch (ErrnoException e) { + Log.wtf(TAG, "Cannot open app uid stats map: " + e); + return null; + } + } + } + + /** + * Observer that watches for {@link INetdUnsolicitedEventListener} alerts. + */ + @VisibleForTesting + public class AlertObserver extends BaseNetdUnsolicitedEventListener { + @Override + public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { + PermissionUtils.enforceNetworkStackPermission(mContext); + + if (LIMIT_GLOBAL_ALERT.equals(alertName)) { + // kick off background poll to collect network stats unless there is already + // such a call pending; UID stats are handled during normal polling interval. + if (!mHandler.hasMessages(MSG_PERFORM_POLL_REGISTER_ALERT)) { + mHandler.sendEmptyMessageDelayed(MSG_PERFORM_POLL_REGISTER_ALERT, + mSettings.getPollDelay()); + } + } + } + } + + public void systemReady() { + synchronized (mStatsLock) { + mSystemReady = true; + + // create data recorders along with historical rotators + mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false); + mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false); + mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false); + mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true); + + updatePersistThresholdsLocked(); + + // upgrade any legacy stats, migrating them to rotated files + maybeUpgradeLegacyStatsLocked(); + + // read historical network stats from disk, since policy service + // might need them right away. + mXtStatsCached = mXtRecorder.getOrLoadCompleteLocked(); + + // bootstrap initial stats to prevent double-counting later + bootstrapStatsLocked(); + } + + // watch for tethering changes + final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class); + tetheringManager.registerTetheringEventCallback( + (command) -> mHandler.post(command), mTetherListener); + + // listen for periodic polling events + final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL); + mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler); + + // listen for uid removal to clean stats + final IntentFilter removedFilter = new IntentFilter(ACTION_UID_REMOVED); + mContext.registerReceiver(mRemovedReceiver, removedFilter, null, mHandler); + + // listen for user changes to clean stats + final IntentFilter userFilter = new IntentFilter(ACTION_USER_REMOVED); + mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler); + + // persist stats during clean shutdown + final IntentFilter shutdownFilter = new IntentFilter(ACTION_SHUTDOWN); + mContext.registerReceiver(mShutdownReceiver, shutdownFilter); + + try { + mNetd.registerUnsolicitedEventListener(mAlertObserver); + } catch (RemoteException | ServiceSpecificException e) { + Log.wtf(TAG, "Error registering event listener :", e); + } + + // schedule periodic pall alarm based on {@link NetworkStatsSettings#getPollInterval()}. + final PendingIntent pollIntent = + PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NETWORK_STATS_POLL), + PendingIntent.FLAG_IMMUTABLE); + + final long currentRealtime = SystemClock.elapsedRealtime(); + mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentRealtime, + mSettings.getPollInterval(), pollIntent); + + mContentResolver.registerContentObserver(Settings.Global + .getUriFor(NETSTATS_COMBINE_SUBTYPE_ENABLED), + false /* notifyForDescendants */, mContentObserver); + + // Post a runnable on handler thread to call onChange(). It's for getting current value of + // NETSTATS_COMBINE_SUBTYPE_ENABLED to decide start or stop monitoring RAT type changes. + mHandler.post(() -> mContentObserver.onChange(false, Settings.Global + .getUriFor(NETSTATS_COMBINE_SUBTYPE_ENABLED))); + + registerGlobalAlert(); + } + + private NetworkStatsRecorder buildRecorder( + String prefix, NetworkStatsSettings.Config config, boolean includeTags) { + final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService( + Context.DROPBOX_SERVICE); + return new NetworkStatsRecorder(new FileRotator( + mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis), + mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags); + } + + @GuardedBy("mStatsLock") + private void shutdownLocked() { + final TetheringManager tetheringManager = mContext.getSystemService(TetheringManager.class); + tetheringManager.unregisterTetheringEventCallback(mTetherListener); + mContext.unregisterReceiver(mPollReceiver); + mContext.unregisterReceiver(mRemovedReceiver); + mContext.unregisterReceiver(mUserReceiver); + mContext.unregisterReceiver(mShutdownReceiver); + + if (!mSettings.getCombineSubtypeEnabled()) { + mNetworkStatsSubscriptionsMonitor.stop(); + } + + mContentResolver.unregisterContentObserver(mContentObserver); + + final long currentTime = mClock.millis(); + + // persist any pending stats + mDevRecorder.forcePersistLocked(currentTime); + mXtRecorder.forcePersistLocked(currentTime); + mUidRecorder.forcePersistLocked(currentTime); + mUidTagRecorder.forcePersistLocked(currentTime); + + mSystemReady = false; + } + + @GuardedBy("mStatsLock") + private void maybeUpgradeLegacyStatsLocked() { + File file; + try { + file = new File(mSystemDir, "netstats.bin"); + if (file.exists()) { + mDevRecorder.importLegacyNetworkLocked(file); + file.delete(); + } + + file = new File(mSystemDir, "netstats_xt.bin"); + if (file.exists()) { + file.delete(); + } + + file = new File(mSystemDir, "netstats_uid.bin"); + if (file.exists()) { + mUidRecorder.importLegacyUidLocked(file); + mUidTagRecorder.importLegacyUidLocked(file); + file.delete(); + } + } catch (IOException e) { + Log.wtf(TAG, "problem during legacy upgrade", e); + } catch (OutOfMemoryError e) { + Log.wtf(TAG, "problem during legacy upgrade", e); + } + } + + /** + * Register for a global alert that is delivered through {@link AlertObserver} + * or {@link NetworkStatsProviderCallback#onAlertReached()} once a threshold amount of data has + * been transferred. + */ + private void registerGlobalAlert() { + try { + mNetd.bandwidthSetGlobalAlert(mGlobalAlertBytes); + } catch (IllegalStateException e) { + Log.w(TAG, "problem registering for global alert: " + e); + } catch (RemoteException e) { + // ignored; service lives in system_server + } + invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetAlert(mGlobalAlertBytes)); + } + + @Override + public INetworkStatsSession openSession() { + return openSessionInternal(NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, null); + } + + @Override + public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) { + return openSessionInternal(flags, callingPackage); + } + + private boolean isRateLimitedForPoll(int callingUid) { + if (callingUid == android.os.Process.SYSTEM_UID) { + return false; + } + + final long lastCallTime; + final long now = SystemClock.elapsedRealtime(); + synchronized (mOpenSessionCallsPerUid) { + int calls = mOpenSessionCallsPerUid.get(callingUid, 0); + mOpenSessionCallsPerUid.put(callingUid, calls + 1); + lastCallTime = mLastStatsSessionPoll; + mLastStatsSessionPoll = now; + } + + return now - lastCallTime < POLL_RATE_LIMIT_MS; + } + + private int restrictFlagsForCaller(int flags) { + // All non-privileged callers are not allowed to turn off POLL_ON_OPEN. + final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK); + if (!isPrivileged) { + flags |= NetworkStatsManager.FLAG_POLL_ON_OPEN; + } + // Non-system uids are rate limited for POLL_ON_OPEN. + final int callingUid = Binder.getCallingUid(); + flags = isRateLimitedForPoll(callingUid) + ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN) + : flags; + return flags; + } + + private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) { + final int restrictedFlags = restrictFlagsForCaller(flags); + if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN + | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) { + final long ident = Binder.clearCallingIdentity(); + try { + performPoll(FLAG_PERSIST_ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + // return an IBinder which holds strong references to any loaded stats + // for its lifetime; when caller closes only weak references remain. + + return new INetworkStatsSession.Stub() { + private final int mCallingUid = Binder.getCallingUid(); + private final String mCallingPackage = callingPackage; + private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel( + callingPackage); + + private NetworkStatsCollection mUidComplete; + private NetworkStatsCollection mUidTagComplete; + + private NetworkStatsCollection getUidComplete() { + synchronized (mStatsLock) { + if (mUidComplete == null) { + mUidComplete = mUidRecorder.getOrLoadCompleteLocked(); + } + return mUidComplete; + } + } + + private NetworkStatsCollection getUidTagComplete() { + synchronized (mStatsLock) { + if (mUidTagComplete == null) { + mUidTagComplete = mUidTagRecorder.getOrLoadCompleteLocked(); + } + return mUidTagComplete; + } + } + + @Override + public int[] getRelevantUids() { + return getUidComplete().getRelevantUids(mAccessLevel); + } + + @Override + public NetworkStats getDeviceSummaryForNetwork( + NetworkTemplate template, long start, long end) { + enforceTemplatePermissions(template, callingPackage); + return internalGetSummaryForNetwork(template, restrictedFlags, start, end, + mAccessLevel, mCallingUid); + } + + @Override + public NetworkStats getSummaryForNetwork( + NetworkTemplate template, long start, long end) { + enforceTemplatePermissions(template, callingPackage); + return internalGetSummaryForNetwork(template, restrictedFlags, start, end, + mAccessLevel, mCallingUid); + } + + // TODO: Remove this after all callers are removed. + @Override + public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) { + enforceTemplatePermissions(template, callingPackage); + return internalGetHistoryForNetwork(template, restrictedFlags, fields, + mAccessLevel, mCallingUid, Long.MIN_VALUE, Long.MAX_VALUE); + } + + @Override + public NetworkStatsHistory getHistoryIntervalForNetwork(NetworkTemplate template, + int fields, long start, long end) { + enforceTemplatePermissions(template, callingPackage); + // TODO(b/200768422): Redact returned history if the template is location + // sensitive but the caller is not privileged. + return internalGetHistoryForNetwork(template, restrictedFlags, fields, + mAccessLevel, mCallingUid, start, end); + } + + @Override + public NetworkStats getSummaryForAllUid( + NetworkTemplate template, long start, long end, boolean includeTags) { + enforceTemplatePermissions(template, callingPackage); + try { + final NetworkStats stats = getUidComplete() + .getSummary(template, start, end, mAccessLevel, mCallingUid); + if (includeTags) { + final NetworkStats tagStats = getUidTagComplete() + .getSummary(template, start, end, mAccessLevel, mCallingUid); + stats.combineAllValues(tagStats); + } + return stats; + } catch (NullPointerException e) { + throw e; + } + } + + @Override + public NetworkStats getTaggedSummaryForAllUid( + NetworkTemplate template, long start, long end) { + enforceTemplatePermissions(template, callingPackage); + try { + final NetworkStats tagStats = getUidTagComplete() + .getSummary(template, start, end, mAccessLevel, mCallingUid); + return tagStats; + } catch (NullPointerException e) { + throw e; + } + } + + @Override + public NetworkStatsHistory getHistoryForUid( + NetworkTemplate template, int uid, int set, int tag, int fields) { + enforceTemplatePermissions(template, callingPackage); + // NOTE: We don't augment UID-level statistics + if (tag == TAG_NONE) { + return getUidComplete().getHistory(template, null, uid, set, tag, fields, + Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid); + } else { + return getUidTagComplete().getHistory(template, null, uid, set, tag, fields, + Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid); + } + } + + @Override + public NetworkStatsHistory getHistoryIntervalForUid( + NetworkTemplate template, int uid, int set, int tag, int fields, + long start, long end) { + enforceTemplatePermissions(template, callingPackage); + // TODO(b/200768422): Redact returned history if the template is location + // sensitive but the caller is not privileged. + // NOTE: We don't augment UID-level statistics + if (tag == TAG_NONE) { + return getUidComplete().getHistory(template, null, uid, set, tag, fields, + start, end, mAccessLevel, mCallingUid); + } else if (uid == Binder.getCallingUid()) { + return getUidTagComplete().getHistory(template, null, uid, set, tag, fields, + start, end, mAccessLevel, mCallingUid); + } else { + throw new SecurityException("Calling package " + mCallingPackage + + " cannot access tag information from a different uid"); + } + } + + @Override + public void close() { + mUidComplete = null; + mUidTagComplete = null; + } + }; + } + + private void enforceTemplatePermissions(@NonNull NetworkTemplate template, + @NonNull String callingPackage) { + // For a template with wifi network keys, it is possible for a malicious + // client to track the user locations via querying data usage. Thus, enforce + // fine location permission check. + if (!template.getWifiNetworkKeys().isEmpty()) { + final boolean canAccessFineLocation = mLocationPermissionChecker + .checkCallersLocationPermission(callingPackage, + null /* featureId */, + Binder.getCallingUid(), + false /* coarseForTargetSdkLessThanQ */, + null /* message */); + if (!canAccessFineLocation) { + throw new SecurityException("Access fine location is required when querying" + + " with wifi network keys, make sure the app has the necessary" + + "permissions and the location toggle is on."); + } + } + } + + private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) { + return NetworkStatsAccess.checkAccessLevel( + mContext, Binder.getCallingPid(), Binder.getCallingUid(), callingPackage); + } + + /** + * Find the most relevant {@link SubscriptionPlan} for the given + * {@link NetworkTemplate} and flags. This is typically used to augment + * local measurement results to match a known anchor from the carrier. + */ + private SubscriptionPlan resolveSubscriptionPlan(NetworkTemplate template, int flags) { + SubscriptionPlan plan = null; + if ((flags & NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN) != 0 + && mSettings.getAugmentEnabled()) { + if (LOGD) Log.d(TAG, "Resolving plan for " + template); + final long token = Binder.clearCallingIdentity(); + try { + plan = mContext.getSystemService(NetworkPolicyManager.class) + .getSubscriptionPlan(template); + } finally { + Binder.restoreCallingIdentity(token); + } + if (LOGD) Log.d(TAG, "Resolved to plan " + plan); + } + return plan; + } + + /** + * Return network summary, splicing between DEV and XT stats when + * appropriate. + */ + private NetworkStats internalGetSummaryForNetwork(NetworkTemplate template, int flags, + long start, long end, @NetworkStatsAccess.Level int accessLevel, int callingUid) { + // We've been using pure XT stats long enough that we no longer need to + // splice DEV and XT together. + final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL, + accessLevel, callingUid, start, end); + + final long now = System.currentTimeMillis(); + final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null); + + final NetworkStats stats = new NetworkStats(end - start, 1); + stats.insertEntry(new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, entry.rxBytes, entry.rxPackets, + entry.txBytes, entry.txPackets, entry.operations)); + return stats; + } + + /** + * Return network history, splicing between DEV and XT stats when + * appropriate. + */ + private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template, + int flags, int fields, @NetworkStatsAccess.Level int accessLevel, int callingUid, + long start, long end) { + // We've been using pure XT stats long enough that we no longer need to + // splice DEV and XT together. + final SubscriptionPlan augmentPlan = resolveSubscriptionPlan(template, flags); + synchronized (mStatsLock) { + return mXtStatsCached.getHistory(template, augmentPlan, + UID_ALL, SET_ALL, TAG_NONE, fields, start, end, accessLevel, callingUid); + } + } + + private long getNetworkTotalBytes(NetworkTemplate template, long start, long end) { + assertSystemReady(); + + return internalGetSummaryForNetwork(template, + NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, start, end, + NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotalBytes(); + } + + private NetworkStats getNetworkUidBytes(NetworkTemplate template, long start, long end) { + assertSystemReady(); + + final NetworkStatsCollection uidComplete; + synchronized (mStatsLock) { + uidComplete = mUidRecorder.getOrLoadCompleteLocked(); + } + return uidComplete.getSummary(template, start, end, NetworkStatsAccess.Level.DEVICE, + android.os.Process.SYSTEM_UID); + } + + @Override + public NetworkStats getDataLayerSnapshotForUid(int uid) throws RemoteException { + if (Binder.getCallingUid() != uid) { + Log.w(TAG, "Snapshots only available for calling UID"); + return new NetworkStats(SystemClock.elapsedRealtime(), 0); + } + + // TODO: switch to data layer stats once kernel exports + // for now, read network layer stats and flatten across all ifaces. + // This function is used to query NeworkStats for calle's uid. The only caller method + // TrafficStats#getDataLayerSnapshotForUid alrady claim no special permission to query + // its own NetworkStats. + final long ident = Binder.clearCallingIdentity(); + final NetworkStats networkLayer; + try { + networkLayer = readNetworkStatsUidDetail(uid, INTERFACES_ALL, TAG_ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + + // splice in operation counts + networkLayer.spliceOperationsFrom(mUidOperations); + + final NetworkStats dataLayer = new NetworkStats( + networkLayer.getElapsedRealtime(), networkLayer.size()); + + NetworkStats.Entry entry = null; + for (int i = 0; i < networkLayer.size(); i++) { + entry = networkLayer.getValues(i, entry); + entry.iface = IFACE_ALL; + dataLayer.combineValues(entry); + } + + return dataLayer; + } + + @Override + public NetworkStats getUidStatsForTransport(int transport) { + PermissionUtils.enforceNetworkStackPermission(mContext); + try { + final String[] relevantIfaces = + transport == TRANSPORT_WIFI ? mWifiIfaces : mMobileIfaces; + // TODO(b/215633405) : mMobileIfaces and mWifiIfaces already contain the stacked + // interfaces, so this is not useful, remove it. + final String[] ifacesToQuery = + mStatsFactory.augmentWithStackedInterfaces(relevantIfaces); + return getNetworkStatsUidDetail(ifacesToQuery); + } catch (RemoteException e) { + Log.wtf(TAG, "Error compiling UID stats", e); + return new NetworkStats(0L, 0); + } + } + + @Override + public String[] getMobileIfaces() { + // TODO (b/192758557): Remove debug log. + if (CollectionUtils.contains(mMobileIfaces, null)) { + throw new NullPointerException( + "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces)); + } + return mMobileIfaces.clone(); + } + + @Override + public void incrementOperationCount(int uid, int tag, int operationCount) { + if (Binder.getCallingUid() != uid) { + mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG); + } + + if (operationCount < 0) { + throw new IllegalArgumentException("operation count can only be incremented"); + } + if (tag == TAG_NONE) { + throw new IllegalArgumentException("operation count must have specific tag"); + } + + synchronized (mStatsLock) { + final int set = mActiveUidCounterSet.get(uid, SET_DEFAULT); + mUidOperations.combineValues( + mActiveIface, uid, set, tag, 0L, 0L, 0L, 0L, operationCount); + mUidOperations.combineValues( + mActiveIface, uid, set, TAG_NONE, 0L, 0L, 0L, 0L, operationCount); + } + } + + private void setKernelCounterSet(int uid, int set) { + if (mUidCounterSetMap == null) { + Log.wtf(TAG, "Fail to set UidCounterSet: Null bpf map"); + return; + } + + if (set == SET_DEFAULT) { + try { + mUidCounterSetMap.deleteEntry(new U32(uid)); + } catch (ErrnoException e) { + Log.w(TAG, "UidCounterSetMap.deleteEntry(" + uid + ") failed with errno: " + e); + } + return; + } + + try { + mUidCounterSetMap.updateEntry(new U32(uid), new U8((short) set)); + } catch (ErrnoException e) { + Log.w(TAG, "UidCounterSetMap.updateEntry(" + uid + ", " + set + + ") failed with errno: " + e); + } + } + + @VisibleForTesting + public void noteUidForeground(int uid, boolean uidForeground) { + PermissionUtils.enforceNetworkStackPermission(mContext); + synchronized (mStatsLock) { + final int set = uidForeground ? SET_FOREGROUND : SET_DEFAULT; + final int oldSet = mActiveUidCounterSet.get(uid, SET_DEFAULT); + if (oldSet != set) { + mActiveUidCounterSet.put(uid, set); + setKernelCounterSet(uid, set); + } + } + } + + /** + * Notify {@code NetworkStatsService} about network status changed. + */ + public void notifyNetworkStatus( + @NonNull Network[] defaultNetworks, + @NonNull NetworkStateSnapshot[] networkStates, + @Nullable String activeIface, + @NonNull UnderlyingNetworkInfo[] underlyingNetworkInfos) { + PermissionUtils.enforceNetworkStackPermission(mContext); + + final long token = Binder.clearCallingIdentity(); + try { + handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Update the VPN underlying interfaces only after the poll is made and tun data has been + // migrated. Otherwise the migration would use the new interfaces instead of the ones that + // were current when the polled data was transferred. + mStatsFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + } + + @Override + public void forceUpdate() { + PermissionUtils.enforceNetworkStackPermission(mContext); + + final long token = Binder.clearCallingIdentity(); + try { + performPoll(FLAG_PERSIST_ALL); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** Advise persistence threshold; may be overridden internally. */ + public void advisePersistThreshold(long thresholdBytes) { + PermissionUtils.enforceNetworkStackPermission(mContext); + // clamp threshold into safe range + mPersistThreshold = NetworkStatsUtils.constrain(thresholdBytes, + 128 * KB_IN_BYTES, 2 * MB_IN_BYTES); + if (LOGV) { + Log.v(TAG, "advisePersistThreshold() given " + thresholdBytes + ", clamped to " + + mPersistThreshold); + } + + final long oldGlobalAlertBytes = mGlobalAlertBytes; + + // update and persist if beyond new thresholds + final long currentTime = mClock.millis(); + synchronized (mStatsLock) { + if (!mSystemReady) return; + + updatePersistThresholdsLocked(); + + mDevRecorder.maybePersistLocked(currentTime); + mXtRecorder.maybePersistLocked(currentTime); + mUidRecorder.maybePersistLocked(currentTime); + mUidTagRecorder.maybePersistLocked(currentTime); + } + + if (oldGlobalAlertBytes != mGlobalAlertBytes) { + registerGlobalAlert(); + } + } + + @Override + public DataUsageRequest registerUsageCallback(@NonNull String callingPackage, + @NonNull DataUsageRequest request, @NonNull IUsageCallback callback) { + Objects.requireNonNull(callingPackage, "calling package is null"); + Objects.requireNonNull(request, "DataUsageRequest is null"); + Objects.requireNonNull(request.template, "NetworkTemplate is null"); + Objects.requireNonNull(callback, "callback is null"); + + int callingUid = Binder.getCallingUid(); + @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage); + DataUsageRequest normalizedRequest; + final long token = Binder.clearCallingIdentity(); + try { + normalizedRequest = mStatsObservers.register(mContext, + request, callback, callingUid, accessLevel); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Create baseline stats + mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL)); + + return normalizedRequest; + } + + @Override + public void unregisterUsageRequest(DataUsageRequest request) { + Objects.requireNonNull(request, "DataUsageRequest is null"); + + int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + mStatsObservers.unregister(request, callingUid); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public long getUidStats(int uid, int type) { + final int callingUid = Binder.getCallingUid(); + if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) { + return UNSUPPORTED; + } + return nativeGetUidStat(uid, type); + } + + @Override + public long getIfaceStats(@NonNull String iface, int type) { + Objects.requireNonNull(iface); + long nativeIfaceStats = nativeGetIfaceStat(iface, type); + if (nativeIfaceStats == -1) { + return nativeIfaceStats; + } else { + // When tethering offload is in use, nativeIfaceStats does not contain usage from + // offload, add it back here. Note that the included statistics might be stale + // since polling newest stats from hardware might impact system health and not + // suitable for TrafficStats API use cases. + return nativeIfaceStats + getProviderIfaceStats(iface, type); + } + } + + @Override + public long getTotalStats(int type) { + long nativeTotalStats = nativeGetTotalStat(type); + if (nativeTotalStats == -1) { + return nativeTotalStats; + } else { + // Refer to comment in getIfaceStats + return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type); + } + } + + private long getProviderIfaceStats(@Nullable String iface, int type) { + final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE); + final HashSet limitIfaces; + if (iface == IFACE_ALL) { + limitIfaces = null; + } else { + limitIfaces = new HashSet<>(); + limitIfaces.add(iface); + } + final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces); + switch (type) { + case TrafficStats.TYPE_RX_BYTES: + return entry.rxBytes; + case TrafficStats.TYPE_RX_PACKETS: + return entry.rxPackets; + case TrafficStats.TYPE_TX_BYTES: + return entry.txBytes; + case TrafficStats.TYPE_TX_PACKETS: + return entry.txPackets; + default: + return 0; + } + } + + /** + * Update {@link NetworkStatsRecorder} and {@link #mGlobalAlertBytes} to + * reflect current {@link #mPersistThreshold} value. Always defers to + * {@link Global} values when defined. + */ + @GuardedBy("mStatsLock") + private void updatePersistThresholdsLocked() { + mDevRecorder.setPersistThreshold(mSettings.getDevPersistBytes(mPersistThreshold)); + mXtRecorder.setPersistThreshold(mSettings.getXtPersistBytes(mPersistThreshold)); + mUidRecorder.setPersistThreshold(mSettings.getUidPersistBytes(mPersistThreshold)); + mUidTagRecorder.setPersistThreshold(mSettings.getUidTagPersistBytes(mPersistThreshold)); + mGlobalAlertBytes = mSettings.getGlobalAlertBytes(mPersistThreshold); + } + + /** + * Listener that watches for {@link TetheringManager} to claim interface pairs. + */ + private final TetheringManager.TetheringEventCallback mTetherListener = + new TetheringManager.TetheringEventCallback() { + @Override + public void onUpstreamChanged(@Nullable Network network) { + performPoll(FLAG_PERSIST_NETWORK); + } + }; + + private BroadcastReceiver mPollReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // on background handler thread, and verified UPDATE_DEVICE_STATS + // permission above. + performPoll(FLAG_PERSIST_ALL); + + // verify that we're watching global alert + registerGlobalAlert(); + } + }; + + private BroadcastReceiver mRemovedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // on background handler thread, and UID_REMOVED is protected + // broadcast. + + final int uid = intent.getIntExtra(EXTRA_UID, -1); + if (uid == -1) return; + + synchronized (mStatsLock) { + mWakeLock.acquire(); + try { + removeUidsLocked(uid); + } finally { + mWakeLock.release(); + } + } + } + }; + + private BroadcastReceiver mUserReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // On background handler thread, and USER_REMOVED is protected + // broadcast. + + final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER); + if (userHandle == null) return; + + synchronized (mStatsLock) { + mWakeLock.acquire(); + try { + removeUserLocked(userHandle); + } finally { + mWakeLock.release(); + } + } + } + }; + + private BroadcastReceiver mShutdownReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // SHUTDOWN is protected broadcast. + synchronized (mStatsLock) { + shutdownLocked(); + } + } + }; + + /** + * Handle collapsed RAT type changed event. + */ + @VisibleForTesting + public void handleOnCollapsedRatTypeChanged() { + // Protect service from frequently updating. Remove pending messages if any. + mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay()); + } + + private void handleNotifyNetworkStatus( + Network[] defaultNetworks, + NetworkStateSnapshot[] snapshots, + String activeIface) { + synchronized (mStatsLock) { + mWakeLock.acquire(); + try { + mActiveIface = activeIface; + handleNotifyNetworkStatusLocked(defaultNetworks, snapshots); + } finally { + mWakeLock.release(); + } + } + } + + /** + * Inspect all current {@link NetworkStateSnapshot}s to derive mapping from {@code iface} to + * {@link NetworkStatsHistory}. When multiple networks are active on a single {@code iface}, + * they are combined under a single {@link NetworkIdentitySet}. + */ + @GuardedBy("mStatsLock") + private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks, + @NonNull NetworkStateSnapshot[] snapshots) { + if (!mSystemReady) return; + if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()"); + + // take one last stats snapshot before updating iface mapping. this + // isn't perfect, since the kernel may already be counting traffic from + // the updated network. + + // poll, but only persist network stats to keep codepath fast. UID stats + // will be persisted during next alarm poll event. + performPollLocked(FLAG_PERSIST_NETWORK); + + // Rebuild active interfaces based on connected networks + mActiveIfaces.clear(); + mActiveUidIfaces.clear(); + // Update the list of default networks. + mDefaultNetworks = defaultNetworks; + + mLastNetworkStateSnapshots = snapshots; + + final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled(); + final ArraySet mobileIfaces = new ArraySet<>(); + final ArraySet wifiIfaces = new ArraySet<>(); + for (NetworkStateSnapshot snapshot : snapshots) { + final int displayTransport = + getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes()); + final boolean isMobile = (NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport); + final boolean isWifi = (NetworkCapabilities.TRANSPORT_WIFI == displayTransport); + final boolean isDefault = CollectionUtils.contains( + mDefaultNetworks, snapshot.getNetwork()); + final int ratType = combineSubtypeEnabled ? NetworkTemplate.NETWORK_TYPE_ALL + : getRatTypeForStateSnapshot(snapshot); + final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, snapshot, + isDefault, ratType); + + // Traffic occurring on the base interface is always counted for + // both total usage and UID details. + final String baseIface = snapshot.getLinkProperties().getInterfaceName(); + if (baseIface != null) { + findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident); + findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident); + + // Build a separate virtual interface for VT (Video Telephony) data usage. + // Only do this when IMS is not metered, but VT is metered. + // If IMS is metered, then the IMS network usage has already included VT usage. + // VT is considered always metered in framework's layer. If VT is not metered + // per carrier's policy, modem will report 0 usage for VT calls. + if (snapshot.getNetworkCapabilities().hasCapability( + NetworkCapabilities.NET_CAPABILITY_IMS) && !ident.isMetered()) { + + // Copy the identify from IMS one but mark it as metered. + NetworkIdentity vtIdent = new NetworkIdentity.Builder() + .setType(ident.getType()) + .setRatType(ident.getRatType()) + .setSubscriberId(ident.getSubscriberId()) + .setWifiNetworkKey(ident.getWifiNetworkKey()) + .setRoaming(ident.isRoaming()).setMetered(true) + .setDefaultNetwork(true) + .setOemManaged(ident.getOemManaged()) + .setSubId(ident.getSubId()).build(); + final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot); + findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent); + findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent); + } + + if (isMobile) { + mobileIfaces.add(baseIface); + } + if (isWifi) { + wifiIfaces.add(baseIface); + } + } + + // Traffic occurring on stacked interfaces is usually clatd. + // + // UID stats are always counted on the stacked interface and never on the base + // interface, because the packets on the base interface do not actually match + // application sockets (they're not IPv4) and thus the app uid is not known. + // For receive this is obvious: packets must be translated from IPv6 to IPv4 + // before the application socket can be found. + // For transmit: either they go through the clat daemon which by virtue of going + // through userspace strips the original socket association during the IPv4 to + // IPv6 translation process, or they are offloaded by eBPF, which doesn't: + // However, on an ebpf device the accounting is done in cgroup ebpf hooks, + // which don't trigger again post ebpf translation. + // (as such stats accounted to the clat uid are ignored) + // + // Interface stats are more complicated. + // + // eBPF offloaded 464xlat'ed packets never hit base interface ip6tables, and thus + // *all* statistics are collected by iptables on the stacked v4-* interface. + // + // Additionally for ingress all packets bound for the clat IPv6 address are dropped + // in ip6tables raw prerouting and thus even non-offloaded packets are only + // accounted for on the stacked interface. + // + // For egress, packets subject to eBPF offload never appear on the base interface + // and only appear on the stacked interface. Thus to ensure packets increment + // interface stats, we must collate data from stacked interfaces. For xt_qtaguid + // (or non eBPF offloaded) TX they would appear on both, however egress interface + // accounting is explicitly bypassed for traffic from the clat uid. + // + // TODO: This code might be combined to above code. + for (String iface : snapshot.getLinkProperties().getAllInterfaceNames()) { + // baseIface has been handled, so ignore it. + if (TextUtils.equals(baseIface, iface)) continue; + if (iface != null) { + findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident); + findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident); + if (isMobile) { + mobileIfaces.add(iface); + } + if (isWifi) { + wifiIfaces.add(iface); + } + + mStatsFactory.noteStackedIface(iface, baseIface); + } + } + } + + mMobileIfaces = mobileIfaces.toArray(new String[0]); + mWifiIfaces = wifiIfaces.toArray(new String[0]); + // TODO (b/192758557): Remove debug log. + if (CollectionUtils.contains(mMobileIfaces, null)) { + throw new NullPointerException( + "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces)); + } + if (CollectionUtils.contains(mWifiIfaces, null)) { + throw new NullPointerException( + "null element in mWifiIfaces: " + Arrays.toString(mWifiIfaces)); + } + } + + private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) { + if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR"); + } + + final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier(); + if (spec instanceof TelephonyNetworkSpecifier) { + return ((TelephonyNetworkSpecifier) spec).getSubscriptionId(); + } else { + Log.wtf(TAG, "getSubIdForState invalid NetworkSpecifier"); + return INVALID_SUBSCRIPTION_ID; + } + } + + /** + * For networks with {@code TRANSPORT_CELLULAR}, get ratType that was obtained through + * {@link PhoneStateListener}. Otherwise, return 0 given that other networks with different + * transport types do not actually fill this value. + */ + private int getRatTypeForStateSnapshot(@NonNull NetworkStateSnapshot state) { + if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return 0; + } + + return mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(state.getSubscriberId()); + } + + private static NetworkIdentitySet findOrCreateNetworkIdentitySet( + ArrayMap map, K key) { + NetworkIdentitySet ident = map.get(key); + if (ident == null) { + ident = new NetworkIdentitySet(); + map.put(key, ident); + } + return ident; + } + + @GuardedBy("mStatsLock") + private void recordSnapshotLocked(long currentTime) throws RemoteException { + // snapshot and record current counters; read UID stats first to + // avoid over counting dev stats. + Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotUid"); + final NetworkStats uidSnapshot = getNetworkStatsUidDetail(INTERFACES_ALL); + Trace.traceEnd(TRACE_TAG_NETWORK); + Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotXt"); + final NetworkStats xtSnapshot = readNetworkStatsSummaryXt(); + Trace.traceEnd(TRACE_TAG_NETWORK); + Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotDev"); + final NetworkStats devSnapshot = readNetworkStatsSummaryDev(); + Trace.traceEnd(TRACE_TAG_NETWORK); + + // Snapshot for dev/xt stats from all custom stats providers. Counts per-interface data + // from stats providers that isn't already counted by dev and XT stats. + Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotStatsProvider"); + final NetworkStats providersnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE); + Trace.traceEnd(TRACE_TAG_NETWORK); + xtSnapshot.combineAllValues(providersnapshot); + devSnapshot.combineAllValues(providersnapshot); + + // For xt/dev, we pass a null VPN array because usage is aggregated by UID, so VPN traffic + // can't be reattributed to responsible apps. + Trace.traceBegin(TRACE_TAG_NETWORK, "recordDev"); + mDevRecorder.recordSnapshotLocked(devSnapshot, mActiveIfaces, currentTime); + Trace.traceEnd(TRACE_TAG_NETWORK); + Trace.traceBegin(TRACE_TAG_NETWORK, "recordXt"); + mXtRecorder.recordSnapshotLocked(xtSnapshot, mActiveIfaces, currentTime); + Trace.traceEnd(TRACE_TAG_NETWORK); + + // For per-UID stats, pass the VPN info so VPN traffic is reattributed to responsible apps. + Trace.traceBegin(TRACE_TAG_NETWORK, "recordUid"); + mUidRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime); + Trace.traceEnd(TRACE_TAG_NETWORK); + Trace.traceBegin(TRACE_TAG_NETWORK, "recordUidTag"); + mUidTagRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime); + Trace.traceEnd(TRACE_TAG_NETWORK); + + // We need to make copies of member fields that are sent to the observer to avoid + // a race condition between the service handler thread and the observer's + mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces), + new ArrayMap<>(mActiveUidIfaces), currentTime); + } + + /** + * Bootstrap initial stats snapshot, usually during {@link #systemReady()} + * so we have baseline values without double-counting. + */ + @GuardedBy("mStatsLock") + private void bootstrapStatsLocked() { + final long currentTime = mClock.millis(); + + try { + recordSnapshotLocked(currentTime); + } catch (IllegalStateException e) { + Log.w(TAG, "problem reading network stats: " + e); + } catch (RemoteException e) { + // ignored; service lives in system_server + } + } + + private void performPoll(int flags) { + synchronized (mStatsLock) { + mWakeLock.acquire(); + + try { + performPollLocked(flags); + } finally { + mWakeLock.release(); + } + } + } + + /** + * Periodic poll operation, reading current statistics and recording into + * {@link NetworkStatsHistory}. + */ + @GuardedBy("mStatsLock") + private void performPollLocked(int flags) { + if (!mSystemReady) return; + if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")"); + Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked"); + + final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0; + final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0; + final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0; + + performPollFromProvidersLocked(); + + // TODO: consider marking "untrusted" times in historical stats + final long currentTime = mClock.millis(); + + try { + recordSnapshotLocked(currentTime); + } catch (IllegalStateException e) { + Log.wtf(TAG, "problem reading network stats", e); + return; + } catch (RemoteException e) { + // ignored; service lives in system_server + return; + } + + // persist any pending data depending on requested flags + Trace.traceBegin(TRACE_TAG_NETWORK, "[persisting]"); + if (persistForce) { + mDevRecorder.forcePersistLocked(currentTime); + mXtRecorder.forcePersistLocked(currentTime); + mUidRecorder.forcePersistLocked(currentTime); + mUidTagRecorder.forcePersistLocked(currentTime); + } else { + if (persistNetwork) { + mDevRecorder.maybePersistLocked(currentTime); + mXtRecorder.maybePersistLocked(currentTime); + } + if (persistUid) { + mUidRecorder.maybePersistLocked(currentTime); + mUidTagRecorder.maybePersistLocked(currentTime); + } + } + Trace.traceEnd(TRACE_TAG_NETWORK); + + if (mSettings.getSampleEnabled()) { + // sample stats after each full poll + performSampleLocked(); + } + + // finally, dispatch updated event to any listeners + mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED)); + + Trace.traceEnd(TRACE_TAG_NETWORK); + } + + @GuardedBy("mStatsLock") + private void performPollFromProvidersLocked() { + // Request asynchronous stats update from all providers for next poll. And wait a bit of + // time to allow providers report-in given that normally binder call should be fast. Note + // that size of list might be changed because addition/removing at the same time. For + // addition, the stats of the missed provider can only be collected in next poll; + // for removal, wait might take up to MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS + // once that happened. + // TODO: request with a valid token. + Trace.traceBegin(TRACE_TAG_NETWORK, "provider.requestStatsUpdate"); + final int registeredCallbackCount = mStatsProviderCbList.size(); + mStatsProviderSem.drainPermits(); + invokeForAllStatsProviderCallbacks( + (cb) -> cb.mProvider.onRequestStatsUpdate(0 /* unused */)); + try { + mStatsProviderSem.tryAcquire(registeredCallbackCount, + MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Strictly speaking it's possible a provider happened to deliver between the timeout + // and the log, and that doesn't matter too much as this is just a debug log. + Log.d(TAG, "requestStatsUpdate - providers responded " + + mStatsProviderSem.availablePermits() + + "/" + registeredCallbackCount + " : " + e); + } + Trace.traceEnd(TRACE_TAG_NETWORK); + } + + /** + * Sample recent statistics summary into {@link EventLog}. + */ + @GuardedBy("mStatsLock") + private void performSampleLocked() { + // TODO: migrate trustedtime fixes to separate binary log events + final long currentTime = mClock.millis(); + + NetworkTemplate template; + NetworkStats.Entry devTotal; + NetworkStats.Entry xtTotal; + NetworkStats.Entry uidTotal; + + // collect mobile sample + template = buildTemplateMobileWildcard(); + devTotal = mDevRecorder.getTotalSinceBootLocked(template); + xtTotal = mXtRecorder.getTotalSinceBootLocked(template); + uidTotal = mUidRecorder.getTotalSinceBootLocked(template); + + EventLog.writeEvent(LOG_TAG_NETSTATS_MOBILE_SAMPLE, + devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets, + xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets, + uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets, + currentTime); + + // collect wifi sample + template = buildTemplateWifiWildcard(); + devTotal = mDevRecorder.getTotalSinceBootLocked(template); + xtTotal = mXtRecorder.getTotalSinceBootLocked(template); + uidTotal = mUidRecorder.getTotalSinceBootLocked(template); + + EventLog.writeEvent(LOG_TAG_NETSTATS_WIFI_SAMPLE, + devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets, + xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets, + uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets, + currentTime); + } + + // deleteKernelTagData can ignore ENOENT; otherwise we should log an error + private void logErrorIfNotErrNoent(final ErrnoException e, final String msg) { + if (e.errno != ENOENT) Log.e(TAG, msg, e); + } + + private void deleteStatsMapTagData( + IBpfMap statsMap, int uid) { + try { + statsMap.forEach((key, value) -> { + if (key.uid == uid) { + try { + statsMap.deleteEntry(key); + } catch (ErrnoException e) { + logErrorIfNotErrNoent(e, "Failed to delete data(uid = " + key.uid + ")"); + } + } + }); + } catch (ErrnoException e) { + Log.e(TAG, "FAILED to delete tag data from stats map", e); + } + } + + /** + * Deletes uid tag data from CookieTagMap, StatsMapA, StatsMapB, and UidStatsMap + * @param uid + */ + private void deleteKernelTagData(int uid) { + try { + mCookieTagMap.forEach((key, value) -> { + // If SkDestroyListener deletes the socket tag while this code is running, + // forEach will either restart iteration from the beginning or return null, + // depending on when the deletion happens. + // If it returns null, continue iteration to delete the data and in fact it would + // just iterate from first key because BpfMap#getNextKey would return first key + // if the current key is not exist. + if (value != null && value.uid == uid) { + try { + mCookieTagMap.deleteEntry(key); + } catch (ErrnoException e) { + logErrorIfNotErrNoent(e, "Failed to delete data(cookie = " + key + ")"); + } + } + }); + } catch (ErrnoException e) { + Log.e(TAG, "Failed to delete tag data from cookie tag map", e); + } + + deleteStatsMapTagData(mStatsMapA, uid); + deleteStatsMapTagData(mStatsMapB, uid); + + try { + mUidCounterSetMap.deleteEntry(new U32(uid)); + } catch (ErrnoException e) { + logErrorIfNotErrNoent(e, "Failed to delete tag data from uid counter set map"); + } + + try { + mAppUidStatsMap.deleteEntry(new UidStatsMapKey(uid)); + } catch (ErrnoException e) { + logErrorIfNotErrNoent(e, "Failed to delete tag data from app uid stats map"); + } + } + + /** + * Clean up {@link #mUidRecorder} after UID is removed. + */ + @GuardedBy("mStatsLock") + private void removeUidsLocked(int... uids) { + if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids)); + + // Perform one last poll before removing + performPollLocked(FLAG_PERSIST_ALL); + + mUidRecorder.removeUidsLocked(uids); + mUidTagRecorder.removeUidsLocked(uids); + + // Clear kernel stats associated with UID + for (int uid : uids) { + deleteKernelTagData(uid); + } + } + + /** + * Clean up {@link #mUidRecorder} after user is removed. + */ + @GuardedBy("mStatsLock") + private void removeUserLocked(@NonNull UserHandle userHandle) { + if (LOGV) Log.v(TAG, "removeUserLocked() for UserHandle=" + userHandle); + + // Build list of UIDs that we should clean up + final ArrayList uids = new ArrayList<>(); + final List apps = mContext.getPackageManager().getInstalledApplications( + PackageManager.MATCH_ANY_USER + | PackageManager.MATCH_DISABLED_COMPONENTS); + for (ApplicationInfo app : apps) { + final int uid = userHandle.getUid(app.uid); + uids.add(uid); + } + + removeUidsLocked(CollectionUtils.toIntArray(uids)); + } + + /** + * Set the warning and limit to all registered custom network stats providers. + * Note that invocation of any interface will be sent to all providers. + */ + public void setStatsProviderWarningAndLimitAsync( + @NonNull String iface, long warning, long limit) { + PermissionUtils.enforceNetworkStackPermission(mContext); + if (LOGV) { + Log.v(TAG, "setStatsProviderWarningAndLimitAsync(" + + iface + "," + warning + "," + limit + ")"); + } + invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetWarningAndLimit(iface, + warning, limit)); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) { + if (!PermissionUtils.checkDumpPermission(mContext, TAG, rawWriter)) return; + + long duration = DateUtils.DAY_IN_MILLIS; + final HashSet argSet = new HashSet(); + for (String arg : args) { + argSet.add(arg); + + if (arg.startsWith("--duration=")) { + try { + duration = Long.parseLong(arg.substring(11)); + } catch (NumberFormatException ignored) { + } + } + } + + // usage: dumpsys netstats --full --uid --tag --poll --checkin + final boolean poll = argSet.contains("--poll") || argSet.contains("poll"); + final boolean checkin = argSet.contains("--checkin"); + final boolean fullHistory = argSet.contains("--full") || argSet.contains("full"); + final boolean includeUid = argSet.contains("--uid") || argSet.contains("detail"); + final boolean includeTag = argSet.contains("--tag") || argSet.contains("detail"); + + final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, " "); + + synchronized (mStatsLock) { + if (args.length > 0 && "--proto".equals(args[0])) { + // In this case ignore all other arguments. + dumpProtoLocked(fd); + return; + } + + if (poll) { + performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE); + pw.println("Forced poll"); + return; + } + + if (checkin) { + final long end = System.currentTimeMillis(); + final long start = end - duration; + + pw.print("v1,"); + pw.print(start / SECOND_IN_MILLIS); pw.print(','); + pw.print(end / SECOND_IN_MILLIS); pw.println(); + + pw.println("xt"); + mXtRecorder.dumpCheckin(rawWriter, start, end); + + if (includeUid) { + pw.println("uid"); + mUidRecorder.dumpCheckin(rawWriter, start, end); + } + if (includeTag) { + pw.println("tag"); + mUidTagRecorder.dumpCheckin(rawWriter, start, end); + } + return; + } + + pw.println("Configs:"); + pw.increaseIndent(); + pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled()); + pw.println(); + pw.decreaseIndent(); + + pw.println("Active interfaces:"); + pw.increaseIndent(); + for (int i = 0; i < mActiveIfaces.size(); i++) { + pw.print("iface", mActiveIfaces.keyAt(i)); + pw.print("ident", mActiveIfaces.valueAt(i)); + pw.println(); + } + pw.decreaseIndent(); + + pw.println("Active UID interfaces:"); + pw.increaseIndent(); + for (int i = 0; i < mActiveUidIfaces.size(); i++) { + pw.print("iface", mActiveUidIfaces.keyAt(i)); + pw.print("ident", mActiveUidIfaces.valueAt(i)); + pw.println(); + } + pw.decreaseIndent(); + + // Get the top openSession callers + final SparseIntArray calls; + synchronized (mOpenSessionCallsPerUid) { + calls = mOpenSessionCallsPerUid.clone(); + } + + final int N = calls.size(); + final long[] values = new long[N]; + for (int j = 0; j < N; j++) { + values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j); + } + Arrays.sort(values); + + pw.println("Top openSession callers (uid=count):"); + pw.increaseIndent(); + final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT); + for (int j = N - 1; j >= end; j--) { + final int uid = (int) (values[j] & 0xffffffff); + final int count = (int) (values[j] >> 32); + pw.print(uid); pw.print("="); pw.println(count); + } + pw.decreaseIndent(); + pw.println(); + + pw.println("Stats Providers:"); + pw.increaseIndent(); + invokeForAllStatsProviderCallbacks((cb) -> { + pw.println(cb.mTag + " Xt:"); + pw.increaseIndent(); + pw.print(cb.getCachedStats(STATS_PER_IFACE).toString()); + pw.decreaseIndent(); + if (includeUid) { + pw.println(cb.mTag + " Uid:"); + pw.increaseIndent(); + pw.print(cb.getCachedStats(STATS_PER_UID).toString()); + pw.decreaseIndent(); + } + }); + pw.decreaseIndent(); + + pw.println("Dev stats:"); + pw.increaseIndent(); + mDevRecorder.dumpLocked(pw, fullHistory); + pw.decreaseIndent(); + + pw.println("Xt stats:"); + pw.increaseIndent(); + mXtRecorder.dumpLocked(pw, fullHistory); + pw.decreaseIndent(); + + if (includeUid) { + pw.println("UID stats:"); + pw.increaseIndent(); + mUidRecorder.dumpLocked(pw, fullHistory); + pw.decreaseIndent(); + } + + if (includeTag) { + pw.println("UID tag stats:"); + pw.increaseIndent(); + mUidTagRecorder.dumpLocked(pw, fullHistory); + pw.decreaseIndent(); + } + } + } + + @GuardedBy("mStatsLock") + private void dumpProtoLocked(FileDescriptor fd) { + final ProtoOutputStream proto = new ProtoOutputStream(new FileOutputStream(fd)); + + // TODO Right now it writes all history. Should it limit to the "since-boot" log? + + dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_INTERFACES, + mActiveIfaces); + dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_UID_INTERFACES, + mActiveUidIfaces); + mDevRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.DEV_STATS); + mXtRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.XT_STATS); + mUidRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.UID_STATS); + mUidTagRecorder.dumpDebugLocked(proto, + NetworkStatsServiceDumpProto.UID_TAG_STATS); + + proto.flush(); + } + + private static void dumpInterfaces(ProtoOutputStream proto, long tag, + ArrayMap ifaces) { + for (int i = 0; i < ifaces.size(); i++) { + final long start = proto.start(tag); + + proto.write(NetworkInterfaceProto.INTERFACE, ifaces.keyAt(i)); + ifaces.valueAt(i).dumpDebug(proto, NetworkInterfaceProto.IDENTITIES); + + proto.end(start); + } + } + + private NetworkStats readNetworkStatsSummaryDev() { + try { + return mStatsFactory.readNetworkStatsSummaryDev(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private NetworkStats readNetworkStatsSummaryXt() { + try { + return mStatsFactory.readNetworkStatsSummaryXt(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private NetworkStats readNetworkStatsUidDetail(int uid, String[] ifaces, int tag) { + try { + return mStatsFactory.readNetworkStatsDetail(uid, ifaces, tag); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * Return snapshot of current UID statistics, including any + * {@link TrafficStats#UID_TETHERING}, video calling data usage, and {@link #mUidOperations} + * values. + * + * @param ifaces A list of interfaces the stats should be restricted to, or + * {@link NetworkStats#INTERFACES_ALL}. + */ + private NetworkStats getNetworkStatsUidDetail(String[] ifaces) + throws RemoteException { + final NetworkStats uidSnapshot = readNetworkStatsUidDetail(UID_ALL, ifaces, TAG_ALL); + + // fold tethering stats and operations into uid snapshot + final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_UID); + tetherSnapshot.filter(UID_ALL, ifaces, TAG_ALL); + mStatsFactory.apply464xlatAdjustments(uidSnapshot, tetherSnapshot); + uidSnapshot.combineAllValues(tetherSnapshot); + + // get a stale copy of uid stats snapshot provided by providers. + final NetworkStats providerStats = getNetworkStatsFromProviders(STATS_PER_UID); + providerStats.filter(UID_ALL, ifaces, TAG_ALL); + mStatsFactory.apply464xlatAdjustments(uidSnapshot, providerStats); + uidSnapshot.combineAllValues(providerStats); + + uidSnapshot.combineAllValues(mUidOperations); + + return uidSnapshot; + } + + /** + * Return snapshot of current non-offloaded tethering statistics. Will return empty + * {@link NetworkStats} if any problems are encountered, or queried by {@code STATS_PER_IFACE} + * since it is already included by {@link #nativeGetIfaceStat}. + * See {@code OffloadTetheringStatsProvider} for offloaded tethering stats. + */ + // TODO: Remove this by implementing {@link NetworkStatsProvider} for non-offloaded + // tethering stats. + private @NonNull NetworkStats getNetworkStatsTethering(int how) throws RemoteException { + // We only need to return per-UID stats. Per-device stats are already counted by + // interface counters. + if (how != STATS_PER_UID) { + return new NetworkStats(SystemClock.elapsedRealtime(), 0); + } + + final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1); + try { + final TetherStatsParcel[] tetherStatsParcels = mNetd.tetherGetStats(); + for (TetherStatsParcel tetherStats : tetherStatsParcels) { + try { + stats.combineValues(new NetworkStats.Entry(tetherStats.iface, UID_TETHERING, + SET_DEFAULT, TAG_NONE, tetherStats.rxBytes, tetherStats.rxPackets, + tetherStats.txBytes, tetherStats.txPackets, 0L)); + } catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalStateException("invalid tethering stats " + e); + } + } + } catch (IllegalStateException e) { + Log.wtf(TAG, "problem reading network stats", e); + } + return stats; + } + + // TODO: It is copied from ConnectivityService, consider refactor these check permission + // functions to a proper util. + private boolean checkAnyPermissionOf(String... permissions) { + for (String permission : permissions) { + if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + return true; + } + } + return false; + } + + private void enforceAnyPermissionOf(String... permissions) { + if (!checkAnyPermissionOf(permissions)) { + throw new SecurityException("Requires one of the following permissions: " + + String.join(", ", permissions) + "."); + } + } + + /** + * Registers a custom provider of {@link android.net.NetworkStats} to combine the network + * statistics that cannot be seen by the kernel to system. To unregister, invoke the + * {@code unregister()} of the returned callback. + * + * @param tag a human readable identifier of the custom network stats provider. + * @param provider the {@link INetworkStatsProvider} binder corresponding to the + * {@link NetworkStatsProvider} to be registered. + * + * @return a {@link INetworkStatsProviderCallback} binder + * interface, which can be used to report events to the system. + */ + public @NonNull INetworkStatsProviderCallback registerNetworkStatsProvider( + @NonNull String tag, @NonNull INetworkStatsProvider provider) { + enforceAnyPermissionOf(NETWORK_STATS_PROVIDER, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + Objects.requireNonNull(provider, "provider is null"); + Objects.requireNonNull(tag, "tag is null"); + final NetworkPolicyManager netPolicyManager = mContext + .getSystemService(NetworkPolicyManager.class); + try { + NetworkStatsProviderCallbackImpl callback = new NetworkStatsProviderCallbackImpl( + tag, provider, mStatsProviderSem, mAlertObserver, + mStatsProviderCbList, netPolicyManager); + mStatsProviderCbList.add(callback); + Log.d(TAG, "registerNetworkStatsProvider from " + callback.mTag + " uid/pid=" + + getCallingUid() + "/" + getCallingPid()); + return callback; + } catch (RemoteException e) { + Log.e(TAG, "registerNetworkStatsProvider failed", e); + } + return null; + } + + // Collect stats from local cache of providers. + private @NonNull NetworkStats getNetworkStatsFromProviders(int how) { + final NetworkStats ret = new NetworkStats(0L, 0); + invokeForAllStatsProviderCallbacks((cb) -> ret.combineAllValues(cb.getCachedStats(how))); + return ret; + } + + @FunctionalInterface + private interface ThrowingConsumer { + void accept(S s) throws T; + } + + private void invokeForAllStatsProviderCallbacks( + @NonNull ThrowingConsumer task) { + for (final NetworkStatsProviderCallbackImpl cb : mStatsProviderCbList) { + try { + task.accept(cb); + } catch (RemoteException e) { + Log.e(TAG, "Fail to broadcast to provider: " + cb.mTag, e); + } + } + } + + private static class NetworkStatsProviderCallbackImpl extends INetworkStatsProviderCallback.Stub + implements IBinder.DeathRecipient { + @NonNull final String mTag; + + @NonNull final INetworkStatsProvider mProvider; + @NonNull private final Semaphore mSemaphore; + @NonNull final AlertObserver mAlertObserver; + @NonNull final CopyOnWriteArrayList mStatsProviderCbList; + @NonNull final NetworkPolicyManager mNetworkPolicyManager; + + @NonNull private final Object mProviderStatsLock = new Object(); + + @GuardedBy("mProviderStatsLock") + // Track STATS_PER_IFACE and STATS_PER_UID separately. + private final NetworkStats mIfaceStats = new NetworkStats(0L, 0); + @GuardedBy("mProviderStatsLock") + private final NetworkStats mUidStats = new NetworkStats(0L, 0); + + NetworkStatsProviderCallbackImpl( + @NonNull String tag, @NonNull INetworkStatsProvider provider, + @NonNull Semaphore semaphore, + @NonNull AlertObserver alertObserver, + @NonNull CopyOnWriteArrayList cbList, + @NonNull NetworkPolicyManager networkPolicyManager) + throws RemoteException { + mTag = tag; + mProvider = provider; + mProvider.asBinder().linkToDeath(this, 0); + mSemaphore = semaphore; + mAlertObserver = alertObserver; + mStatsProviderCbList = cbList; + mNetworkPolicyManager = networkPolicyManager; + } + + @NonNull + public NetworkStats getCachedStats(int how) { + synchronized (mProviderStatsLock) { + NetworkStats stats; + switch (how) { + case STATS_PER_IFACE: + stats = mIfaceStats; + break; + case STATS_PER_UID: + stats = mUidStats; + break; + default: + throw new IllegalArgumentException("Invalid type: " + how); + } + // Callers might be able to mutate the returned object. Return a defensive copy + // instead of local reference. + return stats.clone(); + } + } + + @Override + public void notifyStatsUpdated(int token, @Nullable NetworkStats ifaceStats, + @Nullable NetworkStats uidStats) { + // TODO: 1. Use token to map ifaces to correct NetworkIdentity. + // 2. Store the difference and store it directly to the recorder. + synchronized (mProviderStatsLock) { + if (ifaceStats != null) mIfaceStats.combineAllValues(ifaceStats); + if (uidStats != null) mUidStats.combineAllValues(uidStats); + } + mSemaphore.release(); + } + + @Override + public void notifyAlertReached() throws RemoteException { + // This binder object can only have been obtained by a process that holds + // NETWORK_STATS_PROVIDER. Thus, no additional permission check is required. + BinderUtils.withCleanCallingIdentity(() -> + mAlertObserver.onQuotaLimitReached(LIMIT_GLOBAL_ALERT, null /* unused */)); + } + + @Override + public void notifyWarningReached() { + Log.d(TAG, mTag + ": notifyWarningReached"); + BinderUtils.withCleanCallingIdentity(() -> + mNetworkPolicyManager.notifyStatsProviderWarningReached()); + } + + @Override + public void notifyLimitReached() { + Log.d(TAG, mTag + ": notifyLimitReached"); + BinderUtils.withCleanCallingIdentity(() -> + mNetworkPolicyManager.notifyStatsProviderLimitReached()); + } + + @Override + public void binderDied() { + Log.d(TAG, mTag + ": binderDied"); + mStatsProviderCbList.remove(this); + } + + @Override + public void unregister() { + Log.d(TAG, mTag + ": unregister"); + mStatsProviderCbList.remove(this); + } + + } + + private void assertSystemReady() { + if (!mSystemReady) { + throw new IllegalStateException("System not ready"); + } + } + + private class DropBoxNonMonotonicObserver implements NonMonotonicObserver { + @Override + public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right, + int rightIndex, String cookie) { + Log.w(TAG, "Found non-monotonic values; saving to dropbox"); + + // record error for debugging + final StringBuilder builder = new StringBuilder(); + builder.append("found non-monotonic " + cookie + " values at left[" + leftIndex + + "] - right[" + rightIndex + "]\n"); + builder.append("left=").append(left).append('\n'); + builder.append("right=").append(right).append('\n'); + + mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR, + builder.toString()); + } + + @Override + public void foundNonMonotonic( + NetworkStats stats, int statsIndex, String cookie) { + Log.w(TAG, "Found non-monotonic values; saving to dropbox"); + + final StringBuilder builder = new StringBuilder(); + builder.append("Found non-monotonic " + cookie + " values at [" + statsIndex + "]\n"); + builder.append("stats=").append(stats).append('\n'); + + mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR, + builder.toString()); + } + } + + /** + * Default external settings that read from + * {@link android.provider.Settings.Global}. + */ + private static class DefaultNetworkStatsSettings implements NetworkStatsSettings { + DefaultNetworkStatsSettings() {} + + @Override + public long getPollInterval() { + return 30 * MINUTE_IN_MILLIS; + } + @Override + public long getPollDelay() { + return DEFAULT_PERFORM_POLL_DELAY_MS; + } + @Override + public long getGlobalAlertBytes(long def) { + return def; + } + @Override + public boolean getSampleEnabled() { + return true; + } + @Override + public boolean getAugmentEnabled() { + return true; + } + @Override + public boolean getCombineSubtypeEnabled() { + return false; + } + @Override + public Config getDevConfig() { + return new Config(HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS); + } + @Override + public Config getXtConfig() { + return getDevConfig(); + } + @Override + public Config getUidConfig() { + return new Config(2 * HOUR_IN_MILLIS, 15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS); + } + @Override + public Config getUidTagConfig() { + return new Config(2 * HOUR_IN_MILLIS, 5 * DAY_IN_MILLIS, 15 * DAY_IN_MILLIS); + } + @Override + public long getDevPersistBytes(long def) { + return def; + } + @Override + public long getXtPersistBytes(long def) { + return def; + } + @Override + public long getUidPersistBytes(long def) { + return def; + } + @Override + public long getUidTagPersistBytes(long def) { + return def; + } + } + + private static native long nativeGetTotalStat(int type); + private static native long nativeGetIfaceStat(String iface, int type); + private static native long nativeGetUidStat(int uid, int type); +} diff --git a/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java b/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java new file mode 100644 index 0000000000..65ccd20072 --- /dev/null +++ b/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java @@ -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 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 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> 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 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 getActiveSubIdList(@NonNull SubscriptionManager subscriptionManager) { + final ArrayList 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); + } + } +} diff --git a/service-t/src/com/android/server/net/StatsMapKey.java b/service-t/src/com/android/server/net/StatsMapKey.java new file mode 100644 index 0000000000..ea8d836383 --- /dev/null +++ b/service-t/src/com/android/server/net/StatsMapKey.java @@ -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; + } +} diff --git a/service-t/src/com/android/server/net/StatsMapValue.java b/service-t/src/com/android/server/net/StatsMapValue.java new file mode 100644 index 0000000000..48f26ce686 --- /dev/null +++ b/service-t/src/com/android/server/net/StatsMapValue.java @@ -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; + } +} diff --git a/service-t/src/com/android/server/net/UidStatsMapKey.java b/service-t/src/com/android/server/net/UidStatsMapKey.java new file mode 100644 index 0000000000..2849f94c93 --- /dev/null +++ b/service-t/src/com/android/server/net/UidStatsMapKey.java @@ -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; + } +} diff --git a/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java b/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java new file mode 100644 index 0000000000..e2253a2151 --- /dev/null +++ b/tests/unit/java/com/android/server/NativeDaemonConnectorTest.java @@ -0,0 +1,97 @@ +/* + * 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 static com.android.server.NativeDaemonConnector.appendEscaped; +import static com.android.server.NativeDaemonConnector.makeCommand; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; + +import com.android.server.NativeDaemonConnector.SensitiveArg; + +/** + * Tests for {@link NativeDaemonConnector}. + */ +@MediumTest +public class NativeDaemonConnectorTest extends AndroidTestCase { + private static final String TAG = "NativeDaemonConnectorTest"; + + public void testArgumentNormal() throws Exception { + final StringBuilder builder = new StringBuilder(); + + builder.setLength(0); + appendEscaped(builder, ""); + assertEquals("", builder.toString()); + + builder.setLength(0); + appendEscaped(builder, "foo"); + assertEquals("foo", builder.toString()); + + builder.setLength(0); + appendEscaped(builder, "foo\"bar"); + assertEquals("foo\\\"bar", builder.toString()); + + builder.setLength(0); + appendEscaped(builder, "foo\\bar\\\"baz"); + assertEquals("foo\\\\bar\\\\\\\"baz", builder.toString()); + } + + public void testArgumentWithSpaces() throws Exception { + final StringBuilder builder = new StringBuilder(); + + builder.setLength(0); + appendEscaped(builder, "foo bar"); + assertEquals("\"foo bar\"", builder.toString()); + + builder.setLength(0); + appendEscaped(builder, "foo\"bar\\baz foo"); + assertEquals("\"foo\\\"bar\\\\baz foo\"", builder.toString()); + } + + public void testArgumentWithUtf() throws Exception { + final StringBuilder builder = new StringBuilder(); + + builder.setLength(0); + appendEscaped(builder, "caf\u00E9 c\u00F6ffee"); + assertEquals("\"caf\u00E9 c\u00F6ffee\"", builder.toString()); + } + + public void testSensitiveArgs() throws Exception { + final StringBuilder rawBuilder = new StringBuilder(); + final StringBuilder logBuilder = new StringBuilder(); + + rawBuilder.setLength(0); + logBuilder.setLength(0); + makeCommand(rawBuilder, logBuilder, 1, "foo", "bar", "baz"); + assertEquals("1 foo bar baz\0", rawBuilder.toString()); + assertEquals("1 foo bar baz", logBuilder.toString()); + + rawBuilder.setLength(0); + logBuilder.setLength(0); + makeCommand(rawBuilder, logBuilder, 1, "foo", new SensitiveArg("bar"), "baz"); + assertEquals("1 foo bar baz\0", rawBuilder.toString()); + assertEquals("1 foo [scrubbed] baz", logBuilder.toString()); + + rawBuilder.setLength(0); + logBuilder.setLength(0); + makeCommand(rawBuilder, logBuilder, 1, "foo", new SensitiveArg("foo bar"), "baz baz", + new SensitiveArg("wat")); + assertEquals("1 foo \"foo bar\" \"baz baz\" wat\0", rawBuilder.toString()); + assertEquals("1 foo [scrubbed] \"baz baz\" [scrubbed]", logBuilder.toString()); + } +} diff --git a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java new file mode 100644 index 0000000000..ad0be5862c --- /dev/null +++ b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +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.util.ArrayMap; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Unit tests for {@link IpConfigStore} + */ +@RunWith(AndroidJUnit4.class) +public class IpConfigStoreTest { + private static final int KEY_CONFIG = 17; + private static final String IFACE_1 = "eth0"; + private static final String IFACE_2 = "eth1"; + private static final String IP_ADDR_1 = "192.168.1.10/24"; + private static final String IP_ADDR_2 = "192.168.1.20/24"; + private static final String DNS_IP_ADDR_1 = "1.2.3.4"; + private static final String DNS_IP_ADDR_2 = "5.6.7.8"; + + @Test + public void backwardCompatibility2to3() throws IOException { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(byteStream); + + final IpConfiguration expectedConfig = + newIpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null); + + // Emulate writing to old format. + writeDhcpConfigV2(outputStream, KEY_CONFIG, expectedConfig); + + InputStream in = new ByteArrayInputStream(byteStream.toByteArray()); + ArrayMap configurations = IpConfigStore.readIpConfigurations(in); + + assertNotNull(configurations); + assertEquals(1, configurations.size()); + IpConfiguration actualConfig = configurations.get(String.valueOf(KEY_CONFIG)); + assertNotNull(actualConfig); + assertEquals(expectedConfig, actualConfig); + } + + @Test + public void staticIpMultiNetworks() throws Exception { + final ArrayList dnsServers = new ArrayList<>(); + dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_1)); + dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_2)); + final StaticIpConfiguration staticIpConfiguration1 = new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress(IP_ADDR_1)) + .setDnsServers(dnsServers).build(); + final StaticIpConfiguration staticIpConfiguration2 = new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress(IP_ADDR_2)) + .setDnsServers(dnsServers).build(); + + ProxyInfo proxyInfo = + ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2")); + + IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC, + ProxySettings.STATIC, staticIpConfiguration1, proxyInfo); + IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC, + ProxySettings.STATIC, staticIpConfiguration2, proxyInfo); + + ArrayMap expectedNetworks = new ArrayMap<>(); + expectedNetworks.put(IFACE_1, expectedConfig1); + expectedNetworks.put(IFACE_2, expectedConfig2); + + MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite(); + IpConfigStore store = new IpConfigStore(writer); + store.writeIpConfigurations("file/path/not/used/", expectedNetworks); + + InputStream in = new ByteArrayInputStream(writer.byteStream.toByteArray()); + ArrayMap actualNetworks = IpConfigStore.readIpConfigurations(in); + assertNotNull(actualNetworks); + assertEquals(2, actualNetworks.size()); + assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1)); + assertEquals(expectedNetworks.get(IFACE_2), actualNetworks.get(IFACE_2)); + } + + private IpConfiguration newIpConfiguration(IpAssignment ipAssignment, + ProxySettings proxySettings, StaticIpConfiguration staticIpConfig, ProxyInfo info) { + final IpConfiguration config = new IpConfiguration(); + config.setIpAssignment(ipAssignment); + config.setProxySettings(proxySettings); + config.setStaticIpConfiguration(staticIpConfig); + config.setHttpProxy(info); + return config; + } + + // This is simplified snapshot of code that was used to store values in V2 format (key as int). + private static void writeDhcpConfigV2(DataOutputStream out, int configKey, + IpConfiguration config) throws IOException { + out.writeInt(2); // VERSION 2 + switch (config.getIpAssignment()) { + case DHCP: + out.writeUTF("ipAssignment"); + out.writeUTF(config.getIpAssignment().toString()); + break; + default: + fail("Not supported in test environment"); + } + + out.writeUTF("id"); + out.writeInt(configKey); + out.writeUTF("eos"); + } + + /** Synchronously writes into given byte steam */ + private static class MockedDelayedDiskWrite extends DelayedDiskWrite { + final ByteArrayOutputStream mByteStream = new ByteArrayOutputStream(); + + @Override + public void write(String filePath, Writer w) { + DataOutputStream outputStream = new DataOutputStream(mByteStream); + + try { + w.onWriteCalled(outputStream); + } catch (IOException e) { + fail(); + } + } + } +}