[Tether07] Migrate Tethering into module

Now tethering would be run in dedicated service.
TetheringManager is the interface used to communicate with
TetheringService. The new call flow would be: ConnectivityManager
-> ConnectivityService -> TetheringManager -> TetheringService.
Note: the return value of #tether(), #untether() and #setUsbTethering()
APIs would always be no error. Client can use #getLastTetherError()
or #getTetheredIfaces or listen tether state change to check
status of corresponding interface.

Bug: 136040414
Bug: 144742179
Test: -build, flash, boot
      -atest TetheringTests
      -atest FrameworksNetTests

Change-Id: I7e78c0e0a3e70f940a749ba2a39ece7c7ec5b9b3
Merged-In: I7e78c0e0a3e70f940a749ba2a39ece7c7ec5b9b3
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index cd51f5f..7e8721d 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -22,13 +22,12 @@
         ":framework-tethering-shared-srcs",
         ":net-module-utils-srcs",
         ":services-tethering-shared-srcs",
-        ":servicescore-tethering-src",
     ],
     static_libs: [
         "androidx.annotation_annotation",
-        "netd_aidl_interface-java",
+        "netd_aidl_interface-unstable-java",
         "netlink-client",
-        "networkstack-aidl-interfaces-java",
+        "networkstack-aidl-interfaces-unstable-java",
         "android.hardware.tetheroffload.control-V1.0-java",
         "tethering-client",
     ],
@@ -96,7 +95,16 @@
 }
 
 // Non-updatable tethering running in the system server process for devices not using the module
-// TODO: build in-process tethering APK here.
+android_app {
+    name: "InProcessTethering",
+    defaults: ["TetheringAppDefaults"],
+    static_libs: ["TetheringApiCurrentLib"],
+    certificate: "platform",
+    manifest: "AndroidManifest_InProcess.xml",
+    // InProcessTethering is a replacement for Tethering
+    overrides: ["Tethering"],
+    // TODO: use PlatformNetworkPermissionConfig.
+}
 
 // Updatable tethering packaged as an application
 android_app {
@@ -109,36 +117,3 @@
     // The permission configuration *must* be included to ensure security of the device
     required: ["NetworkPermissionConfig"],
 }
-
-// This group will be removed when tethering migration is done.
-filegroup {
-    name: "tethering-servicescore-srcs",
-    srcs: [
-        "src/com/android/server/connectivity/tethering/EntitlementManager.java",
-        "src/com/android/server/connectivity/tethering/OffloadController.java",
-        "src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java",
-        "src/com/android/server/connectivity/tethering/TetheringConfiguration.java",
-        "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java",
-    ],
-}
-
-// This group will be removed when tethering migration is done.
-filegroup {
-    name: "tethering-servicesnet-srcs",
-    srcs: [
-        "src/android/net/dhcp/DhcpServerCallbacks.java",
-        "src/android/net/dhcp/DhcpServingParamsParcelExt.java",
-        "src/android/net/ip/IpServer.java",
-        "src/android/net/ip/RouterAdvertisementDaemon.java",
-        "src/android/net/util/InterfaceSet.java",
-        "src/android/net/util/PrefixUtils.java",
-    ],
-}
-
-// This group would be removed when tethering migration is done.
-filegroup {
-    name: "tethering-jni-srcs",
-    srcs: [
-        "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp",
-    ],
-}
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index eb51593..1430ed0 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -25,5 +25,11 @@
         android:process="com.android.networkstack.process"
         android:extractNativeLibs="false"
         android:persistent="true">
+        <service android:name="com.android.server.connectivity.tethering.TetheringService"
+                 android:permission="android.permission.MAINLINE_NETWORK_STACK">
+            <intent-filter>
+                <action android:name="android.net.ITetheringConnector"/>
+            </intent-filter>
+        </service>
     </application>
 </manifest>
diff --git a/Tethering/AndroidManifest_InProcess.xml b/Tethering/AndroidManifest_InProcess.xml
new file mode 100644
index 0000000..28d405c
--- /dev/null
+++ b/Tethering/AndroidManifest_InProcess.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 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.tethering.inprocess"
+          android:sharedUserId="android.uid.system"
+          android:process="system">
+    <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+    <application>
+        <!-- TODO: Using MAINLINE_NETWORK_STACK instead of NETWORK_STACK when tethering run in the
+                   same process with networkstack -->
+        <service android:name="com.android.server.connectivity.tethering.TetheringService"
+                 android:process="system"
+                 android:permission="android.permission.NETWORK_STACK">
+            <intent-filter>
+                <action android:name="android.net.ITetheringConnector.InProcess"/>
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 5b01b1e..adc5a72 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -18,8 +18,12 @@
 aidl_interface {
     name: "tethering-aidl-interfaces",
     local_include_dir: "src",
+    include_dirs: ["frameworks/base/core/java"], // For framework parcelables.
     srcs: [
+        "src/android/net/ITetherInternalCallback.aidl",
         "src/android/net/ITetheringConnector.aidl",
+        "src/android/net/TetheringConfigurationParcel.aidl",
+        "src/android/net/TetherStatesParcel.aidl",
     ],
     backend: {
         ndk: {
@@ -33,8 +37,15 @@
 
 java_library {
     name: "tethering-client",
-    platform_apis: true,
+    sdk_version: "system_current",
     static_libs: [
         "tethering-aidl-interfaces-java",
     ],
 }
+
+// This is temporary file group which would be removed after TetheringManager is built
+// into tethering-client. Will be done by aosp/1156906.
+filegroup {
+    name: "tethering-manager",
+    srcs: ["src/android/net/TetheringManager.java"],
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetherInternalCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetherInternalCallback.aidl
new file mode 100644
index 0000000..abb00e8
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/ITetherInternalCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 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.Network;
+import android.net.TetheringConfigurationParcel;
+import android.net.TetherStatesParcel;
+
+/**
+ * Callback class for receiving tethering changed events
+ * @hide
+ */
+oneway interface ITetherInternalCallback
+{
+    void onUpstreamChanged(in Network network);
+    void onConfigurationChanged(in TetheringConfigurationParcel config);
+    void onTetherStatesChanged(in TetherStatesParcel states);
+    void onCallbackCreated(in Network network, in TetheringConfigurationParcel config,
+            in TetherStatesParcel states);
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
index 443481e..bfe502f 100644
--- a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl
@@ -15,6 +15,23 @@
  */
 package android.net;
 
+import android.net.ITetherInternalCallback;
+import android.os.ResultReceiver;
+
 /** @hide */
 oneway interface ITetheringConnector {
+    void tether(String iface);
+
+    void untether(String iface);
+
+    void setUsbTethering(boolean enable);
+
+    void startTethering(int type, in ResultReceiver receiver, boolean showProvisioningUi);
+
+    void stopTethering(int type);
+
+    void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver,
+            boolean showEntitlementUi);
+
+    void registerTetherInternalCallback(ITetherInternalCallback callback);
 }
diff --git a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
new file mode 100644
index 0000000..3d842b3
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 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;
+
+/**
+ * Status details for tethering downstream interfaces.
+ * {@hide}
+ */
+parcelable TetherStatesParcel {
+    String[] availableList;
+    String[] tetheredList;
+    String[] localOnlyList;
+    String[] erroredIfaceList;
+    // List of Last error code corresponding to each errored iface in erroredIfaceList. */
+    // TODO: Improve this as b/143122247.
+    int[] lastErrorList;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl
new file mode 100644
index 0000000..fdf0d16
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 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;
+
+/**
+ * Configuration details for a tethering.
+ * @hide
+ */
+parcelable TetheringConfigurationParcel {
+    int subId;
+    String[] tetherableUsbRegexs;
+    String[] tetherableWifiRegexs;
+    String[] tetherableBluetoothRegexs;
+    boolean isDunRequired;
+    boolean chooseUpstreamAutomatically;
+    int[] preferredUpstreamIfaceTypes;
+    String[] legacyDhcpRanges;
+    String[] defaultIPv4DNS;
+    boolean enableLegacyDhcpServer;
+    String[] provisioningApp;
+    String provisioningAppNoUi;
+    int provisioningCheckPeriod;
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
new file mode 100644
index 0000000..9e8726e
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2019 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.NETWORK_STACK;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.util.SharedLog;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.StringJoiner;
+
+/**
+ * Service used to communicate with the tethering, which is running in a separate module.
+ * @hide
+ */
+public class TetheringManager {
+    private static final String TAG = TetheringManager.class.getSimpleName();
+
+    private static TetheringManager sInstance;
+
+    @Nullable
+    private ITetheringConnector mConnector;
+    private TetherInternalCallback mCallback;
+    private Network mTetherUpstream;
+    private TetheringConfigurationParcel mTetheringConfiguration;
+    private TetherStatesParcel mTetherStatesParcel;
+
+    private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
+            new RemoteCallbackList<>();
+    @GuardedBy("mLog")
+    private final SharedLog mLog = new SharedLog(TAG);
+
+    private TetheringManager() { }
+
+    /**
+     * Get the TetheringManager singleton instance.
+     */
+    public static synchronized TetheringManager getInstance() {
+        if (sInstance == null) {
+            sInstance = new TetheringManager();
+        }
+        return sInstance;
+    }
+
+    private class TetheringConnection implements
+            ConnectivityModuleConnector.ModuleServiceCallback {
+        @Override
+        public void onModuleServiceConnected(@NonNull IBinder service) {
+            logi("Tethering service connected");
+            registerTetheringService(service);
+        }
+    }
+
+    private void registerTetheringService(@NonNull IBinder service) {
+        final ITetheringConnector connector = ITetheringConnector.Stub.asInterface(service);
+
+        log("Tethering service registered");
+
+        // Currently TetheringManager instance is only used by ConnectivityService and mConnector
+        // only expect to assign once when system server start and bind tethering service.
+        // STOPSHIP: Change mConnector to final before TetheringManager put into boot classpath.
+        mConnector = connector;
+        mCallback = new TetherInternalCallback();
+        try {
+            mConnector.registerTetherInternalCallback(mCallback);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    private class TetherInternalCallback extends ITetherInternalCallback.Stub {
+        private final ConditionVariable mWaitForCallback = new ConditionVariable(false);
+        private static final int EVENT_CALLBACK_TIMEOUT_MS = 60_000;
+
+        @Override
+        public void onUpstreamChanged(Network network) {
+            mTetherUpstream = network;
+            reportUpstreamChanged(network);
+        }
+
+        @Override
+        public void onConfigurationChanged(TetheringConfigurationParcel config) {
+            mTetheringConfiguration = config;
+        }
+
+        @Override
+        public void onTetherStatesChanged(TetherStatesParcel states) {
+            mTetherStatesParcel = states;
+        }
+
+        @Override
+        public void onCallbackCreated(Network network, TetheringConfigurationParcel config,
+                TetherStatesParcel states) {
+            mTetherUpstream = network;
+            mTetheringConfiguration = config;
+            mTetherStatesParcel = states;
+            mWaitForCallback.open();
+        }
+
+        boolean awaitCallbackCreation() {
+            return mWaitForCallback.block(EVENT_CALLBACK_TIMEOUT_MS);
+        }
+    }
+
+    private void reportUpstreamChanged(Network network) {
+        final int length = mTetheringEventCallbacks.beginBroadcast();
+        try {
+            for (int i = 0; i < length; i++) {
+                try {
+                    mTetheringEventCallbacks.getBroadcastItem(i).onUpstreamChanged(network);
+                } catch (RemoteException e) {
+                    // Not really very much to do here.
+                }
+            }
+        } finally {
+            mTetheringEventCallbacks.finishBroadcast();
+        }
+    }
+
+    /**
+     * Start the tethering service. Should be called only once on device startup.
+     *
+     * <p>This method will start the tethering service either in the network stack process,
+     * or inside the system server on devices that do not support the tethering module.
+     *
+     * {@hide}
+     */
+    public void start() {
+        // Using MAINLINE_NETWORK_STACK permission after cutting off the dpendency of system server.
+        ConnectivityModuleConnector.getInstance().startModuleService(
+                ITetheringConnector.class.getName(), NETWORK_STACK,
+                new TetheringConnection());
+        log("Tethering service start requested");
+    }
+
+    /**
+     * Attempt to tether the named interface.  This will setup a dhcp server
+     * on the interface, forward and NAT IP v4 packets and forward DNS requests
+     * to the best active upstream network interface.  Note that if no upstream
+     * IP network interface is available, dhcp will still run and traffic will be
+     * allowed between the tethered devices and this device, though upstream net
+     * access will of course fail until an upstream network interface becomes
+     * active. Note: return value do not have any meaning. It is better to use
+     * #getTetherableIfaces() to ensure corresponding interface is available for
+     * tethering before calling #tether().
+     *
+     * TODO: Deprecate this API. The only usages should be in PanService and Wifi P2P which
+     * need direct access.
+     *
+     * {@hide}
+     */
+    public int tether(@NonNull String iface) {
+        try {
+            mConnector.tether(iface);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
+    /**
+     * Stop tethering the named interface.
+     *
+     * {@hide}
+     */
+    public int untether(@NonNull String iface) {
+        try {
+            mConnector.untether(iface);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
+    /**
+     * Attempt to both alter the mode of USB and Tethering of USB. WARNING: New client should not
+     * use this API anymore. All clients should use #startTethering or #stopTethering which
+     * encapsulate proper entitlement logic. If the API is used and an entitlement check is needed,
+     * downstream USB tethering will be enabled but will not have any upstream.
+     *
+     * @Deprecated
+     * {@hide}
+     */
+    public int setUsbTethering(boolean enable) {
+        try {
+            mConnector.setUsbTethering(enable);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
+    /**
+     * Starts tethering and runs tether provisioning for the given type if needed. If provisioning
+     * fails, stopTethering will be called automatically.
+     *
+     * {@hide}
+     */
+    // TODO: improve the usage of ResultReceiver, b/145096122
+    public void startTethering(int type, @NonNull ResultReceiver receiver,
+            boolean showProvisioningUi) {
+        try {
+            mConnector.startTethering(type, receiver, showProvisioningUi);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
+     * applicable.
+     *
+     * {@hide}
+     */
+    public void stopTethering(int type) {
+        try {
+            mConnector.stopTethering(type);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request the latest value of the tethering entitlement check.
+     *
+     * Note: Allow privileged apps who have TETHER_PRIVILEGED permission to access. If it turns
+     * out some such apps are observed to abuse this API, change to per-UID limits on this API
+     * if it's really needed.
+     */
+    // TODO: improve the usage of ResultReceiver, b/145096122
+    public void requestLatestTetheringEntitlementResult(int type, @NonNull ResultReceiver receiver,
+            boolean showEntitlementUi) {
+        try {
+            mConnector.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Register tethering event callback.
+     *
+     * {@hide}
+     */
+    public void registerTetheringEventCallback(@NonNull ITetheringEventCallback callback) {
+        mTetheringEventCallbacks.register(callback);
+    }
+
+    /**
+     * Unregister tethering event callback.
+     *
+     * {@hide}
+     */
+    public void unregisterTetheringEventCallback(@NonNull ITetheringEventCallback callback) {
+        mTetheringEventCallbacks.unregister(callback);
+    }
+
+    /**
+     * Get a more detailed error code after a Tethering or Untethering
+     * request asynchronously failed.
+     *
+     * {@hide}
+     */
+    public int getLastTetherError(@NonNull String iface) {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        if (mTetherStatesParcel == null) return TETHER_ERROR_NO_ERROR;
+
+        int i = 0;
+        for (String errored : mTetherStatesParcel.erroredIfaceList) {
+            if (iface.equals(errored)) return mTetherStatesParcel.lastErrorList[i];
+
+            i++;
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * USB network interfaces.  If USB tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetherableUsbRegexs() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        return mTetheringConfiguration.tetherableUsbRegexs;
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Wifi network interfaces.  If Wifi tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetherableWifiRegexs() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        return mTetheringConfiguration.tetherableWifiRegexs;
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Bluetooth network interfaces.  If Bluetooth tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetherableBluetoothRegexs() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        return mTetheringConfiguration.tetherableBluetoothRegexs;
+    }
+
+    /**
+     * Get the set of tetherable, available interfaces.  This list is limited by
+     * device configuration and current interface existence.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetherableIfaces() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        if (mTetherStatesParcel == null) return new String[0];
+        return mTetherStatesParcel.availableList;
+    }
+
+    /**
+     * Get the set of tethered interfaces.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetheredIfaces() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        if (mTetherStatesParcel == null) return new String[0];
+        return mTetherStatesParcel.tetheredList;
+    }
+
+    /**
+     * Get the set of interface names which attempted to tether but
+     * failed.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetheringErroredIfaces() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        if (mTetherStatesParcel == null) return new String[0];
+        return mTetherStatesParcel.erroredIfaceList;
+    }
+
+    /**
+     * Get the set of tethered dhcp ranges.
+     *
+     * {@hide}
+     */
+    public @NonNull String[] getTetheredDhcpRanges() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        return mTetheringConfiguration.legacyDhcpRanges;
+    }
+
+    /**
+     * Check if the device allows for tethering.
+     *
+     * {@hide}
+     */
+    public boolean hasTetherableConfiguration() {
+        if (!mCallback.awaitCallbackCreation()) {
+            throw new NullPointerException("callback was not ready yet");
+        }
+        final boolean hasDownstreamConfiguration =
+                (mTetheringConfiguration.tetherableUsbRegexs.length != 0)
+                || (mTetheringConfiguration.tetherableWifiRegexs.length != 0)
+                || (mTetheringConfiguration.tetherableBluetoothRegexs.length != 0);
+        final boolean hasUpstreamConfiguration =
+                (mTetheringConfiguration.preferredUpstreamIfaceTypes.length != 0)
+                || mTetheringConfiguration.chooseUpstreamAutomatically;
+
+        return hasDownstreamConfiguration && hasUpstreamConfiguration;
+    }
+
+    /**
+     * Log a message in the local log.
+     */
+    private void log(@NonNull String message) {
+        synchronized (mLog) {
+            mLog.log(message);
+        }
+    }
+
+    /**
+     * Log a condition that should never happen.
+     */
+    private void logWtf(@NonNull String message, @Nullable Throwable e) {
+        Slog.wtf(TAG, message);
+        synchronized (mLog) {
+            mLog.e(message, e);
+        }
+    }
+
+    /**
+     * Log a ERROR level message in the local and system logs.
+     */
+    private void loge(@NonNull String message, @Nullable Throwable e) {
+        synchronized (mLog) {
+            mLog.e(message, e);
+        }
+    }
+
+    /**
+     * Log a INFO level message in the local and system logs.
+     */
+    private void logi(@NonNull String message) {
+        synchronized (mLog) {
+            mLog.i(message);
+        }
+    }
+
+    /**
+     * Dump TetheringManager logs to the specified {@link PrintWriter}.
+     */
+    public void dump(@NonNull PrintWriter pw) {
+        // dump is thread-safe on SharedLog
+        mLog.dump(null, pw, null);
+
+        pw.print("subId: ");
+        pw.println(mTetheringConfiguration.subId);
+
+        dumpStringArray(pw, "tetherableUsbRegexs",
+                mTetheringConfiguration.tetherableUsbRegexs);
+        dumpStringArray(pw, "tetherableWifiRegexs",
+                mTetheringConfiguration.tetherableWifiRegexs);
+        dumpStringArray(pw, "tetherableBluetoothRegexs",
+                mTetheringConfiguration.tetherableBluetoothRegexs);
+
+        pw.print("isDunRequired: ");
+        pw.println(mTetheringConfiguration.isDunRequired);
+
+        pw.print("chooseUpstreamAutomatically: ");
+        pw.println(mTetheringConfiguration.chooseUpstreamAutomatically);
+
+        dumpStringArray(pw, "legacyDhcpRanges", mTetheringConfiguration.legacyDhcpRanges);
+        dumpStringArray(pw, "defaultIPv4DNS", mTetheringConfiguration.defaultIPv4DNS);
+
+        dumpStringArray(pw, "provisioningApp", mTetheringConfiguration.provisioningApp);
+        pw.print("provisioningAppNoUi: ");
+        pw.println(mTetheringConfiguration.provisioningAppNoUi);
+
+        pw.print("enableLegacyDhcpServer: ");
+        pw.println(mTetheringConfiguration.enableLegacyDhcpServer);
+
+        pw.println();
+    }
+
+    private static void dumpStringArray(@NonNull PrintWriter pw, @NonNull String label,
+            @Nullable String[] values) {
+        pw.print(label);
+        pw.print(": ");
+
+        if (values != null) {
+            final StringJoiner sj = new StringJoiner(", ", "[", "]");
+            for (String value : values) sj.add(value);
+
+            pw.print(sj.toString());
+        } else {
+            pw.print("null");
+        }
+
+        pw.println();
+    }
+}
diff --git a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
index 3eaf488..663154a 100644
--- a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
+++ b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
@@ -145,4 +145,18 @@
             gMethods, NELEM(gMethods));
 }
 
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+    JNIEnv *env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        ALOGE("ERROR: GetEnv failed");
+        return JNI_ERR;
+    }
+
+    if (register_android_server_connectivity_tethering_OffloadHardwareInterface(env) < 0) {
+        return JNI_ERR;
+    }
+
+    return JNI_VERSION_1_6;
+}
+
 }; // namespace android
diff --git a/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java b/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java
new file mode 100644
index 0000000..3218c0b
--- /dev/null
+++ b/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import android.net.INetdUnsolicitedEventListener;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Base {@link INetdUnsolicitedEventListener} that provides no-op implementations which can be
+ * overridden.
+ */
+public class BaseNetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
+
+    @Override
+    public void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs,
+            int uid) { }
+
+    @Override
+    public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceDnsServerInfo(@NonNull String ifName, long lifetimeS,
+            @NonNull String[] servers) { }
+
+    @Override
+    public void onInterfaceAddressUpdated(@NonNull String addr, String ifName, int flags,
+            int scope) { }
+
+    @Override
+    public void onInterfaceAddressRemoved(@NonNull String addr, @NonNull String ifName, int flags,
+            int scope) { }
+
+    @Override
+    public void onInterfaceAdded(@NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceRemoved(@NonNull String ifName) { }
+
+    @Override
+    public void onInterfaceChanged(@NonNull String ifName, boolean up) { }
+
+    @Override
+    public void onInterfaceLinkStateChanged(@NonNull String ifName, boolean up) { }
+
+    @Override
+    public void onRouteChanged(boolean updated, @NonNull String route, @NonNull String gateway,
+            @NonNull String ifName) { }
+
+    @Override
+    public void onStrictCleartextDetected(int uid, @NonNull String hex) { }
+
+    @Override
+    public int getInterfaceVersion() {
+        return INetdUnsolicitedEventListener.VERSION;
+    }
+}
diff --git a/Tethering/src/android/net/util/VersionedBroadcastListener.java b/Tethering/src/android/net/util/VersionedBroadcastListener.java
new file mode 100644
index 0000000..e2804ab
--- /dev/null
+++ b/Tethering/src/android/net/util/VersionedBroadcastListener.java
@@ -0,0 +1,106 @@
+/*
+ * 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.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+
+/**
+ * A utility class that runs the provided callback on the provided handler when
+ * intents matching the provided filter arrive. Intents received by a stale
+ * receiver are safely ignored.
+ *
+ * Calls to startListening() and stopListening() must happen on the same thread.
+ *
+ * @hide
+ */
+public class VersionedBroadcastListener {
+    private static final boolean DBG = false;
+
+    private final String mTag;
+    private final Context mContext;
+    private final Handler mHandler;
+    private final IntentFilter mFilter;
+    private final Consumer<Intent> mCallback;
+    private final AtomicInteger mGenerationNumber;
+    private BroadcastReceiver mReceiver;
+
+    public VersionedBroadcastListener(String tag, Context ctx, Handler handler,
+            IntentFilter filter, Consumer<Intent> callback) {
+        mTag = tag;
+        mContext = ctx;
+        mHandler = handler;
+        mFilter = filter;
+        mCallback = callback;
+        mGenerationNumber = new AtomicInteger(0);
+    }
+
+    /** Start listening to intent broadcast. */
+    public void startListening() {
+        if (DBG) Log.d(mTag, "startListening");
+        if (mReceiver != null) return;
+
+        mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
+        mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+    }
+
+    /** Stop listening to intent broadcast. */
+    public void stopListening() {
+        if (DBG) Log.d(mTag, "stopListening");
+        if (mReceiver == null) return;
+
+        mGenerationNumber.incrementAndGet();
+        mContext.unregisterReceiver(mReceiver);
+        mReceiver = null;
+    }
+
+    private static class Receiver extends BroadcastReceiver {
+        public final String tag;
+        public final AtomicInteger atomicGenerationNumber;
+        public final Consumer<Intent> callback;
+        // Used to verify this receiver is still current.
+        public final int generationNumber;
+
+        Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
+            this.tag = tag;
+            this.atomicGenerationNumber = atomicGenerationNumber;
+            this.callback = callback;
+            generationNumber = atomicGenerationNumber.incrementAndGet();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final int currentGenerationNumber = atomicGenerationNumber.get();
+
+            if (DBG) {
+                Log.d(tag, "receiver generationNumber=" + generationNumber
+                        + ", current generationNumber=" + currentGenerationNumber);
+            }
+            if (generationNumber != currentGenerationNumber) return;
+
+            callback.accept(intent);
+        }
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java b/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
index 6b0f1de..ba5d08d 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
@@ -47,6 +47,7 @@
 import android.os.PersistableBundle;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
@@ -55,7 +56,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.StateMachine;
-import com.android.server.connectivity.MockableSystemProperties;
 
 import java.io.PrintWriter;
 
@@ -94,7 +94,6 @@
     private final ArraySet<Integer> mCurrentTethers;
     private final Context mContext;
     private final int mPermissionChangeMessageCode;
-    private final MockableSystemProperties mSystemProperties;
     private final SharedLog mLog;
     private final SparseIntArray mEntitlementCacheValue;
     private final EntitlementHandler mHandler;
@@ -110,12 +109,12 @@
     private TetheringConfigurationFetcher mFetcher;
 
     public EntitlementManager(Context ctx, StateMachine tetherMasterSM, SharedLog log,
-            int permissionChangeMessageCode, MockableSystemProperties systemProperties) {
+            int permissionChangeMessageCode) {
+
         mContext = ctx;
         mLog = log.forSubComponent(TAG);
         mCurrentTethers = new ArraySet<Integer>();
         mCellularPermitted = new SparseIntArray();
-        mSystemProperties = systemProperties;
         mEntitlementCacheValue = new SparseIntArray();
         mTetherMasterSM = tetherMasterSM;
         mPermissionChangeMessageCode = permissionChangeMessageCode;
@@ -287,7 +286,7 @@
      */
     @VisibleForTesting
     protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
-        if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
+        if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
                 || config.provisioningApp.length == 0) {
             return false;
         }
@@ -526,8 +525,8 @@
                     handleMaybeRunProvisioning(config);
                     break;
                 case EVENT_GET_ENTITLEMENT_VALUE:
-                    handleGetLatestTetheringEntitlementValue(msg.arg1, (ResultReceiver) msg.obj,
-                            toBool(msg.arg2));
+                    handleRequestLatestTetheringEntitlementValue(msg.arg1,
+                            (ResultReceiver) msg.obj, toBool(msg.arg2));
                     break;
                 default:
                     mLog.log("Unknown event: " + msg.what);
@@ -651,15 +650,15 @@
     }
 
     /** Get the last value of the tethering entitlement check. */
-    public void getLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
+    public void requestLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
             boolean showEntitlementUi) {
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_GET_ENTITLEMENT_VALUE,
                 downstream, encodeBool(showEntitlementUi), receiver));
 
     }
 
-    private void handleGetLatestTetheringEntitlementValue(int downstream, ResultReceiver receiver,
-            boolean showEntitlementUi) {
+    private void handleRequestLatestTetheringEntitlementValue(int downstream,
+            ResultReceiver receiver, boolean showEntitlementUi) {
         final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
         if (!isTetherProvisioningRequired(config)) {
             receiver.send(TETHER_ERROR_NO_ERROR, null);
diff --git a/Tethering/src/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
new file mode 100644
index 0000000..edfe3ca
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/IPv6TetheringCoordinator.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkState;
+import android.net.RouteInfo;
+import android.net.ip.IpServer;
+import android.net.util.NetworkConstants;
+import android.net.util.SharedLog;
+import android.util.Log;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Random;
+
+
+/**
+ * IPv6 tethering is rather different from IPv4 owing to the absence of NAT.
+ * This coordinator is responsible for evaluating the dedicated prefixes
+ * assigned to the device and deciding how to divvy them up among downstream
+ * interfaces.
+ *
+ * @hide
+ */
+public class IPv6TetheringCoordinator {
+    private static final String TAG = IPv6TetheringCoordinator.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    private static class Downstream {
+        public final IpServer ipServer;
+        public final int mode;  // IpServer.STATE_*
+        // Used to append to a ULA /48, constructing a ULA /64 for local use.
+        public final short subnetId;
+
+        Downstream(IpServer ipServer, int mode, short subnetId) {
+            this.ipServer = ipServer;
+            this.mode = mode;
+            this.subnetId = subnetId;
+        }
+    }
+
+    private final ArrayList<IpServer> mNotifyList;
+    private final SharedLog mLog;
+    // NOTE: mActiveDownstreams is a list and not a hash data structure because
+    // we keep active downstreams in arrival order.  This is done so /64s can
+    // be parceled out on a "first come, first served" basis and a /64 used by
+    // a downstream that is no longer active can be redistributed to any next
+    // waiting active downstream (again, in arrival order).
+    private final LinkedList<Downstream> mActiveDownstreams;
+    private final byte[] mUniqueLocalPrefix;
+    private short mNextSubnetId;
+    private NetworkState mUpstreamNetworkState;
+
+    public IPv6TetheringCoordinator(ArrayList<IpServer> notifyList, SharedLog log) {
+        mNotifyList = notifyList;
+        mLog = log.forSubComponent(TAG);
+        mActiveDownstreams = new LinkedList<>();
+        mUniqueLocalPrefix = generateUniqueLocalPrefix();
+        mNextSubnetId = 0;
+    }
+
+    /** Add active downstream to ipv6 tethering candidate list. */
+    public void addActiveDownstream(IpServer downstream, int mode) {
+        if (findDownstream(downstream) == null) {
+            // Adding a new downstream appends it to the list. Adding a
+            // downstream a second time without first removing it has no effect.
+            // We never change the mode of a downstream except by first removing
+            // it and then re-adding it (with its new mode specified);
+            if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) {
+                // Make sure subnet IDs are always positive. They are appended
+                // to a ULA /48 to make a ULA /64 for local use.
+                mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1);
+            }
+            updateIPv6TetheringInterfaces();
+        }
+    }
+
+    /** Remove downstream from ipv6 tethering candidate list. */
+    public void removeActiveDownstream(IpServer downstream) {
+        stopIPv6TetheringOn(downstream);
+        if (mActiveDownstreams.remove(findDownstream(downstream))) {
+            updateIPv6TetheringInterfaces();
+        }
+
+        // When tethering is stopping we can reset the subnet counter.
+        if (mNotifyList.isEmpty()) {
+            if (!mActiveDownstreams.isEmpty()) {
+                Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty.");
+            }
+            mNextSubnetId = 0;
+        }
+    }
+
+    /**
+     * Call when upstream NetworkState may be changed.
+     * If upstream has ipv6 for tethering, update this new NetworkState
+     * to IpServer. Otherwise stop ipv6 tethering on downstream interfaces.
+     */
+    public void updateUpstreamNetworkState(NetworkState ns) {
+        if (VDBG) {
+            Log.d(TAG, "updateUpstreamNetworkState: " + toDebugString(ns));
+        }
+        if (TetheringInterfaceUtils.getIPv6Interface(ns) == null) {
+            stopIPv6TetheringOnAllInterfaces();
+            setUpstreamNetworkState(null);
+            return;
+        }
+
+        if (mUpstreamNetworkState != null
+                && !ns.network.equals(mUpstreamNetworkState.network)) {
+            stopIPv6TetheringOnAllInterfaces();
+        }
+
+        setUpstreamNetworkState(ns);
+        updateIPv6TetheringInterfaces();
+    }
+
+    private void stopIPv6TetheringOnAllInterfaces() {
+        for (IpServer ipServer : mNotifyList) {
+            stopIPv6TetheringOn(ipServer);
+        }
+    }
+
+    private void setUpstreamNetworkState(NetworkState ns) {
+        if (ns == null) {
+            mUpstreamNetworkState = null;
+        } else {
+            // Make a deep copy of the parts we need.
+            mUpstreamNetworkState = new NetworkState(
+                    null,
+                    new LinkProperties(ns.linkProperties),
+                    new NetworkCapabilities(ns.networkCapabilities),
+                    new Network(ns.network),
+                    null,
+                    null);
+        }
+
+        mLog.log("setUpstreamNetworkState: " + toDebugString(mUpstreamNetworkState));
+    }
+
+    private void updateIPv6TetheringInterfaces() {
+        for (IpServer ipServer : mNotifyList) {
+            final LinkProperties lp = getInterfaceIPv6LinkProperties(ipServer);
+            ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, lp);
+            break;
+        }
+    }
+
+    private LinkProperties getInterfaceIPv6LinkProperties(IpServer ipServer) {
+        final Downstream ds = findDownstream(ipServer);
+        if (ds == null) return null;
+
+        if (ds.mode == IpServer.STATE_LOCAL_ONLY) {
+            // Build a Unique Locally-assigned Prefix configuration.
+            return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
+        }
+
+        // This downstream is in IpServer.STATE_TETHERED mode.
+        if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) {
+            return null;
+        }
+
+        // NOTE: Here, in future, we would have policies to decide how to divvy
+        // up the available dedicated prefixes among downstream interfaces.
+        // At this time we have no such mechanism--we only support tethering
+        // IPv6 toward the oldest (first requested) active downstream.
+
+        final Downstream currentActive = mActiveDownstreams.peek();
+        if (currentActive != null && currentActive.ipServer == ipServer) {
+            final LinkProperties lp = getIPv6OnlyLinkProperties(
+                    mUpstreamNetworkState.linkProperties);
+            if (lp.hasIpv6DefaultRoute() && lp.hasGlobalIpv6Address()) {
+                return lp;
+            }
+        }
+
+        return null;
+    }
+
+    Downstream findDownstream(IpServer ipServer) {
+        for (Downstream ds : mActiveDownstreams) {
+            if (ds.ipServer == ipServer) return ds;
+        }
+        return null;
+    }
+
+    private static LinkProperties getIPv6OnlyLinkProperties(LinkProperties lp) {
+        final LinkProperties v6only = new LinkProperties();
+        if (lp == null) {
+            return v6only;
+        }
+
+        // NOTE: At this time we don't copy over any information about any
+        // stacked links. No current stacked link configuration has IPv6.
+
+        v6only.setInterfaceName(lp.getInterfaceName());
+
+        v6only.setMtu(lp.getMtu());
+
+        for (LinkAddress linkAddr : lp.getLinkAddresses()) {
+            if (linkAddr.isGlobalPreferred() && linkAddr.getPrefixLength() == 64) {
+                v6only.addLinkAddress(linkAddr);
+            }
+        }
+
+        for (RouteInfo routeInfo : lp.getRoutes()) {
+            final IpPrefix destination = routeInfo.getDestination();
+            if ((destination.getAddress() instanceof Inet6Address)
+                    && (destination.getPrefixLength() <= 64)) {
+                v6only.addRoute(routeInfo);
+            }
+        }
+
+        for (InetAddress dnsServer : lp.getDnsServers()) {
+            if (isIPv6GlobalAddress(dnsServer)) {
+                // For now we include ULAs.
+                v6only.addDnsServer(dnsServer);
+            }
+        }
+
+        v6only.setDomains(lp.getDomains());
+
+        return v6only;
+    }
+
+    // TODO: Delete this and switch to LinkAddress#isGlobalPreferred once we
+    // announce our own IPv6 address as DNS server.
+    private static boolean isIPv6GlobalAddress(InetAddress ip) {
+        return (ip instanceof Inet6Address)
+               && !ip.isAnyLocalAddress()
+               && !ip.isLoopbackAddress()
+               && !ip.isLinkLocalAddress()
+               && !ip.isSiteLocalAddress()
+               && !ip.isMulticastAddress();
+    }
+
+    private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) {
+        final LinkProperties lp = new LinkProperties();
+
+        final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48);
+        lp.addRoute(new RouteInfo(local48, null, null));
+
+        final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64);
+        // Because this is a locally-generated ULA, we don't have an upstream
+        // address. But because the downstream IP address management code gets
+        // its prefix from the upstream's IP address, we create a fake one here.
+        lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64));
+
+        lp.setMtu(NetworkConstants.ETHER_MTU);
+        return lp;
+    }
+
+    private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) {
+        final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length);
+        bytes[7] = (byte) (subnetId >> 8);
+        bytes[8] = (byte) subnetId;
+        return new IpPrefix(bytes, prefixlen);
+    }
+
+    // Generates a Unique Locally-assigned Prefix:
+    //
+    //     https://tools.ietf.org/html/rfc4193#section-3.1
+    //
+    // The result is a /48 that can be used for local-only communications.
+    private static byte[] generateUniqueLocalPrefix() {
+        final byte[] ulp = new byte[6];  // 6 = 48bits / 8bits/byte
+        (new Random()).nextBytes(ulp);
+
+        final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN);
+        in6addr[0] = (byte) 0xfd;  // fc00::/7 and L=1
+
+        return in6addr;
+    }
+
+    private static String toDebugString(NetworkState ns) {
+        if (ns == null) {
+            return "NetworkState{null}";
+        }
+        return String.format("NetworkState{%s, %s, %s}",
+                ns.network,
+                ns.networkCapabilities,
+                ns.linkProperties);
+    }
+
+    private static void stopIPv6TetheringOn(IpServer ipServer) {
+        ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
index 01339a4..00a6773 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
@@ -107,6 +107,8 @@
     public OffloadHardwareInterface(Handler h, SharedLog log) {
         mHandler = h;
         mLog = log.forSubComponent(TAG);
+
+        System.loadLibrary("tetheroffloadjni");
     }
 
     /** Get default value indicating whether offload is supported. */
diff --git a/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
new file mode 100644
index 0000000..058fb4f
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java
@@ -0,0 +1,2029 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import static android.hardware.usb.UsbManager.USB_CONFIGURED;
+import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
+import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_TETHER;
+import static android.net.ConnectivityManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.ConnectivityManager.EXTRA_ERRORED_TETHER;
+import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
+import static android.net.ConnectivityManager.TETHERING_BLUETOOTH;
+import static android.net.ConnectivityManager.TETHERING_INVALID;
+import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHERING_WIFI_P2P;
+import static android.net.ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
+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;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_UNSPECIFIED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
+import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.net.INetd;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.ITetherInternalCallback;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.NetworkState;
+import android.net.NetworkUtils;
+import android.net.TetherStatesParcel;
+import android.net.TetheringConfigurationParcel;
+import android.net.ip.IpServer;
+import android.net.util.BaseNetdUnsolicitedEventListener;
+import android.net.util.InterfaceSet;
+import android.net.util.PrefixUtils;
+import android.net.util.SharedLog;
+import android.net.util.VersionedBroadcastListener;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.UserManagerInternal;
+import android.os.UserManagerInternal.UserRestrictionsListener;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Protocol;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.server.LocalServices;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ *
+ * This class holds much of the business logic to allow Android devices
+ * to act as IP gateways via USB, BT, and WiFi interfaces.
+ */
+public class Tethering {
+
+    private static final String TAG = Tethering.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false;
+
+    private static final Class[] sMessageClasses = {
+            Tethering.class, TetherMasterSM.class, IpServer.class
+    };
+    private static final SparseArray<String> sMagicDecoderRing =
+            MessageUtils.findMessageNames(sMessageClasses);
+
+    private static class TetherState {
+        public final IpServer ipServer;
+        public int lastState;
+        public int lastError;
+
+        TetherState(IpServer ipServer) {
+            this.ipServer = ipServer;
+            // Assume all state machines start out available and with no errors.
+            lastState = IpServer.STATE_AVAILABLE;
+            lastError = TETHER_ERROR_NO_ERROR;
+        }
+
+        public boolean isCurrentlyServing() {
+            switch (lastState) {
+                case IpServer.STATE_TETHERED:
+                case IpServer.STATE_LOCAL_ONLY:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+    }
+
+    private final SharedLog mLog = new SharedLog(TAG);
+
+    // used to synchronize public access to members
+    private final Object mPublicSync;
+    private final Context mContext;
+    private final ArrayMap<String, TetherState> mTetherStates;
+    private final BroadcastReceiver mStateReceiver;
+    // Stopship: replace mNMService before production.
+    private final INetworkManagementService mNMService;
+    private final INetworkStatsService mStatsService;
+    private final INetworkPolicyManager mPolicyManager;
+    private final Looper mLooper;
+    private final StateMachine mTetherMasterSM;
+    private final OffloadController mOffloadController;
+    private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+    // TODO: Figure out how to merge this and other downstream-tracking objects
+    // into a single coherent structure.
+    private final HashSet<IpServer> mForwardedDownstreams;
+    private final VersionedBroadcastListener mCarrierConfigChange;
+    private final TetheringDependencies mDeps;
+    private final EntitlementManager mEntitlementMgr;
+    private final Handler mHandler;
+    private final PhoneStateListener mPhoneStateListener;
+    private final INetd mNetd;
+    private final NetdCallback mNetdCallback;
+    private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
+    // All the usage of mTetherInternalCallback should run in the same thread.
+    private ITetherInternalCallback mTetherInternalCallback = null;
+
+    private volatile TetheringConfiguration mConfig;
+    private InterfaceSet mCurrentUpstreamIfaceSet;
+    private Notification.Builder mTetheredNotificationBuilder;
+    private int mLastNotificationId;
+
+    private boolean mRndisEnabled;       // track the RNDIS function enabled state
+    // True iff. WiFi tethering should be started when soft AP is ready.
+    private boolean mWifiTetherRequested;
+    private Network mTetherUpstream;
+    private TetherStatesParcel mTetherStatesParcel;
+
+    public Tethering(TetheringDependencies deps) {
+        mLog.mark("Tethering.constructed");
+        mDeps = deps;
+        mContext = mDeps.getContext();
+        mNMService = mDeps.getINetworkManagementService();
+        mStatsService = mDeps.getINetworkStatsService();
+        mPolicyManager = mDeps.getINetworkPolicyManager();
+        mNetd = mDeps.getINetd(mContext);
+        mLooper = mDeps.getTetheringLooper();
+
+        mPublicSync = new Object();
+
+        mTetherStates = new ArrayMap<>();
+
+        mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps);
+        mTetherMasterSM.start();
+
+        mHandler = mTetherMasterSM.getHandler();
+        mOffloadController = new OffloadController(mHandler,
+                mDeps.getOffloadHardwareInterface(mHandler, mLog),
+                mContext.getContentResolver(), mNMService,
+                mLog);
+        mUpstreamNetworkMonitor = deps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
+                TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
+        mForwardedDownstreams = new HashSet<>();
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
+        // EntitlementManager will send EVENT_UPSTREAM_PERMISSION_CHANGED when cellular upstream
+        // permission is changed according to entitlement check result.
+        mEntitlementMgr = mDeps.getEntitlementManager(mContext, mTetherMasterSM, mLog,
+                TetherMasterSM.EVENT_UPSTREAM_PERMISSION_CHANGED);
+        mEntitlementMgr.setOnUiEntitlementFailedListener((int downstream) -> {
+            mLog.log("OBSERVED UiEnitlementFailed");
+            stopTethering(downstream);
+        });
+        mEntitlementMgr.setTetheringConfigurationFetcher(() -> {
+            return mConfig;
+        });
+
+        mCarrierConfigChange = new VersionedBroadcastListener(
+                "CarrierConfigChangeListener", mContext, mHandler, filter,
+                (Intent ignored) -> {
+                    mLog.log("OBSERVED carrier config change");
+                    updateConfiguration();
+                    mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
+                });
+
+        mPhoneStateListener = new PhoneStateListener(mLooper) {
+            @Override
+            public void onActiveDataSubscriptionIdChanged(int subId) {
+                mLog.log("OBSERVED active data subscription change, from " + mActiveDataSubId
+                        + " to " + subId);
+                if (subId == mActiveDataSubId) return;
+
+                mActiveDataSubId = subId;
+                updateConfiguration();
+                // To avoid launching unexpected provisioning checks, ignore re-provisioning when
+                // no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning() will be
+                // triggered again when CarrierConfig is loaded.
+                if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
+                    mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
+                } else {
+                    mLog.log("IGNORED reevaluate provisioning due to no carrier config loaded");
+                }
+            }
+        };
+
+        mStateReceiver = new StateReceiver();
+
+        mNetdCallback = new NetdCallback();
+        try {
+            mNetd.registerUnsolicitedEventListener(mNetdCallback);
+        } catch (RemoteException e) {
+            mLog.e("Unable to register netd UnsolicitedEventListener");
+        }
+
+        // Load tethering configuration.
+        updateConfiguration();
+
+        startStateMachineUpdaters(mHandler);
+        startTrackDefaultNetwork();
+    }
+
+    private void startStateMachineUpdaters(Handler handler) {
+        mCarrierConfigChange.startListening();
+        mContext.getSystemService(TelephonyManager.class).listen(
+                mPhoneStateListener, PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(UsbManager.ACTION_USB_STATE);
+        filter.addAction(CONNECTIVITY_ACTION);
+        filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+        mContext.registerReceiver(mStateReceiver, filter, null, handler);
+
+        filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_MEDIA_SHARED);
+        filter.addAction(Intent.ACTION_MEDIA_UNSHARED);
+        filter.addDataScheme("file");
+        mContext.registerReceiver(mStateReceiver, filter, null, handler);
+
+        final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+        // This check is useful only for some unit tests; example: ConnectivityServiceTest.
+        if (umi != null) {
+            umi.addUserRestrictionsListener(new TetheringUserRestrictionListener(this));
+        }
+    }
+
+    private WifiManager getWifiManager() {
+        return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+    }
+
+    // NOTE: This is always invoked on the mLooper thread.
+    private void updateConfiguration() {
+        mConfig = mDeps.generateTetheringConfiguration(mContext, mLog, mActiveDataSubId);
+        mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired);
+        reportConfigurationChanged(mConfig.toStableParcelable());
+    }
+
+    private void maybeDunSettingChanged() {
+        final boolean isDunRequired = TetheringConfiguration.checkDunRequired(
+                mContext, mActiveDataSubId);
+        if (isDunRequired == mConfig.isDunRequired) return;
+        updateConfiguration();
+    }
+
+    private class NetdCallback extends BaseNetdUnsolicitedEventListener {
+        @Override
+        public void onInterfaceChanged(String ifName, boolean up) {
+            mHandler.post(() -> interfaceStatusChanged(ifName, up));
+        }
+
+        @Override
+        public void onInterfaceLinkStateChanged(String ifName, boolean up) {
+            mHandler.post(() -> interfaceLinkStateChanged(ifName, up));
+        }
+
+        @Override
+        public void onInterfaceAdded(String ifName) {
+            mHandler.post(() -> interfaceAdded(ifName));
+        }
+
+        @Override
+        public void onInterfaceRemoved(String ifName) {
+            mHandler.post(() -> interfaceRemoved(ifName));
+        }
+    }
+
+    void interfaceStatusChanged(String iface, boolean up) {
+        // Never called directly: only called from interfaceLinkStateChanged.
+        // See NetlinkHandler.cpp: notifyInterfaceChanged.
+        if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up);
+        synchronized (mPublicSync) {
+            if (up) {
+                maybeTrackNewInterfaceLocked(iface);
+            } else {
+                if (ifaceNameToType(iface) == TETHERING_BLUETOOTH) {
+                    stopTrackingInterfaceLocked(iface);
+                } else {
+                    // Ignore usb0 down after enabling RNDIS.
+                    // We will handle disconnect in interfaceRemoved.
+                    // Similarly, ignore interface down for WiFi.  We monitor WiFi AP status
+                    // through the WifiManager.WIFI_AP_STATE_CHANGED_ACTION intent.
+                    if (VDBG) Log.d(TAG, "ignore interface down for " + iface);
+                }
+            }
+        }
+    }
+
+    void interfaceLinkStateChanged(String iface, boolean up) {
+        interfaceStatusChanged(iface, up);
+    }
+
+    private int ifaceNameToType(String iface) {
+        final TetheringConfiguration cfg = mConfig;
+
+        if (cfg.isWifi(iface)) {
+            return TETHERING_WIFI;
+        } else if (cfg.isWifiP2p(iface)) {
+            return TETHERING_WIFI_P2P;
+        } else if (cfg.isUsb(iface)) {
+            return TETHERING_USB;
+        } else if (cfg.isBluetooth(iface)) {
+            return TETHERING_BLUETOOTH;
+        }
+        return TETHERING_INVALID;
+    }
+
+    void interfaceAdded(String iface) {
+        if (VDBG) Log.d(TAG, "interfaceAdded " + iface);
+        synchronized (mPublicSync) {
+            maybeTrackNewInterfaceLocked(iface);
+        }
+    }
+
+
+    void interfaceRemoved(String iface) {
+        if (VDBG) Log.d(TAG, "interfaceRemoved " + iface);
+        synchronized (mPublicSync) {
+            stopTrackingInterfaceLocked(iface);
+        }
+    }
+
+    void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi) {
+        mEntitlementMgr.startProvisioningIfNeeded(type, showProvisioningUi);
+        enableTetheringInternal(type, true /* enabled */, receiver);
+    }
+
+    void stopTethering(int type) {
+        enableTetheringInternal(type, false /* disabled */, null);
+        mEntitlementMgr.stopProvisioningIfNeeded(type);
+    }
+
+    /**
+     * Enables or disables tethering for the given type. If provisioning is required, it will
+     * schedule provisioning rechecks for the specified interface.
+     */
+    private void enableTetheringInternal(int type, boolean enable, ResultReceiver receiver) {
+        int result;
+        switch (type) {
+            case TETHERING_WIFI:
+                result = setWifiTethering(enable);
+                sendTetherResult(receiver, result);
+                break;
+            case TETHERING_USB:
+                result = setUsbTethering(enable);
+                sendTetherResult(receiver, result);
+                break;
+            case TETHERING_BLUETOOTH:
+                setBluetoothTethering(enable, receiver);
+                break;
+            default:
+                Log.w(TAG, "Invalid tether type.");
+                sendTetherResult(receiver, TETHER_ERROR_UNKNOWN_IFACE);
+        }
+    }
+
+    private void sendTetherResult(ResultReceiver receiver, int result) {
+        if (receiver != null) {
+            receiver.send(result, null);
+        }
+    }
+
+    private int setWifiTethering(final boolean enable) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mPublicSync) {
+                final WifiManager mgr = getWifiManager();
+                if (mgr == null) {
+                    mLog.e("setWifiTethering: failed to get WifiManager!");
+                    return TETHER_ERROR_SERVICE_UNAVAIL;
+                }
+                if ((enable && mgr.startSoftAp(null /* use existing wifi config */))
+                        || (!enable && mgr.stopSoftAp())) {
+                    mWifiTetherRequested = enable;
+                    return TETHER_ERROR_NO_ERROR;
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+
+        return TETHER_ERROR_MASTER_ERROR;
+    }
+
+    private void setBluetoothTethering(final boolean enable, final ResultReceiver receiver) {
+        final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter == null || !adapter.isEnabled()) {
+            Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
+                    + (adapter == null));
+            sendTetherResult(receiver, TETHER_ERROR_SERVICE_UNAVAIL);
+            return;
+        }
+
+        adapter.getProfileProxy(mContext, new ServiceListener() {
+            @Override
+            public void onServiceDisconnected(int profile) { }
+
+            @Override
+            public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                // Clear identify is fine because caller already pass tethering permission at
+                // ConnectivityService#startTethering()(or stopTethering) before the control comes
+                // here. Bluetooth will check tethering permission again that there is
+                // Context#getOpPackageName() under BluetoothPan#setBluetoothTethering() to get
+                // caller's package name for permission check.
+                // Calling BluetoothPan#setBluetoothTethering() here means the package name always
+                // be system server. If calling identity is not cleared, that package's uid might
+                // not match calling uid and end up in permission denied.
+                final long identityToken = Binder.clearCallingIdentity();
+                try {
+                    ((BluetoothPan) proxy).setBluetoothTethering(enable);
+                } finally {
+                    Binder.restoreCallingIdentity(identityToken);
+                }
+                // TODO: Enabling bluetooth tethering can fail asynchronously here.
+                // We should figure out a way to bubble up that failure instead of sending success.
+                final int result = (((BluetoothPan) proxy).isTetheringOn() == enable)
+                        ? TETHER_ERROR_NO_ERROR
+                        : TETHER_ERROR_MASTER_ERROR;
+                sendTetherResult(receiver, result);
+                adapter.closeProfileProxy(BluetoothProfile.PAN, proxy);
+            }
+        }, BluetoothProfile.PAN);
+    }
+
+    int tether(String iface) {
+        return tether(iface, IpServer.STATE_TETHERED);
+    }
+
+    private int tether(String iface, int requestedState) {
+        if (DBG) Log.d(TAG, "Tethering " + iface);
+        synchronized (mPublicSync) {
+            TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState == null) {
+                Log.e(TAG, "Tried to Tether an unknown iface: " + iface + ", ignoring");
+                return TETHER_ERROR_UNKNOWN_IFACE;
+            }
+            // Ignore the error status of the interface.  If the interface is available,
+            // the errors are referring to past tethering attempts anyway.
+            if (tetherState.lastState != IpServer.STATE_AVAILABLE) {
+                Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
+                return TETHER_ERROR_UNAVAIL_IFACE;
+            }
+            // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's
+            // queue but not yet processed, this will be a no-op and it will not
+            // return an error.
+            //
+            // TODO: reexamine the threading and messaging model.
+            tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, requestedState);
+            return TETHER_ERROR_NO_ERROR;
+        }
+    }
+
+    int untether(String iface) {
+        if (DBG) Log.d(TAG, "Untethering " + iface);
+        synchronized (mPublicSync) {
+            TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState == null) {
+                Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
+                return TETHER_ERROR_UNKNOWN_IFACE;
+            }
+            if (!tetherState.isCurrentlyServing()) {
+                Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
+                return TETHER_ERROR_UNAVAIL_IFACE;
+            }
+            tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_UNREQUESTED);
+            return TETHER_ERROR_NO_ERROR;
+        }
+    }
+
+    void untetherAll() {
+        stopTethering(TETHERING_WIFI);
+        stopTethering(TETHERING_WIFI_P2P);
+        stopTethering(TETHERING_USB);
+        stopTethering(TETHERING_BLUETOOTH);
+    }
+
+    int getLastTetherError(String iface) {
+        synchronized (mPublicSync) {
+            TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState == null) {
+                Log.e(TAG, "Tried to getLastTetherError on an unknown iface :" + iface
+                        + ", ignoring");
+                return TETHER_ERROR_UNKNOWN_IFACE;
+            }
+            return tetherState.lastError;
+        }
+    }
+
+    // TODO: Figure out how to update for local hotspot mode interfaces.
+    private void sendTetherStateChangedBroadcast() {
+        if (!mDeps.isTetheringSupported()) return;
+
+        final ArrayList<String> availableList = new ArrayList<>();
+        final ArrayList<String> tetherList = new ArrayList<>();
+        final ArrayList<String> localOnlyList = new ArrayList<>();
+        final ArrayList<String> erroredList = new ArrayList<>();
+        final ArrayList<Integer> lastErrorList = new ArrayList<>();
+
+        boolean wifiTethered = false;
+        boolean usbTethered = false;
+        boolean bluetoothTethered = false;
+
+        final TetheringConfiguration cfg = mConfig;
+        final TetherStatesParcel mTetherStatesParcel = new TetherStatesParcel();
+
+        synchronized (mPublicSync) {
+            for (int i = 0; i < mTetherStates.size(); i++) {
+                TetherState tetherState = mTetherStates.valueAt(i);
+                String iface = mTetherStates.keyAt(i);
+                if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
+                    erroredList.add(iface);
+                    lastErrorList.add(tetherState.lastError);
+                } else if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+                    availableList.add(iface);
+                } else if (tetherState.lastState == IpServer.STATE_LOCAL_ONLY) {
+                    localOnlyList.add(iface);
+                } else if (tetherState.lastState == IpServer.STATE_TETHERED) {
+                    if (cfg.isUsb(iface)) {
+                        usbTethered = true;
+                    } else if (cfg.isWifi(iface)) {
+                        wifiTethered = true;
+                    } else if (cfg.isBluetooth(iface)) {
+                        bluetoothTethered = true;
+                    }
+                    tetherList.add(iface);
+                }
+            }
+        }
+
+        mTetherStatesParcel.availableList = availableList.toArray(new String[0]);
+        mTetherStatesParcel.tetheredList = tetherList.toArray(new String[0]);
+        mTetherStatesParcel.localOnlyList = localOnlyList.toArray(new String[0]);
+        mTetherStatesParcel.erroredIfaceList = erroredList.toArray(new String[0]);
+        mTetherStatesParcel.lastErrorList = new int[lastErrorList.size()];
+        Iterator<Integer> iterator = lastErrorList.iterator();
+        for (int i = 0; i < lastErrorList.size(); i++) {
+            mTetherStatesParcel.lastErrorList[i] = iterator.next().intValue();
+        }
+        reportTetherStateChanged(mTetherStatesParcel);
+
+        final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED);
+        bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
+                | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, availableList);
+        bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, localOnlyList);
+        bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, tetherList);
+        bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, erroredList);
+        mContext.sendStickyBroadcastAsUser(bcast, UserHandle.ALL);
+        if (DBG) {
+            Log.d(TAG, String.format(
+                    "sendTetherStateChangedBroadcast %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
+                    "avail", TextUtils.join(",", availableList),
+                    "local_only", TextUtils.join(",", localOnlyList),
+                    "tether", TextUtils.join(",", tetherList),
+                    "error", TextUtils.join(",", erroredList)));
+        }
+
+        if (usbTethered) {
+            if (wifiTethered || bluetoothTethered) {
+                showTetheredNotification(SystemMessage.NOTE_TETHER_GENERAL);
+            } else {
+                showTetheredNotification(SystemMessage.NOTE_TETHER_USB);
+            }
+        } else if (wifiTethered) {
+            if (bluetoothTethered) {
+                showTetheredNotification(SystemMessage.NOTE_TETHER_GENERAL);
+            } else {
+                /* We now have a status bar icon for WifiTethering, so drop the notification */
+                clearTetheredNotification();
+            }
+        } else if (bluetoothTethered) {
+            showTetheredNotification(SystemMessage.NOTE_TETHER_BLUETOOTH);
+        } else {
+            clearTetheredNotification();
+        }
+    }
+
+    private void showTetheredNotification(int id) {
+        showTetheredNotification(id, true);
+    }
+
+    @VisibleForTesting
+    protected void showTetheredNotification(int id, boolean tetheringOn) {
+        NotificationManager notificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        if (notificationManager == null) {
+            return;
+        }
+        int icon = 0;
+        switch(id) {
+            case SystemMessage.NOTE_TETHER_USB:
+                icon = com.android.internal.R.drawable.stat_sys_tether_usb;
+                break;
+            case SystemMessage.NOTE_TETHER_BLUETOOTH:
+                icon = com.android.internal.R.drawable.stat_sys_tether_bluetooth;
+                break;
+            case SystemMessage.NOTE_TETHER_GENERAL:
+            default:
+                icon = com.android.internal.R.drawable.stat_sys_tether_general;
+                break;
+        }
+
+        if (mLastNotificationId != 0) {
+            if (mLastNotificationId == icon) {
+                return;
+            }
+            notificationManager.cancelAsUser(null, mLastNotificationId,
+                    UserHandle.ALL);
+            mLastNotificationId = 0;
+        }
+
+        Intent intent = new Intent();
+        intent.setClassName("com.android.settings", "com.android.settings.TetherSettings");
+        intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+
+        PendingIntent pi = PendingIntent.getActivityAsUser(mContext, 0, intent, 0,
+                null, UserHandle.CURRENT);
+
+        Resources r = Resources.getSystem();
+        final CharSequence title;
+        final CharSequence message;
+
+        if (tetheringOn) {
+            title = r.getText(com.android.internal.R.string.tethered_notification_title);
+            message = r.getText(com.android.internal.R.string.tethered_notification_message);
+        } else {
+            title = r.getText(com.android.internal.R.string.disable_tether_notification_title);
+            message = r.getText(com.android.internal.R.string.disable_tether_notification_message);
+        }
+
+        if (mTetheredNotificationBuilder == null) {
+            mTetheredNotificationBuilder =
+                    new Notification.Builder(mContext, SystemNotificationChannels.NETWORK_STATUS);
+            mTetheredNotificationBuilder.setWhen(0)
+                    .setOngoing(true)
+                    .setColor(mContext.getColor(
+                            com.android.internal.R.color.system_notification_accent_color))
+                    .setVisibility(Notification.VISIBILITY_PUBLIC)
+                    .setCategory(Notification.CATEGORY_STATUS);
+        }
+        mTetheredNotificationBuilder.setSmallIcon(icon)
+                .setContentTitle(title)
+                .setContentText(message)
+                .setContentIntent(pi);
+        mLastNotificationId = id;
+
+        notificationManager.notifyAsUser(null, mLastNotificationId,
+                mTetheredNotificationBuilder.buildInto(new Notification()), UserHandle.ALL);
+    }
+
+    @VisibleForTesting
+    protected void clearTetheredNotification() {
+        NotificationManager notificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        if (notificationManager != null && mLastNotificationId != 0) {
+            notificationManager.cancelAsUser(null, mLastNotificationId,
+                    UserHandle.ALL);
+            mLastNotificationId = 0;
+        }
+    }
+
+    private class StateReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context content, Intent intent) {
+            final String action = intent.getAction();
+            if (action == null) return;
+
+            if (action.equals(UsbManager.ACTION_USB_STATE)) {
+                handleUsbAction(intent);
+            } else if (action.equals(CONNECTIVITY_ACTION)) {
+                handleConnectivityAction(intent);
+            } else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
+                handleWifiApAction(intent);
+            } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
+                handleWifiP2pAction(intent);
+            } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+                mLog.log("OBSERVED configuration changed");
+                updateConfiguration();
+            }
+        }
+
+        private void handleConnectivityAction(Intent intent) {
+            final NetworkInfo networkInfo =
+                    (NetworkInfo) intent.getParcelableExtra(EXTRA_NETWORK_INFO);
+            if (networkInfo == null
+                    || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED) {
+                return;
+            }
+
+            if (VDBG) Log.d(TAG, "Tethering got CONNECTIVITY_ACTION: " + networkInfo.toString());
+            mTetherMasterSM.sendMessage(TetherMasterSM.CMD_UPSTREAM_CHANGED);
+        }
+
+        private void handleUsbAction(Intent intent) {
+            final boolean usbConnected = intent.getBooleanExtra(USB_CONNECTED, false);
+            final boolean usbConfigured = intent.getBooleanExtra(USB_CONFIGURED, false);
+            final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false);
+
+            mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s",
+                    usbConnected, usbConfigured, rndisEnabled));
+
+            // There are three types of ACTION_USB_STATE:
+            //
+            //     - DISCONNECTED (USB_CONNECTED and USB_CONFIGURED are 0)
+            //       Meaning: USB connection has ended either because of
+            //       software reset or hard unplug.
+            //
+            //     - CONNECTED (USB_CONNECTED is 1, USB_CONFIGURED is 0)
+            //       Meaning: the first stage of USB protocol handshake has
+            //       occurred but it is not complete.
+            //
+            //     - CONFIGURED (USB_CONNECTED and USB_CONFIGURED are 1)
+            //       Meaning: the USB handshake is completely done and all the
+            //       functions are ready to use.
+            //
+            // For more explanation, see b/62552150 .
+            synchronized (Tethering.this.mPublicSync) {
+                if (!usbConnected && mRndisEnabled) {
+                    // Turn off tethering if it was enabled and there is a disconnect.
+                    tetherMatchingInterfaces(IpServer.STATE_AVAILABLE, TETHERING_USB);
+                    mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB);
+                } else if (usbConfigured && rndisEnabled) {
+                    // Tether if rndis is enabled and usb is configured.
+                    tetherMatchingInterfaces(IpServer.STATE_TETHERED, TETHERING_USB);
+                }
+                mRndisEnabled = usbConfigured && rndisEnabled;
+            }
+        }
+
+        private void handleWifiApAction(Intent intent) {
+            final int curState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED);
+            final String ifname = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+            final int ipmode = intent.getIntExtra(EXTRA_WIFI_AP_MODE, IFACE_IP_MODE_UNSPECIFIED);
+
+            synchronized (Tethering.this.mPublicSync) {
+                switch (curState) {
+                    case WifiManager.WIFI_AP_STATE_ENABLING:
+                        // We can see this state on the way to both enabled and failure states.
+                        break;
+                    case WifiManager.WIFI_AP_STATE_ENABLED:
+                        enableWifiIpServingLocked(ifname, ipmode);
+                        break;
+                    case WifiManager.WIFI_AP_STATE_DISABLED:
+                    case WifiManager.WIFI_AP_STATE_DISABLING:
+                    case WifiManager.WIFI_AP_STATE_FAILED:
+                    default:
+                        disableWifiIpServingLocked(ifname, curState);
+                        break;
+                }
+            }
+        }
+
+        private void handleWifiP2pAction(Intent intent) {
+            if (mConfig.isWifiP2pLegacyTetheringMode()) return;
+
+            final WifiP2pInfo p2pInfo =
+                    (WifiP2pInfo) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
+            final WifiP2pGroup group =
+                    (WifiP2pGroup) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
+
+            if (VDBG) {
+                Log.d(TAG, "WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group);
+            }
+
+            if (p2pInfo == null) return;
+            // When a p2p group is disconnected, p2pInfo would be cleared.
+            // group is still valid for detecting whether this device is group owner.
+            if (group == null || !group.isGroupOwner()
+                    || TextUtils.isEmpty(group.getInterface())) return;
+
+            synchronized (Tethering.this.mPublicSync) {
+                // Enter below only if this device is Group Owner with a valid interface.
+                if (p2pInfo.groupFormed) {
+                    TetherState tetherState = mTetherStates.get(group.getInterface());
+                    if (tetherState == null
+                            || (tetherState.lastState != IpServer.STATE_TETHERED
+                                && tetherState.lastState != IpServer.STATE_LOCAL_ONLY)) {
+                        enableWifiIpServingLocked(group.getInterface(), IFACE_IP_MODE_LOCAL_ONLY);
+                    }
+                } else {
+                    disableWifiP2pIpServingLocked(group.getInterface());
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected static class TetheringUserRestrictionListener implements UserRestrictionsListener {
+        private final Tethering mWrapper;
+
+        public TetheringUserRestrictionListener(Tethering wrapper) {
+            mWrapper = wrapper;
+        }
+
+        public void onUserRestrictionsChanged(int userId,
+                                              Bundle newRestrictions,
+                                              Bundle prevRestrictions) {
+            final boolean newlyDisallowed =
+                    newRestrictions.getBoolean(UserManager.DISALLOW_CONFIG_TETHERING);
+            final boolean previouslyDisallowed =
+                    prevRestrictions.getBoolean(UserManager.DISALLOW_CONFIG_TETHERING);
+            final boolean tetheringDisallowedChanged = (newlyDisallowed != previouslyDisallowed);
+
+            if (!tetheringDisallowedChanged) {
+                return;
+            }
+
+            mWrapper.clearTetheredNotification();
+            final boolean isTetheringActiveOnDevice = (mWrapper.getTetheredIfaces().length != 0);
+
+            if (newlyDisallowed && isTetheringActiveOnDevice) {
+                mWrapper.showTetheredNotification(
+                        com.android.internal.R.drawable.stat_sys_tether_general, false);
+                mWrapper.untetherAll();
+            }
+        }
+    }
+
+    private void disableWifiIpServingLockedCommon(int tetheringType, String ifname, int apState) {
+        mLog.log("Canceling WiFi tethering request -"
+                + " type=" + tetheringType
+                + " interface=" + ifname
+                + " state=" + apState);
+
+        if (!TextUtils.isEmpty(ifname)) {
+            final TetherState ts = mTetherStates.get(ifname);
+            if (ts != null) {
+                ts.ipServer.unwanted();
+                return;
+            }
+        }
+
+        for (int i = 0; i < mTetherStates.size(); i++) {
+            final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
+            if (ipServer.interfaceType() == tetheringType) {
+                ipServer.unwanted();
+                return;
+            }
+        }
+
+        mLog.log("Error disabling Wi-Fi IP serving; "
+                + (TextUtils.isEmpty(ifname) ? "no interface name specified"
+                                           : "specified interface: " + ifname));
+    }
+
+    private void disableWifiIpServingLocked(String ifname, int apState) {
+        // Regardless of whether we requested this transition, the AP has gone
+        // down.  Don't try to tether again unless we're requested to do so.
+        // TODO: Remove this altogether, once Wi-Fi reliably gives us an
+        // interface name with every broadcast.
+        mWifiTetherRequested = false;
+
+        disableWifiIpServingLockedCommon(TETHERING_WIFI, ifname, apState);
+    }
+
+    private void disableWifiP2pIpServingLocked(String ifname) {
+        disableWifiIpServingLockedCommon(TETHERING_WIFI_P2P, ifname, /* dummy */ 0);
+    }
+
+    private void enableWifiIpServingLocked(String ifname, int wifiIpMode) {
+        // Map wifiIpMode values to IpServer.Callback serving states, inferring
+        // from mWifiTetherRequested as a final "best guess".
+        final int ipServingMode;
+        switch (wifiIpMode) {
+            case IFACE_IP_MODE_TETHERED:
+                ipServingMode = IpServer.STATE_TETHERED;
+                break;
+            case IFACE_IP_MODE_LOCAL_ONLY:
+                ipServingMode = IpServer.STATE_LOCAL_ONLY;
+                break;
+            default:
+                mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
+                return;
+        }
+
+        if (!TextUtils.isEmpty(ifname)) {
+            maybeTrackNewInterfaceLocked(ifname);
+            changeInterfaceState(ifname, ipServingMode);
+        } else {
+            mLog.e(String.format(
+                    "Cannot enable IP serving in mode %s on missing interface name",
+                    ipServingMode));
+        }
+    }
+
+    // TODO: Consider renaming to something more accurate in its description.
+    // This method:
+    //     - allows requesting either tethering or local hotspot serving states
+    //     - handles both enabling and disabling serving states
+    //     - only tethers the first matching interface in listInterfaces()
+    //       order of a given type
+    private void tetherMatchingInterfaces(int requestedState, int interfaceType) {
+        if (VDBG) {
+            Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")");
+        }
+
+        String[] ifaces = null;
+        try {
+            ifaces = mNMService.listInterfaces();
+        } catch (Exception e) {
+            Log.e(TAG, "Error listing Interfaces", e);
+            return;
+        }
+        String chosenIface = null;
+        if (ifaces != null) {
+            for (String iface : ifaces) {
+                if (ifaceNameToType(iface) == interfaceType) {
+                    chosenIface = iface;
+                    break;
+                }
+            }
+        }
+        if (chosenIface == null) {
+            Log.e(TAG, "could not find iface of type " + interfaceType);
+            return;
+        }
+
+        changeInterfaceState(chosenIface, requestedState);
+    }
+
+    private void changeInterfaceState(String ifname, int requestedState) {
+        final int result;
+        switch (requestedState) {
+            case IpServer.STATE_UNAVAILABLE:
+            case IpServer.STATE_AVAILABLE:
+                result = untether(ifname);
+                break;
+            case IpServer.STATE_TETHERED:
+            case IpServer.STATE_LOCAL_ONLY:
+                result = tether(ifname, requestedState);
+                break;
+            default:
+                Log.wtf(TAG, "Unknown interface state: " + requestedState);
+                return;
+        }
+        if (result != TETHER_ERROR_NO_ERROR) {
+            Log.e(TAG, "unable start or stop tethering on iface " + ifname);
+            return;
+        }
+    }
+
+    TetheringConfiguration getTetheringConfiguration() {
+        return mConfig;
+    }
+
+    boolean hasTetherableConfiguration() {
+        final TetheringConfiguration cfg = mConfig;
+        final boolean hasDownstreamConfiguration =
+                (cfg.tetherableUsbRegexs.length != 0)
+                || (cfg.tetherableWifiRegexs.length != 0)
+                || (cfg.tetherableBluetoothRegexs.length != 0);
+        final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
+                || cfg.chooseUpstreamAutomatically;
+
+        return hasDownstreamConfiguration && hasUpstreamConfiguration;
+    }
+
+    // TODO - update callers to use getTetheringConfiguration(),
+    // which has only final members.
+    String[] getTetherableUsbRegexs() {
+        return copy(mConfig.tetherableUsbRegexs);
+    }
+
+    String[] getTetherableWifiRegexs() {
+        return copy(mConfig.tetherableWifiRegexs);
+    }
+
+    String[] getTetherableBluetoothRegexs() {
+        return copy(mConfig.tetherableBluetoothRegexs);
+    }
+
+    int setUsbTethering(boolean enable) {
+        if (VDBG) Log.d(TAG, "setUsbTethering(" + enable + ")");
+        UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+        if (usbManager == null) {
+            mLog.e("setUsbTethering: failed to get UsbManager!");
+            return TETHER_ERROR_SERVICE_UNAVAIL;
+        }
+
+        synchronized (mPublicSync) {
+            usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_RNDIS
+                    : UsbManager.FUNCTION_NONE);
+        }
+        return TETHER_ERROR_NO_ERROR;
+    }
+
+    // TODO review API - figure out how to delete these entirely.
+    String[] getTetheredIfaces() {
+        ArrayList<String> list = new ArrayList<String>();
+        synchronized (mPublicSync) {
+            for (int i = 0; i < mTetherStates.size(); i++) {
+                TetherState tetherState = mTetherStates.valueAt(i);
+                if (tetherState.lastState == IpServer.STATE_TETHERED) {
+                    list.add(mTetherStates.keyAt(i));
+                }
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    String[] getTetherableIfaces() {
+        ArrayList<String> list = new ArrayList<String>();
+        synchronized (mPublicSync) {
+            for (int i = 0; i < mTetherStates.size(); i++) {
+                TetherState tetherState = mTetherStates.valueAt(i);
+                if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+                    list.add(mTetherStates.keyAt(i));
+                }
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    String[] getTetheredDhcpRanges() {
+        // TODO: this is only valid for the old DHCP server. Latest search suggests it is only used
+        // by WifiP2pServiceImpl to start dnsmasq: remove/deprecate after migrating callers.
+        return mConfig.legacyDhcpRanges;
+    }
+
+    String[] getErroredIfaces() {
+        ArrayList<String> list = new ArrayList<String>();
+        synchronized (mPublicSync) {
+            for (int i = 0; i < mTetherStates.size(); i++) {
+                TetherState tetherState = mTetherStates.valueAt(i);
+                if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
+                    list.add(mTetherStates.keyAt(i));
+                }
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    private void logMessage(State state, int what) {
+        mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+    }
+
+    private boolean upstreamWanted() {
+        if (!mForwardedDownstreams.isEmpty()) return true;
+
+        synchronized (mPublicSync) {
+            return mWifiTetherRequested;
+        }
+    }
+
+    // Needed because the canonical source of upstream truth is just the
+    // upstream interface set, |mCurrentUpstreamIfaceSet|.
+    private boolean pertainsToCurrentUpstream(NetworkState ns) {
+        if (ns != null && ns.linkProperties != null && mCurrentUpstreamIfaceSet != null) {
+            for (String ifname : ns.linkProperties.getAllInterfaceNames()) {
+                if (mCurrentUpstreamIfaceSet.ifnames.contains(ifname)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    class TetherMasterSM extends StateMachine {
+        private static final int BASE_MASTER                    = Protocol.BASE_TETHERING;
+        // an interface SM has requested Tethering/Local Hotspot
+        static final int EVENT_IFACE_SERVING_STATE_ACTIVE       = BASE_MASTER + 1;
+        // an interface SM has unrequested Tethering/Local Hotspot
+        static final int EVENT_IFACE_SERVING_STATE_INACTIVE     = BASE_MASTER + 2;
+        // upstream connection change - do the right thing
+        static final int CMD_UPSTREAM_CHANGED                   = BASE_MASTER + 3;
+        // we don't have a valid upstream conn, check again after a delay
+        static final int CMD_RETRY_UPSTREAM                     = BASE_MASTER + 4;
+        // Events from NetworkCallbacks that we process on the master state
+        // machine thread on behalf of the UpstreamNetworkMonitor.
+        static final int EVENT_UPSTREAM_CALLBACK                = BASE_MASTER + 5;
+        // we treated the error and want now to clear it
+        static final int CMD_CLEAR_ERROR                        = BASE_MASTER + 6;
+        static final int EVENT_IFACE_UPDATE_LINKPROPERTIES      = BASE_MASTER + 7;
+        // Events from EntitlementManager to choose upstream again.
+        static final int EVENT_UPSTREAM_PERMISSION_CHANGED      = BASE_MASTER + 8;
+
+        private final State mInitialState;
+        private final State mTetherModeAliveState;
+
+        private final State mSetIpForwardingEnabledErrorState;
+        private final State mSetIpForwardingDisabledErrorState;
+        private final State mStartTetheringErrorState;
+        private final State mStopTetheringErrorState;
+        private final State mSetDnsForwardersErrorState;
+
+        // This list is a little subtle.  It contains all the interfaces that currently are
+        // requesting tethering, regardless of whether these interfaces are still members of
+        // mTetherStates.  This allows us to maintain the following predicates:
+        //
+        // 1) mTetherStates contains the set of all currently existing, tetherable, link state up
+        //    interfaces.
+        // 2) mNotifyList contains all state machines that may have outstanding tethering state
+        //    that needs to be torn down.
+        //
+        // Because we excise interfaces immediately from mTetherStates, we must maintain mNotifyList
+        // so that the garbage collector does not clean up the state machine before it has a chance
+        // to tear itself down.
+        private final ArrayList<IpServer> mNotifyList;
+        private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+        private final OffloadWrapper mOffload;
+
+        private static final int UPSTREAM_SETTLE_TIME_MS     = 10000;
+
+        TetherMasterSM(String name, Looper looper, TetheringDependencies deps) {
+            super(name, looper);
+
+            mInitialState = new InitialState();
+            mTetherModeAliveState = new TetherModeAliveState();
+            mSetIpForwardingEnabledErrorState = new SetIpForwardingEnabledErrorState();
+            mSetIpForwardingDisabledErrorState = new SetIpForwardingDisabledErrorState();
+            mStartTetheringErrorState = new StartTetheringErrorState();
+            mStopTetheringErrorState = new StopTetheringErrorState();
+            mSetDnsForwardersErrorState = new SetDnsForwardersErrorState();
+
+            addState(mInitialState);
+            addState(mTetherModeAliveState);
+            addState(mSetIpForwardingEnabledErrorState);
+            addState(mSetIpForwardingDisabledErrorState);
+            addState(mStartTetheringErrorState);
+            addState(mStopTetheringErrorState);
+            addState(mSetDnsForwardersErrorState);
+
+            mNotifyList = new ArrayList<>();
+            mIPv6TetheringCoordinator = deps.getIPv6TetheringCoordinator(mNotifyList, mLog);
+            mOffload = new OffloadWrapper();
+
+            setInitialState(mInitialState);
+        }
+
+        class InitialState extends State {
+            @Override
+            public boolean processMessage(Message message) {
+                logMessage(this, message.what);
+                switch (message.what) {
+                    case EVENT_IFACE_SERVING_STATE_ACTIVE: {
+                        final IpServer who = (IpServer) message.obj;
+                        if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
+                        handleInterfaceServingStateActive(message.arg1, who);
+                        transitionTo(mTetherModeAliveState);
+                        break;
+                    }
+                    case EVENT_IFACE_SERVING_STATE_INACTIVE: {
+                        final IpServer who = (IpServer) message.obj;
+                        if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
+                        handleInterfaceServingStateInactive(who);
+                        break;
+                    }
+                    case EVENT_IFACE_UPDATE_LINKPROPERTIES:
+                        // Silently ignore these for now.
+                        break;
+                    default:
+                        return NOT_HANDLED;
+                }
+                return HANDLED;
+            }
+        }
+
+        protected boolean turnOnMasterTetherSettings() {
+            final TetheringConfiguration cfg = mConfig;
+            try {
+                mNMService.setIpForwardingEnabled(true);
+            } catch (Exception e) {
+                mLog.e(e);
+                transitionTo(mSetIpForwardingEnabledErrorState);
+                return false;
+            }
+            // TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
+            // Legacy DHCP server is disabled if passed an empty ranges array
+            final String[] dhcpRanges = cfg.enableLegacyDhcpServer
+                    ? cfg.legacyDhcpRanges
+                    : new String[0];
+            try {
+                // TODO: Find a more accurate method name (startDHCPv4()?).
+                mNMService.startTethering(dhcpRanges);
+            } catch (Exception e) {
+                try {
+                    mNMService.stopTethering();
+                    mNMService.startTethering(dhcpRanges);
+                } catch (Exception ee) {
+                    mLog.e(ee);
+                    transitionTo(mStartTetheringErrorState);
+                    return false;
+                }
+            }
+            mLog.log("SET master tether settings: ON");
+            return true;
+        }
+
+        protected boolean turnOffMasterTetherSettings() {
+            try {
+                mNMService.stopTethering();
+            } catch (Exception e) {
+                mLog.e(e);
+                transitionTo(mStopTetheringErrorState);
+                return false;
+            }
+            try {
+                mNMService.setIpForwardingEnabled(false);
+            } catch (Exception e) {
+                mLog.e(e);
+                transitionTo(mSetIpForwardingDisabledErrorState);
+                return false;
+            }
+            transitionTo(mInitialState);
+            mLog.log("SET master tether settings: OFF");
+            return true;
+        }
+
+        protected void chooseUpstreamType(boolean tryCell) {
+            // We rebuild configuration on ACTION_CONFIGURATION_CHANGED, but we
+            // do not currently know how to watch for changes in DUN settings.
+            maybeDunSettingChanged();
+
+            final TetheringConfiguration config = mConfig;
+            final NetworkState ns = (config.chooseUpstreamAutomatically)
+                    ? mUpstreamNetworkMonitor.getCurrentPreferredUpstream()
+                    : mUpstreamNetworkMonitor.selectPreferredUpstreamType(
+                            config.preferredUpstreamIfaceTypes);
+            if (ns == null) {
+                if (tryCell) {
+                    mUpstreamNetworkMonitor.registerMobileNetworkRequest();
+                    // We think mobile should be coming up; don't set a retry.
+                } else {
+                    sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
+                }
+            }
+            setUpstreamNetwork(ns);
+            final Network newUpstream = (ns != null) ? ns.network : null;
+            if (mTetherUpstream != newUpstream) {
+                mTetherUpstream = newUpstream;
+                mUpstreamNetworkMonitor.setCurrentUpstream(mTetherUpstream);
+                reportUpstreamChanged(mTetherUpstream);
+            }
+        }
+
+        protected void setUpstreamNetwork(NetworkState ns) {
+            InterfaceSet ifaces = null;
+            if (ns != null) {
+                // Find the interface with the default IPv4 route. It may be the
+                // interface described by linkProperties, or one of the interfaces
+                // stacked on top of it.
+                mLog.i("Looking for default routes on: " + ns.linkProperties);
+                ifaces = TetheringInterfaceUtils.getTetheringInterfaces(ns);
+                mLog.i("Found upstream interface(s): " + ifaces);
+            }
+
+            if (ifaces != null) {
+                setDnsForwarders(ns.network, ns.linkProperties);
+            }
+            notifyDownstreamsOfNewUpstreamIface(ifaces);
+            if (ns != null && pertainsToCurrentUpstream(ns)) {
+                // If we already have NetworkState for this network update it immediately.
+                handleNewUpstreamNetworkState(ns);
+            } else if (mCurrentUpstreamIfaceSet == null) {
+                // There are no available upstream networks.
+                handleNewUpstreamNetworkState(null);
+            }
+        }
+
+        protected void setDnsForwarders(final Network network, final LinkProperties lp) {
+            // TODO: Set v4 and/or v6 DNS per available connectivity.
+            String[] dnsServers = mConfig.defaultIPv4DNS;
+            final Collection<InetAddress> dnses = lp.getDnsServers();
+            // TODO: Properly support the absence of DNS servers.
+            if (dnses != null && !dnses.isEmpty()) {
+                // TODO: remove this invocation of NetworkUtils.makeStrings().
+                dnsServers = NetworkUtils.makeStrings(dnses);
+            }
+            try {
+                mNMService.setDnsForwarders(network, dnsServers);
+                mLog.log(String.format(
+                        "SET DNS forwarders: network=%s dnsServers=%s",
+                        network, Arrays.toString(dnsServers)));
+            } catch (Exception e) {
+                // TODO: Investigate how this can fail and what exactly
+                // happens if/when such failures occur.
+                mLog.e("setting DNS forwarders failed, " + e);
+                transitionTo(mSetDnsForwardersErrorState);
+            }
+        }
+
+        protected void notifyDownstreamsOfNewUpstreamIface(InterfaceSet ifaces) {
+            mCurrentUpstreamIfaceSet = ifaces;
+            for (IpServer ipServer : mNotifyList) {
+                ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifaces);
+            }
+        }
+
+        protected void handleNewUpstreamNetworkState(NetworkState ns) {
+            mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
+            mOffload.updateUpstreamNetworkState(ns);
+        }
+
+        private void handleInterfaceServingStateActive(int mode, IpServer who) {
+            if (mNotifyList.indexOf(who) < 0) {
+                mNotifyList.add(who);
+                mIPv6TetheringCoordinator.addActiveDownstream(who, mode);
+            }
+
+            if (mode == IpServer.STATE_TETHERED) {
+                // No need to notify OffloadController just yet as there are no
+                // "offload-able" prefixes to pass along. This will handled
+                // when the TISM informs Tethering of its LinkProperties.
+                mForwardedDownstreams.add(who);
+            } else {
+                mOffload.excludeDownstreamInterface(who.interfaceName());
+                mForwardedDownstreams.remove(who);
+            }
+
+            // If this is a Wi-Fi interface, notify WifiManager of the active serving state.
+            if (who.interfaceType() == TETHERING_WIFI) {
+                final WifiManager mgr = getWifiManager();
+                final String iface = who.interfaceName();
+                switch (mode) {
+                    case IpServer.STATE_TETHERED:
+                        mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_TETHERED);
+                        break;
+                    case IpServer.STATE_LOCAL_ONLY:
+                        mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_LOCAL_ONLY);
+                        break;
+                    default:
+                        Log.wtf(TAG, "Unknown active serving mode: " + mode);
+                        break;
+                }
+            }
+        }
+
+        private void handleInterfaceServingStateInactive(IpServer who) {
+            mNotifyList.remove(who);
+            mIPv6TetheringCoordinator.removeActiveDownstream(who);
+            mOffload.excludeDownstreamInterface(who.interfaceName());
+            mForwardedDownstreams.remove(who);
+
+            // If this is a Wi-Fi interface, tell WifiManager of any errors
+            // or the inactive serving state.
+            if (who.interfaceType() == TETHERING_WIFI) {
+                if (who.lastError() != TETHER_ERROR_NO_ERROR) {
+                    getWifiManager().updateInterfaceIpState(
+                            who.interfaceName(), IFACE_IP_MODE_CONFIGURATION_ERROR);
+                } else {
+                    getWifiManager().updateInterfaceIpState(
+                            who.interfaceName(), IFACE_IP_MODE_UNSPECIFIED);
+                }
+            }
+        }
+
+        private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
+            if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
+                mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
+                return;
+            }
+
+            final NetworkState ns = (NetworkState) o;
+
+            if (ns == null || !pertainsToCurrentUpstream(ns)) {
+                // TODO: In future, this is where upstream evaluation and selection
+                // could be handled for notifications which include sufficient data.
+                // For example, after CONNECTIVITY_ACTION listening is removed, here
+                // is where we could observe a Wi-Fi network becoming available and
+                // passing validation.
+                if (mCurrentUpstreamIfaceSet == null) {
+                    // If we have no upstream interface, try to run through upstream
+                    // selection again.  If, for example, IPv4 connectivity has shown up
+                    // after IPv6 (e.g., 464xlat became available) we want the chance to
+                    // notice and act accordingly.
+                    chooseUpstreamType(false);
+                }
+                return;
+            }
+
+            switch (arg1) {
+                case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
+                    handleNewUpstreamNetworkState(ns);
+                    break;
+                case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
+                    chooseUpstreamType(false);
+                    break;
+                case UpstreamNetworkMonitor.EVENT_ON_LOST:
+                    // TODO: Re-evaluate possible upstreams. Currently upstream
+                    // reevaluation is triggered via received CONNECTIVITY_ACTION
+                    // broadcasts that result in being passed a
+                    // TetherMasterSM.CMD_UPSTREAM_CHANGED.
+                    handleNewUpstreamNetworkState(null);
+                    break;
+                default:
+                    mLog.e("Unknown arg1 value: " + arg1);
+                    break;
+            }
+        }
+
+        class TetherModeAliveState extends State {
+            boolean mUpstreamWanted = false;
+            boolean mTryCell = true;
+
+            @Override
+            public void enter() {
+                // If turning on master tether settings fails, we have already
+                // transitioned to an error state; exit early.
+                if (!turnOnMasterTetherSettings()) {
+                    return;
+                }
+
+                mUpstreamNetworkMonitor.startObserveAllNetworks();
+
+                // TODO: De-duplicate with updateUpstreamWanted() below.
+                if (upstreamWanted()) {
+                    mUpstreamWanted = true;
+                    mOffload.start();
+                    chooseUpstreamType(true);
+                    mTryCell = false;
+                }
+            }
+
+            @Override
+            public void exit() {
+                mOffload.stop();
+                mUpstreamNetworkMonitor.stop();
+                notifyDownstreamsOfNewUpstreamIface(null);
+                handleNewUpstreamNetworkState(null);
+                if (mTetherUpstream != null) {
+                    mTetherUpstream = null;
+                    reportUpstreamChanged(null);
+                }
+            }
+
+            private boolean updateUpstreamWanted() {
+                final boolean previousUpstreamWanted = mUpstreamWanted;
+                mUpstreamWanted = upstreamWanted();
+                if (mUpstreamWanted != previousUpstreamWanted) {
+                    if (mUpstreamWanted) {
+                        mOffload.start();
+                    } else {
+                        mOffload.stop();
+                    }
+                }
+                return previousUpstreamWanted;
+            }
+
+            @Override
+            public boolean processMessage(Message message) {
+                logMessage(this, message.what);
+                boolean retValue = true;
+                switch (message.what) {
+                    case EVENT_IFACE_SERVING_STATE_ACTIVE: {
+                        IpServer who = (IpServer) message.obj;
+                        if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
+                        handleInterfaceServingStateActive(message.arg1, who);
+                        who.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED,
+                                mCurrentUpstreamIfaceSet);
+                        // If there has been a change and an upstream is now
+                        // desired, kick off the selection process.
+                        final boolean previousUpstreamWanted = updateUpstreamWanted();
+                        if (!previousUpstreamWanted && mUpstreamWanted) {
+                            chooseUpstreamType(true);
+                        }
+                        break;
+                    }
+                    case EVENT_IFACE_SERVING_STATE_INACTIVE: {
+                        IpServer who = (IpServer) message.obj;
+                        if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
+                        handleInterfaceServingStateInactive(who);
+
+                        if (mNotifyList.isEmpty()) {
+                            // This transitions us out of TetherModeAliveState,
+                            // either to InitialState or an error state.
+                            turnOffMasterTetherSettings();
+                            break;
+                        }
+
+                        if (DBG) {
+                            Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size()
+                                    + " live requests:");
+                            for (IpServer o : mNotifyList) {
+                                Log.d(TAG, "  " + o);
+                            }
+                        }
+                        // If there has been a change and an upstream is no
+                        // longer desired, release any mobile requests.
+                        final boolean previousUpstreamWanted = updateUpstreamWanted();
+                        if (previousUpstreamWanted && !mUpstreamWanted) {
+                            mUpstreamNetworkMonitor.releaseMobileNetworkRequest();
+                        }
+                        break;
+                    }
+                    case EVENT_IFACE_UPDATE_LINKPROPERTIES: {
+                        final LinkProperties newLp = (LinkProperties) message.obj;
+                        if (message.arg1 == IpServer.STATE_TETHERED) {
+                            mOffload.updateDownstreamLinkProperties(newLp);
+                        } else {
+                            mOffload.excludeDownstreamInterface(newLp.getInterfaceName());
+                        }
+                        break;
+                    }
+                    case EVENT_UPSTREAM_PERMISSION_CHANGED:
+                    case CMD_UPSTREAM_CHANGED:
+                        updateUpstreamWanted();
+                        if (!mUpstreamWanted) break;
+
+                        // Need to try DUN immediately if Wi-Fi goes down.
+                        chooseUpstreamType(true);
+                        mTryCell = false;
+                        break;
+                    case CMD_RETRY_UPSTREAM:
+                        updateUpstreamWanted();
+                        if (!mUpstreamWanted) break;
+
+                        chooseUpstreamType(mTryCell);
+                        mTryCell = !mTryCell;
+                        break;
+                    case EVENT_UPSTREAM_CALLBACK: {
+                        updateUpstreamWanted();
+                        if (mUpstreamWanted) {
+                            handleUpstreamNetworkMonitorCallback(message.arg1, message.obj);
+                        }
+                        break;
+                    }
+                    default:
+                        retValue = false;
+                        break;
+                }
+                return retValue;
+            }
+        }
+
+        class ErrorState extends State {
+            private int mErrorNotification;
+
+            @Override
+            public boolean processMessage(Message message) {
+                boolean retValue = true;
+                switch (message.what) {
+                    case EVENT_IFACE_SERVING_STATE_ACTIVE:
+                        IpServer who = (IpServer) message.obj;
+                        who.sendMessage(mErrorNotification);
+                        break;
+                    case CMD_CLEAR_ERROR:
+                        mErrorNotification = TETHER_ERROR_NO_ERROR;
+                        transitionTo(mInitialState);
+                        break;
+                    default:
+                        retValue = false;
+                }
+                return retValue;
+            }
+
+            void notify(int msgType) {
+                mErrorNotification = msgType;
+                for (IpServer ipServer : mNotifyList) {
+                    ipServer.sendMessage(msgType);
+                }
+            }
+
+        }
+
+        class SetIpForwardingEnabledErrorState extends ErrorState {
+            @Override
+            public void enter() {
+                Log.e(TAG, "Error in setIpForwardingEnabled");
+                notify(IpServer.CMD_IP_FORWARDING_ENABLE_ERROR);
+            }
+        }
+
+        class SetIpForwardingDisabledErrorState extends ErrorState {
+            @Override
+            public void enter() {
+                Log.e(TAG, "Error in setIpForwardingDisabled");
+                notify(IpServer.CMD_IP_FORWARDING_DISABLE_ERROR);
+            }
+        }
+
+        class StartTetheringErrorState extends ErrorState {
+            @Override
+            public void enter() {
+                Log.e(TAG, "Error in startTethering");
+                notify(IpServer.CMD_START_TETHERING_ERROR);
+                try {
+                    mNMService.setIpForwardingEnabled(false);
+                } catch (Exception e) { }
+            }
+        }
+
+        class StopTetheringErrorState extends ErrorState {
+            @Override
+            public void enter() {
+                Log.e(TAG, "Error in stopTethering");
+                notify(IpServer.CMD_STOP_TETHERING_ERROR);
+                try {
+                    mNMService.setIpForwardingEnabled(false);
+                } catch (Exception e) { }
+            }
+        }
+
+        class SetDnsForwardersErrorState extends ErrorState {
+            @Override
+            public void enter() {
+                Log.e(TAG, "Error in setDnsForwarders");
+                notify(IpServer.CMD_SET_DNS_FORWARDERS_ERROR);
+                try {
+                    mNMService.stopTethering();
+                } catch (Exception e) { }
+                try {
+                    mNMService.setIpForwardingEnabled(false);
+                } catch (Exception e) { }
+            }
+        }
+
+        // A wrapper class to handle multiple situations where several calls to
+        // the OffloadController need to happen together.
+        //
+        // TODO: This suggests that the interface between OffloadController and
+        // Tethering is in need of improvement. Refactor these calls into the
+        // OffloadController implementation.
+        class OffloadWrapper {
+            public void start() {
+                mOffloadController.start();
+                sendOffloadExemptPrefixes();
+            }
+
+            public void stop() {
+                mOffloadController.stop();
+            }
+
+            public void updateUpstreamNetworkState(NetworkState ns) {
+                mOffloadController.setUpstreamLinkProperties(
+                        (ns != null) ? ns.linkProperties : null);
+            }
+
+            public void updateDownstreamLinkProperties(LinkProperties newLp) {
+                // Update the list of offload-exempt prefixes before adding
+                // new prefixes on downstream interfaces to the offload HAL.
+                sendOffloadExemptPrefixes();
+                mOffloadController.notifyDownstreamLinkProperties(newLp);
+            }
+
+            public void excludeDownstreamInterface(String ifname) {
+                // This and other interfaces may be in local-only hotspot mode;
+                // resend all local prefixes to the OffloadController.
+                sendOffloadExemptPrefixes();
+                mOffloadController.removeDownstreamInterface(ifname);
+            }
+
+            public void sendOffloadExemptPrefixes() {
+                sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
+            }
+
+            public void sendOffloadExemptPrefixes(final Set<IpPrefix> localPrefixes) {
+                // Add in well-known minimum set.
+                PrefixUtils.addNonForwardablePrefixes(localPrefixes);
+                // Add tragically hardcoded prefixes.
+                localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
+
+                // Maybe add prefixes or addresses for downstreams, depending on
+                // the IP serving mode of each.
+                for (IpServer ipServer : mNotifyList) {
+                    final LinkProperties lp = ipServer.linkProperties();
+
+                    switch (ipServer.servingMode()) {
+                        case IpServer.STATE_UNAVAILABLE:
+                        case IpServer.STATE_AVAILABLE:
+                            // No usable LinkProperties in these states.
+                            continue;
+                        case IpServer.STATE_TETHERED:
+                            // Only add IPv4 /32 and IPv6 /128 prefixes. The
+                            // directly-connected prefixes will be sent as
+                            // downstream "offload-able" prefixes.
+                            for (LinkAddress addr : lp.getAllLinkAddresses()) {
+                                final InetAddress ip = addr.getAddress();
+                                if (ip.isLinkLocalAddress()) continue;
+                                localPrefixes.add(PrefixUtils.ipAddressAsPrefix(ip));
+                            }
+                            break;
+                        case IpServer.STATE_LOCAL_ONLY:
+                            // Add prefixes covering all local IPs.
+                            localPrefixes.addAll(PrefixUtils.localPrefixesFrom(lp));
+                            break;
+                    }
+                }
+
+                mOffloadController.setLocalPrefixes(localPrefixes);
+            }
+        }
+    }
+
+    private void startTrackDefaultNetwork() {
+        mUpstreamNetworkMonitor.startTrackDefaultNetwork(mDeps.getDefaultNetworkRequest(),
+                mEntitlementMgr);
+    }
+
+    /** Get the latest value of the tethering entitlement check. */
+    void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
+            boolean showEntitlementUi) {
+        if (receiver != null) {
+            mEntitlementMgr.requestLatestTetheringEntitlementResult(type, receiver,
+                    showEntitlementUi);
+        }
+    }
+
+    /** Register tethering event callback */
+    void registerTetherInternalCallback(ITetherInternalCallback callback) {
+        mHandler.post(() -> {
+            mTetherInternalCallback = callback;
+            try {
+                mTetherInternalCallback.onCallbackCreated(mTetherUpstream,
+                        mConfig.toStableParcelable(), mTetherStatesParcel);
+            } catch (RemoteException e) {
+                // Not really very much to do here.
+            }
+        });
+    }
+
+    private void reportUpstreamChanged(Network network) {
+        // Don't need to synchronized mTetherInternalCallback because all the usage of this variable
+        // should run at the same thread.
+        if (mTetherInternalCallback == null) return;
+
+        try {
+            mTetherInternalCallback.onUpstreamChanged(network);
+        } catch (RemoteException e) {
+            // Not really very much to do here.
+        }
+    }
+
+    private void reportConfigurationChanged(TetheringConfigurationParcel config) {
+        if (mTetherInternalCallback == null) return;
+
+        try {
+            mTetherInternalCallback.onConfigurationChanged(config);
+        } catch (RemoteException e) {
+            // Not really very much to do here.
+        }
+    }
+
+    private void reportTetherStateChanged(TetherStatesParcel states) {
+        if (mTetherInternalCallback == null) return;
+
+        try {
+            mTetherInternalCallback.onTetherStatesChanged(states);
+        } catch (RemoteException e) {
+            // Not really very much to do here.
+        }
+    }
+
+    void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        // Binder.java closes the resource for us.
+        @SuppressWarnings("resource")
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+        pw.println("Tethering:");
+        pw.increaseIndent();
+
+        pw.println("Configuration:");
+        pw.increaseIndent();
+        final TetheringConfiguration cfg = mConfig;
+        cfg.dump(pw);
+        pw.decreaseIndent();
+
+        pw.println("Entitlement:");
+        pw.increaseIndent();
+        mEntitlementMgr.dump(pw);
+        pw.decreaseIndent();
+
+        synchronized (mPublicSync) {
+            pw.println("Tether state:");
+            pw.increaseIndent();
+            for (int i = 0; i < mTetherStates.size(); i++) {
+                final String iface = mTetherStates.keyAt(i);
+                final TetherState tetherState = mTetherStates.valueAt(i);
+                pw.print(iface + " - ");
+
+                switch (tetherState.lastState) {
+                    case IpServer.STATE_UNAVAILABLE:
+                        pw.print("UnavailableState");
+                        break;
+                    case IpServer.STATE_AVAILABLE:
+                        pw.print("AvailableState");
+                        break;
+                    case IpServer.STATE_TETHERED:
+                        pw.print("TetheredState");
+                        break;
+                    case IpServer.STATE_LOCAL_ONLY:
+                        pw.print("LocalHotspotState");
+                        break;
+                    default:
+                        pw.print("UnknownState");
+                        break;
+                }
+                pw.println(" - lastError = " + tetherState.lastError);
+            }
+            pw.println("Upstream wanted: " + upstreamWanted());
+            pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
+            pw.decreaseIndent();
+        }
+
+        pw.println("Hardware offload:");
+        pw.increaseIndent();
+        mOffloadController.dump(pw);
+        pw.decreaseIndent();
+
+        pw.println("Log:");
+        pw.increaseIndent();
+        if (argsContain(args, "--short")) {
+            pw.println("<log removed for brevity>");
+        } else {
+            mLog.dump(fd, pw, args);
+        }
+        pw.decreaseIndent();
+
+        pw.decreaseIndent();
+    }
+
+    private static boolean argsContain(String[] args, String target) {
+        for (String arg : args) {
+            if (target.equals(arg)) return true;
+        }
+        return false;
+    }
+
+    private IpServer.Callback makeControlCallback() {
+        return new IpServer.Callback() {
+            @Override
+            public void updateInterfaceState(IpServer who, int state, int lastError) {
+                notifyInterfaceStateChange(who, state, lastError);
+            }
+
+            @Override
+            public void updateLinkProperties(IpServer who, LinkProperties newLp) {
+                notifyLinkPropertiesChanged(who, newLp);
+            }
+        };
+    }
+
+    // TODO: Move into TetherMasterSM.
+    private void notifyInterfaceStateChange(IpServer who, int state, int error) {
+        final String iface = who.interfaceName();
+        synchronized (mPublicSync) {
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.ipServer.equals(who)) {
+                tetherState.lastState = state;
+                tetherState.lastError = error;
+            } else {
+                if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+            }
+        }
+
+        mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
+
+        try {
+            // Notify that we're tethering (or not) this interface.
+            // This is how data saver for instance knows if the user explicitly
+            // turned on tethering (thus keeping us from being in data saver mode).
+            mPolicyManager.onTetheringChanged(iface, state == IpServer.STATE_TETHERED);
+        } catch (RemoteException e) {
+            // Not really very much we can do here.
+        }
+
+        // If TetherMasterSM is in ErrorState, TetherMasterSM stays there.
+        // Thus we give a chance for TetherMasterSM to recover to InitialState
+        // by sending CMD_CLEAR_ERROR
+        if (error == TETHER_ERROR_MASTER_ERROR) {
+            mTetherMasterSM.sendMessage(TetherMasterSM.CMD_CLEAR_ERROR, who);
+        }
+        int which;
+        switch (state) {
+            case IpServer.STATE_UNAVAILABLE:
+            case IpServer.STATE_AVAILABLE:
+                which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
+                break;
+            case IpServer.STATE_TETHERED:
+            case IpServer.STATE_LOCAL_ONLY:
+                which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
+                break;
+            default:
+                Log.wtf(TAG, "Unknown interface state: " + state);
+                return;
+        }
+        mTetherMasterSM.sendMessage(which, state, 0, who);
+        sendTetherStateChangedBroadcast();
+    }
+
+    private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
+        final String iface = who.interfaceName();
+        final int state;
+        synchronized (mPublicSync) {
+            final TetherState tetherState = mTetherStates.get(iface);
+            if (tetherState != null && tetherState.ipServer.equals(who)) {
+                state = tetherState.lastState;
+            } else {
+                mLog.log("got notification from stale iface " + iface);
+                return;
+            }
+        }
+
+        mLog.log(String.format(
+                "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
+                iface, IpServer.getStateString(state), newLp));
+        final int which = TetherMasterSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+        mTetherMasterSM.sendMessage(which, state, 0, newLp);
+    }
+
+    private void maybeTrackNewInterfaceLocked(final String iface) {
+        // If we don't care about this type of interface, ignore.
+        final int interfaceType = ifaceNameToType(iface);
+        if (interfaceType == TETHERING_INVALID) {
+            mLog.log(iface + " is not a tetherable iface, ignoring");
+            return;
+        }
+        maybeTrackNewInterfaceLocked(iface, interfaceType);
+    }
+
+    private void maybeTrackNewInterfaceLocked(final String iface, int interfaceType) {
+        // If we have already started a TISM for this interface, skip.
+        if (mTetherStates.containsKey(iface)) {
+            mLog.log("active iface (" + iface + ") reported as added, ignoring");
+            return;
+        }
+
+        mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
+        final TetherState tetherState = new TetherState(
+                new IpServer(iface, mLooper, interfaceType, mLog, mNMService, mStatsService,
+                             makeControlCallback(), mConfig.enableLegacyDhcpServer,
+                             mDeps.getIpServerDependencies()));
+        mTetherStates.put(iface, tetherState);
+        tetherState.ipServer.start();
+    }
+
+    private void stopTrackingInterfaceLocked(final String iface) {
+        final TetherState tetherState = mTetherStates.get(iface);
+        if (tetherState == null) {
+            mLog.log("attempting to remove unknown iface (" + iface + "), ignoring");
+            return;
+        }
+        tetherState.ipServer.stop();
+        mLog.log("removing TetheringInterfaceStateMachine for: " + iface);
+        mTetherStates.remove(iface);
+    }
+
+    private static String[] copy(String[] strarray) {
+        return Arrays.copyOf(strarray, strarray.length);
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
index ca9b168..0ab4d63 100644
--- a/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringConfiguration.java
@@ -38,6 +38,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
+import android.net.TetheringConfigurationParcel;
 import android.net.util.SharedLog;
 import android.provider.Settings;
 import android.telephony.SubscriptionManager;
@@ -384,4 +385,32 @@
         }
         return false;
     }
+
+    /**
+     * Convert this TetheringConfiguration to a TetheringConfigurationParcel.
+     */
+    public TetheringConfigurationParcel toStableParcelable() {
+        final TetheringConfigurationParcel parcel = new TetheringConfigurationParcel();
+        parcel.subId = subId;
+        parcel.tetherableUsbRegexs = tetherableUsbRegexs;
+        parcel.tetherableWifiRegexs = tetherableWifiRegexs;
+        parcel.tetherableBluetoothRegexs = tetherableBluetoothRegexs;
+        parcel.isDunRequired = isDunRequired;
+        parcel.chooseUpstreamAutomatically = chooseUpstreamAutomatically;
+
+        int[] preferredTypes = new int[preferredUpstreamIfaceTypes.size()];
+        int index = 0;
+        for (Integer type : preferredUpstreamIfaceTypes) {
+            preferredTypes[index++] = type;
+        }
+        parcel.preferredUpstreamIfaceTypes = preferredTypes;
+
+        parcel.legacyDhcpRanges = legacyDhcpRanges;
+        parcel.defaultIPv4DNS = defaultIPv4DNS;
+        parcel.enableLegacyDhcpServer = enableLegacyDhcpServer;
+        parcel.provisioningApp = provisioningApp;
+        parcel.provisioningAppNoUi = provisioningAppNoUi;
+        parcel.provisioningCheckPeriod = provisioningCheckPeriod;
+        return parcel;
+    }
 }
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java
new file mode 100644
index 0000000..0ba8412
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import android.content.Context;
+import android.net.INetd;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.NetworkRequest;
+import android.net.ip.IpServer;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.ServiceManager;
+
+import com.android.internal.util.StateMachine;
+
+import java.util.ArrayList;
+
+
+/**
+ * Capture tethering dependencies, for injection.
+ *
+ * @hide
+ */
+public class TetheringDependencies {
+    /**
+     * Get a reference to the offload hardware interface to be used by tethering.
+     */
+    public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+        return new OffloadHardwareInterface(h, log);
+    }
+
+    /**
+     * Get a reference to the UpstreamNetworkMonitor to be used by tethering.
+     */
+    public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
+            SharedLog log, int what) {
+        return new UpstreamNetworkMonitor(ctx, target, log, what);
+    }
+
+    /**
+     * Get a reference to the IPv6TetheringCoordinator to be used by tethering.
+     */
+    public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+            ArrayList<IpServer> notifyList, SharedLog log) {
+        return new IPv6TetheringCoordinator(notifyList, log);
+    }
+
+    /**
+     * Get dependencies to be used by IpServer.
+     */
+    public IpServer.Dependencies getIpServerDependencies() {
+        return new IpServer.Dependencies();
+    }
+
+    /**
+     * Indicates whether tethering is supported on the device.
+     */
+    public boolean isTetheringSupported() {
+        return true;
+    }
+
+    /**
+     * Get the NetworkRequest that should be fulfilled by the default network.
+     */
+    public NetworkRequest getDefaultNetworkRequest() {
+        return null;
+    }
+
+    /**
+     * Get a reference to the EntitlementManager to be used by tethering.
+     */
+    public EntitlementManager getEntitlementManager(Context ctx, StateMachine target,
+            SharedLog log, int what) {
+        return new EntitlementManager(ctx, target, log, what);
+    }
+
+    /**
+     * Generate a new TetheringConfiguration according to input sub Id.
+     */
+    public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
+            int subId) {
+        return new TetheringConfiguration(ctx, log, subId);
+    }
+
+    /**
+     * Get a reference to INetworkManagementService to registerTetheringStatsProvider from
+     * OffloadController. Note: This should be removed soon by Usage refactor work in R
+     * development cycle.
+     */
+    public INetworkManagementService getINetworkManagementService() {
+        return INetworkManagementService.Stub.asInterface(
+                ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
+    }
+
+    /**
+     *  Get a reference to INetworkStatsService to force update tethering usage.
+     *  Note: This should be removed in R development cycle.
+     */
+    public INetworkStatsService getINetworkStatsService() {
+        return INetworkStatsService.Stub.asInterface(
+                ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+    }
+
+    /**
+     * Get a reference to INetworkPolicyManager to be used by tethering.
+     */
+    public INetworkPolicyManager getINetworkPolicyManager() {
+        return INetworkPolicyManager.Stub.asInterface(
+                ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+    }
+
+    /**
+     * Get a reference to INetd to be used by tethering.
+     */
+    public INetd getINetd(Context context) {
+        return INetd.Stub.asInterface(
+                (IBinder) context.getSystemService(Context.NETD_SERVICE));
+    }
+
+    /**
+     * Get tethering thread looper.
+     */
+    public Looper getTetheringLooper() {
+        return null;
+    }
+
+    /**
+     *  Get Context of TetheringSerice.
+     */
+    public Context getContext() {
+        return null;
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringInterfaceUtils.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringInterfaceUtils.java
new file mode 100644
index 0000000..0ef3805
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringInterfaceUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.NetworkCapabilities;
+import android.net.NetworkState;
+import android.net.RouteInfo;
+import android.net.util.InterfaceSet;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/**
+ * @hide
+ */
+public final class TetheringInterfaceUtils {
+    /**
+     * Get upstream interfaces for tethering based on default routes for IPv4/IPv6.
+     * @return null if there is no usable interface, or a set of at least one interface otherwise.
+     */
+    public static @Nullable InterfaceSet getTetheringInterfaces(NetworkState ns) {
+        if (ns == null) {
+            return null;
+        }
+
+        final LinkProperties lp = ns.linkProperties;
+        final String if4 = getInterfaceForDestination(lp, Inet4Address.ANY);
+        final String if6 = getIPv6Interface(ns);
+
+        return (if4 == null && if6 == null) ? null : new InterfaceSet(if4, if6);
+    }
+
+    /**
+     * Get the upstream interface for IPv6 tethering.
+     * @return null if there is no usable interface, or the interface name otherwise.
+     */
+    public static @Nullable String getIPv6Interface(NetworkState ns) {
+        // Broadly speaking:
+        //
+        //     [1] does the upstream have an IPv6 default route?
+        //
+        // and
+        //
+        //     [2] does the upstream have one or more global IPv6 /64s
+        //         dedicated to this device?
+        //
+        // In lieu of Prefix Delegation and other evaluation of whether a
+        // prefix may or may not be dedicated to this device, for now just
+        // check whether the upstream is TRANSPORT_CELLULAR. This works
+        // because "[t]he 3GPP network allocates each default bearer a unique
+        // /64 prefix", per RFC 6459, Section 5.2.
+        final boolean canTether =
+                (ns != null) && (ns.network != null)
+                && (ns.linkProperties != null) && (ns.networkCapabilities != null)
+                // At least one upstream DNS server:
+                && ns.linkProperties.hasIpv6DnsServer()
+                // Minimal amount of IPv6 provisioning:
+                && ns.linkProperties.hasGlobalIpv6Address()
+                // Temporary approximation of "dedicated prefix":
+                && ns.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
+
+        return canTether
+                ? getInterfaceForDestination(ns.linkProperties, Inet6Address.ANY)
+                : null;
+    }
+
+    private static String getInterfaceForDestination(LinkProperties lp, InetAddress dst) {
+        final RouteInfo ri = (lp != null)
+                ? RouteInfo.selectBestRoute(lp.getAllRoutes(), dst)
+                : null;
+        return (ri != null) ? ri.getInterface() : null;
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
new file mode 100644
index 0000000..456f2f7
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringService.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2019 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.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.ITetherInternalCallback;
+import android.net.ITetheringConnector;
+import android.net.NetworkRequest;
+import android.net.util.SharedLog;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.os.SystemProperties;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Android service used to manage tethering.
+ *
+ * <p>The service returns a binder for the system server to communicate with the tethering.
+ */
+public class TetheringService extends Service {
+    private static final String TAG = TetheringService.class.getSimpleName();
+
+    private final SharedLog mLog = new SharedLog(TAG);
+    private TetheringConnector mConnector;
+    private Context mContext;
+    private TetheringDependencies mDeps;
+    private Tethering mTethering;
+
+    @Override
+    public void onCreate() {
+        mLog.mark("onCreate");
+        mDeps = getTetheringDependencies();
+        mContext = mDeps.getContext();
+        mTethering = makeTethering(mDeps);
+    }
+
+    /**
+     * Make a reference to Tethering object.
+     */
+    @VisibleForTesting
+    public Tethering makeTethering(TetheringDependencies deps) {
+        return new Tethering(deps);
+    }
+
+    /**
+     * Create a binder connector for the system server to communicate with the tethering.
+     */
+    private synchronized IBinder makeConnector() {
+        if (mConnector == null) {
+            mConnector = new TetheringConnector(mTethering);
+        }
+        return mConnector;
+    }
+
+    @NonNull
+    @Override
+    public IBinder onBind(Intent intent) {
+        mLog.mark("onBind");
+        return makeConnector();
+    }
+
+    private static class TetheringConnector extends ITetheringConnector.Stub {
+        private final Tethering mService;
+
+        TetheringConnector(Tethering tether) {
+            mService = tether;
+        }
+
+        @Override
+        public void tether(String iface) {
+            mService.tether(iface);
+        }
+
+        @Override
+        public void untether(String iface) {
+            mService.untether(iface);
+        }
+
+        @Override
+        public void setUsbTethering(boolean enable) {
+            mService.setUsbTethering(enable);
+        }
+
+        @Override
+        public void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi) {
+            mService.startTethering(type, receiver, showProvisioningUi);
+        }
+
+        @Override
+        public void stopTethering(int type) {
+            mService.stopTethering(type);
+        }
+
+        @Override
+        public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
+                boolean showEntitlementUi) {
+            mService.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
+        }
+
+        @Override
+        public void registerTetherInternalCallback(ITetherInternalCallback callback) {
+            mService.registerTetherInternalCallback(callback);
+        }
+    }
+
+    @Override
+    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+                @Nullable String[] args) {
+        mTethering.dump(fd, writer, args);
+    }
+
+    /**
+     * An injection method for testing.
+     */
+    @VisibleForTesting
+    public TetheringDependencies getTetheringDependencies() {
+        if (mDeps == null) {
+            mDeps = new TetheringDependencies() {
+                @Override
+                public NetworkRequest getDefaultNetworkRequest() {
+                    ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
+                            Context.CONNECTIVITY_SERVICE);
+                    return cm.getDefaultRequest();
+                }
+
+                @Override
+                public Looper getTetheringLooper() {
+                    final HandlerThread tetherThread = new HandlerThread("android.tethering");
+                    tetherThread.start();
+                    return tetherThread.getLooper();
+                }
+
+                @Override
+                public boolean isTetheringSupported() {
+                    int defaultVal =
+                            SystemProperties.get("ro.tether.denied").equals("true") ? 0 : 1;
+                    boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
+                            Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
+                    return tetherSupported;
+                }
+
+                @Override
+                public Context getContext() {
+                    return TetheringService.this;
+                }
+            };
+        }
+
+        return mDeps;
+    }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 363be18..5b018df 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -18,7 +18,6 @@
     name: "TetheringTests",
     certificate: "platform",
     srcs: [
-        ":servicescore-tethering-src",
         "src/**/*.java",
     ],
     test_suites: ["device-tests"],
@@ -41,17 +40,3 @@
         "libstaticjvmtiagent",
     ],
 }
-
-// This group would be removed when tethering migration is done.
-filegroup {
-    name: "tethering-tests-src",
-    srcs: [
-        "src/com/android/server/connectivity/tethering/EntitlementManagerTest.java",
-        "src/com/android/server/connectivity/tethering/OffloadControllerTest.java",
-        "src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java",
-        "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java",
-        "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
-        "src/android/net/ip/IpServerTest.java",
-        "src/android/net/util/InterfaceSetTest.java",
-    ],
-}
diff --git a/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java
new file mode 100644
index 0000000..5a9b6e3
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.reset;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VersionedBroadcastListenerTest {
+    private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName();
+    private static final String ACTION_TEST = "action.test.happy.broadcasts";
+
+    @Mock private Context mContext;
+    private BroadcastInterceptingContext mServiceContext;
+    private Handler mHandler;
+    private VersionedBroadcastListener mListener;
+    private int mCallbackCount;
+
+    private void doCallback() {
+        mCallbackCount++;
+    }
+
+    private class MockContext extends BroadcastInterceptingContext {
+        MockContext(Context base) {
+            super(base);
+        }
+    }
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+    }
+
+    @Before public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        reset(mContext);
+        mServiceContext = new MockContext(mContext);
+        mHandler = new Handler(Looper.myLooper());
+        mCallbackCount = 0;
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_TEST);
+        mListener = new VersionedBroadcastListener(
+                TAG, mServiceContext, mHandler, filter, (Intent intent) -> doCallback());
+    }
+
+    @After public void tearDown() throws Exception {
+        if (mListener != null) {
+            mListener.stopListening();
+            mListener = null;
+        }
+    }
+
+    private void sendBroadcast() {
+        final Intent intent = new Intent(ACTION_TEST);
+        mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    @Test
+    public void testBasicListening() {
+        assertEquals(0, mCallbackCount);
+        mListener.startListening();
+        for (int i = 0; i < 5; i++) {
+            sendBroadcast();
+            assertEquals(i + 1, mCallbackCount);
+        }
+        mListener.stopListening();
+    }
+
+    @Test
+    public void testBroadcastsBeforeStartAreIgnored() {
+        assertEquals(0, mCallbackCount);
+        for (int i = 0; i < 5; i++) {
+            sendBroadcast();
+            assertEquals(0, mCallbackCount);
+        }
+
+        mListener.startListening();
+        sendBroadcast();
+        assertEquals(1, mCallbackCount);
+    }
+
+    @Test
+    public void testBroadcastsAfterStopAreIgnored() {
+        mListener.startListening();
+        sendBroadcast();
+        assertEquals(1, mCallbackCount);
+        mListener.stopListening();
+
+        for (int i = 0; i < 5; i++) {
+            sendBroadcast();
+            assertEquals(1, mCallbackCount);
+        }
+    }
+}
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
index 5217e26..99cf9e9 100644
--- a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
@@ -24,6 +24,9 @@
 import static android.net.ConnectivityManager.TETHER_ERROR_PROVISION_FAILED;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -44,6 +47,7 @@
 import android.os.Message;
 import android.os.PersistableBundle;
 import android.os.ResultReceiver;
