Merge "Build tethering against the module SDK"
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 2fbba68..00d0d9c 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -60,6 +60,7 @@
     hostdex: true, // for hiddenapi check
     visibility: ["//frameworks/base/packages/Tethering:__subpackages__"],
     apex_available: ["com.android.tethering"],
+    permitted_packages: ["android.net"],
 }
 
 stubs_defaults {
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 2f5ea6a..edd1ebb 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -23,7 +23,6 @@
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
-    method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index b70165a..0107a7e 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -571,9 +571,8 @@
             /**
              * Configure tethering with static IPv4 assignment.
              *
-             * The clientAddress must be in the localIPv4Address prefix. A DHCP server will be
-             * started, but will only be able to offer the client address. The two addresses must
-             * be in the same prefix.
+             * A DHCP server will be started, but will only be able to offer the client address.
+             * The two addresses must be in the same prefix.
              *
              * @param localIPv4Address The preferred local IPv4 link address to use.
              * @param clientAddress The static client address.
@@ -584,10 +583,7 @@
                     @NonNull final LinkAddress clientAddress) {
                 Objects.requireNonNull(localIPv4Address);
                 Objects.requireNonNull(clientAddress);
-                if (localIPv4Address.getPrefixLength() != clientAddress.getPrefixLength()
-                        || !localIPv4Address.isIpv4() || !clientAddress.isIpv4()
-                        || !new IpPrefix(localIPv4Address.toString()).equals(
-                        new IpPrefix(clientAddress.toString()))) {
+                if (!checkStaticAddressConfiguration(localIPv4Address, clientAddress)) {
                     throw new IllegalArgumentException("Invalid server or client addresses");
                 }
 
@@ -657,6 +653,19 @@
         }
 
         /**
+         * Check whether the two addresses are ipv4 and in the same prefix.
+         * @hide
+         */
+        public static boolean checkStaticAddressConfiguration(
+                @NonNull final LinkAddress localIPv4Address,
+                @NonNull final LinkAddress clientAddress) {
+            return localIPv4Address.getPrefixLength() == clientAddress.getPrefixLength()
+                    && localIPv4Address.isIpv4() && clientAddress.isIpv4()
+                    && new IpPrefix(localIPv4Address.toString()).equals(
+                    new IpPrefix(clientAddress.toString()));
+        }
+
+        /**
          * Get a TetheringRequestParcel from the configuration
          * @hide
          */
@@ -739,11 +748,13 @@
      * @param type The tethering type, on of the {@code TetheringManager#TETHERING_*} constants.
      * @param executor {@link Executor} to specify the thread upon which the callback of
      *         TetheringRequest will be invoked.
+     * @hide
      */
     @RequiresPermission(anyOf = {
             android.Manifest.permission.TETHER_PRIVILEGED,
             android.Manifest.permission.WRITE_SETTINGS
     })
+    @SystemApi(client = MODULE_LIBRARIES)
     public void startTethering(int type, @NonNull final Executor executor,
             @NonNull final StartTetheringCallback callback) {
         startTethering(new TetheringRequest.Builder(type).build(), executor, callback);
diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
index d6bc063..82a26be 100644
--- a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
+++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -18,10 +18,12 @@
 
 import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
 
-import android.annotation.NonNull;
 import android.net.LinkAddress;
 import android.util.ArraySet;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.net.Inet4Address;
 import java.util.Collection;
 import java.util.Collections;
@@ -160,6 +162,17 @@
         return this;
     }
 
+    /**
+     * Set the client address to tell DHCP server only offer this address.
+     * The client's prefix length is the same as server's.
+     *
+     * <p>If not set, the default value is null.
+     */
+    public DhcpServingParamsParcelExt setSingleClientAddr(@Nullable Inet4Address clientAddr) {
+        this.clientAddr = clientAddr == null ? 0 : inet4AddressToIntHTH(clientAddr);
+        return this;
+    }
+
     private static int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
         int[] res = new int[addrs.size()];
         int i = 0;
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c5478d2..82b17ac 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -18,6 +18,7 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.TetheringRequest.checkStaticAddressConfiguration;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
 import static android.net.util.NetworkConstants.FF;
@@ -492,17 +493,24 @@
         }
     }
 
-    private boolean startDhcp(Inet4Address addr, int prefixLen) {
+    private boolean startDhcp(final LinkAddress serverLinkAddr, final LinkAddress clientLinkAddr) {
         if (mUsingLegacyDhcp) {
             return true;
         }
+
+        final Inet4Address addr = (Inet4Address) serverLinkAddr.getAddress();
+        final int prefixLen = serverLinkAddr.getPrefixLength();
+        final Inet4Address clientAddr = clientLinkAddr == null ? null :
+                (Inet4Address) clientLinkAddr.getAddress();
+
         final DhcpServingParamsParcel params;
         params = new DhcpServingParamsParcelExt()
                 .setDefaultRouters(addr)
                 .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS)
                 .setDnsServers(addr)
-                .setServerAddr(new LinkAddress(addr, prefixLen))
-                .setMetered(true);
+                .setServerAddr(serverLinkAddr)
+                .setMetered(true)
+                .setSingleClientAddr(clientAddr);
         // TODO: also advertise link MTU
 
         mDhcpServerStartIndex++;
@@ -537,9 +545,10 @@
         }
     }
 
