diff options
Diffstat (limited to 'framework-t/src')
53 files changed, 16319 insertions, 0 deletions
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..74fe4bd46c --- /dev/null +++ b/framework-t/src/android/app/usage/NetworkStats.java @@ -0,0 +1,744 @@ +/** + * 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.annotation.Nullable; +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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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:<p /> + * <ul> + * <li>{@link #UID_REMOVED}</li> + * <li>{@link #UID_TETHERING}</li> + * <li>{@link android.os.Process#SYSTEM_UID}</li> + * </ul> + * @return Bucket key. + */ + public int getUid() { + return mUid; + } + + /** + * Tag of the bucket.<p /> + * @return Bucket tag. + */ + public int getTag() { + return mTag; + } + + /** + * Usage state. One of the following values:<p/> + * <ul> + * <li>{@link #STATE_ALL}</li> + * <li>{@link #STATE_DEFAULT}</li> + * <li>{@link #STATE_FOREGROUND}</li> + * </ul> + * @return Usage state. + */ + public @State int getState() { + return mState; + } + + /** + * Metered state. One of the following values:<p/> + * <ul> + * <li>{@link #METERED_ALL}</li> + * <li>{@link #METERED_NO}</li> + * <li>{@link #METERED_YES}</li> + * </ul> + * <p>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:<p/> + * <ul> + * <li>{@link #ROAMING_ALL}</li> + * <li>{@link #ROAMING_NO}</li> + * <li>{@link #ROAMING_YES}</li> + * </ul> + */ + public @Roaming int getRoaming() { + return mRoaming; + } + + /** + * Default network status. One of the following values:<p/> + * <ul> + * <li>{@link #DEFAULT_NETWORK_ALL}</li> + * <li>{@link #DEFAULT_NETWORK_NO}</li> + * <li>{@link #DEFAULT_NETWORK_YES}</li> + * </ul> + */ + 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. If null, the method does + * nothing and returning false. + * @return true if successfully filled the bucket, false otherwise. + */ + public boolean getNextBucket(@Nullable 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<Integer> 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(@Nullable 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(@Nullable 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..f41475bea1 --- /dev/null +++ b/framework-t/src/android/app/usage/NetworkStatsManager.java @@ -0,0 +1,1238 @@ +/** + * 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. + * <p /> + * 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. + * <h3> + * Summary queries + * </h3> + * {@link #querySummaryForDevice} <p /> + * {@link #querySummaryForUser} <p /> + * {@link #querySummary} <p /> + * 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. + * <h3> + * History queries + * </h3> + * {@link #queryDetailsForUid} <p /> + * {@link #queryDetails} <p /> + * 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}. + * <p /> + * <b>NOTE:</b> 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. + * <p /> + * 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. + * <p /> + * 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. + * <p /> + * <b>NOTE:</b> 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. + * <p>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()}. + * <p>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, @Nullable 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. + * <p>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()}. + * <p>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, @Nullable 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. + * <p>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()}. + * <p>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, @Nullable 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. + * <p>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) + */ + @NonNull + @WorkerThread + public NetworkStats queryDetailsForUid(int networkType, @Nullable 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 */ + @NonNull + public NetworkStats queryDetailsForUid(@NonNull 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. + * 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}. + * <p>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. + * <p>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()}. + * <p>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. + * @return Statistics which is described above. + * @throws SecurityException if permissions are insufficient to read network statistics. + */ + @NonNull + @WorkerThread + public NetworkStats queryDetailsForUidTag(int networkType, @Nullable 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}. + * <p>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. + * <p>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()}. + * <p>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 which is described above. + * @throws SecurityException if permissions are insufficient to read network statistics. + */ + @NonNull + @WorkerThread + public NetworkStats queryDetailsForUidTagState(int networkType, @Nullable 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}. + * <p>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}. + * <p>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. + * <p>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()}. + * <p>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, @Nullable 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(client = MODULE_LIBRARIES) + @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(client = MODULE_LIBRARIES) + @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. + * + * <p>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. + * + * <p>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. + * <p>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()}. + * <p>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. + */ + public void registerUsageCallback(int networkType, @Nullable String subscriberId, + long thresholdBytes, @NonNull UsageCallback callback) { + registerUsageCallback(networkType, subscriberId, thresholdBytes, callback, + null /* handler */); + } + + /** + * Registers to receive notifications about data usage on specified networks. + * + * <p>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. + * <p>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()}. + * <p>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, @Nullable String subscriberId, + long thresholdBytes, @NonNull 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(@NonNull 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, @Nullable 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}) + 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}) + public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) { + try { + provider.getProviderCallbackBinderOrThrow().unregister(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + } + + private static NetworkTemplate createTemplate(int networkType, @Nullable 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<Network> defaultNetworks, + @NonNull List<NetworkStateSnapshot> networkStateSnapshots, + @Nullable String activeIface, + @NonNull List<UnderlyingNetworkInfo> 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..d9c9d74032 --- /dev/null +++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java @@ -0,0 +1,93 @@ +/* + * 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.mdns.aidl.IMDns; +import android.net.nsd.INsdManager; +import android.net.nsd.MDnsManager; +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); + } + ); + + SystemServiceRegistry.registerStaticService( + MDnsManager.MDNS_SERVICE, + MDnsManager.class, + (serviceBinder) -> { + IMDns service = IMDns.Stub.asInterface(serviceBinder); + return new MDnsManager(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..f0ff46522d --- /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<DataUsageRequest> CREATOR = + new Creator<DataUsageRequest>() { + @Override + public DataUsageRequest createFromParcel(Parcel in) { + int requestId = in.readInt(); + NetworkTemplate template = in.readParcelable(null, android.net.NetworkTemplate.class); + 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..886d19499c --- /dev/null +++ b/framework-t/src/android/net/EthernetManager.java @@ -0,0 +1,709 @@ +/* + * 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 android.util.ArrayMap; + +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.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 ArrayMap<InterfaceStateListener, IEthernetServiceListener> + mIfaceServiceListeners = new ArrayMap<>(); + @GuardedBy("mListenerLock") + private final ArrayMap<IntConsumer, IEthernetServiceListener> mStateServiceListeners = + new ArrayMap<>(); + final Object mListenerLock = new Object(); + + /** + * 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; + + /** + * 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. + * <p>{@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}. + * <p>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"); + } + + final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() { + @Override + public void onEthernetStateChanged(int state) {} + + @Override + public void onInterfaceStateChanged(String iface, int state, int role, + IpConfiguration configuration) { + executor.execute(() -> + listener.onInterfaceStateChanged(iface, state, role, configuration)); + } + }; + synchronized (mListenerLock) { + addServiceListener(serviceListener); + mIfaceServiceListeners.put(listener, serviceListener); + } + } + + @GuardedBy("mListenerLock") + private void addServiceListener(@NonNull final IEthernetServiceListener listener) { + try { + mService.addListener(listener); + } 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) { + maybeRemoveServiceListener(mIfaceServiceListeners.remove(listener)); + } + } + + @GuardedBy("mListenerLock") + private void maybeRemoveServiceListener(@Nullable final IEthernetServiceListener listener) { + if (listener == null) return; + + try { + mService.removeListener(listener); + } 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. + * + * <p>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<String, EthernetNetworkManagementException> mCallback; + + NetworkInterfaceOutcomeReceiver( + @NonNull final Executor executor, + @NonNull final OutcomeReceiver<String, EthernetNetworkManagementException> + 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<String, EthernetNetworkManagementException> 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}. + * + * The provided {@link OutcomeReceiver} 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 the {@link NetworkCapabilities} are updated on a + * non-automotive device or this function is called 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<String, EthernetNetworkManagementException> 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. An attempt to enable an already-enabled + * interface is ignored. + * The provided {@link OutcomeReceiver} 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<String, EthernetNetworkManagementException> 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 specified interface. If this interface is in use in a connected + * {@link android.net.Network}, then that {@code Network} will be torn down. + * The provided {@link OutcomeReceiver} 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<String, EthernetNetworkManagementException> 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); + final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() { + @Override + public void onEthernetStateChanged(int state) { + executor.execute(() -> listener.accept(state)); + } + + @Override + public void onInterfaceStateChanged(String iface, int state, int role, + IpConfiguration configuration) {} + }; + synchronized (mListenerLock) { + addServiceListener(serviceListener); + mStateServiceListeners.put(listener, serviceListener); + } + } + + /** + * 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) { + maybeRemoveServiceListener(mStateServiceListeners.remove(listener)); + } + } + + /** + * 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<String> 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<EthernetNetworkManagementException> CREATOR = + new Parcelable.Creator<EthernetNetworkManagementException>() { + @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<EthernetNetworkSpecifier> CREATOR = + new Parcelable.Creator<EthernetNetworkSpecifier>() { + 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<EthernetNetworkUpdateRequest> CREATOR = + new Parcelable.Creator<EthernetNetworkUpdateRequest>() { + @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<String> 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. + * + * <p>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 <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the + * Internet Protocol</a> + */ +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. + * + * <p>Valid lengths for this key are {128, 192, 256}. + */ + public static final String CRYPT_AES_CBC = "cbc(aes)"; + + /** + * AES-CTR Encryption/Ciphering Algorithm. + * + * <p>Valid lengths for keying material are {160, 224, 288}. + * + * <p>As per <a href="https://tools.ietf.org/html/rfc3686#section-5.1">RFC3686 (Section + * 5.1)</a>, 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. + * + * <p>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. + * + * <p>@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. <b>This algorithm is not recommended for use in + * new applications and is provided for legacy compatibility with 3gpp infrastructure.</b> + * + * <p>Keys for this algorithm must be 128 bits in length. + * + * <p>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. <b>This algorithm is not recommended for use in + * new applications and is provided for legacy compatibility with 3gpp infrastructure.</b> + * + * <p>Keys for this algorithm must be 160 bits in length. + * + * <p>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. + * + * <p>Keys for this algorithm must be 256 bits in length. + * + * <p>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. + * + * <p>Keys for this algorithm must be 384 bits in length. + * + * <p>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. + * + * <p>Keys for this algorithm must be 512 bits in length. + * + * <p>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. + * + * <p>Keys for this algorithm must be 128 bits in length. + * + * <p>The only valid truncation length is 96 bits. + * + * <p>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. + * + * <p>@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. + * + * <p>Keys for this algorithm must be 128 bits in length. + * + * <p>The only valid truncation length is 96 bits. + * + * <p>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. + * + * <p>@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. + * + * <p>Valid lengths for keying material are {160, 224, 288}. + * + * <p>As per <a href="https://tools.ietf.org/html/rfc4106#section-8.1">RFC4106 (Section + * 8.1)</a>, 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. + * + * <p>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. + * + * <p>Keys for this algorithm must be 288 bits in length. + * + * <p>As per <a href="https://tools.ietf.org/html/rfc7634#section-2">RFC7634 (Section 2)</a>, + * keying material consists of a 256 bit key followed by a 32-bit salt. The salt is fixed per + * security association. + * + * <p>The only valid ICV (truncation) length is 128 bits. + * + * <p>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. + * + * <p>@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<String, Integer> 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<String> 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. + * + * <p>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. + * + * <p>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<IpSecAlgorithm> CREATOR = + new Parcelable.Creator<IpSecAlgorithm>() { + 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. + * + * <p>Some algorithms may not be supported on old devices. Callers MUST check if an algorithm is + * supported before using it. + */ + @NonNull + public static Set<String> getSupportedAlgorithms() { + return ENABLED_ALGOS; + } + + /** @hide */ + @VisibleForTesting + public static Set<String> loadAlgos(Resources systemResources) { + final Set<String> 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<String, Integer> 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..03bb187f11 --- /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 + * + * <p>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 + * + * <p>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(), android.net.Network.class); + mSpiResourceId = in.readInt(); + mEncryption = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader(), android.net.IpSecAlgorithm.class); + mAuthentication = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader(), android.net.IpSecAlgorithm.class); + mAuthenticatedEncryption = + (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader(), android.net.IpSecAlgorithm.class); + 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<IpSecConfig> CREATOR = + new Parcelable.Creator<IpSecConfig>() { + 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. + * + * <p>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 <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the + * Internet Protocol</a> + */ +@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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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}. + * + * <p>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. + * + * <h4>Rekey Procedure</h4> + * + * <p>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. + * + * <p>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. + * + * <p>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}. + * + * <h4>Rekey Procedure</h4> + * + * <p>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. + * + * <p>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. + * + * <p>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}. + * + * <p>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. + * + * <h4>Rekey Procedure</h4> + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>{@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. + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>See {@link UdpEncapsulationSocket} for the proper way to close the returned socket. + * + * <p>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 + * + * <p>IpSecTunnelInterface objects track tunnel interfaces that serve as + * local endpoints for IPsec tunnels. + * + * <p>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 + * + * <p>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 + * + * <p>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. + * + * <p>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. + * + * <p>To migrate IPsec tunnel mode traffic, a caller should: + * + * <ol> + * <li>Update the IpSecTunnelInterface’s underlying network. + * <li>Apply {@link IpSecTransform}(s) with matching addresses to this + * IpSecTunnelInterface. + * </ol> + * + * @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 + * + * <p>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. + * + * <p>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). + * <p>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<IpSecSpiResponse> CREATOR = + new Parcelable.Creator<IpSecSpiResponse>() { + 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. + * + * <p>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 <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the + * Internet Protocol</a> + */ +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 + * <a href="https://tools.ietf.org/html/rfc3948">RFC 3498</a>. + * + * @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. + * + * <p>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). + * + * <p>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 + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>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 + * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>). + * + * <p>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. + * + * <p>This allows IPsec traffic to pass through a NAT. + * + * @see <a href="https://tools.ietf.org/html/rfc3948">RFC 3948, UDP Encapsulation of IPsec + * ESP Packets</a> + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.23">RFC 7296 section 2.23, + * NAT Traversal of IKEv2</a> + * @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}. + * + * <p>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<IpSecTransformResponse> CREATOR = + new Parcelable.Creator<IpSecTransformResponse>() { + 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<IpSecTunnelInterfaceResponse> CREATOR = + new Parcelable.Creator<IpSecTunnelInterfaceResponse>() { + 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..390af82366 --- /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.os.ParcelFileDescriptor.class); + } + + @android.annotation.NonNull + public static final Parcelable.Creator<IpSecUdpEncapResponse> CREATOR = + new Parcelable.Creator<IpSecUdpEncapResponse>() { + 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<String> oemManagedNames = new ArrayList<String>(); + 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..d88408eef0 --- /dev/null +++ b/framework-t/src/android/net/NetworkIdentitySet.java @@ -0,0 +1,231 @@ +/* + * 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<NetworkIdentity> { + 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<NetworkIdentity> 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() && right.isEmpty()) return 0; + 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..c018e91655 --- /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, android.net.Network.class); + mNetworkCapabilities = in.readParcelable(null, android.net.NetworkCapabilities.class); + mLinkProperties = in.readParcelable(null, android.net.LinkProperties.class); + 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<NetworkStateSnapshot> CREATOR = + new Creator<NetworkStateSnapshot>() { + @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..0bb98f8f1f --- /dev/null +++ b/framework-t/src/android/net/NetworkStats.java @@ -0,0 +1,1847 @@ +/* + * 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<NetworkStats.Entry> { + 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. */ + 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<Entry> iterator() { + return new Iterator<Entry>() { + 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<String> ifaces = new HashSet<String>(); + 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<String> 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<String> 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. + * <p> + * If counters have rolled backwards, they are clamped to {@code 0} and + * reported to the given {@link NonMonotonicObserver}. + * @hide + */ + public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right, + NonMonotonicObserver<C> 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. + * <p> + * If counters have rolled backwards, they are clamped to {@code 0} and + * reported to the given {@link NonMonotonicObserver}. + * <p> + * If <var>recycle</var> 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 <C> NetworkStats subtract(NetworkStats left, NetworkStats right, + NonMonotonicObserver<C> 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. + * + * <p>This mutates stacked traffic stats, to account for IPv4/IPv6 header size difference. + * + * <p>UID stats, which are only accounted on the stacked interface, need to be increased + * by 20 bytes/packet to account for translation overhead. + * + * <p>The potential additional overhead of 8 bytes/packet for ip fragments is ignored. + * + * <p>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) + * + * <p>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<String, String> 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. + * + * <p>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<String, String> 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; + } + + /** + * Removes the interface name from all entries. + * This mutates the original structure in place. + * @hide + */ + public void clearInterfaces() { + for (int i = 0; i < size; i++) { + iface[i] = null; + } + } + + /** + * Only keep entries that match all specified filters. + * + * <p>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}. + * + * <p>This mutates the original structure in place. + * @hide + */ + public void filterDebugEntries() { + filter(e -> e.set < SET_DEBUG_START); + } + + private void filter(Predicate<Entry> 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<NetworkStats> CREATOR = new Creator<NetworkStats>() { + @Override + public NetworkStats createFromParcel(Parcel in) { + return new NetworkStats(in); + } + + @Override + public NetworkStats[] newArray(int size) { + return new NetworkStats[size]; + } + }; + + /** @hide */ + public interface NonMonotonicObserver<C> { + 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. + * + * <p>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. + * + * <p>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<String> 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. + * + * <p>This is the first pass iteration which does the following work: + * + * <ul> + * <li>Adds up all the traffic through the tunUid's underlyingIfaces (both foreground and + * background). + * <li>Adds up all the traffic through tun0 excluding traffic from the vpn app itself. + * </ul> + * + * @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<String> 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<String> 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<String> 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. + * + * <p>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. + * + * <p>Can only access usage for one's own UID. + * + * <p>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. + * + * <p>Granted to: + * <ul> + * <li>Profile owners. + * </ul> + */ + 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. + * + * <p>Granted to: + * <ul> + * <li>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. + * <li>Apps with the (signature/privileged) READ_NETWORK_USAGE_HISTORY permission. + * </ul> + */ + int DEVICESUMMARY = 2; + + /** + * Access level for apps which can access usage for any app on the device, including apps + * running on other users/profiles. + * + * <p>Granted to: + * <ul> + * <li>Device owners. + * <li>Carrier-privileged applications. + * <li>The system UID. + * </ul> + */ + 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..6a1d2dd222 --- /dev/null +++ b/framework-t/src/android/net/NetworkStatsCollection.java @@ -0,0 +1,991 @@ +/* + * 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<Key, NetworkStatsHistory> 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<Integer> 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<Range<ZonedDateTime>> it = augmentPlan.cycleIterator(); + while (it.hasNext()) { + final Range<ZonedDateTime> 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<NetworkIdentitySet, ArrayList<Key>> keysByIdent = new HashMap<>(); + for (Key key : mStats.keySet()) { + ArrayList<Key> 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<Key> 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<Key> 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; + } + } + } + + /** + * Remove histories which contains or is before the cutoff timestamp. + * @hide + */ + public void removeHistoryBefore(long cutoffMillis) { + final ArrayList<Key> knownKeys = new ArrayList<>(); + knownKeys.addAll(mStats.keySet()); + + for (Key key : knownKeys) { + final NetworkStatsHistory history = mStats.get(key); + if (history.getStart() > cutoffMillis) continue; + + history.removeBucketsStartingBefore(cutoffMillis); + if (history.size() == 0) { + 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<Key> getSortedKeys() { + final ArrayList<Key> 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<Key, NetworkStatsHistory> 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(new NetworkIdentitySet(), 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<Key, NetworkStatsHistory> getEntries() { + return new ArrayMap(mStats); + } + + /** + * Builder class for {@link NetworkStatsCollection}. + */ + public static final class Builder { + private final long mBucketDurationMillis; + private final ArrayMap<Key, NetworkStatsHistory> 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}. + * If history already exists for this key, then the passed-in history is appended + * to the previously-passed in history. The caller must ensure that the history + * passed-in timestamps are greater than all previously-passed-in timestamps. + * @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<Entry> historyEntries = history.getEntries(); + final NetworkStatsHistory existing = mEntries.get(key); + + final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0); + final NetworkStatsHistory.Builder historyBuilder = + new NetworkStatsHistory.Builder(mBucketDurationMillis, size); + + // TODO: this simply appends the entries to any entries that were already present in + // the builder, which requires the caller to pass in entries in order. We might be + // able to do better with something like recordHistory. + if (existing != null) { + for (Entry entry : existing.getEntries()) { + historyBuilder.addEntry(entry); + } + } + + 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<NetworkIdentity> 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..738e9cc521 --- /dev/null +++ b/framework-t/src/android/net/NetworkStatsHistory.java @@ -0,0 +1,1210 @@ +/* + * 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.Nullable; +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; +import java.util.TreeMap; + +/** + * Collection of historical network statistics, recorded into equally-sized + * "buckets" in time. Internally it stores data in {@code long} series for more + * efficient persistence. + * <p> + * 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 + + "}"; + } + + /** + * Add the given {@link Entry} with this instance and return a new {@link Entry} + * instance as the result. + * + * @hide + */ + @NonNull + public Entry plus(@NonNull Entry another, long bucketDuration) { + if (this.bucketStart != another.bucketStart) { + throw new IllegalArgumentException("bucketStart " + this.bucketStart + + " is not equal to " + another.bucketStart); + } + return new Entry(this.bucketStart, + // Active time should not go over bucket duration. + Math.min(this.activeTime + another.activeTime, bucketDuration), + this.rxBytes + another.rxBytes, + this.rxPackets + another.rxPackets, + this.txBytes + another.txBytes, + this.txPackets + another.txPackets, + this.operations + another.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<Entry> getEntries() { + // TODO: Return a wrapper that uses this list instead, to prevent the returned result + // from being changed. + final ArrayList<Entry> 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 that start older than requested cutoff. + * + * This method will remove any bucket that contains any data older than the requested + * cutoff, even if that same bucket includes some data from after the cutoff. + * + * @hide + */ + public void removeBucketsStartingBefore(final long cutoff) { + // TODO: Consider use getIndexBefore. + int i; + for (i = 0; i < bucketCount; i++) { + final long curStart = bucketStart[i]; + + // This bucket starts after or at the cutoff, so it should be kept. + if (curStart >= 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. + * + * <p>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(); + } + + /** + * Same as "equals", but not actually called equals as this would affect public API behavior. + * @hide + */ + @Nullable + public boolean isSameAs(NetworkStatsHistory other) { + return bucketCount == other.bucketCount + && Arrays.equals(bucketStart, other.bucketStart) + // Don't check activeTime since it can change on import due to the importer using + // recordHistory. It's also not exposed by the APIs or present in dumpsys or + // toString(). + && Arrays.equals(rxBytes, other.rxBytes) + && Arrays.equals(rxPackets, other.rxPackets) + && Arrays.equals(txBytes, other.txBytes) + && Arrays.equals(txPackets, other.txPackets) + && Arrays.equals(operations, other.operations) + && totalBytes == other.totalBytes; + } + + @UnsupportedAppUsage + public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() { + @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 TreeMap<Long, Entry> mEntries; + private final long mBucketDuration; + + /** + * 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; + // Create a collection that is always sorted and can deduplicate items by the timestamp. + mEntries = new TreeMap<>(); + } + + /** + * Add an {@link Entry} into the {@link NetworkStatsHistory} instance. If the timestamp + * already exists, the given {@link Entry} will be combined into existing entry. + * + * @param entry The target {@link Entry} object. + * @return The builder object. + */ + @NonNull + public Builder addEntry(@NonNull Entry entry) { + final Entry existing = mEntries.get(entry.bucketStart); + if (existing != null) { + mEntries.put(entry.bucketStart, existing.plus(entry, mBucketDuration)); + } else { + mEntries.put(entry.bucketStart, entry); + } + return this; + } + + private static long sum(@NonNull long[] array) { + long sum = 0L; + for (long entry : array) { + sum += entry; + } + return sum; + } + + /** + * Builds the instance of the {@link NetworkStatsHistory}. + * + * @return the built instance of {@link NetworkStatsHistory}. + */ + @NonNull + public NetworkStatsHistory build() { + int size = mEntries.size(); + final long[] bucketStart = new long[size]; + final long[] activeTime = new long[size]; + final long[] rxBytes = new long[size]; + final long[] rxPackets = new long[size]; + final long[] txBytes = new long[size]; + final long[] txPackets = new long[size]; + final long[] operations = new long[size]; + + int i = 0; + for (Entry entry : mEntries.values()) { + bucketStart[i] = entry.bucketStart; + activeTime[i] = entry.activeTime; + rxBytes[i] = entry.rxBytes; + rxPackets[i] = entry.rxPackets; + txBytes[i] = entry.txBytes; + txPackets[i] = entry.txPackets; + operations[i] = entry.operations; + i++; + } + + return new NetworkStatsHistory(mBucketDuration, bucketStart, activeTime, + rxBytes, rxPackets, txBytes, txPackets, operations, + size, sum(rxBytes) + sum(txBytes)); + } + } +} diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java new file mode 100644 index 0000000000..b82a126333 --- /dev/null +++ b/framework-t/src/android/net/NetworkTemplate.java @@ -0,0 +1,1120 @@ +/* + * 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. + * <p> + * Since the merge set is dynamic, it should <em>not</em> 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 + || matchRule == MATCH_CARRIER) ? 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<String> getSubscriberIds() { + return new ArraySet<>(Arrays.asList(mMatchSubscriberIds)); + } + + /** + * Get the set of Wifi Network Keys of the template. + * See {@link WifiInfo#getNetworkKey()}. + */ + @NonNull + public Set<String> 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. + * <p> + * 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.<String[]>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. + * + * <p> + * 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<String[]> 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<NetworkTemplate> CREATOR = new Creator<NetworkTemplate>() { + @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<String> mMatchSubscriberIds = + new TreeSet<>(Comparator.nullsFirst(Comparator.naturalOrder())); + @NonNull + private final SortedSet<String> 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<String> 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<String> 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..dc4ac552a4 --- /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. + * <p> + * These statistics may not be available on all platforms. If the statistics are + * not supported by this device, {@link #UNSUPPORTED} will be returned. + * <p> + * 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 = 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<UidTag> sThreadUidTag = new ThreadLocal<UidTag>() { + @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. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + * <p> + * 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. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + * <p> + * 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(client = MODULE_LIBRARIES) + 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. + * <p> + * Any app can <em>accept</em> 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 + * <em>assign</em> blame to another UIDs. + * <p> + * 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. + * <p> + * 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(@NonNull Socket socket) throws SocketException { + SocketTagger.get().tag(socket); + } + + /** + * Remove any statistics parameters from the given {@link Socket}. + * <p> + * 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(@NonNull 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(@NonNull DatagramSocket socket) throws SocketException { + SocketTagger.get().tag(socket); + } + + /** + * Remove any statistics parameters from the given {@link DatagramSocket}. + */ + public static void untagDatagramSocket(@NonNull 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(@NonNull FileDescriptor fd) throws IOException { + SocketTagger.get().tag(fd); + } + + /** + * Remove any statistics parameters from the given {@link FileDescriptor} + * socket. + */ + public static void untagFileDescriptor(@NonNull 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may + * return {@link #UNSUPPORTED} on devices where statistics aren't available. + * <p> + * 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. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. + * <p> + * 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. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. + * <p> + * 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. + * <p> + * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return + * {@link #UNSUPPORTED} on devices where statistics aren't available. + * <p> + * 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..7ab53b1da8 --- /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<String> mUnderlyingIfaces; + + public UnderlyingNetworkInfo(int ownerUid, @NonNull String iface, + @NonNull List<String> 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<String> underlyingIfaces = new ArrayList<>(); + in.readList(underlyingIfaces, null /*classLoader*/, java.lang.String.class); + 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<String> 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<UnderlyingNetworkInfo> CREATOR = + new Parcelable.Creator<UnderlyingNetworkInfo>() { + @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/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java new file mode 100644 index 0000000000..c11e60c49d --- /dev/null +++ b/framework-t/src/android/net/nsd/MDnsManager.java @@ -0,0 +1,200 @@ +/* + * 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.nsd; + +import android.annotation.NonNull; +import android.net.mdns.aidl.DiscoveryInfo; +import android.net.mdns.aidl.GetAddressInfo; +import android.net.mdns.aidl.IMDns; +import android.net.mdns.aidl.IMDnsEventListener; +import android.net.mdns.aidl.RegistrationInfo; +import android.net.mdns.aidl.ResolutionInfo; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +/** + * A manager class for mdns service. + * + * @hide + */ +public class MDnsManager { + private static final String TAG = MDnsManager.class.getSimpleName(); + private final IMDns mMdns; + + /** Service name for this. */ + public static final String MDNS_SERVICE = "mdns"; + + private static final int NO_RESULT = -1; + private static final int NETID_UNSET = 0; + + public MDnsManager(IMDns mdns) { + mMdns = mdns; + } + + /** + * Start the MDNSResponder daemon. + */ + public void startDaemon() { + try { + mMdns.startDaemon(); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Start mdns failed.", e); + } + } + + /** + * Stop the MDNSResponder daemon. + */ + public void stopDaemon() { + try { + mMdns.stopDaemon(); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Stop mdns failed.", e); + } + } + + /** + * Start registering a service. + * + * @param id The operation ID. + * @param serviceName The service name to be registered. + * @param registrationType The service type to be registered. + * @param port The port on which the service accepts connections. + * @param txtRecord The txt record. Refer to {@code NsdServiceInfo#setTxtRecords} for details. + * @param interfaceIdx The interface index on which to register the service. + * @return {@code true} if registration is successful, else {@code false}. + */ + public boolean registerService(int id, @NonNull String serviceName, + @NonNull String registrationType, int port, @NonNull byte[] txtRecord, + int interfaceIdx) { + final RegistrationInfo info = new RegistrationInfo(id, NO_RESULT, serviceName, + registrationType, port, txtRecord, interfaceIdx); + try { + mMdns.registerService(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Register service failed.", e); + return false; + } + return true; + } + + /** + * Start discovering services. + * + * @param id The operation ID. + * @param registrationType The service type to be discovered. + * @param interfaceIdx The interface index on which to discover for services. + * @return {@code true} if discovery is started successfully, else {@code false}. + */ + public boolean discover(int id, @NonNull String registrationType, int interfaceIdx) { + final DiscoveryInfo info = new DiscoveryInfo(id, NO_RESULT, "" /* serviceName */, + registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET); + try { + mMdns.discover(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Discover service failed.", e); + return false; + } + return true; + } + + /** + * Start resolving the target service. + * + * @param id The operation ID. + * @param serviceName The service name to be resolved. + * @param registrationType The service type to be resolved. + * @param domain The service domain to be resolved. + * @param interfaceIdx The interface index on which to resolve the service. + * @return {@code true} if resolution is started successfully, else {@code false}. + */ + public boolean resolve(int id, @NonNull String serviceName, @NonNull String registrationType, + @NonNull String domain, int interfaceIdx) { + final ResolutionInfo info = new ResolutionInfo(id, NO_RESULT, serviceName, + registrationType, domain, "" /* serviceFullName */, "" /* hostname */, 0 /* port */, + new byte[0] /* txtRecord */, interfaceIdx); + try { + mMdns.resolve(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Resolve service failed.", e); + return false; + } + return true; + } + + /** + * Start getting the target service address. + * + * @param id The operation ID. + * @param hostname The fully qualified domain name of the host to be queried for. + * @param interfaceIdx The interface index on which to issue the query. + * @return {@code true} if getting address is started successful, else {@code false}. + */ + public boolean getServiceAddress(int id, @NonNull String hostname, int interfaceIdx) { + final GetAddressInfo info = new GetAddressInfo(id, NO_RESULT, hostname, + "" /* address */, interfaceIdx, NETID_UNSET); + try { + mMdns.getServiceAddress(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Get service address failed.", e); + return false; + } + return true; + } + + /** + * Stop an operation which was requested before. + * + * @param id the operation id to be stopped. + * @return {@code true} if operation is stopped successfully, else {@code false}. + */ + public boolean stopOperation(int id) { + try { + mMdns.stopOperation(id); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Stop operation failed.", e); + return false; + } + return true; + } + + /** + * Register an event listener. + * + * @param listener The listener to be registered. + */ + public void registerEventListener(@NonNull IMDnsEventListener listener) { + try { + mMdns.registerEventListener(listener); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Register listener failed.", e); + } + } + + /** + * Unregister an event listener. + * + * @param listener The listener to be unregistered. + */ + public void unregisterEventListener(@NonNull IMDnsEventListener listener) { + try { + mMdns.unregisterEventListener(listener); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Unregister listener failed.", e); + } + } +} 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..f19bf4a6fb --- /dev/null +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -0,0 +1,1100 @@ +/* + * 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. + * + * <p> 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 + * + * <p> The API is asynchronous, and responses to requests from an application are on listener + * callbacks on a separate internal thread. + * + * <p> There are three main operations the API supports - registration, discovery and resolution. + * <pre> + * 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 + * + * </pre> + * 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} + * + * <p> 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}. + * + * <p> 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 MDNS_SERVICE_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<String> 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(MDNS_SERVICE_EVENT, "MDNS_SERVICE_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<NsdServiceInfo> mServiceMap = new SparseArray<>(); + @GuardedBy("mMapLock") + private final SparseArray<Executor> 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<Integer, PerNetworkDiscoveryTracker> + mPerNetworkDiscoveryMap = new ArrayMap<>(); + + private final ServiceHandler mHandler; + + private class PerNetworkDiscoveryTracker { + final String mServiceType; + final int mProtocolType; + final DiscoveryListener mBaseListener; + final Executor mBaseExecutor; + final ArrayMap<Network, DelegatingDiscoveryListener> mPerNetworkListeners = + new ArrayMap<>(); + + final NetworkCallback mNetworkCb = new NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + final DelegatingDiscoveryListener wrappedListener = new DelegatingDiscoveryListener( + network, mBaseListener, mBaseExecutor); + mPerNetworkListeners.put(network, wrappedListener); + // Run discovery callbacks inline on the service handler thread, which is the + // same thread used by this NetworkCallback, but DelegatingDiscoveryListener will + // use the base executor to run the wrapped callbacks. + discoverServices(mServiceType, mProtocolType, network, Runnable::run, + 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(() -> mBaseExecutor.execute(() -> + 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) { + mBaseExecutor.execute(() -> 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); + } + } + + /** + * A listener wrapping calls to an app-provided listener, while keeping track of found + * services, so they can all be reported lost when the underlying network is lost. + * + * This should be registered to run on the service handler. + */ + private class DelegatingDiscoveryListener implements DiscoveryListener { + private final Network mNetwork; + private final DiscoveryListener mWrapped; + private final Executor mWrappedExecutor; + private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>(); + + private DelegatingDiscoveryListener(Network network, DiscoveryListener listener, + Executor executor) { + mNetwork = network; + mWrapped = listener; + mWrappedExecutor = executor; + } + + 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); + mWrappedExecutor.execute(() -> 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. + mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType)); + } + } + + @Override + public void onDiscoveryStopped(String serviceType) { + mPerNetworkListeners.remove(mNetwork); + if (mStopRequested && mPerNetworkListeners.size() == 0) { + mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType)); + } + } + + @Override + public void onServiceFound(NsdServiceInfo serviceInfo) { + mFoundInfo.add(new TrackedNsdInfo(serviceInfo)); + mWrappedExecutor.execute(() -> mWrapped.onServiceFound(serviceInfo)); + } + + @Override + public void onServiceLost(NsdServiceInfo serviceInfo) { + mFoundInfo.remove(new TrackedNsdInfo(serviceInfo)); + mWrappedExecutor.execute(() -> 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) { + // Do not use message in the executor lambdas, as it will be recycled once this method + // returns. Keep references to its content instead. + final int what = message.what; + final int errorCode = message.arg1; + final int key = message.arg2; + final Object obj = message.obj; + 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 " + key); + 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) obj); + executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s)); + break; + case DISCOVER_SERVICES_FAILED: + removeListener(key); + executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed( + getNsdServiceInfoType(ns), errorCode)); + break; + case SERVICE_FOUND: + executor.execute(() -> ((DiscoveryListener) listener).onServiceFound( + (NsdServiceInfo) obj)); + break; + case SERVICE_LOST: + executor.execute(() -> ((DiscoveryListener) listener).onServiceLost( + (NsdServiceInfo) 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), errorCode)); + 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, errorCode)); + break; + case REGISTER_SERVICE_SUCCEEDED: + executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered( + (NsdServiceInfo) obj)); + break; + case UNREGISTER_SERVICE_FAILED: + removeListener(key); + executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed( + ns, errorCode)); + break; + case UNREGISTER_SERVICE_SUCCEEDED: + // TODO: do not unregister listener until service is unregistered, or provide + // alternative way for unregistering ? + removeListener(key); + executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered( + ns)); + break; + case RESOLVE_SERVICE_FAILED: + removeListener(key); + executor.execute(() -> ((ResolveListener) listener).onResolveFailed( + ns, errorCode)); + break; + case RESOLVE_SERVICE_SUCCEEDED: + removeListener(key); + executor.execute(() -> ((ResolveListener) listener).onServiceResolved( + (NsdServiceInfo) 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. + * + * <p> 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}. + * + * <p> 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. + * + * <p> 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}. + * + * <p> 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}. + * + * <p> 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}. + * + * <p> 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}. + * + * <p> Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + * <p> 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}. + * + * <p> 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}. + * + * <p> 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}. + * + * <p> Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + * <p> 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}. + * + * <p> 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}. + * + * <p> 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}. + * + * <p> Upon failure to start, service discovery is not active and application does + * not need to invoke {@link #stopServiceDiscovery} + * + * <p> 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. + * + * <p> 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}. + * + * <p> 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..200c808565 --- /dev/null +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -0,0 +1,442 @@ +/* + * 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.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<String, byte[]> mTxtRecord = new ArrayMap<>(); + + private InetAddress mHost; + + private int mPort; + + @Nullable + private Network mNetwork; + + private int mInterfaceIndex; + + 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 txtRecordsRawBytes The raw base64 encoded byte array. + * + * @hide + */ + public void setTxtRecords(@NonNull byte[] txtRecordsRawBytes) { + // 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. + * + * <p> Service attributes are included as DNS-SD TXT record pairs. + * + * <p> 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. + * + * <p> 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. + * + * <p> The returned map is unmodifiable; changes must be made through {@link #setAttribute} and + * {@link #removeAttribute}. + */ + public Map<String, byte[]> getAttributes() { + return Collections.unmodifiableMap(mTxtRecord); + } + + private int getTxtRecordSize() { + int txtRecordSize = 0; + for (Map.Entry<String, byte[]> 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<String, byte[]> 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 set if this {@link NsdServiceInfo} was obtained from + * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}, unless the service + * was found on a network interface that does not have a {@link Network} (such as a tethering + * downstream, where services are advertised from devices connected to this device via + * tethering). + */ + @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; + } + + /** + * Get the index of the network interface where the service was found. + * + * This is only set when the service was found on an interface that does not have a usable + * Network, in which case {@link #getNetwork()} returns null. + * @return The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset. + * @hide + */ + public int getInterfaceIndex() { + return mInterfaceIndex; + } + + /** + * Set the index of the network interface where the service was found. + * @hide + */ + public void setInterfaceIndex(int interfaceIndex) { + mInterfaceIndex = interfaceIndex; + } + + @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); + dest.writeInt(mInterfaceIndex); + } + + /** Implement the Parcelable interface */ + public static final @android.annotation.NonNull Creator<NsdServiceInfo> CREATOR = + new Creator<NsdServiceInfo>() { + 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); + info.mInterfaceIndex = in.readInt(); + return info; + } + + public NsdServiceInfo[] newArray(int size) { + return new NsdServiceInfo[size]; + } + }; +} |