+import android.os.SystemProperties;
 import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.telephony.CarrierConfigManager;
@@ -57,7 +61,6 @@
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.connectivity.MockableSystemProperties;
 
 import org.junit.After;
 import org.junit.Before;
@@ -65,6 +68,8 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
 import java.util.concurrent.CountDownLatch;
@@ -80,7 +85,6 @@
 
     @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private Context mContext;
-    @Mock private MockableSystemProperties mSystemProperties;
     @Mock private Resources mResources;
     @Mock private SharedLog mLog;
     @Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
@@ -95,6 +99,7 @@
     private TestStateMachine mSM;
     private WrappedEntitlementManager mEnMgr;
     private TetheringConfiguration mConfig;
+    private MockitoSession mMockingSession;
 
     private class MockContext extends BroadcastInterceptingContext {
         MockContext(Context base) {
@@ -118,8 +123,8 @@
         public int silentProvisionCount = 0;
 
         public WrappedEntitlementManager(Context ctx, StateMachine target,
-                SharedLog log, int what, MockableSystemProperties systemProperties) {
-            super(ctx, target, log, what, systemProperties);
+                SharedLog log, int what) {
+            super(ctx, target, log, what);
         }
 
         public void reset() {
@@ -144,6 +149,15 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .spyStatic(SystemProperties.class)
+                .strictness(Strictness.WARN)
+                .startMocking();
+        // Don't disable tethering provisioning unless requested.
+        doReturn(false).when(
+                () -> SystemProperties.getBoolean(
+                eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean()));
 
         when(mResources.getStringArray(R.array.config_tether_dhcp_range))
             .thenReturn(new String[0]);
@@ -161,8 +175,7 @@
         mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
         mMockContext = new MockContext(mContext);
         mSM = new TestStateMachine();
-        mEnMgr = new WrappedEntitlementManager(mMockContext, mSM, mLog, EVENT_EM_UPDATE,
-                mSystemProperties);
+        mEnMgr = new WrappedEntitlementManager(mMockContext, mSM, mLog, EVENT_EM_UPDATE);
         mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
         mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         mEnMgr.setTetheringConfigurationFetcher(() -> {
@@ -176,6 +189,7 @@
             mSM.quit();
             mSM = null;
         }
+        mMockingSession.finishMocking();
     }
 
     private void setupForRequiredProvisioning() {
@@ -184,9 +198,6 @@
                 .thenReturn(PROVISIONING_APP_NAME);
         when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui))
                 .thenReturn(PROVISIONING_NO_UI_APP_NAME);
-       // Don't disable tethering provisioning unless requested.
-        when(mSystemProperties.getBoolean(eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY),
-                anyBoolean())).thenReturn(false);
         // Act like the CarrierConfigManager is present and ready unless told otherwise.
         when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
                 .thenReturn(mCarrierConfigManager);
@@ -244,7 +255,7 @@
     }
 
     @Test