-    private boolean configureDhcp(boolean enable, Inet4Address addr, int prefixLen) {
+    private boolean configureDhcp(boolean enable, final LinkAddress serverAddr,
+            final LinkAddress clientAddr) {
         if (enable) {
-            return startDhcp(addr, prefixLen);
+            return startDhcp(serverAddr, clientAddr);
         } else {
             stopDhcp();
             return true;
@@ -587,7 +596,7 @@
                 // code that calls into NetworkManagementService directly.
                 srvAddr = (Inet4Address) parseNumericAddress(BLUETOOTH_IFACE_ADDR);
                 mIpv4Address = new LinkAddress(srvAddr, BLUETOOTH_DHCP_PREFIX_LENGTH);
-                return configureDhcp(enabled, srvAddr, BLUETOOTH_DHCP_PREFIX_LENGTH);
+                return configureDhcp(enabled, mIpv4Address, null /* clientAddress */);
             }
             mIpv4Address = new LinkAddress(srvAddr, prefixLen);
         } catch (IllegalArgumentException e) {
@@ -624,7 +633,7 @@
             mLinkProperties.removeRoute(route);
         }
 
-        return configureDhcp(enabled, srvAddr, prefixLen);
+        return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr);
     }
 
     private String getRandomWifiIPv4Address() {
@@ -945,7 +954,14 @@
     }
 
     private void maybeConfigureStaticIp(final TetheringRequestParcel request) {
-        if (request == null) return;
+        // Ignore static address configuration if they are invalid or null. In theory, static
+        // addresses should not be invalid here because TetheringManager do not allow caller to
+        // specify invalid static address configuration.
+        if (request == null || request.localIPv4Address == null
+                || request.staticClientAddress == null || !checkStaticAddressConfiguration(
+                request.localIPv4Address, request.staticClientAddress)) {
+            return;
+        }
 
         mStaticIpv4ServerAddr = request.localIPv4Address;
         mStaticIpv4ClientAddr = request.staticClientAddress;
diff --git a/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
index 3dcc15f..343ed4b 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
@@ -303,31 +303,26 @@
 
         final UserManager userManager = (UserManager) mContext.getSystemService(
                 Context.USER_SERVICE);
-        mTetheringRestriction = new UserRestrictionActionListener(userManager, this);
+        mTetheringRestriction = new UserRestrictionActionListener(
+                userManager, this, mNotificationUpdater);
         mExecutor = new TetheringThreadExecutor(mHandler);
         mActiveDataSubIdListener = new ActiveDataSubIdListener(mExecutor);
+        mNetdCallback = new NetdCallback();
 
         // Load tethering configuration.
         updateConfiguration();
-        // NetdCallback should be registered after updateConfiguration() to ensure
-        // TetheringConfiguration is created.
-        mNetdCallback = new NetdCallback();
+    }
+
+    /**
+     * Start to register callbacks.
+     * Call this function when tethering is ready to handle callback events.
+     */
+    public void startStateMachineUpdaters() {
         try {
             mNetd.registerUnsolicitedEventListener(mNetdCallback);
         } catch (RemoteException e) {
             mLog.e("Unable to register netd UnsolicitedEventListener");
         }
-
-        startStateMachineUpdaters(mHandler);
-        startTrackDefaultNetwork();
-
-        final WifiManager wifiManager = getWifiManager();
-        if (wifiManager != null) {
-            wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
-        }
-    }
-
-    private void startStateMachineUpdaters(Handler handler) {
         mCarrierConfigChange.startListening();
         mContext.getSystemService(TelephonyManager.class).listen(mActiveDataSubIdListener,
                 PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
@@ -340,7 +335,14 @@
         filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
         filter.addAction(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
         filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED);
-        mContext.registerReceiver(mStateReceiver, filter, null, handler);
+        mContext.registerReceiver(mStateReceiver, filter, null, mHandler);
+
+        final WifiManager wifiManager = getWifiManager();
+        if (wifiManager != null) {
+            wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+        }
+
+        startTrackDefaultNetwork();
     }
 
     private class TetheringThreadExecutor implements Executor {
@@ -369,9 +371,10 @@
 
             mActiveDataSubId = subId;
             updateConfiguration();
+            mNotificationUpdater.onActiveDataSubscriptionIdChanged(subId);
             // To avoid launching unexpected provisioning checks, ignore re-provisioning
             // when no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning()
-            // ill be triggered again when CarrierConfig is loaded.
+            // will be triggered again when CarrierConfig is loaded.
             if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
                 mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
             } else {
@@ -431,9 +434,7 @@
         // Called by wifi when the number of soft AP clients changed.
         @Override
         public void onConnectedClientsChanged(final List<WifiClient> clients) {
-            if (mConnectedClientsTracker.updateConnectedClients(mForwardedDownstreams, clients)) {
-                reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
-            }
+            updateConnectedClients(clients);
         }
     }
 
@@ -635,7 +636,10 @@
                 Context.ETHERNET_SERVICE);
         synchronized (mPublicSync) {
             if (enable) {
-                if (mEthernetCallback != null) return TETHER_ERROR_NO_ERROR;
+                if (mEthernetCallback != null) {
+                    Log.d(TAG, "Ethernet tethering already started");
+                    return TETHER_ERROR_NO_ERROR;
+                }
 
                 mEthernetCallback = new EthernetCallback();
                 mEthernetIfaceRequest = em.requestTetheredInterface(mExecutor, mEthernetCallback);
@@ -996,11 +1000,14 @@
     protected static class UserRestrictionActionListener {
         private final UserManager mUserManager;
         private final Tethering mWrapper;
+        private final TetheringNotificationUpdater mNotificationUpdater;
         public boolean mDisallowTethering;
 
-        public UserRestrictionActionListener(UserManager um, Tethering wrapper) {
+        public UserRestrictionActionListener(@NonNull UserManager um, @NonNull Tethering wrapper,
+                @NonNull TetheringNotificationUpdater updater) {
             mUserManager = um;
             mWrapper = wrapper;
+            mNotificationUpdater = updater;
             mDisallowTethering = false;
         }
 
@@ -1019,13 +1026,21 @@
                 return;
             }
 
-            // TODO: Add user restrictions notification.
-            final boolean isTetheringActiveOnDevice = (mWrapper.getTetheredIfaces().length != 0);
-
-            if (newlyDisallowed && isTetheringActiveOnDevice) {
-                mWrapper.untetherAll();
-                // TODO(b/148139325): send tetheringSupported on restriction change
+            if (!newlyDisallowed) {
+                // Clear the restricted notification when user is allowed to have tethering
+                // function.
+                mNotificationUpdater.tetheringRestrictionLifted();
+                return;
             }
+
+            // Restricted notification is shown when tethering function is disallowed on
+            // user's device.
+            mNotificationUpdater.notifyTetheringDisabledByRestriction();
+
+            // Untether from all downstreams since tethering is disallowed.
+            mWrapper.untetherAll();
+
+            // TODO(b/148139325): send tetheringSupported on restriction change
         }
     }
 
@@ -1494,7 +1509,7 @@
             } else {
                 dnsServers = mConfig.defaultIPv4DNS;
             }
-            final int netId = (network != null) ? network.netId : NETID_UNSET;
+            final int netId = (network != null) ? network.getNetId() : NETID_UNSET;
             try {
                 mNetd.tetherDnsSet(netId, dnsServers);
                 mLog.log(String.format(
@@ -1559,6 +1574,7 @@
             mIPv6TetheringCoordinator.removeActiveDownstream(who);
             mOffload.excludeDownstreamInterface(who.interfaceName());
             mForwardedDownstreams.remove(who);
+            updateConnectedClients(null /* wifiClients */);
 
             // If this is a Wi-Fi interface, tell WifiManager of any errors
             // or the inactive serving state.
@@ -2141,6 +2157,12 @@
         return false;
     }
 
+    private void updateConnectedClients(final List<WifiClient> wifiClients) {
+        if (mConnectedClientsTracker.updateConnectedClients(mForwardedDownstreams, wifiClients)) {
+            reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
+        }
+    }
+
     private IpServer.Callback makeControlCallback() {
         return new IpServer.Callback() {
             @Override
@@ -2155,10 +2177,7 @@
 
             @Override
             public void dhcpLeasesChanged() {
-                if (mConnectedClientsTracker.updateConnectedClients(
-                        mForwardedDownstreams, null /* wifiClients */)) {
-                    reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
-                }
+                updateConnectedClients(null /* wifiClients */);
             }
         };
     }
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java
index b97f752..992cdd8 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java
@@ -29,12 +29,14 @@
 import android.content.res.Resources;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.telephony.SubscriptionManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
 import androidx.annotation.ArrayRes;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 
@@ -54,10 +56,15 @@
 public class TetheringNotificationUpdater {
     private static final String TAG = TetheringNotificationUpdater.class.getSimpleName();
     private static final String CHANNEL_ID = "TETHERING_STATUS";
+    private static final String WIFI_DOWNSTREAM = "WIFI";
+    private static final String USB_DOWNSTREAM = "USB";
+    private static final String BLUETOOTH_DOWNSTREAM = "BT";
     private static final boolean NOTIFY_DONE = true;
     private static final boolean NO_NOTIFY = false;
     // Id to update and cancel tethering notification. Must be unique within the tethering app.
-    private static final int NOTIFY_ID = 20191115;
+    private static final int ENABLE_NOTIFICATION_ID = 1000;
+    // Id to update and cancel restricted notification. Must be unique within the tethering app.
+    private static final int RESTRICTED_NOTIFICATION_ID = 1001;
     @VisibleForTesting
     static final int NO_ICON_ID = 0;
     @VisibleForTesting
@@ -65,14 +72,25 @@
     private final Context mContext;
     private final NotificationManager mNotificationManager;
     private final NotificationChannel mChannel;
-    // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
-    // This value has to be made 1 2 and 4, and OR'd with the others.
+
     // WARNING : the constructor is called on a different thread. Thread safety therefore
     // relies on this value being initialized to 0, and not any other value. If you need
     // to change this, you will need to change the thread where the constructor is invoked,
     // or to introduce synchronization.
+    // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
+    // This value has to be made 1 2 and 4, and OR'd with the others.
     private int mDownstreamTypesMask = DOWNSTREAM_NONE;
 
+    // WARNING : this value is not able to being initialized to 0 and must have volatile because
+    // telephony service is not guaranteed that is up before tethering service starts. If telephony
+    // is up later than tethering, TetheringNotificationUpdater will use incorrect and valid
+    // subscription id(0) to query resources. Therefore, initialized subscription id must be
+    // INVALID_SUBSCRIPTION_ID.
+    private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+    @IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID})
+    @interface NotificationId {}
+
     public TetheringNotificationUpdater(@NonNull final Context context) {
         mContext = context;
         mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
@@ -88,19 +106,46 @@
     public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) {
         if (mDownstreamTypesMask == downstreamTypesMask) return;
         mDownstreamTypesMask = downstreamTypesMask;
-        updateNotification();
+        updateEnableNotification();
     }
 
-    private void updateNotification() {
+    /** Called when active data subscription id changed */
+    public void onActiveDataSubscriptionIdChanged(final int subId) {
+        if (mActiveDataSubId == subId) return;
+        mActiveDataSubId = subId;
+        updateEnableNotification();
+    }
+
+    @VisibleForTesting
+    Resources getResourcesForSubId(@NonNull final Context c, final int subId) {
+        return SubscriptionManager.getResourcesForSubId(c, subId);
+    }
+
+    private void updateEnableNotification() {
         final boolean tetheringInactive = mDownstreamTypesMask <= DOWNSTREAM_NONE;
 
         if (tetheringInactive || setupNotification() == NO_NOTIFY) {
-            clearNotification();
+            clearNotification(ENABLE_NOTIFICATION_ID);
         }
     }
 
-    private void clearNotification() {
-        mNotificationManager.cancel(null /* tag */, NOTIFY_ID);
+    @VisibleForTesting
+    void tetheringRestrictionLifted() {
+        clearNotification(RESTRICTED_NOTIFICATION_ID);
+    }
+
+    private void clearNotification(@NotificationId final int id) {
+        mNotificationManager.cancel(null /* tag */, id);
+    }
+
+    @VisibleForTesting
+    void notifyTetheringDisabledByRestriction() {
+        final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+        final String title = res.getString(R.string.disable_tether_notification_title);
+        final String message = res.getString(R.string.disable_tether_notification_message);
+
+        showNotification(R.drawable.stat_sys_tether_general, title, message,
+                RESTRICTED_NOTIFICATION_ID);
     }
 
     /**
@@ -110,16 +155,17 @@
      *
      * @return downstream types mask value.
      */
+    @VisibleForTesting
     @IntRange(from = 0, to = 7)
-    private int getDownstreamTypesMask(@NonNull final String types) {
+    int getDownstreamTypesMask(@NonNull final String types) {
         int downstreamTypesMask = DOWNSTREAM_NONE;
         final String[] downstreams = types.split("\\|");
         for (String downstream : downstreams) {
-            if ("USB".equals(downstream.trim())) {
+            if (USB_DOWNSTREAM.equals(downstream.trim())) {
                 downstreamTypesMask |= (1 << TETHERING_USB);
-            } else if ("WIFI".equals(downstream.trim())) {
+            } else if (WIFI_DOWNSTREAM.equals(downstream.trim())) {
                 downstreamTypesMask |= (1 << TETHERING_WIFI);
-            } else if ("BT".equals(downstream.trim())) {
+            } else if (BLUETOOTH_DOWNSTREAM.equals(downstream.trim())) {
                 downstreamTypesMask |= (1 << TETHERING_BLUETOOTH);
             }
         }
@@ -134,9 +180,8 @@
      *
      * @return {@link android.util.SparseArray} with downstream types and icon id info.
      */
-    @NonNull
-    private SparseArray<Integer> getIcons(@ArrayRes int id) {
-        final Resources res = mContext.getResources();
+    @VisibleForTesting
+    SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) {
         final String[] array = res.getStringArray(id);
         final SparseArray<Integer> icons = new SparseArray<>();
         for (String config : array) {
@@ -161,8 +206,9 @@
     }
 
     private boolean setupNotification() {
-        final Resources res = mContext.getResources();
-        final SparseArray<Integer> downstreamIcons = getIcons(R.array.tethering_notification_icons);
+        final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
+        final SparseArray<Integer> downstreamIcons =
+                getIcons(R.array.tethering_notification_icons, res);
 
         final int iconId = downstreamIcons.get(mDownstreamTypesMask, NO_ICON_ID);
         if (iconId == NO_ICON_ID) return NO_NOTIFY;
@@ -170,12 +216,12 @@
         final String title = res.getString(R.string.tethering_notification_title);
         final String message = res.getString(R.string.tethering_notification_message);
 
-        showNotification(iconId, title, message);
+        showNotification(iconId, title, message, ENABLE_NOTIFICATION_ID);
         return NOTIFY_DONE;
     }
 
     private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
-            @NonNull final String message) {
+            @NonNull final String message, @NotificationId final int id) {
         final Intent intent = new Intent(Settings.ACTION_TETHER_SETTINGS);
         final PendingIntent pi = PendingIntent.getActivity(
                 mContext.createContextAsUser(UserHandle.CURRENT, 0),
@@ -193,6 +239,6 @@
                         .setContentIntent(pi)
                         .build();
 
-        mNotificationManager.notify(null /* tag */, NOTIFY_ID, notification);
+        mNotificationManager.notify(null /* tag */, id, notification);
     }
 }
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
index c5329d8..c30be25 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
@@ -80,6 +80,7 @@
         mContext = mDeps.getContext();
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         mTethering = makeTethering(mDeps);
+        mTethering.startStateMachineUpdaters();
     }
 
     /**
diff --git a/Tethering/src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
index 7ac7f5f..45bb4ab 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -586,21 +586,21 @@
      */
     @VisibleForTesting
     public static NetworkCapabilities networkCapabilitiesForType(int type) {
-        final NetworkCapabilities nc = new NetworkCapabilities();
+        final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
 
         // Map from type to transports.
         final int notFound = -1;
         final int transport = sLegacyTypeToTransport.get(type, notFound);
         Preconditions.checkArgument(transport != notFound, "unknown legacy type: " + type);
-        nc.addTransportType(transport);
+        builder.addTransportType(transport);
 
         if (type == TYPE_MOBILE_DUN) {
-            nc.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
             // DUN is restricted network, see NetworkCapabilities#FORCE_RESTRICTED_CAPABILITIES.
-            nc.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+            builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         } else {
-            nc.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
         }
-        return nc;
+        return builder.build();
     }
 }
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
new file mode 100644
index 0000000..1a1c30d
--- /dev/null
+++ b/Tethering/tests/integration/Android.bp
@@ -0,0 +1,42 @@
+//
+// 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.
+//
+
+android_test {
+    name: "TetheringIntegrationTests",
+    certificate: "platform",
+    platform_apis: true,
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    test_suites: [
+        "device-tests",
+        "mts",
+    ],
+    static_libs: [
+        "NetworkStackApiStableLib",
+        "androidx.test.rules",
+        "frameworks-base-testutils",
+        "mockito-target-extended-minus-junit4",
+        "net-tests-utils",
+        "testables",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+}
diff --git a/Tethering/tests/integration/AndroidManifest.xml b/Tethering/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..233ba40
--- /dev/null
+++ b/Tethering/tests/integration/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.networkstack.tethering.tests.integration">
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.TETHER_PRIVILEGED"/>
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.networkstack.tethering.tests.integration"
+        android:label="Tethering integration tests">
+    </instrumentation>
+</manifest>
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
new file mode 100644
index 0000000..492ce3d
--- /dev/null
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.EthernetManager.TetheredInterfaceCallback;
+import android.net.EthernetManager.TetheredInterfaceRequest;
+import android.net.TetheringManager.StartTetheringCallback;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.dhcp.DhcpAckPacket;
+import android.net.dhcp.DhcpOfferPacket;
+import android.net.dhcp.DhcpPacket;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.SystemClock;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtilsKt;
+import com.android.testutils.TapPacketReader;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.net.Inet4Address;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class EthernetTetheringTest {
+
+    private static final String TAG = EthernetTetheringTest.class.getSimpleName();
+    private static final int TIMEOUT_MS = 1000;
+    private static final int PACKET_READ_TIMEOUT_MS = 100;
+    private static final int DHCP_DISCOVER_ATTEMPTS = 10;
+    private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] {
+            DhcpPacket.DHCP_SUBNET_MASK,
+            DhcpPacket.DHCP_ROUTER,
+            DhcpPacket.DHCP_DNS_SERVER,
+            DhcpPacket.DHCP_LEASE_TIME,
+    };
+    private static final String DHCP_HOSTNAME = "testhostname";
+
+    private final Context mContext = InstrumentationRegistry.getContext();
+    private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
+    private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
+
+    private TestNetworkInterface mTestIface;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private TapPacketReader mTapPacketReader;
+
+    private TetheredInterfaceRequester mTetheredInterfaceRequester;
+    private MyTetheringEventCallback mTetheringEventCallback;
+
+    private UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+    @Before
+    public void setUp() throws Exception {
+        mHandlerThread = new HandlerThread(getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+        // Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive
+        // tethered client callbacks.
+        mUiAutomation.adoptShellPermissionIdentity(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS);
+    }
+
+    private void cleanUp() throws Exception {
+        mTm.stopTethering(TetheringManager.TETHERING_ETHERNET);
+        if (mTetheringEventCallback != null) {
+            mTetheringEventCallback.awaitInterfaceUntethered();
+            mTetheringEventCallback.unregister();
+            mTetheringEventCallback = null;
+        }
+        if (mTapPacketReader != null) {
+            TapPacketReader reader = mTapPacketReader;
+            mHandler.post(() -> reader.stop());
+            mTapPacketReader = null;
+        }
+        mHandlerThread.quitSafely();
+        mTetheredInterfaceRequester.release();
+        mEm.setIncludeTestInterfaces(false);
+        maybeDeleteTestInterface();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        try {
+            cleanUp();
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    @Test
+    public void testVirtualEthernetAlreadyExists() throws Exception {
+        // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
+        assumeFalse(mEm.isAvailable());
+
+        mTestIface = createTestInterface();
+        // This must be done now because as soon as setIncludeTestInterfaces(true) is called, the
+        // interface will be placed in client mode, which will delete the link-local address.
+        // At that point NetworkInterface.getByName() will cease to work on the interface, because
+        // starting in R NetworkInterface can no longer see interfaces without IP addresses.
+        int mtu = getMTU(mTestIface);
+
+        Log.d(TAG, "Including test interfaces");
+        mEm.setIncludeTestInterfaces(true);
+
+        final String iface = mTetheredInterfaceRequester.getInterface();
+        assertEquals("TetheredInterfaceCallback for unexpected interface",
+                mTestIface.getInterfaceName(), iface);
+
+        checkVirtualEthernet(mTestIface, mtu);
+    }
+
+    @Test
+    public void testVirtualEthernet() throws Exception {
+        // This test requires manipulating packets. Skip if there is a physical Ethernet connected.
+        assumeFalse(mEm.isAvailable());
+
+        CompletableFuture<String> futureIface = mTetheredInterfaceRequester.requestInterface();
+
+        mEm.setIncludeTestInterfaces(true);
+
+        mTestIface = createTestInterface();
+
+        final String iface = futureIface.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        assertEquals("TetheredInterfaceCallback for unexpected interface",
+                mTestIface.getInterfaceName(), iface);
+
+        checkVirtualEthernet(mTestIface, getMTU(mTestIface));
+    }
+
+    @Test
+    public void testPhysicalEthernet() throws Exception {
+        assumeTrue(mEm.isAvailable());
+
+        // Get an interface to use.
+        final String iface = mTetheredInterfaceRequester.getInterface();
+
+        // Enable Ethernet tethering and check that it starts.
+        mTetheringEventCallback = enableEthernetTethering(iface);
+
+        // There is nothing more we can do on a physical interface without connecting an actual
+        // client, which is not possible in this test.
+    }
+
+    private static final class MyTetheringEventCallback implements TetheringEventCallback {
+        private final TetheringManager mTm;
+        private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
+        private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
+        private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1);
+        private final String mIface;
+
+        private volatile boolean mInterfaceWasTethered = false;
+        private volatile boolean mUnregistered = false;
+        private volatile Collection<TetheredClient> mClients = null;
+
+        MyTetheringEventCallback(TetheringManager tm, String iface) {
+            mTm = tm;
+            mIface = iface;
+        }
+
+        public void unregister() {
+            mTm.unregisterTetheringEventCallback(this);
+            mUnregistered = true;
+        }
+
+        @Override
+        public void onTetheredInterfacesChanged(List<String> interfaces) {
+            // Ignore stale callbacks registered by previous test cases.
+            if (mUnregistered) return;
+
+            final boolean wasTethered = mTetheringStartedLatch.getCount() == 0;
+            if (!mInterfaceWasTethered && (mIface == null || interfaces.contains(mIface))) {
+                // This interface is being tethered for the first time.
+                Log.d(TAG, "Tethering started: " + interfaces);
+                mInterfaceWasTethered = true;
+                mTetheringStartedLatch.countDown();
+            } else if (mInterfaceWasTethered && !interfaces.contains(mIface)) {
+                Log.d(TAG, "Tethering stopped: " + interfaces);
+                mTetheringStoppedLatch.countDown();
+            }
+        }
+
+        public void awaitInterfaceTethered() throws Exception {
+            assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms",
+                    mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+
+        public void awaitInterfaceUntethered() throws Exception {
+            // Don't block teardown if the interface was never tethered.
+            // This is racy because the interface might become tethered right after this check, but
+            // that can only happen in tearDown if startTethering timed out, which likely means
+            // the test has already failed.
+            if (!mInterfaceWasTethered) return;
+
+            assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
+                    mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+
+        @Override
+        public void onError(String ifName, int error) {
+            // Ignore stale callbacks registered by previous test cases.
+            if (mUnregistered) return;
+
+            fail("TetheringEventCallback got error:" + error + " on iface " + ifName);
+        }
+
+        @Override
+        public void onClientsChanged(Collection<TetheredClient> clients) {
+            // Ignore stale callbacks registered by previous test cases.
+            if (mUnregistered) return;
+
+            Log.d(TAG, "Got clients changed: " + clients);
+            mClients = clients;
+            if (clients.size() > 0) {
+                mClientConnectedLatch.countDown();
+            }
+        }
+
+        public Collection<TetheredClient> awaitClientConnected() throws Exception {
+            assertTrue("Did not receive client connected callback after " + TIMEOUT_MS + "ms",
+                    mClientConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            return mClients;
+        }
+    }
+
+    private MyTetheringEventCallback enableEthernetTethering(String iface) throws Exception {
+        MyTetheringEventCallback callback = new MyTetheringEventCallback(mTm, iface);
+        mTm.registerTetheringEventCallback(mHandler::post, callback);
+
+        StartTetheringCallback startTetheringCallback = new StartTetheringCallback() {
+            @Override
+            public void onTetheringFailed(int resultCode) {
+                fail("Unexpectedly got onTetheringFailed");
+            }
+        };
+        Log.d(TAG, "Starting Ethernet tethering");
+        mTm.startTethering(
+                new TetheringRequest.Builder(TetheringManager.TETHERING_ETHERNET).build(),
+                mHandler::post /* executor */,  startTetheringCallback);
+        callback.awaitInterfaceTethered();
+        return callback;
+    }
+
+    private int getMTU(TestNetworkInterface iface) throws SocketException {
+        NetworkInterface nif = NetworkInterface.getByName(iface.getInterfaceName());
+        assertNotNull("Can't get NetworkInterface object for " + iface.getInterfaceName(), nif);
+        return nif.getMTU();
+    }
+
+    private void checkVirtualEthernet(TestNetworkInterface iface, int mtu) throws Exception {
+        FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor();
+        mTapPacketReader = new TapPacketReader(mHandler, fd, mtu);
+        mHandler.post(() -> mTapPacketReader.start());
+        HandlerUtilsKt.waitForIdle(mHandler, TIMEOUT_MS);
+
+        mTetheringEventCallback = enableEthernetTethering(iface.getInterfaceName());
+        checkTetheredClientCallbacks(fd);
+    }
+
+    private void checkTetheredClientCallbacks(FileDescriptor fd) throws Exception {
+        // Create a fake client.
+        byte[] clientMacAddr = new byte[6];
+        new Random().nextBytes(clientMacAddr);
+
+        // We have to retransmit DHCP requests because IpServer declares itself to be ready before
+        // its DhcpServer is actually started. TODO: fix this race and remove this loop.
+        DhcpPacket offerPacket = null;
+        for (int i = 0; i < DHCP_DISCOVER_ATTEMPTS; i++) {
+            Log.d(TAG, "Sending DHCP discover");
+            sendDhcpDiscover(fd, clientMacAddr);
+            offerPacket = getNextDhcpPacket();
+            if (offerPacket instanceof DhcpOfferPacket) break;
+        }
+        assertTrue("No DHCPOFFER received on interface within timeout",
+                offerPacket instanceof DhcpOfferPacket);
+
+        sendDhcpRequest(fd, offerPacket, clientMacAddr);
+        DhcpPacket ackPacket = getNextDhcpPacket();
+        assertTrue("No DHCPACK received on interface within timeout",
+                ackPacket instanceof DhcpAckPacket);
+
+        final Collection<TetheredClient> clients = mTetheringEventCallback.awaitClientConnected();
+        assertEquals(1, clients.size());
+        final TetheredClient client = clients.iterator().next();
+
+        // Check the MAC address.
+        assertEquals(MacAddress.fromBytes(clientMacAddr), client.getMacAddress());
+        assertEquals(TetheringManager.TETHERING_ETHERNET, client.getTetheringType());
+
+        // Check the hostname.
+        assertEquals(1, client.getAddresses().size());
+        TetheredClient.AddressInfo info = client.getAddresses().get(0);
+        assertEquals(DHCP_HOSTNAME, info.getHostname());
+
+        // Check the address is the one that was handed out in the DHCP ACK.
+        DhcpResults dhcpResults = offerPacket.toDhcpResults();
+        assertLinkAddressMatches(dhcpResults.ipAddress, info.getAddress());
+
+        // Check that the lifetime is correct +/- 10s.
+        final long now = SystemClock.elapsedRealtime();
+        final long actualLeaseDuration = (info.getAddress().getExpirationTime() - now) / 1000;
+        final String msg = String.format("IP address should have lifetime of %d, got %d",
+                dhcpResults.leaseDuration, actualLeaseDuration);
+        assertTrue(msg, Math.abs(dhcpResults.leaseDuration - actualLeaseDuration) < 10);
+    }
+
+    private DhcpPacket getNextDhcpPacket() throws ParseException {
+        byte[] packet;
+        while ((packet = mTapPacketReader.popPacket(PACKET_READ_TIMEOUT_MS)) != null) {
+            try {
+                return DhcpPacket.decodeFullPacket(packet, packet.length, DhcpPacket.ENCAP_L2);
+            } catch (DhcpPacket.ParseException e) {
+                // Not a DHCP packet. Continue.
+            }
+        }
+        return null;
+    }
+
+    private static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
+        private final CountDownLatch mInterfaceAvailableLatch = new CountDownLatch(1);
+        private final Handler mHandler;
+        private final EthernetManager mEm;
+
+        private TetheredInterfaceRequest mRequest;
+        private final CompletableFuture<String> mFuture = new CompletableFuture<>();
+
+        TetheredInterfaceRequester(Handler handler, EthernetManager em) {
+            mHandler = handler;
+            mEm = em;
+        }
+
+        @Override
+        public void onAvailable(String iface) {
+            Log.d(TAG, "Ethernet interface available: " + iface);
+            mFuture.complete(iface);
+        }
+
+        @Override
+        public void onUnavailable() {
+            mFuture.completeExceptionally(new IllegalStateException("onUnavailable received"));
+        }
+
+        public CompletableFuture<String> requestInterface() {
+            assertNull("BUG: more than one tethered interface request", mRequest);
+            Log.d(TAG, "Requesting tethered interface");
+            mRequest = mEm.requestTetheredInterface(mHandler::post, this);
+            return mFuture;
+        }
+
+        public String getInterface() throws Exception {
+            return requestInterface().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        public void release() {
+            if (mRequest != null) {
+                mFuture.obtrudeException(new IllegalStateException("Request already released"));
+                mRequest.release();
+                mRequest = null;
+            }
+        }
+    }
+
+    private void sendDhcpDiscover(FileDescriptor fd, byte[] macAddress) throws Exception {
+        ByteBuffer packet = DhcpPacket.buildDiscoverPacket(DhcpPacket.ENCAP_L2,
+                new Random().nextInt() /* transactionId */, (short) 0 /* secs */,
+                macAddress,  false /* unicast */, DHCP_REQUESTED_PARAMS,
+                false /* rapid commit */,  DHCP_HOSTNAME);
+        sendPacket(fd, packet);
+    }
+
+    private void sendDhcpRequest(FileDescriptor fd, DhcpPacket offerPacket, byte[] macAddress)
+            throws Exception {
+        DhcpResults results = offerPacket.toDhcpResults();
+        Inet4Address clientIp = (Inet4Address) results.ipAddress.getAddress();
+        Inet4Address serverIdentifier = results.serverAddress;
+        ByteBuffer packet = DhcpPacket.buildRequestPacket(DhcpPacket.ENCAP_L2,
+                0 /* transactionId */, (short) 0 /* secs */, DhcpPacket.INADDR_ANY /* clientIp */,
+                false /* broadcast */, macAddress, clientIp /* requestedIpAddress */,
+                serverIdentifier, DHCP_REQUESTED_PARAMS, DHCP_HOSTNAME);
+        sendPacket(fd, packet);
+    }
+
+    private void sendPacket(FileDescriptor fd, ByteBuffer packet) throws Exception {
+        assertNotNull("Only tests on virtual interfaces can send packets", fd);
+        Os.write(fd, packet);
+    }
+
+    public void assertLinkAddressMatches(LinkAddress l1, LinkAddress l2) {
+        // Check all fields except the deprecation and expiry times.
+        String msg = String.format("LinkAddresses do not match. expected: %s actual: %s", l1, l2);
+        assertTrue(msg, l1.isSameAddressAs(l2));
+        assertEquals("LinkAddress flags do not match", l1.getFlags(), l2.getFlags());
+        assertEquals("LinkAddress scope does not match", l1.getScope(), l2.getScope());
+    }
+
+    private TestNetworkInterface createTestInterface() throws Exception {
+        TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
+        TestNetworkInterface iface = tnm.createTapInterface();
+        Log.d(TAG, "Created test interface " + iface.getInterfaceName());
+        assertNotNull(NetworkInterface.getByName(iface.getInterfaceName()));
+        return iface;
+    }
+
+    private void maybeDeleteTestInterface() throws Exception {
+        if (mTestIface != null) {
+            mTestIface.getFileDescriptor().close();
+            Log.d(TAG, "Deleted test interface " + mTestIface.getInterfaceName());
+            mTestIface = null;
+        }
+    }
+}
diff --git a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
index e8add98..f8eb147 100644
--- a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
+++ b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
@@ -42,7 +42,9 @@
 @SmallTest
 public class DhcpServingParamsParcelExtTest {
     private static final Inet4Address TEST_ADDRESS = inet4Addr("192.168.0.123");
+    private static final Inet4Address TEST_CLIENT_ADDRESS = inet4Addr("192.168.0.42");
     private static final int TEST_ADDRESS_PARCELED = 0xc0a8007b;
+    private static final int TEST_CLIENT_ADDRESS_PARCELED = 0xc0a8002a;
     private static final int TEST_PREFIX_LENGTH = 17;
     private static final int TEST_LEASE_TIME_SECS = 120;
     private static final int TEST_MTU = 1000;
@@ -105,6 +107,12 @@
         assertFalse(mParcel.metered);
     }
 
+    @Test
+    public void testSetClientAddr() {
+        mParcel.setSingleClientAddr(TEST_CLIENT_ADDRESS);
+        assertEquals(TEST_CLIENT_ADDRESS_PARCELED, mParcel.clientAddr);
+    }
+
     private static Inet4Address inet4Addr(String addr) {
         return (Inet4Address) parseNumericAddress(addr);
     }
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringNotificationUpdaterTest.kt b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringNotificationUpdaterTest.kt
new file mode 100644
index 0000000..b869491
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringNotificationUpdaterTest.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.content.Context
+import android.content.res.Resources
+import android.net.ConnectivityManager.TETHERING_BLUETOOTH
+import android.net.ConnectivityManager.TETHERING_USB
+import android.net.ConnectivityManager.TETHERING_WIFI
+import android.os.UserHandle
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.internal.util.test.BroadcastInterceptingContext
+import com.android.networkstack.tethering.R
+import com.android.server.connectivity.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+const val TEST_SUBID = 1
+const val WIFI_ICON_ID = 1
+const val USB_ICON_ID = 2
+const val BT_ICON_ID = 3
+const val GENERAL_ICON_ID = 4
+const val WIFI_MASK = 1 shl TETHERING_WIFI
+const val USB_MASK = 1 shl TETHERING_USB
+const val BT_MASK = 1 shl TETHERING_BLUETOOTH
+const val TITTLE = "Tethering active"
+const val MESSAGE = "Tap here to set up."
+const val TEST_TITTLE = "Hotspot active"
+const val TEST_MESSAGE = "Tap to set up hotspot."
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TetheringNotificationUpdaterTest {
+    // lateinit used here for mocks as they need to be reinitialized between each test and the test
+    // should crash if they are used before being initialized.
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var notificationManager: NotificationManager
+    @Mock private lateinit var defaultResources: Resources
+    @Mock private lateinit var testResources: Resources
+
+    // lateinit for this class under test, as it should be reset to a different instance for every
+    // tests but should always be initialized before use (or the test should crash).
+    private lateinit var notificationUpdater: TetheringNotificationUpdater
+
+    private val ENABLE_ICON_CONFIGS = arrayOf(
+            "USB;android.test:drawable/usb", "BT;android.test:drawable/bluetooth",
+            "WIFI|BT;android.test:drawable/general", "WIFI|USB;android.test:drawable/general",
+            "USB|BT;android.test:drawable/general", "WIFI|USB|BT;android.test:drawable/general")
+
+    private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) {
+        override fun createContextAsUser(user: UserHandle, flags: Int) =
+                if (user == UserHandle.ALL) mockContext else this
+    }
+
+    private inner class WrappedNotificationUpdater(c: Context) : TetheringNotificationUpdater(c) {
+        override fun getResourcesForSubId(context: Context, subId: Int) =
+                if (subId == TEST_SUBID) testResources else defaultResources
+    }
+
+    private fun setupResources() {
+        doReturn(ENABLE_ICON_CONFIGS).`when`(defaultResources)
+                .getStringArray(R.array.tethering_notification_icons)
+        doReturn(arrayOf("WIFI;android.test:drawable/wifi")).`when`(testResources)
+                .getStringArray(R.array.tethering_notification_icons)
+        doReturn(TITTLE).`when`(defaultResources).getString(R.string.tethering_notification_title)
+        doReturn(MESSAGE).`when`(defaultResources)
+                .getString(R.string.tethering_notification_message)
+        doReturn(TEST_TITTLE).`when`(testResources).getString(R.string.tethering_notification_title)
+        doReturn(TEST_MESSAGE).`when`(testResources)
+                .getString(R.string.tethering_notification_message)
+        doReturn(USB_ICON_ID).`when`(defaultResources)
+                .getIdentifier(eq("android.test:drawable/usb"), any(), any())
+        doReturn(BT_ICON_ID).`when`(defaultResources)
+                .getIdentifier(eq("android.test:drawable/bluetooth"), any(), any())
+        doReturn(GENERAL_ICON_ID).`when`(defaultResources)
+                .getIdentifier(eq("android.test:drawable/general"), any(), any())
+        doReturn(WIFI_ICON_ID).`when`(testResources)
+                .getIdentifier(eq("android.test:drawable/wifi"), any(), any())
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val context = TestContext(InstrumentationRegistry.getInstrumentation().context)
+        doReturn(notificationManager).`when`(mockContext)
+                .getSystemService(Context.NOTIFICATION_SERVICE)
+        notificationUpdater = WrappedNotificationUpdater(context)
+        setupResources()
+    }
+
+    private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE)
+    private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT)
+
+    private fun verifyNotification(iconId: Int = 0, title: String = "", text: String = "") {
+        verify(notificationManager, never()).cancel(any(), anyInt())
+
+        val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
+        verify(notificationManager, times(1))
+                .notify(any(), anyInt(), notificationCaptor.capture())
+
+        val notification = notificationCaptor.getValue()
+        assertEquals(iconId, notification.smallIcon.resId)
+        assertEquals(title, notification.title())
+        assertEquals(text, notification.text())
+
+        reset(notificationManager)
+    }
+
+    private fun verifyNoNotification() {
+        verify(notificationManager, times(1)).cancel(any(), anyInt())
+        verify(notificationManager, never()).notify(any(), anyInt(), any())
+
+        reset(notificationManager)
+    }
+
+    @Test
+    fun testNotificationWithDownstreamChanged() {
+        // Wifi downstream. No notification.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        verifyNoNotification()
+
+        // Same downstream changed. Nothing happened.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        verifyZeroInteractions(notificationManager)
+
+        // Wifi and usb downstreams. Show enable notification
+        notificationUpdater.onDownstreamChanged(WIFI_MASK or USB_MASK)
+        verifyNotification(GENERAL_ICON_ID, TITTLE, MESSAGE)
+
+        // Usb downstream. Still show enable notification.
+        notificationUpdater.onDownstreamChanged(USB_MASK)
+        verifyNotification(USB_ICON_ID, TITTLE, MESSAGE)
+
+        // No downstream. No notification.
+        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+        verifyNoNotification()
+    }
+
+    @Test
+    fun testNotificationWithActiveDataSubscriptionIdChanged() {
+        // Usb downstream. Showed enable notification with default resource.
+        notificationUpdater.onDownstreamChanged(USB_MASK)
+        verifyNotification(USB_ICON_ID, TITTLE, MESSAGE)
+
+        // Same subId changed. Nothing happened.
+        notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
+        verifyZeroInteractions(notificationManager)
+
+        // Set test sub id. Clear notification with test resource.
+        notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+        verifyNoNotification()
+
+        // Wifi downstream. Show enable notification with test resource.
+        notificationUpdater.onDownstreamChanged(WIFI_MASK)
+        verifyNotification(WIFI_ICON_ID, TEST_TITTLE, TEST_MESSAGE)
+
+        // No downstream. No notification.
+        notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
+        verifyNoNotification()
+    }
+
+    private fun assertIconNumbers(number: Int, configs: Array<String?>) {
+        doReturn(configs).`when`(defaultResources)
+                .getStringArray(R.array.tethering_notification_icons)
+        assertEquals(number, notificationUpdater.getIcons(
+                R.array.tethering_notification_icons, defaultResources).size())
+    }
+
+    @Test
+    fun testGetIcons() {
+        assertIconNumbers(0, arrayOfNulls<String>(0))
+        assertIconNumbers(0, arrayOf(null, ""))
+        assertIconNumbers(3, arrayOf(
+                // These configurations are invalid with wrong strings or symbols.
+                ";", ",", "|", "|,;", "WIFI", "1;2", " U SB; ", "bt;", "WIFI;USB;BT", "WIFI|USB|BT",
+                "WIFI,BT,USB", " WIFI| |  | USB, test:drawable/test",
+                // This configuration is valid with two downstream types (USB, BT).
+                "USB|,,,,,|BT;drawable/test ",
+                // This configuration is valid with one downstream types (WIFI).
+                "     WIFI     ; android.test:drawable/xxx "))
+    }
+
+    @Test
+    fun testGetDownstreamTypesMask() {
+        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask(""))
+        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask("1"))
+        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask("WIFI_P2P"))
+        assertEquals(DOWNSTREAM_NONE, notificationUpdater.getDownstreamTypesMask("usb"))
+        assertEquals(WIFI_MASK, notificationUpdater.getDownstreamTypesMask(" WIFI "))
+        assertEquals(USB_MASK, notificationUpdater.getDownstreamTypesMask("USB | B T"))
+        assertEquals(BT_MASK, notificationUpdater.getDownstreamTypesMask(" WIFI: | BT"))
+        assertEquals(WIFI_MASK or USB_MASK,
+                notificationUpdater.getDownstreamTypesMask("1|2|USB|WIFI|BLUETOOTH||"))
+    }
+
+    @Test
+    fun testSetupRestrictedNotification() {
+        val title = InstrumentationRegistry.getInstrumentation().context.resources
+                .getString(R.string.disable_tether_notification_title)
+        val message = InstrumentationRegistry.getInstrumentation().context.resources
+                .getString(R.string.disable_tether_notification_message)
+        val disallowTitle = "Tether function is disallowed"
+        val disallowMessage = "Please contact your admin"
+        doReturn(title).`when`(defaultResources)
+                .getString(R.string.disable_tether_notification_title)
+        doReturn(message).`when`(defaultResources)
+                .getString(R.string.disable_tether_notification_message)
+        doReturn(disallowTitle).`when`(testResources)
+                .getString(R.string.disable_tether_notification_title)
+        doReturn(disallowMessage).`when`(testResources)
+                .getString(R.string.disable_tether_notification_message)
+
+        // User restrictions on. Show restricted notification.
+        notificationUpdater.notifyTetheringDisabledByRestriction()
+        verifyNotification(R.drawable.stat_sys_tether_general, title, message)
+
+        // User restrictions off. Clear notification.
+        notificationUpdater.tetheringRestrictionLifted()
+        verifyNoNotification()
+
+        // Set test sub id. No notification.
+        notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
+        verifyNoNotification()
+
+        // User restrictions on again. Show restricted notification with test resource.
+        notificationUpdater.notifyTetheringDisabledByRestriction()
+        verifyNotification(R.drawable.stat_sys_tether_general, disallowTitle, disallowMessage)
+    }
+}
\ No newline at end of file
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
index d983fae..a418c4a 100644
--- a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
@@ -38,6 +38,7 @@
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
@@ -210,7 +211,6 @@
     private PhoneStateListener mPhoneStateListener;
     private InterfaceConfigurationParcel mInterfaceConfiguration;
 
-
     private class TestContext extends BroadcastInterceptingContext {
         TestContext(Context base) {
             super(base);
@@ -485,6 +485,7 @@
         mServiceContext.registerReceiver(mBroadcastReceiver,
                 new IntentFilter(ACTION_TETHER_STATE_CHANGED));
         mTethering = makeTethering();
+        mTethering.startStateMachineUpdaters();
         verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
         verify(mNetd).registerUnsolicitedEventListener(any());
         final ArgumentCaptor<PhoneStateListener> phoneListenerCaptor =
@@ -1073,13 +1074,15 @@
         when(mUserManager.getUserRestrictions()).thenReturn(newRestrictions);
 
         final Tethering.UserRestrictionActionListener ural =
-                new Tethering.UserRestrictionActionListener(mUserManager, mockTethering);
+                new Tethering.UserRestrictionActionListener(
+                        mUserManager, mockTethering, mNotificationUpdater);
         ural.mDisallowTethering = currentDisallow;
 
         ural.onUserRestrictionsChanged();
 
-        verify(mockTethering, times(expectedInteractionsWithShowNotification))
-                .untetherAll();
+        verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification))
+                .notifyTetheringDisabledByRestriction();
+        verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll();
     }
 
     @Test
@@ -1087,7 +1090,7 @@
         final String[] emptyActiveIfacesList = new String[]{};
         final boolean currDisallow = false;
         final boolean nextDisallow = true;
-        final int expectedInteractionsWithShowNotification = 0;
+        final int expectedInteractionsWithShowNotification = 1;
 
         runUserRestrictionsChange(currDisallow, nextDisallow, emptyActiveIfacesList,
                 expectedInteractionsWithShowNotification);
@@ -1399,6 +1402,7 @@
         mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
         final TetheringConfiguration newConfig = mTethering.getTetheringConfiguration();
         assertEquals(fakeSubId, newConfig.activeDataSubId);
+        verify(mNotificationUpdater, times(1)).onActiveDataSubscriptionIdChanged(eq(fakeSubId));
     }
 
     @Test
@@ -1651,10 +1655,13 @@
     }
 
     @Test
-    public void testRequestStaticServerIp() throws Exception {
-        final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
-        final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
-        final String serverAddr = "192.168.20.1";
+    public void testRequestStaticIp() throws Exception {
+        final LinkAddress serverLinkAddr = new LinkAddress("192.168.0.123/24");
+        final LinkAddress clientLinkAddr = new LinkAddress("192.168.0.42/24");
+        final String serverAddr = "192.168.0.123";
+        final int clientAddrParceled = 0xc0a8002a;
+        final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
+                ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
                   serverLinkAddr, clientLinkAddr), null);
         mLooper.dispatchAll();
@@ -1663,8 +1670,12 @@
         sendUsbBroadcast(true, true, true, TETHERING_USB);
         mLooper.dispatchAll();
         verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
-
-        // TODO: test static client address.
+        verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(),
+                any());
+        final DhcpServingParamsParcel params = dhcpParamsCaptor.getValue();
+        assertEquals(serverAddr, intToInet4AddressHTH(params.serverAddr).getHostAddress());
+        assertEquals(24, params.serverAddrPrefixLength);
+        assertEquals(clientAddrParceled, params.clientAddr);
     }
 
     // TODO: Test that a request for hotspot mode doesn't interfere with an