Merge ab/7633965

Bug: 169893837
Merged-In: I3ef19b77bc33546a3e80bca75532d017b4712054
Change-Id: I595fb801f4519177825f3fdc0021fb874a36aa31
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index 6ccbab7..288b06e 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -24,6 +24,7 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.Process;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
 
@@ -1487,8 +1488,31 @@
                 continue;
             }
 
-            if (recycle.uid == tunUid) {
-                // Add up traffic through tunUid's underlying interfaces.
+            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);
@@ -1497,7 +1521,7 @@
                     }
                 }
             } else if (tunIface.equals(recycle.iface)) {
-                // Add up all tunIface traffic excluding traffic from the vpn app itself.
+                // VpnService VPN; traffic sent by apps on the VPN network
                 tunIfaceTotal.add(recycle);
             }
         }
@@ -1532,9 +1556,13 @@
                 // Consider only entries that go onto the VPN interface.
                 continue;
             }
-            if (uid[i] == tunUid) {
+
+            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];
@@ -1641,6 +1669,12 @@
             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
diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java
index 5a25cfc..ae8d010 100644
--- a/core/java/android/net/nsd/NsdManager.java
+++ b/core/java/android/net/nsd/NsdManager.java
@@ -23,6 +23,9 @@
 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.os.Handler;
 import android.os.HandlerThread;
@@ -126,6 +129,24 @@
     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.
@@ -203,6 +224,9 @@
     public static final int DAEMON_CLEANUP                          = BASE + 21;
 
     /** @hide */
+    public static final int DAEMON_STARTUP                          = BASE + 22;
+
+    /** @hide */
     public static final int ENABLE                                  = BASE + 24;
     /** @hide */
     public static final int DISABLE                                 = BASE + 25;
@@ -232,6 +256,8 @@
         EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE");
         EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED");
         EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
+        EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
         EVENT_NAMES.put(ENABLE, "ENABLE");
         EVENT_NAMES.put(DISABLE, "DISABLE");
         EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT");
@@ -494,6 +520,12 @@
         } catch (InterruptedException e) {
             fatal("Interrupted wait at init");
         }