-    public void testGetLastEntitlementCacheValue() throws Exception {
+    public void testRequestLastEntitlementCacheValue() throws Exception {
         final CountDownLatch mCallbacklatch = new CountDownLatch(1);
         // 1. Entitlement check is not required.
         mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
@@ -255,7 +266,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
@@ -270,7 +281,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
@@ -284,7 +295,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(1, mEnMgr.uiProvisionCount);
@@ -298,7 +309,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
@@ -312,7 +323,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(1, mEnMgr.uiProvisionCount);
@@ -326,7 +337,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
@@ -339,7 +350,7 @@
                 mCallbacklatch.countDown();
             }
         };
-        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
+        mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
         mLooper.dispatchAll();
         callbackTimeoutHelper(mCallbacklatch);
         assertEquals(0, mEnMgr.uiProvisionCount);
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
new file mode 100644
index 0000000..4f6f61b
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java
@@ -0,0 +1,1319 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import static android.hardware.usb.UsbManager.USB_CONFIGURED;
+import static android.hardware.usb.UsbManager.USB_CONNECTED;
+import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
+import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY;
+import static android.net.ConnectivityManager.EXTRA_ACTIVE_TETHER;
+import static android.net.ConnectivityManager.EXTRA_AVAILABLE_TETHER;
+import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+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;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
+import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
+import static android.provider.Settings.Global.TETHER_ENABLE_LEGACY_DHCP_SERVER;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.hardware.usb.UsbManager;
+import android.net.INetd;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.ITetherInternalCallback;
+import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkState;
+import android.net.NetworkUtils;
+import android.net.RouteInfo;
+import android.net.TetherStatesParcel;
+import android.net.TetheringConfigurationParcel;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpServer;
+import android.net.ip.IpServer;
+import android.net.ip.RouterAdvertisementDaemon;
+import android.net.util.InterfaceParams;
+import android.net.util.NetworkConstants;
+import android.net.util.SharedLog;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Vector;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class TetheringTest {
+    private static final int IFINDEX_OFFSET = 100;
+
+    private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
+    private static final String TEST_XLAT_MOBILE_IFNAME = "v4-test_rmnet_data0";
+    private static final String TEST_USB_IFNAME = "test_rndis0";
+    private static final String TEST_WLAN_IFNAME = "test_wlan0";
+    private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
+
+    private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
+
+    @Mock private ApplicationInfo mApplicationInfo;
+    @Mock private Context mContext;
+    @Mock private INetworkManagementService mNMService;
+    @Mock private INetworkStatsService mStatsService;
+    @Mock private INetworkPolicyManager mPolicyManager;
+    @Mock private OffloadHardwareInterface mOffloadHardwareInterface;
+    @Mock private Resources mResources;
+    @Mock private TelephonyManager mTelephonyManager;
+    @Mock private UsbManager mUsbManager;
+    @Mock private WifiManager mWifiManager;
+    @Mock private CarrierConfigManager mCarrierConfigManager;
+    @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+    @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+    @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
+    @Mock private IDhcpServer mDhcpServer;
+    @Mock private INetd mNetd;
+
+    private final MockIpServerDependencies mIpServerDependencies =
+            spy(new MockIpServerDependencies());
+    private final MockTetheringDependencies mTetheringDependencies =
+            new MockTetheringDependencies();
+
+    // Like so many Android system APIs, these cannot be mocked because it is marked final.
+    // We have to use the real versions.
+    private final PersistableBundle mCarrierConfig = new PersistableBundle();
+    private final TestLooper mLooper = new TestLooper();
+
+    private Vector<Intent> mIntents;
+    private BroadcastInterceptingContext mServiceContext;
+    private MockContentResolver mContentResolver;
+    private BroadcastReceiver mBroadcastReceiver;
+    private Tethering mTethering;
+    private PhoneStateListener mPhoneStateListener;
+
+    private class TestContext extends BroadcastInterceptingContext {
+        TestContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public ApplicationInfo getApplicationInfo() {
+            return mApplicationInfo;
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+
+        @Override
+        public String getPackageName() {
+            return "TetheringTest";
+        }
+
+        @Override
+        public Resources getResources() {
+            return mResources;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (Context.WIFI_SERVICE.equals(name)) return mWifiManager;
+            if (Context.USB_SERVICE.equals(name)) return mUsbManager;
+            if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
+            return super.getSystemService(name);
+        }
+
+        @Override
+        public String getSystemServiceName(Class<?> serviceClass) {
+            if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
+            return super.getSystemServiceName(serviceClass);
+        }
+    }
+
+    public class MockIpServerDependencies extends IpServer.Dependencies {
+        @Override
+        public RouterAdvertisementDaemon getRouterAdvertisementDaemon(
+                InterfaceParams ifParams) {
+            return mRouterAdvertisementDaemon;
+        }
+
+        @Override
+        public InterfaceParams getInterfaceParams(String ifName) {
+            assertTrue("Non-mocked interface " + ifName,
+                    ifName.equals(TEST_USB_IFNAME)
+                            || ifName.equals(TEST_WLAN_IFNAME)
+                            || ifName.equals(TEST_MOBILE_IFNAME)
+                            || ifName.equals(TEST_P2P_IFNAME));
+            final String[] ifaces = new String[] {
+                    TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME, TEST_P2P_IFNAME};
+            return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
+                    MacAddress.ALL_ZEROS_ADDRESS);
+        }
+
+        @Override
+        public INetd getNetdService() {
+            return mNetd;
+        }
+
+        @Override
+        public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+                DhcpServerCallbacks cb) {
+            new Thread(() -> {
+                try {
+                    cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+                } catch (RemoteException e) {
+                    fail(e.getMessage());
+                }
+            }).run();
+        }
+    }
+
+    private class MockTetheringConfiguration extends TetheringConfiguration {
+        MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
+            super(ctx, log, id);
+        }
+
+        @Override
+        protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
+            return mResources;
+        }
+    }
+
+    public class MockTetheringDependencies extends TetheringDependencies {
+        StateMachine mUpstreamNetworkMonitorMasterSM;
+        ArrayList<IpServer> mIpv6CoordinatorNotifyList;
+        int mIsTetheringSupportedCalls;
+
+        public void reset() {
+            mUpstreamNetworkMonitorMasterSM = null;
+            mIpv6CoordinatorNotifyList = null;
+            mIsTetheringSupportedCalls = 0;
+        }
+
+        @Override
+        public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
+            return mOffloadHardwareInterface;
+        }
+
+        @Override
+        public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
+                StateMachine target, SharedLog log, int what) {
+            mUpstreamNetworkMonitorMasterSM = target;
+            return mUpstreamNetworkMonitor;
+        }
+
+        @Override
+        public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
+                ArrayList<IpServer> notifyList, SharedLog log) {
+            mIpv6CoordinatorNotifyList = notifyList;
+            return mIPv6TetheringCoordinator;
+        }
+
+        @Override
+        public IpServer.Dependencies getIpServerDependencies() {
+            return mIpServerDependencies;
+        }
+
+        @Override
+        public boolean isTetheringSupported() {
+            mIsTetheringSupportedCalls++;
+            return true;
+        }
+
+        @Override
+        public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
+                int subId) {
+            return new MockTetheringConfiguration(ctx, log, subId);
+        }
+
+        @Override
+        public INetworkManagementService getINetworkManagementService() {
+            return mNMService;
+        }
+
+        @Override
+        public INetworkStatsService getINetworkStatsService() {
+            return mStatsService;
+        }
+
+        @Override
+        public INetworkPolicyManager getINetworkPolicyManager() {
+            return mPolicyManager;
+        }
+
+        @Override
+        public INetd getINetd(Context context) {
+            return mNetd;
+        }
+
+        @Override
+        public Looper getTetheringLooper() {
+            return mLooper.getLooper();
+        }
+
+        @Override
+        public Context getContext() {
+            return mServiceContext;
+        }
+    }
+
+    private static NetworkState buildMobileUpstreamState(boolean withIPv4, boolean withIPv6,
+            boolean with464xlat) {
+        final NetworkInfo info = new NetworkInfo(TYPE_MOBILE, 0, null, null);
+        info.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null, null);
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_MOBILE_IFNAME);
+
+        if (withIPv4) {
+            prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+                    NetworkUtils.numericToInetAddress("10.0.0.1"), TEST_MOBILE_IFNAME));
+        }
+
+        if (withIPv6) {
+            prop.addDnsServer(NetworkUtils.numericToInetAddress("2001:db8::2"));
+            prop.addLinkAddress(
+                    new LinkAddress(NetworkUtils.numericToInetAddress("2001:db8::"),
+                            NetworkConstants.RFC7421_PREFIX_LENGTH));
+            prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
+                    NetworkUtils.numericToInetAddress("2001:db8::1"), TEST_MOBILE_IFNAME));
+        }
+
+        if (with464xlat) {
+            final LinkProperties stackedLink = new LinkProperties();
+            stackedLink.setInterfaceName(TEST_XLAT_MOBILE_IFNAME);
+            stackedLink.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+                    NetworkUtils.numericToInetAddress("192.0.0.1"), TEST_XLAT_MOBILE_IFNAME));
+
+            prop.addStackedLink(stackedLink);
+        }
+
+
+        final NetworkCapabilities capabilities = new NetworkCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        return new NetworkState(info, prop, capabilities, new Network(100), null, "netid");
+    }
+
+    private static NetworkState buildMobileIPv4UpstreamState() {
+        return buildMobileUpstreamState(true, false, false);
+    }
+
+    private static NetworkState buildMobileIPv6UpstreamState() {
+        return buildMobileUpstreamState(false, true, false);
+    }
+
+    private static NetworkState buildMobileDualStackUpstreamState() {
+        return buildMobileUpstreamState(true, true, false);
+    }
+
+    private static NetworkState buildMobile464xlatUpstreamState() {
+        return buildMobileUpstreamState(false, true, true);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mResources.getStringArray(com.android.internal.R.array.config_tether_dhcp_range))
+                .thenReturn(new String[0]);
+        when(mResources.getStringArray(com.android.internal.R.array.config_tether_usb_regexs))
+                .thenReturn(new String[] { "test_rndis\\d" });
+        when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_regexs))
+                .thenReturn(new String[]{ "test_wlan\\d" });
+        when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_p2p_regexs))
+                .thenReturn(new String[]{ "test_p2p-p2p\\d-.*" });
+        when(mResources.getStringArray(com.android.internal.R.array.config_tether_bluetooth_regexs))
+                .thenReturn(new String[0]);
+        when(mResources.getIntArray(com.android.internal.R.array.config_tether_upstream_types))
+                .thenReturn(new int[0]);
+        when(mResources.getBoolean(com.android.internal.R.bool.config_tether_upstream_automatic))
+                .thenReturn(false);
+        when(mNMService.listInterfaces())
+                .thenReturn(new String[] {
+                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME});
+        when(mNMService.getInterfaceConfig(anyString()))
+                .thenReturn(new InterfaceConfiguration());
+        when(mRouterAdvertisementDaemon.start())
+                .thenReturn(true);
+
+        mServiceContext = new TestContext(mContext);
+        mContentResolver = new MockContentResolver(mServiceContext);
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        Settings.Global.putInt(mContentResolver, TETHER_ENABLE_LEGACY_DHCP_SERVER, 0);
+        mIntents = new Vector<>();
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                mIntents.addElement(intent);
+            }
+        };
+        mServiceContext.registerReceiver(mBroadcastReceiver,
+                new IntentFilter(ACTION_TETHER_STATE_CHANGED));
+        mTethering = makeTethering();
+        verify(mNMService).registerTetheringStatsProvider(any(), anyString());
+        verify(mNetd).registerUnsolicitedEventListener(any());
+        final ArgumentCaptor<PhoneStateListener> phoneListenerCaptor =
+                ArgumentCaptor.forClass(PhoneStateListener.class);
+        verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
+                eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE));
+        mPhoneStateListener = phoneListenerCaptor.getValue();
+    }
+
+    private Tethering makeTethering() {
+        mTetheringDependencies.reset();
+        return new Tethering(mTetheringDependencies);
+    }
+
+    @After
+    public void tearDown() {
+        mServiceContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    private void sendWifiApStateChanged(int state) {
+        final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+        intent.putExtra(EXTRA_WIFI_AP_STATE, state);
+        mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private void sendWifiApStateChanged(int state, String ifname, int ipmode) {
+        final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
+        intent.putExtra(EXTRA_WIFI_AP_STATE, state);
+        intent.putExtra(EXTRA_WIFI_AP_INTERFACE_NAME, ifname);
+        intent.putExtra(EXTRA_WIFI_AP_MODE, ipmode);
+        mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = {
+            android.Manifest.permission.ACCESS_FINE_LOCATION,
+            android.Manifest.permission.ACCESS_WIFI_STATE
+    };
+
+    private void sendWifiP2pConnectionChanged(
+            boolean isGroupFormed, boolean isGroupOwner, String ifname) {
+        WifiP2pInfo p2pInfo = new WifiP2pInfo();
+        p2pInfo.groupFormed = isGroupFormed;
+        p2pInfo.isGroupOwner = isGroupOwner;
+
+        NetworkInfo networkInfo = new NetworkInfo(TYPE_WIFI_P2P, 0, null, null);
+
+        WifiP2pGroup group = new WifiP2pGroup();
+        group.setIsGroupOwner(isGroupOwner);
+        group.setInterface(ifname);
+
+        final Intent intent = new Intent(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+        intent.putExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO, p2pInfo);
+        intent.putExtra(WifiP2pManager.EXTRA_NETWORK_INFO, networkInfo);
+        intent.putExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP, group);
+        mServiceContext.sendBroadcastAsUserMultiplePermissions(intent, UserHandle.ALL,
+                P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST);
+    }
+
+    private void sendUsbBroadcast(boolean connected, boolean configured, boolean rndisFunction) {
+        final Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
+        intent.putExtra(USB_CONNECTED, connected);
+        intent.putExtra(USB_CONFIGURED, configured);
+        intent.putExtra(USB_FUNCTION_RNDIS, rndisFunction);
+        mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private void sendConfigurationChanged() {
+        final Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
+        mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
+        verify(mNMService, times(1)).getInterfaceConfig(ifname);
+        verify(mNMService, times(1))
+                .setInterfaceConfig(eq(ifname), any(InterfaceConfiguration.class));
+        verify(mNMService, times(1)).tetherInterface(ifname);
+    }
+
+    private void verifyTetheringBroadcast(String ifname, String whichExtra) {
+        // Verify that ifname is in the whichExtra array of the tether state changed broadcast.
+        final Intent bcast = mIntents.get(0);
+        assertEquals(ACTION_TETHER_STATE_CHANGED, bcast.getAction());
+        final ArrayList<String> ifnames = bcast.getStringArrayListExtra(whichExtra);
+        assertTrue(ifnames.contains(ifname));
+        mIntents.remove(bcast);
+    }
+
+    public void failingLocalOnlyHotspotLegacyApBroadcast(
+            boolean emulateInterfaceStatusChanged) throws Exception {
+        // Emulate externally-visible WifiManager effects, causing the
+        // per-interface state machine to start up, and telling us that
+        // hotspot mode is to be started.
+        if (emulateInterfaceStatusChanged) {
+            mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        }
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+        mLooper.dispatchAll();
+
+        // If, and only if, Tethering received an interface status changed then
+        // it creates a IpServer and sends out a broadcast indicating that the
+        // interface is "available".
+        if (emulateInterfaceStatusChanged) {
+            assertEquals(1, mTetheringDependencies.mIsTetheringSupportedCalls);
+            verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+            verify(mWifiManager).updateInterfaceIpState(
+                    TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        }
+        verifyNoMoreInteractions(mNMService);
+        verifyNoMoreInteractions(mWifiManager);
+    }
+
+    private void prepareUsbTethering(NetworkState upstreamState) {
+        when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
+        when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
+                .thenReturn(upstreamState);
+
+        // Emulate pressing the USB tethering button in Settings UI.
+        mTethering.startTethering(TETHERING_USB, null, false);
+        mLooper.dispatchAll();
+        verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+        mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+    }
+
+    @Test
+    public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+        NetworkState upstreamState = buildMobileIPv4UpstreamState();
+        prepareUsbTethering(upstreamState);
+
+        // This should produce no activity of any kind.
+        verifyNoMoreInteractions(mNMService);
+
+        // Pretend we then receive USB configured broadcast.
+        sendUsbBroadcast(true, true, true);
+        mLooper.dispatchAll();
+        // Now we should see the start of tethering mechanics (in this case:
+        // tetherMatchingInterfaces() which starts by fetching all interfaces).
+        verify(mNMService, times(1)).listInterfaces();
+
+        // UpstreamNetworkMonitor should receive selected upstream
+        verify(mUpstreamNetworkMonitor, times(1)).selectPreferredUpstreamType(any());
+        verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
+    }
+
+    @Test
+    public void failingLocalOnlyHotspotLegacyApBroadcastWithIfaceStatusChanged() throws Exception {
+        failingLocalOnlyHotspotLegacyApBroadcast(true);
+    }
+
+    @Test
+    public void failingLocalOnlyHotspotLegacyApBroadcastSansIfaceStatusChanged() throws Exception {
+        failingLocalOnlyHotspotLegacyApBroadcast(false);
+    }
+
+    public void workingLocalOnlyHotspotEnrichedApBroadcast(
+            boolean emulateInterfaceStatusChanged) throws Exception {
+        // Emulate externally-visible WifiManager effects, causing the
+        // per-interface state machine to start up, and telling us that
+        // hotspot mode is to be started.
+        if (emulateInterfaceStatusChanged) {
+            mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        }
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+        mLooper.dispatchAll();
+
+        verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+        verify(mNMService, times(1)).setIpForwardingEnabled(true);
+        verify(mNMService, times(1)).startTethering(any(String[].class));
+        verifyNoMoreInteractions(mNMService);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_LOCAL_ONLY);
+        verifyNoMoreInteractions(mWifiManager);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+        verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+        // This will be called twice, one is on entering IpServer.STATE_AVAILABLE,
+        // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY.
+        assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
+
+        // Emulate externally-visible WifiManager effects, when hotspot mode
+        // is being torn down.
+        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
+        mLooper.dispatchAll();
+
+        verify(mNMService, times(1)).untetherInterface(TEST_WLAN_IFNAME);
+        // {g,s}etInterfaceConfig() called twice for enabling and disabling IPv4.
+        verify(mNMService, times(2)).getInterfaceConfig(TEST_WLAN_IFNAME);
+        verify(mNMService, times(2))
+                .setInterfaceConfig(eq(TEST_WLAN_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNMService, times(1)).stopTethering();
+        verify(mNMService, times(1)).setIpForwardingEnabled(false);
+        verify(mWifiManager, times(3)).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        verifyNoMoreInteractions(mNMService);
+        verifyNoMoreInteractions(mWifiManager);
+        // Asking for the last error after the per-interface state machine
+        // has been reaped yields an unknown interface error.
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_WLAN_IFNAME));
+    }
+
+    /**
+     * Send CMD_IPV6_TETHER_UPDATE to IpServers as would be done by IPv6TetheringCoordinator.
+     */
+    private void sendIPv6TetherUpdates(NetworkState upstreamState) {
+        // IPv6TetheringCoordinator must have been notified of downstream
+        verify(mIPv6TetheringCoordinator, times(1)).addActiveDownstream(
+                argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_USB_IFNAME)),
+                eq(IpServer.STATE_TETHERED));
+
+        for (IpServer ipSrv : mTetheringDependencies.mIpv6CoordinatorNotifyList) {
+            NetworkState ipv6OnlyState = buildMobileUpstreamState(false, true, false);
+            ipSrv.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0,
+                    upstreamState.linkProperties.isIpv6Provisioned()
+                            ? ipv6OnlyState.linkProperties
+                            : null);
+        }
+        mLooper.dispatchAll();
+    }
+
+    private void runUsbTethering(NetworkState upstreamState) {
+        prepareUsbTethering(upstreamState);
+        sendUsbBroadcast(true, true, true);
+        mLooper.dispatchAll();
+    }
+
+    @Test
+    public void workingMobileUsbTethering_IPv4() throws Exception {
+        NetworkState upstreamState = buildMobileIPv4UpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+        sendIPv6TetherUpdates(upstreamState);
+        verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull());
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+    }
+
+    @Test
+    public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+        Settings.Global.putInt(mContentResolver, TETHER_ENABLE_LEGACY_DHCP_SERVER, 1);
+        sendConfigurationChanged();
+        final NetworkState upstreamState = buildMobileIPv4UpstreamState();
+        runUsbTethering(upstreamState);
+        sendIPv6TetherUpdates(upstreamState);
+
+        verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any());
+    }
+
+    @Test
+    public void workingMobileUsbTethering_IPv6() throws Exception {
+        NetworkState upstreamState = buildMobileIPv6UpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+        sendIPv6TetherUpdates(upstreamState);
+        verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+        verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+    }
+
+    @Test
+    public void workingMobileUsbTethering_DualStack() throws Exception {
+        NetworkState upstreamState = buildMobileDualStackUpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mRouterAdvertisementDaemon, times(1)).start();
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+
+        sendIPv6TetherUpdates(upstreamState);
+        verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+        verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+    }
+
+    @Test
+    public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+        NetworkState upstreamState = buildMobile464xlatUpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME,
+                TEST_XLAT_MOBILE_IFNAME);
+
+        sendIPv6TetherUpdates(upstreamState);
+        verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull());
+        verify(mNetd, times(1)).tetherApplyDnsInterfaces();
+    }
+
+    @Test
+    public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+        // Setup IPv6
+        NetworkState upstreamState = buildMobileIPv6UpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+
+        // Then 464xlat comes up
+        upstreamState = buildMobile464xlatUpstreamState();
+        when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
+                .thenReturn(upstreamState);
+
+        // Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES.
+        mTetheringDependencies.mUpstreamNetworkMonitorMasterSM.sendMessage(
+                Tethering.TetherMasterSM.EVENT_UPSTREAM_CALLBACK,
+                UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
+                0,
+                upstreamState);
+        mLooper.dispatchAll();
+
+        // Forwarding is added for 464xlat
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME,
+                TEST_XLAT_MOBILE_IFNAME);
+        // Forwarding was not re-added for v6 (still times(1))
+        verify(mNMService, times(1)).enableNat(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNMService, times(1)).startInterfaceForwarding(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        // DHCP not restarted on downstream (still times(1))
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any());
+    }
+
+    @Test
+    public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+        when(mResources.getBoolean(com.android.internal.R.bool.config_tether_upstream_automatic))
+                .thenReturn(true);
+        sendConfigurationChanged();
+
+        // Setup IPv6
+        final NetworkState upstreamState = buildMobileIPv6UpstreamState();
+        runUsbTethering(upstreamState);
+
+        // UpstreamNetworkMonitor should choose upstream automatically
+        // (in this specific case: choose the default network).
+        verify(mUpstreamNetworkMonitor, times(1)).getCurrentPreferredUpstream();
+        verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any());
+
+        verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
+    }
+
+    @Test
+    public void workingLocalOnlyHotspotEnrichedApBroadcastWithIfaceChanged() throws Exception {
+        workingLocalOnlyHotspotEnrichedApBroadcast(true);
+    }
+
+    @Test
+    public void workingLocalOnlyHotspotEnrichedApBroadcastSansIfaceChanged() throws Exception {
+        workingLocalOnlyHotspotEnrichedApBroadcast(false);
+    }
+
+    // TODO: Test with and without interfaceStatusChanged().
+    @Test
+    public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+        when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+
+        // Emulate pressing the WiFi tethering button.
+        mTethering.startTethering(TETHERING_WIFI, null, false);
+        mLooper.dispatchAll();
+        verify(mWifiManager, times(1)).startSoftAp(null);
+        verifyNoMoreInteractions(mWifiManager);
+        verifyNoMoreInteractions(mNMService);
+
+        // Emulate externally-visible WifiManager effects, causing the
+        // per-interface state machine to start up, and telling us that
+        // tethering mode is to be started.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
+        mLooper.dispatchAll();
+
+        assertEquals(1, mTetheringDependencies.mIsTetheringSupportedCalls);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        verifyNoMoreInteractions(mNMService);
+        verifyNoMoreInteractions(mWifiManager);
+    }
+
+    // TODO: Test with and without interfaceStatusChanged().
+    @Test
+    public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+        when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+
+        // Emulate pressing the WiFi tethering button.
+        mTethering.startTethering(TETHERING_WIFI, null, false);
+        mLooper.dispatchAll();
+        verify(mWifiManager, times(1)).startSoftAp(null);
+        verifyNoMoreInteractions(mWifiManager);
+        verifyNoMoreInteractions(mNMService);
+
+        // Emulate externally-visible WifiManager effects, causing the
+        // per-interface state machine to start up, and telling us that
+        // tethering mode is to be started.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
+
+        verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+        verify(mNMService, times(1)).setIpForwardingEnabled(true);
+        verify(mNMService, times(1)).startTethering(any(String[].class));
+        verifyNoMoreInteractions(mNMService);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
+        verifyNoMoreInteractions(mWifiManager);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER);
+        verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+        // In tethering mode, in the default configuration, an explicit request
+        // for a mobile network is also made.
+        verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest();
+        // This will be called twice, one is on entering IpServer.STATE_AVAILABLE,
+        // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY.
+        assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
+
+        /////
+        // We do not currently emulate any upstream being found.
+        //
+        // This is why there are no calls to verify mNMService.enableNat() or
+        // mNMService.startInterfaceForwarding().
+        /////
+
+        // Emulate pressing the WiFi tethering button.
+        mTethering.stopTethering(TETHERING_WIFI);
+        mLooper.dispatchAll();
+        verify(mWifiManager, times(1)).stopSoftAp();
+        verifyNoMoreInteractions(mWifiManager);
+        verifyNoMoreInteractions(mNMService);
+
+        // Emulate externally-visible WifiManager effects, when tethering mode
+        // is being torn down.
+        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
+        mLooper.dispatchAll();
+
+        verify(mNMService, times(1)).untetherInterface(TEST_WLAN_IFNAME);
+        // {g,s}etInterfaceConfig() called twice for enabling and disabling IPv4.
+        verify(mNMService, atLeastOnce()).getInterfaceConfig(TEST_WLAN_IFNAME);
+        verify(mNMService, atLeastOnce())
+                .setInterfaceConfig(eq(TEST_WLAN_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNMService, times(1)).stopTethering();
+        verify(mNMService, times(1)).setIpForwardingEnabled(false);
+        verify(mWifiManager, times(3)).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        verifyNoMoreInteractions(mNMService);
+        verifyNoMoreInteractions(mWifiManager);
+        // Asking for the last error after the per-interface state machine
+        // has been reaped yields an unknown interface error.
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_WLAN_IFNAME));
+    }
+
+    // TODO: Test with and without interfaceStatusChanged().
+    @Test
+    public void failureEnablingIpForwarding() throws Exception {
+        when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+        doThrow(new RemoteException()).when(mNMService).setIpForwardingEnabled(true);
+
+        // Emulate pressing the WiFi tethering button.
+        mTethering.startTethering(TETHERING_WIFI, null, false);
+        mLooper.dispatchAll();
+        verify(mWifiManager, times(1)).startSoftAp(null);
+        verifyNoMoreInteractions(mWifiManager);
+        verifyNoMoreInteractions(mNMService);
+
+        // Emulate externally-visible WifiManager effects, causing the
+        // per-interface state machine to start up, and telling us that
+        // tethering mode is to be started.
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
+
+        // We verify get/set called thrice here: twice for setup (on NMService) and once during
+        // teardown (on Netd) because all events happen over the course of the single
+        // dispatchAll() above. Note that once the IpServer IPv4 address config
+        // code is refactored the two calls during shutdown will revert to one.
+        verify(mNMService, times(2)).getInterfaceConfig(TEST_WLAN_IFNAME);
+        verify(mNMService, times(2))
+                .setInterfaceConfig(eq(TEST_WLAN_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNetd, times(1)).interfaceSetCfg(argThat(p -> TEST_WLAN_IFNAME.equals(p.ifName)));
+        verify(mNMService, times(1)).tetherInterface(TEST_WLAN_IFNAME);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
+        // There are 3 state change event:
+        // AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE.
+        assertEquals(3, mTetheringDependencies.mIsTetheringSupportedCalls);
+        verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
+        // This is called, but will throw.
+        verify(mNMService, times(1)).setIpForwardingEnabled(true);
+        // This never gets called because of the exception thrown above.
+        verify(mNMService, times(0)).startTethering(any(String[].class));
+        // When the master state machine transitions to an error state it tells
+        // downstream interfaces, which causes us to tell Wi-Fi about the error
+        // so it can take down AP mode.
+        verify(mNMService, times(1)).untetherInterface(TEST_WLAN_IFNAME);
+        verify(mWifiManager).updateInterfaceIpState(
+                TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
+
+        verifyNoMoreInteractions(mWifiManager);
+        verifyNoMoreInteractions(mNMService);
+    }
+
+    private void userRestrictionsListenerBehaviour(
+            boolean currentDisallow, boolean nextDisallow, String[] activeTetheringIfacesList,
+            int expectedInteractionsWithShowNotification) throws  Exception {
+        final int userId = 0;
+        final Bundle currRestrictions = new Bundle();
+        final Bundle newRestrictions = new Bundle();
+        Tethering tethering = mock(Tethering.class);
+        Tethering.TetheringUserRestrictionListener turl =
+                new Tethering.TetheringUserRestrictionListener(tethering);
+
+        currRestrictions.putBoolean(UserManager.DISALLOW_CONFIG_TETHERING, currentDisallow);
+        newRestrictions.putBoolean(UserManager.DISALLOW_CONFIG_TETHERING, nextDisallow);
+        when(tethering.getTetheredIfaces()).thenReturn(activeTetheringIfacesList);
+
+        turl.onUserRestrictionsChanged(userId, newRestrictions, currRestrictions);
+
+        verify(tethering, times(expectedInteractionsWithShowNotification))
+                .showTetheredNotification(anyInt(), eq(false));
+
+        verify(tethering, times(expectedInteractionsWithShowNotification)).untetherAll();
+    }
+
+    @Test
+    public void testDisallowTetheringWhenNoTetheringInterfaceIsActive() throws Exception {
+        final String[] emptyActiveIfacesList = new String[]{};
+        final boolean currDisallow = false;
+        final boolean nextDisallow = true;
+        final int expectedInteractionsWithShowNotification = 0;
+
+        userRestrictionsListenerBehaviour(currDisallow, nextDisallow, emptyActiveIfacesList,
+                expectedInteractionsWithShowNotification);
+    }
+
+    @Test
+    public void testDisallowTetheringWhenAtLeastOneTetheringInterfaceIsActive() throws Exception {
+        final String[] nonEmptyActiveIfacesList = new String[]{TEST_WLAN_IFNAME};
+        final boolean currDisallow = false;
+        final boolean nextDisallow = true;
+        final int expectedInteractionsWithShowNotification = 1;
+
+        userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+                expectedInteractionsWithShowNotification);
+    }
+
+    @Test
+    public void testAllowTetheringWhenNoTetheringInterfaceIsActive() throws Exception {
+        final String[] nonEmptyActiveIfacesList = new String[]{};
+        final boolean currDisallow = true;
+        final boolean nextDisallow = false;
+        final int expectedInteractionsWithShowNotification = 0;
+
+        userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+                expectedInteractionsWithShowNotification);
+    }
+
+    @Test
+    public void testAllowTetheringWhenAtLeastOneTetheringInterfaceIsActive() throws Exception {
+        final String[] nonEmptyActiveIfacesList = new String[]{TEST_WLAN_IFNAME};
+        final boolean currDisallow = true;
+        final boolean nextDisallow = false;
+        final int expectedInteractionsWithShowNotification = 0;
+
+        userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+                expectedInteractionsWithShowNotification);
+    }
+
+    @Test
+    public void testDisallowTetheringUnchanged() throws Exception {
+        final String[] nonEmptyActiveIfacesList = new String[]{TEST_WLAN_IFNAME};
+        final int expectedInteractionsWithShowNotification = 0;
+        boolean currDisallow = true;
+        boolean nextDisallow = true;
+
+        userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+                expectedInteractionsWithShowNotification);
+
+        currDisallow = false;
+        nextDisallow = false;
+
+        userRestrictionsListenerBehaviour(currDisallow, nextDisallow, nonEmptyActiveIfacesList,
+                expectedInteractionsWithShowNotification);
+    }
+
+    private class TestTetherInternalCallback extends ITetherInternalCallback.Stub {
+        private final ArrayList<Network> mActualUpstreams = new ArrayList<>();
+        private final ArrayList<TetheringConfigurationParcel> mTetheringConfigs =
+                new ArrayList<>();
+        private final ArrayList<TetherStatesParcel> mTetherStates = new ArrayList<>();
+
+        // This function will remove the recorded callbacks, so it must be called once for
+        // each callback. If this is called after multiple callback, the order matters.
+        // onCallbackCreated counts as the first call to expectUpstreamChanged with
+        // @see onCallbackCreated.
+        public void expectUpstreamChanged(Network... networks) {
+            if (networks == null) {
+                assertNoUpstreamChangeCallback();
+                return;
+            }
+
+            final ArrayList<Network> expectedUpstreams =
+                    new ArrayList<Network>(Arrays.asList(networks));
+            for (Network upstream : expectedUpstreams) {
+                // throws OOB if no expectations
+                assertEquals(mActualUpstreams.remove(0), upstream);
+            }
+            assertNoUpstreamChangeCallback();
+        }
+
+        // This function will remove the recorded callbacks, so it must be called once
+        // for each callback. If this is called after multiple callback, the order matters.
+        // onCallbackCreated counts as the first call to onConfigurationChanged with
+        // @see onCallbackCreated.
+        public void expectConfigurationChanged(TetheringConfigurationParcel... tetherConfigs) {
+            final ArrayList<TetheringConfigurationParcel> expectedTetherConfig =
+                    new ArrayList<TetheringConfigurationParcel>(Arrays.asList(tetherConfigs));
+            for (TetheringConfigurationParcel config : expectedTetherConfig) {
+                // throws OOB if no expectations
+                final TetheringConfigurationParcel actualConfig = mTetheringConfigs.remove(0);
+                assertTetherConfigParcelEqual(actualConfig, config);
+            }
+            assertNoConfigChangeCallback();
+        }
+
+        public TetherStatesParcel pollTetherStatesChanged() {
+            assertStateChangeCallback();
+            return mTetherStates.remove(0);
+        }
+
+        @Override
+        public void onUpstreamChanged(Network network) {
+            mActualUpstreams.add(network);
+        }
+
+        @Override
+        public void onConfigurationChanged(TetheringConfigurationParcel config) {
+            mTetheringConfigs.add(config);
+        }
+
+        @Override
+        public void onTetherStatesChanged(TetherStatesParcel states) {
+            mTetherStates.add(states);
+        }
+
+        @Override
+        public void onCallbackCreated(Network network, TetheringConfigurationParcel config,
+                TetherStatesParcel states) {
+            mActualUpstreams.add(network);
+            mTetheringConfigs.add(config);
+            mTetherStates.add(states);
+        }
+
+        public void assertNoUpstreamChangeCallback() {
+            assertTrue(mActualUpstreams.isEmpty());
+        }
+
+        public void assertNoConfigChangeCallback() {
+            assertTrue(mTetheringConfigs.isEmpty());
+        }
+
+        public void assertStateChangeCallback() {
+            assertFalse(mTetherStates.isEmpty());
+        }
+
+        private void assertTetherConfigParcelEqual(@NonNull TetheringConfigurationParcel actual,
+                @NonNull TetheringConfigurationParcel expect) {
+            assertEquals(actual.subId, expect.subId);
+            assertArrayEquals(actual.tetherableUsbRegexs, expect.tetherableUsbRegexs);
+            assertArrayEquals(actual.tetherableWifiRegexs, expect.tetherableWifiRegexs);
+            assertArrayEquals(actual.tetherableBluetoothRegexs, expect.tetherableBluetoothRegexs);
+            assertEquals(actual.isDunRequired, expect.isDunRequired);
+            assertEquals(actual.chooseUpstreamAutomatically, expect.chooseUpstreamAutomatically);
+            assertArrayEquals(actual.preferredUpstreamIfaceTypes,
+                    expect.preferredUpstreamIfaceTypes);
+            assertArrayEquals(actual.legacyDhcpRanges, expect.legacyDhcpRanges);
+            assertArrayEquals(actual.defaultIPv4DNS, expect.defaultIPv4DNS);
+            assertEquals(actual.enableLegacyDhcpServer, expect.enableLegacyDhcpServer);
+            assertArrayEquals(actual.provisioningApp, expect.provisioningApp);
+            assertEquals(actual.provisioningAppNoUi, expect.provisioningAppNoUi);
+            assertEquals(actual.provisioningCheckPeriod, expect.provisioningCheckPeriod);
+        }
+    }
+
+    @Test
+    public void testRegisterTetherInternalCallback() throws Exception {
+        TestTetherInternalCallback callback = new TestTetherInternalCallback();
+
+        // 1. Register one callback before running any tethering.
+        mTethering.registerTetherInternalCallback(callback);
+        mLooper.dispatchAll();
+        callback.expectUpstreamChanged(new Network[] {null});
+        callback.expectConfigurationChanged(
+                mTethering.getTetheringConfiguration().toStableParcelable());
+        TetherStatesParcel tetherState = callback.pollTetherStatesChanged();
+        assertEquals(tetherState, null);
+        // 2. Enable wifi tethering
+        NetworkState upstreamState = buildMobileDualStackUpstreamState();
+        when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
+        when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any()))
+                .thenReturn(upstreamState);
+        when(mWifiManager.startSoftAp(any(WifiConfiguration.class))).thenReturn(true);
+        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+        mLooper.dispatchAll();
+        tetherState = callback.pollTetherStatesChanged();
+        assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
+
+        mTethering.startTethering(TETHERING_WIFI, null, false);
+        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
+        tetherState = callback.pollTetherStatesChanged();
+        assertArrayEquals(tetherState.tetheredList, new String[] {TEST_WLAN_IFNAME});
+        callback.expectUpstreamChanged(upstreamState.network);
+
+        // 3. Disable wifi tethering.
+        mTethering.stopTethering(TETHERING_WIFI);
+        sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+        mLooper.dispatchAll();
+        tetherState = callback.pollTetherStatesChanged();
+        assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
+        mLooper.dispatchAll();
+        callback.expectUpstreamChanged(new Network[] {null});
+    }
+
+    @Test
+    public void testMultiSimAware() throws Exception {
+        final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
+        assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.subId);
+
+        final int fakeSubId = 1234;
+        mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
+        final TetheringConfiguration newConfig = mTethering.getTetheringConfiguration();
+        assertEquals(fakeSubId, newConfig.subId);
+    }
+
+    private void workingWifiP2pGroupOwner(
+            boolean emulateInterfaceStatusChanged) throws Exception {
+        if (emulateInterfaceStatusChanged) {
+            mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+        }
+        sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+        mLooper.dispatchAll();
+
+        verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
+        verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
+        verify(mNMService, times(1)).setIpForwardingEnabled(true);
+        verify(mNMService, times(1)).startTethering(any(String[].class));
+        verifyNoMoreInteractions(mNMService);
+        verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
+        verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
+        // This will be called twice, one is on entering IpServer.STATE_AVAILABLE,
+        // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY.
+        assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
+
+        assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+
+        // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
+        // is being removed.
+        sendWifiP2pConnectionChanged(false, true, TEST_P2P_IFNAME);
+        mTethering.interfaceRemoved(TEST_P2P_IFNAME);
+        mLooper.dispatchAll();
+
+        verify(mNMService, times(1)).untetherInterface(TEST_P2P_IFNAME);
+        // {g,s}etInterfaceConfig() called twice for enabling and disabling IPv4.
+        verify(mNMService, times(2)).getInterfaceConfig(TEST_P2P_IFNAME);
+        verify(mNMService, times(2))
+                .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNMService, times(1)).stopTethering();
+        verify(mNMService, times(1)).setIpForwardingEnabled(false);
+        verify(mUpstreamNetworkMonitor, never()).getCurrentPreferredUpstream();
+        verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any());
+        verifyNoMoreInteractions(mNMService);
+        // Asking for the last error after the per-interface state machine
+        // has been reaped yields an unknown interface error.
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+    }
+
+    private void workingWifiP2pGroupClient(
+            boolean emulateInterfaceStatusChanged) throws Exception {
+        if (emulateInterfaceStatusChanged) {
+            mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+        }
+        sendWifiP2pConnectionChanged(true, false, TEST_P2P_IFNAME);
+        mLooper.dispatchAll();
+
+        verify(mNMService, never()).getInterfaceConfig(TEST_P2P_IFNAME);
+        verify(mNMService, never())
+                .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNMService, never()).tetherInterface(TEST_P2P_IFNAME);
+        verify(mNMService, never()).setIpForwardingEnabled(true);
+        verify(mNMService, never()).startTethering(any(String[].class));
+
+        // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
+        // is being removed.
+        sendWifiP2pConnectionChanged(false, false, TEST_P2P_IFNAME);
+        mTethering.interfaceRemoved(TEST_P2P_IFNAME);
+        mLooper.dispatchAll();
+
+        verify(mNMService, never()).untetherInterface(TEST_P2P_IFNAME);
+        verify(mNMService, never()).getInterfaceConfig(TEST_P2P_IFNAME);
+        verify(mNMService, never())
+                .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNMService, never()).stopTethering();
+        verify(mNMService, never()).setIpForwardingEnabled(false);
+        verifyNoMoreInteractions(mNMService);
+        // Asking for the last error after the per-interface state machine
+        // has been reaped yields an unknown interface error.
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+    }
+
+    @Test
+    public void workingWifiP2pGroupOwnerWithIfaceChanged() throws Exception {
+        workingWifiP2pGroupOwner(true);
+    }
+
+    @Test
+    public void workingWifiP2pGroupOwnerSansIfaceChanged() throws Exception {
+        workingWifiP2pGroupOwner(false);
+    }
+
+    private void workingWifiP2pGroupOwnerLegacyMode(
+            boolean emulateInterfaceStatusChanged) throws Exception {
+        // change to legacy mode and update tethering information by chaning SIM
+        when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_p2p_regexs))
+                .thenReturn(new String[]{});
+        final int fakeSubId = 1234;
+        mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId);
+
+        if (emulateInterfaceStatusChanged) {
+            mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
+        }
+        sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
+        mLooper.dispatchAll();
+
+        verify(mNMService, never()).getInterfaceConfig(TEST_P2P_IFNAME);
+        verify(mNMService, never())
+                .setInterfaceConfig(eq(TEST_P2P_IFNAME), any(InterfaceConfiguration.class));
+        verify(mNMService, never()).tetherInterface(TEST_P2P_IFNAME);
+        verify(mNMService, never()).setIpForwardingEnabled(true);
+        verify(mNMService, never()).startTethering(any(String[].class));
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+    }
+    @Test
+    public void workingWifiP2pGroupOwnerLegacyModeWithIfaceChanged() throws Exception {
+        workingWifiP2pGroupOwnerLegacyMode(true);
+    }
+
+    @Test
+    public void workingWifiP2pGroupOwnerLegacyModeSansIfaceChanged() throws Exception {
+        workingWifiP2pGroupOwnerLegacyMode(false);
+    }
+
+    @Test
+    public void workingWifiP2pGroupClientWithIfaceChanged() throws Exception {
+        workingWifiP2pGroupClient(true);
+    }
+
+    @Test
+    public void workingWifiP2pGroupClientSansIfaceChanged() throws Exception {
+        workingWifiP2pGroupClient(false);
+    }
+
+    // TODO: Test that a request for hotspot mode doesn't interfere with an
+    // already operating tethering mode interface.
+}