+        if (CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)) {
+            return;
+        }
+        // Only proactively start the daemon if the target SDK < S, otherwise the internal service
+        // would automatically start/stop the native daemon as needed.
+        mAsyncChannel.sendMessage(DAEMON_STARTUP);
     }
 
     private static void fatal(String msg) {
diff --git a/services/core/java/com/android/server/NsdService.java b/services/core/java/com/android/server/NsdService.java
index a481a6a..c9608a5 100644
--- a/services/core/java/com/android/server/NsdService.java
+++ b/services/core/java/com/android/server/NsdService.java
@@ -61,7 +61,7 @@
     private static final String MDNS_TAG = "mDnsConnector";
 
     private static final boolean DBG = true;
-    private static final long CLEANUP_DELAY_MS = 3000;
+    private static final long CLEANUP_DELAY_MS = 10000;
 
     private final Context mContext;
     private final NsdSettings mNsdSettings;
@@ -82,6 +82,8 @@
 
     private static final int INVALID_ID = 0;
     private int mUniqueId = 1;
+    // The count of the connected legacy clients.
+    private int mLegacyClientCount = 0;
 
     private class NsdStateMachine extends StateMachine {
 
@@ -94,19 +96,27 @@
             return NsdManager.nameOf(what);
         }
 
-        void maybeStartDaemon() {
+        private void maybeStartDaemon() {
             mDaemon.maybeStart();
             maybeScheduleStop();
         }
 
-        void maybeScheduleStop() {
-            if (!isAnyRequestActive()) {
-                cancelStop();
-                sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs);
+        private boolean isAnyRequestActive() {
+            return mIdToClientInfoMap.size() != 0;
+        }
+
+        private void scheduleStop() {
+            sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs);
+        }
+        private void maybeScheduleStop() {
+            // The native daemon should stay alive and can't be cleanup
+            // if any legacy client connected.
+            if (!isAnyRequestActive() && mLegacyClientCount == 0) {
+                scheduleStop();
             }
         }
 
-        void cancelStop() {
+        private void cancelStop() {
             this.removeMessages(NsdManager.DAEMON_CLEANUP);
         }
 
@@ -164,11 +174,16 @@
                                 if (DBG) Slog.d(TAG, "Client connection lost with reason: " + msg.arg1);
                                 break;
                         }
+
                         cInfo = mClients.get(msg.replyTo);
                         if (cInfo != null) {
                             cInfo.expungeAllRequests();
                             mClients.remove(msg.replyTo);
+                            if (cInfo.isLegacy()) {
+                                mLegacyClientCount -= 1;
+                            }
                         }
+                        maybeScheduleStop();
                         break;
                     case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION:
                         AsyncChannel ac = new AsyncChannel();
@@ -197,6 +212,17 @@
                     case NsdManager.DAEMON_CLEANUP:
                         mDaemon.maybeStop();
                         break;
+                    // This event should be only sent by the legacy (target SDK < S) clients.
+                    // Mark the sending client as legacy.
+                    case NsdManager.DAEMON_STARTUP:
+                        cInfo = mClients.get(msg.replyTo);
+                        if (cInfo != null) {
+                            cancelStop();
+                            cInfo.setLegacy();
+                            mLegacyClientCount += 1;
+                            maybeStartDaemon();
+                        }
+                        break;
                     case NsdManager.NATIVE_DAEMON_EVENT:
                     default:
                         Slog.e(TAG, "Unhandled " + msg);
@@ -235,7 +261,7 @@
             public void exit() {
                 // TODO: it is incorrect to stop the daemon without expunging all requests
                 // and sending error callbacks to clients.
-                maybeScheduleStop();
+                scheduleStop();
             }
 
             private boolean requestLimitReached(ClientInfo clientInfo) {
@@ -271,9 +297,6 @@
                         return NOT_HANDLED;
                     case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
                         return NOT_HANDLED;
-                }
-
-                switch (msg.what) {
                     case NsdManager.DISABLE:
                         //TODO: cleanup clients
                         transitionTo(mDisabledState);
@@ -531,10 +554,6 @@
        }
     }
 
-    private boolean isAnyRequestActive() {
-        return mIdToClientInfoMap.size() != 0;
-    }
-
     private String unescape(String s) {
         StringBuilder sb = new StringBuilder(s.length());
         for (int i = 0; i < s.length(); ++i) {
@@ -859,6 +878,9 @@
         /* A map from client id to the type of the request we had received */
         private final SparseIntArray mClientRequests = new SparseIntArray();
 
+        // The target SDK of this client < Build.VERSION_CODES.S
+        private boolean mIsLegacy = false;
+
         private ClientInfo(AsyncChannel c, Messenger m) {
             mChannel = c;
             mMessenger = m;
@@ -871,6 +893,7 @@
             sb.append("mChannel ").append(mChannel).append("\n");
             sb.append("mMessenger ").append(mMessenger).append("\n");
             sb.append("mResolvedService ").append(mResolvedService).append("\n");
+            sb.append("mIsLegacy ").append(mIsLegacy).append("\n");
             for(int i = 0; i< mClientIds.size(); i++) {
                 int clientID = mClientIds.keyAt(i);
                 sb.append("clientId ").append(clientID).
@@ -880,6 +903,14 @@
             return sb.toString();
         }
 
+        private boolean isLegacy() {
+            return mIsLegacy;
+        }
+
+        private void setLegacy() {
+            mIsLegacy = true;
+        }
+
         // Remove any pending requests from the global map when we get rid of a client,
         // and send cancellations to the daemon.
         private void expungeAllRequests() {
@@ -907,7 +938,6 @@
             }
             mClientIds.clear();
             mClientRequests.clear();
-            mNsdStateMachine.maybeScheduleStop();
         }
 
         // mClientIds is a sparse array of listener id -> mDnsClient id.  For a given mDnsClient id,
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index 4ee867b..097b071 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -289,8 +289,7 @@
     private String mActiveIface;
 
     /** Set of any ifaces associated with mobile networks since boot. */
-    @GuardedBy("mStatsLock")
-    private String[] mMobileIfaces = new String[0];
+    private volatile String[] mMobileIfaces = new String[0];
 
     /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
     @GuardedBy("mStatsLock")
@@ -935,7 +934,12 @@
 
     @Override
     public String[] getMobileIfaces() {
-        return mMobileIfaces;
+        // TODO (b/192758557): Remove debug log.
+        if (ArrayUtils.contains(mMobileIfaces, null)) {
+            throw new NullPointerException(
+                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
+        }
+        return mMobileIfaces.clone();
     }
 
     @Override
@@ -1084,7 +1088,8 @@
     }
 
     @Override
-    public long getIfaceStats(String iface, int type) {
+    public long getIfaceStats(@NonNull String iface, int type) {
+        Objects.requireNonNull(iface);
         long nativeIfaceStats = nativeGetIfaceStat(iface, type, checkBpfStatsEnable());
         if (nativeIfaceStats == -1) {
             return nativeIfaceStats;
@@ -1382,7 +1387,12 @@
             }
         }
 
-        mMobileIfaces = mobileIfaces.toArray(new String[mobileIfaces.size()]);
+        mMobileIfaces = mobileIfaces.toArray(new String[0]);
+        // TODO (b/192758557): Remove debug log.
+        if (ArrayUtils.contains(mMobileIfaces, null)) {
+            throw new NullPointerException(
+                    "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
+        }
     }
 
     private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {