Merge "Add Entitlement Manger to encapsulate provisioning mechanics" am: 1c66fd2d67
am: 8a01337f5c

Change-Id: I18240295245c7e9b3b5299910d17643bc7f3141a
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index a8f7259..3c14393 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -23,24 +23,18 @@
 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_ADD_TETHER_TYPE;
 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.EXTRA_PROVISION_CALLBACK;
-import static android.net.ConnectivityManager.EXTRA_REM_TETHER_TYPE;
-import static android.net.ConnectivityManager.EXTRA_RUN_PROVISION;
-import static android.net.ConnectivityManager.EXTRA_SET_ALARM;
-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_UNKNOWN_IFACE;
-import static android.net.ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
 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.NetworkCapabilities.TRANSPORT_CELLULAR;
+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;
@@ -50,6 +44,7 @@
 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 com.android.server.ConnectivityService.SHORT_ARG;
 
 import android.app.Notification;
@@ -60,7 +55,6 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothProfile.ServiceListener;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -68,7 +62,6 @@
 import android.hardware.usb.UsbManager;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
-import android.net.ip.IpServer;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -76,7 +69,7 @@
 import android.net.NetworkInfo;
 import android.net.NetworkState;
 import android.net.NetworkUtils;
-import android.net.RouteInfo;
+import android.net.ip.IpServer;
 import android.net.util.InterfaceSet;
 import android.net.util.PrefixUtils;
 import android.net.util.SharedLog;
@@ -89,15 +82,12 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.Parcel;
-import android.os.PersistableBundle;
 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.provider.Settings;
-import android.telephony.CarrierConfigManager;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -113,6 +103,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.server.LocalServices;
+import com.android.server.connectivity.tethering.EntitlementManager;
 import com.android.server.connectivity.tethering.IPv6TetheringCoordinator;
 import com.android.server.connectivity.tethering.OffloadController;
 import com.android.server.connectivity.tethering.TetheringConfiguration;
@@ -123,8 +114,6 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -145,18 +134,12 @@
     private final static boolean DBG = false;
     private final static boolean VDBG = false;
 
-    protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning";
-
     private static final Class[] messageClasses = {
             Tethering.class, TetherMasterSM.class, IpServer.class
     };
     private static final SparseArray<String> sMagicDecoderRing =
             MessageUtils.findMessageNames(messageClasses);
 
-    // {@link ComponentName} of the Service used to run tether provisioning.
-    private static final ComponentName TETHER_SERVICE = ComponentName.unflattenFromString(Resources
-            .getSystem().getString(com.android.internal.R.string.config_wifi_tether_enable));
-
     private static class TetherState {
         public final IpServer ipServer;
         public int lastState;
@@ -191,7 +174,6 @@
     private final INetworkStatsService mStatsService;
     private final INetworkPolicyManager mPolicyManager;
     private final Looper mLooper;
-    private final MockableSystemProperties mSystemProperties;
     private final StateMachine mTetherMasterSM;
     private final OffloadController mOffloadController;
     private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
@@ -200,6 +182,7 @@
     private final HashSet<IpServer> mForwardedDownstreams;
     private final VersionedBroadcastListener mCarrierConfigChange;
     private final TetheringDependencies mDeps;
+    private final EntitlementManager mEntitlementMgr;
 
     private volatile TetheringConfiguration mConfig;
     private InterfaceSet mCurrentUpstreamIfaceSet;
@@ -220,7 +203,6 @@
         mStatsService = statsService;
         mPolicyManager = policyManager;
         mLooper = looper;
-        mSystemProperties = systemProperties;
         mDeps = deps;
 
         mPublicSync = new Object();
@@ -241,12 +223,13 @@
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
+        mEntitlementMgr = mDeps.getEntitlementManager(mContext, mLog, systemProperties);
         mCarrierConfigChange = new VersionedBroadcastListener(
                 "CarrierConfigChangeListener", mContext, smHandler, filter,
                 (Intent ignored) -> {
                     mLog.log("OBSERVED carrier config change");
                     updateConfiguration();
-                    reevaluateSimCardProvisioning();
+                    mEntitlementMgr.reevaluateSimCardProvisioning();
                 });
 
         mStateReceiver = new StateReceiver();
@@ -289,6 +272,7 @@
     private void updateConfiguration() {
         mConfig = new TetheringConfiguration(mContext, mLog);
         mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired);
+        mEntitlementMgr.updateConfiguration(mConfig);
     }
 
     private void maybeUpdateConfiguration() {
@@ -354,83 +338,54 @@
     }
 
     public void startTethering(int type, ResultReceiver receiver, boolean showProvisioningUi) {
-        if (!isTetherProvisioningRequired()) {
+        mEntitlementMgr.startTethering(type);
+        if (!mEntitlementMgr.isTetherProvisioningRequired()) {
             enableTetheringInternal(type, true, receiver);
             return;
         }
 
+        final ResultReceiver proxyReceiver = getProxyReceiver(type, receiver);
         if (showProvisioningUi) {
-            runUiTetherProvisioningAndEnable(type, receiver);
+            mEntitlementMgr.runUiTetherProvisioningAndEnable(type, proxyReceiver);
         } else {
-            runSilentTetherProvisioningAndEnable(type, receiver);
+            mEntitlementMgr.runSilentTetherProvisioningAndEnable(type, proxyReceiver);
         }
     }
 
     public void stopTethering(int type) {
         enableTetheringInternal(type, false, null);
-        if (isTetherProvisioningRequired()) {
-            cancelTetherProvisioningRechecks(type);
+        mEntitlementMgr.stopTethering(type);
+        if (mEntitlementMgr.isTetherProvisioningRequired()) {
+            // There are lurking bugs where the notion of "provisioning required" or
+            // "tethering supported" may change without notifying tethering properly, then
+            // tethering can't shutdown correctly.
+            // TODO: cancel re-check all the time
+            if (mDeps.isTetheringSupported()) {
+                mEntitlementMgr.cancelTetherProvisioningRechecks(type);
+            }
         }
     }
 
     /**
-     * Check if the device requires a provisioning check in order to enable tethering.
-     *
-     * @return a boolean - {@code true} indicating tether provisioning is required by the carrier.
-     */
-    @VisibleForTesting
-    protected boolean isTetherProvisioningRequired() {
-        final TetheringConfiguration cfg = mConfig;
-        if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
-                || cfg.provisioningApp.length == 0) {
-            return false;
-        }
-        if (carrierConfigAffirmsEntitlementCheckNotRequired()) {
-            return false;
-        }
-        return (cfg.provisioningApp.length == 2);
-    }
-
-    // The logic here is aimed solely at confirming that a CarrierConfig exists
-    // and affirms that entitlement checks are not required.
-    //
-    // TODO: find a better way to express this, or alter the checking process
-    // entirely so that this is more intuitive.
-    private boolean carrierConfigAffirmsEntitlementCheckNotRequired() {
-        // Check carrier config for entitlement checks
-        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
-             .getSystemService(Context.CARRIER_CONFIG_SERVICE);
-        if (configManager == null) return false;
-
-        final PersistableBundle carrierConfig = configManager.getConfig();
-        if (carrierConfig == null) return false;
-
-        // A CarrierConfigManager was found and it has a config.
-        final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
-                CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
-        return !isEntitlementCheckRequired;
-    }
-
-    /**
      * Enables or disables tethering for the given type. This should only be called once
      * provisioning has succeeded or is not necessary. It will also schedule provisioning rechecks
      * for the specified interface.
      */
     private void enableTetheringInternal(int type, boolean enable, ResultReceiver receiver) {
-        boolean isProvisioningRequired = enable && isTetherProvisioningRequired();
+        boolean isProvisioningRequired = enable && mEntitlementMgr.isTetherProvisioningRequired();
         int result;
         switch (type) {
             case TETHERING_WIFI:
                 result = setWifiTethering(enable);
                 if (isProvisioningRequired && result == TETHER_ERROR_NO_ERROR) {
-                    scheduleProvisioningRechecks(type);
+                    mEntitlementMgr.scheduleProvisioningRechecks(type);
                 }
                 sendTetherResult(receiver, result);
                 break;
             case TETHERING_USB:
                 result = setUsbTethering(enable);
                 if (isProvisioningRequired && result == TETHER_ERROR_NO_ERROR) {
-                    scheduleProvisioningRechecks(type);
+                    mEntitlementMgr.scheduleProvisioningRechecks(type);
                 }
                 sendTetherResult(receiver, result);
                 break;
@@ -489,32 +444,14 @@
                         ? TETHER_ERROR_NO_ERROR
                         : TETHER_ERROR_MASTER_ERROR;
                 sendTetherResult(receiver, result);
-                if (enable && isTetherProvisioningRequired()) {
-                    scheduleProvisioningRechecks(TETHERING_BLUETOOTH);
+                if (enable && mEntitlementMgr.isTetherProvisioningRequired()) {
+                    mEntitlementMgr.scheduleProvisioningRechecks(TETHERING_BLUETOOTH);
                 }
                 adapter.closeProfileProxy(BluetoothProfile.PAN, proxy);
             }
         }, BluetoothProfile.PAN);
     }
 
-    private void runUiTetherProvisioningAndEnable(int type, ResultReceiver receiver) {
-        ResultReceiver proxyReceiver = getProxyReceiver(type, receiver);
-        sendUiTetherProvisionIntent(type, proxyReceiver);
-    }
-
-    private void sendUiTetherProvisionIntent(int type, ResultReceiver receiver) {
-        Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING);
-        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
-        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
     /**
      * Creates a proxy {@link ResultReceiver} which enables tethering if the provisioning result
      * is successful before firing back up to the wrapped receiver.
@@ -546,62 +483,6 @@
         return receiverForSending;
     }
 
-    private void scheduleProvisioningRechecks(int type) {
-        Intent intent = new Intent();
-        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
-        intent.putExtra(EXTRA_SET_ALARM, true);
-        intent.setComponent(TETHER_SERVICE);
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            mContext.startServiceAsUser(intent, UserHandle.CURRENT);
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
-    private void runSilentTetherProvisioningAndEnable(int type, ResultReceiver receiver) {
-        ResultReceiver proxyReceiver = getProxyReceiver(type, receiver);
-        sendSilentTetherProvisionIntent(type, proxyReceiver);
-    }
-
-    private void sendSilentTetherProvisionIntent(int type, ResultReceiver receiver) {
-        Intent intent = new Intent();
-        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
-        intent.putExtra(EXTRA_RUN_PROVISION, true);
-        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
-        intent.setComponent(TETHER_SERVICE);
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            mContext.startServiceAsUser(intent, UserHandle.CURRENT);
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
-    private void cancelTetherProvisioningRechecks(int type) {
-        if (mDeps.isTetheringSupported()) {
-            Intent intent = new Intent();
-            intent.putExtra(EXTRA_REM_TETHER_TYPE, type);
-            intent.setComponent(TETHER_SERVICE);
-            final long ident = Binder.clearCallingIdentity();
-            try {
-                mContext.startServiceAsUser(intent, UserHandle.CURRENT);
-            } finally {
-                Binder.restoreCallingIdentity(ident);
-            }
-        }
-    }
-
-    // Used by the SIM card change observation code.
-    // TODO: De-duplicate with above code, where possible.
-    private void startProvisionIntent(int tetherType) {
-        final Intent startProvIntent = new Intent();
-        startProvIntent.putExtra(EXTRA_ADD_TETHER_TYPE, tetherType);
-        startProvIntent.putExtra(EXTRA_RUN_PROVISION, true);
-        startProvIntent.setComponent(TETHER_SERVICE);
-        mContext.startServiceAsUser(startProvIntent, UserHandle.CURRENT);
-    }
-
     public int tether(String iface) {
         return tether(iface, IpServer.STATE_TETHERED);
     }
@@ -1166,30 +1047,6 @@
         return false;
     }
 
-    private void reevaluateSimCardProvisioning() {
-        if (!mConfig.hasMobileHotspotProvisionApp()) return;
-        if (carrierConfigAffirmsEntitlementCheckNotRequired()) return;
-
-        ArrayList<Integer> tethered = new ArrayList<>();
-        synchronized (mPublicSync) {
-            for (int i = 0; i < mTetherStates.size(); i++) {
-                TetherState tetherState = mTetherStates.valueAt(i);
-                if (tetherState.lastState != IpServer.STATE_TETHERED) {
-                    continue;  // Skip interfaces that aren't tethered.
-                }
-                String iface = mTetherStates.keyAt(i);
-                int interfaceType = ifaceNameToType(iface);
-                if (interfaceType != TETHERING_INVALID) {
-                    tethered.add(interfaceType);
-                }
-            }
-        }
-
-        for (int tetherType : tethered) {
-            startProvisionIntent(tetherType);
-        }
-    }
-
     class TetherMasterSM extends StateMachine {
         private static final int BASE_MASTER                    = Protocol.BASE_TETHERING;
         // an interface SM has requested Tethering/Local Hotspot
diff --git a/services/core/java/com/android/server/connectivity/tethering/EntitlementManager.java b/services/core/java/com/android/server/connectivity/tethering/EntitlementManager.java
new file mode 100644
index 0000000..a4e3e1d
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/tethering/EntitlementManager.java
@@ -0,0 +1,224 @@
+/*
+ * 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 static android.net.ConnectivityManager.EXTRA_ADD_TETHER_TYPE;
+import static android.net.ConnectivityManager.EXTRA_PROVISION_CALLBACK;
+import static android.net.ConnectivityManager.EXTRA_REM_TETHER_TYPE;
+import static android.net.ConnectivityManager.EXTRA_RUN_PROVISION;
+import static android.net.ConnectivityManager.EXTRA_SET_ALARM;
+
+import static com.android.internal.R.string.config_wifi_tether_enable;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.util.SharedLog;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.MockableSystemProperties;
+
+/**
+ * This class encapsulates entitlement/provisioning mechanics
+ * provisioning check only applies to the use of the mobile network as an upstream
+ *
+ * @hide
+ */
+public class EntitlementManager {
+    private static final String TAG = EntitlementManager.class.getSimpleName();
+
+    // {@link ComponentName} of the Service used to run tether provisioning.
+    private static final ComponentName TETHER_SERVICE = ComponentName.unflattenFromString(
+            Resources.getSystem().getString(config_wifi_tether_enable));
+    protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning";
+
+    // The ArraySet contains enabled downstream types, ex:
+    // {@link ConnectivityManager.TETHERING_WIFI}
+    // {@link ConnectivityManager.TETHERING_USB}
+    // {@link ConnectivityManager.TETHERING_BLUETOOTH}
+    @GuardedBy("mCurrentTethers")
+    private final ArraySet<Integer> mCurrentTethers;
+    private final Context mContext;
+    private final MockableSystemProperties mSystemProperties;
+    private final SharedLog mLog;
+    @Nullable
+    private TetheringConfiguration mConfig;
+
+    public EntitlementManager(Context ctx, SharedLog log,
+            MockableSystemProperties systemProperties) {
+        mContext = ctx;
+        mLog = log;
+        mCurrentTethers = new ArraySet<Integer>();
+        mSystemProperties = systemProperties;
+    }
+
+    /**
+     * Pass a new TetheringConfiguration instance each time when
+     * Tethering#updateConfiguration() is called.
+     */
+    public void updateConfiguration(TetheringConfiguration conf) {
+        mConfig = conf;
+    }
+
+    /**
+     * Tell EntitlementManager that a given type of tethering has been enabled
+     *
+     * @param type Tethering type
+     */
+    public void startTethering(int type) {
+        synchronized (mCurrentTethers) {
+            mCurrentTethers.add(type);
+        }
+    }
+
+    /**
+     * Tell EntitlementManager that a given type of tethering has been disabled
+     *
+     * @param type Tethering type
+     */
+    public void stopTethering(int type) {
+        synchronized (mCurrentTethers) {
+            mCurrentTethers.remove(type);
+        }
+    }
+
+    /**
+     * Check if the device requires a provisioning check in order to enable tethering.
+     *
+     * @return a boolean - {@code true} indicating tether provisioning is required by the carrier.
+     */
+    @VisibleForTesting
+    public boolean isTetherProvisioningRequired() {
+        if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
+                || mConfig.provisioningApp.length == 0) {
+            return false;
+        }
+        if (carrierConfigAffirmsEntitlementCheckNotRequired()) {
+            return false;
+        }
+        return (mConfig.provisioningApp.length == 2);
+    }
+
+    /**
+     * Re-check tethering provisioning for enabled downstream tether types.
+     * Reference ConnectivityManager.TETHERING_{@code *} for each tether type.
+     */
+    public void reevaluateSimCardProvisioning() {
+        if (!mConfig.hasMobileHotspotProvisionApp()) return;
+        if (carrierConfigAffirmsEntitlementCheckNotRequired()) return;
+
+        final ArraySet<Integer> reevaluateType;
+        synchronized (mCurrentTethers) {
+            reevaluateType = new ArraySet<Integer>(mCurrentTethers);
+        }
+        for (Integer type : reevaluateType) {
+            startProvisionIntent(type);
+        }
+    }
+
+    // The logic here is aimed solely at confirming that a CarrierConfig exists
+    // and affirms that entitlement checks are not required.
+    //
+    // TODO: find a better way to express this, or alter the checking process
+    // entirely so that this is more intuitive.
+    private boolean carrierConfigAffirmsEntitlementCheckNotRequired() {
+        // Check carrier config for entitlement checks
+        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) return false;
+
+        final PersistableBundle carrierConfig = configManager.getConfig();
+        if (carrierConfig == null) return false;
+
+        // A CarrierConfigManager was found and it has a config.
+        final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
+                CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
+        return !isEntitlementCheckRequired;
+    }
+
+    public void runSilentTetherProvisioningAndEnable(int type, ResultReceiver receiver) {
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+        intent.putExtra(EXTRA_RUN_PROVISION, true);
+        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+        intent.setComponent(TETHER_SERVICE);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.startServiceAsUser(intent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    public void runUiTetherProvisioningAndEnable(int type, ResultReceiver receiver) {
+        Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING);
+        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    // Used by the SIM card change observation code.
+    // TODO: De-duplicate with above code, where possible.
+    private void startProvisionIntent(int tetherType) {
+        final Intent startProvIntent = new Intent();
+        startProvIntent.putExtra(EXTRA_ADD_TETHER_TYPE, tetherType);
+        startProvIntent.putExtra(EXTRA_RUN_PROVISION, true);
+        startProvIntent.setComponent(TETHER_SERVICE);
+        mContext.startServiceAsUser(startProvIntent, UserHandle.CURRENT);
+    }
+
+    public void scheduleProvisioningRechecks(int type) {
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+        intent.putExtra(EXTRA_SET_ALARM, true);
+        intent.setComponent(TETHER_SERVICE);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.startServiceAsUser(intent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    public void cancelTetherProvisioningRechecks(int type) {
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_REM_TETHER_TYPE, type);
+        intent.setComponent(TETHER_SERVICE);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.startServiceAsUser(intent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java b/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java
index 8b40069..d56b167 100644
--- a/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java
+++ b/services/core/java/com/android/server/connectivity/tethering/TetheringDependencies.java
@@ -17,19 +17,13 @@
 package com.android.server.connectivity.tethering;
 
 import android.content.Context;
-import android.net.INetd;
 import android.net.NetworkRequest;
-import android.net.dhcp.DhcpServer;
-import android.net.dhcp.DhcpServingParams;
 import android.net.ip.IpServer;
-import android.net.ip.RouterAdvertisementDaemon;
-import android.net.util.InterfaceParams;
-import android.net.util.NetdService;
-import android.os.Handler;
 import android.net.util.SharedLog;
-import android.os.Looper;
+import android.os.Handler;
 
 import com.android.internal.util.StateMachine;
+import com.android.server.connectivity.MockableSystemProperties;
 
 import java.util.ArrayList;
 
@@ -65,4 +59,9 @@
     public NetworkRequest getDefaultNetworkRequest() {
         return null;
     }
+
+    public EntitlementManager getEntitlementManager(Context ctx, SharedLog log,
+            MockableSystemProperties systemProperties) {
+        return new EntitlementManager(ctx, log, systemProperties);
+    }
 }
diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java
index a6ed9f2..8081812 100644
--- a/tests/net/java/com/android/server/connectivity/TetheringTest.java
+++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java
@@ -23,15 +23,15 @@
 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.TETHER_ERROR_UNKNOWN_IFACE;
-import static android.net.ConnectivityManager.TETHERING_WIFI;
 import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
-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.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;
 
@@ -39,19 +39,18 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.notNull;
-import static org.mockito.Matchers.anyBoolean;
 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.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.mock;
 
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -91,9 +90,9 @@
 import android.os.Looper;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
-import android.os.test.TestLooper;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -126,7 +125,6 @@
 public class TetheringTest {
     private static final int IFINDEX_OFFSET = 100;
 
-    private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
     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";
@@ -370,61 +368,6 @@
         mServiceContext.unregisterReceiver(mBroadcastReceiver);
     }
 
-    private void setupForRequiredProvisioning() {
-        // Produce some acceptable looking provision app setting if requested.
-        when(mResources.getStringArray(
-                com.android.internal.R.array.config_mobile_hotspot_provision_app))
-                .thenReturn(PROVISIONING_APP_NAME);
-        // Don't disable tethering provisioning unless requested.
-        when(mSystemProperties.getBoolean(eq(Tethering.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);
-        when(mCarrierConfigManager.getConfig()).thenReturn(mCarrierConfig);
-        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
-    }
-
-    @Test
-    public void canRequireProvisioning() {
-        setupForRequiredProvisioning();
-        sendConfigurationChanged();
-        assertTrue(mTethering.isTetherProvisioningRequired());
-    }
-
-    @Test
-    public void toleratesCarrierConfigManagerMissing() {
-        setupForRequiredProvisioning();
-        when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
-                .thenReturn(null);
-        sendConfigurationChanged();
-        // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
-        // We therefore still require provisioning.
-        assertTrue(mTethering.isTetherProvisioningRequired());
-    }
-
-    @Test
-    public void toleratesCarrierConfigMissing() {
-        setupForRequiredProvisioning();
-        when(mCarrierConfigManager.getConfig()).thenReturn(null);
-        sendConfigurationChanged();
-        // We still have a provisioning app configured, so still require provisioning.
-        assertTrue(mTethering.isTetherProvisioningRequired());
-    }
-
-    @Test
-    public void provisioningNotRequiredWhenAppNotFound() {
-        setupForRequiredProvisioning();
-        when(mResources.getStringArray(
-                com.android.internal.R.array.config_mobile_hotspot_provision_app))
-                .thenReturn(null);
-        assertTrue(!mTethering.isTetherProvisioningRequired());
-        when(mResources.getStringArray(
-                com.android.internal.R.array.config_mobile_hotspot_provision_app))
-                .thenReturn(new String[] {"malformedApp"});
-        assertTrue(!mTethering.isTetherProvisioningRequired());
-    }
-
     private void sendWifiApStateChanged(int state) {
         final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
         intent.putExtra(EXTRA_WIFI_AP_STATE, state);
diff --git a/tests/net/java/com/android/server/connectivity/tethering/EntitlementManagerTest.java b/tests/net/java/com/android/server/connectivity/tethering/EntitlementManagerTest.java
new file mode 100644
index 0000000..0f72229
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/tethering/EntitlementManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.util.SharedLog;
+import android.os.PersistableBundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.telephony.CarrierConfigManager;
+
+import com.android.internal.R;
+import com.android.server.connectivity.MockableSystemProperties;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class EntitlementManagerTest {
+
+    private static final int EVENT_EM_UPDATE = 1;
+    private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+
+    @Mock private CarrierConfigManager mCarrierConfigManager;
+    @Mock private Context mContext;
+    @Mock private ContentResolver mContent;
+    @Mock private MockableSystemProperties mSystemProperties;
+    @Mock private Resources mResources;
+    @Mock private SharedLog mLog;
+
+    // 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 EntitlementManager mEnMgr;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mContext.getContentResolver()).thenReturn(mContent);
+        when(mResources.getStringArray(R.array.config_tether_dhcp_range))
+            .thenReturn(new String[0]);
+        when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+            .thenReturn(new String[0]);
+        when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
+            .thenReturn(new String[0]);
+        when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
+            .thenReturn(new String[0]);
+        when(mResources.getIntArray(R.array.config_tether_upstream_types))
+            .thenReturn(new int[0]);
+        when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+
+        mEnMgr = new EntitlementManager(mContext, mLog, mSystemProperties);
+        mEnMgr.updateConfiguration(new TetheringConfiguration(mContext, mLog));
+    }
+
+    @After
+    public void tearDown() throws Exception {}
+
+    private void setupForRequiredProvisioning() {
+        // Produce some acceptable looking provision app setting if requested.
+        when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+                .thenReturn(PROVISIONING_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);
+        when(mCarrierConfigManager.getConfig()).thenReturn(mCarrierConfig);
+        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+    }
+
+    @Test
+    public void canRequireProvisioning() {
+        setupForRequiredProvisioning();
+        mEnMgr.updateConfiguration(new TetheringConfiguration(mContext, mLog));
+        assertTrue(mEnMgr.isTetherProvisioningRequired());
+    }
+
+    @Test
+    public void toleratesCarrierConfigManagerMissing() {
+        setupForRequiredProvisioning();
+        when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
+            .thenReturn(null);
+        mEnMgr.updateConfiguration(new TetheringConfiguration(mContext, mLog));
+        // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
+        // Therefore provisioning still be required.
+        assertTrue(mEnMgr.isTetherProvisioningRequired());
+    }
+
+    @Test
+    public void toleratesCarrierConfigMissing() {
+        setupForRequiredProvisioning();
+        when(mCarrierConfigManager.getConfig()).thenReturn(null);
+        mEnMgr.updateConfiguration(new TetheringConfiguration(mContext, mLog));
+        // We still have a provisioning app configured, so still require provisioning.
+        assertTrue(mEnMgr.isTetherProvisioningRequired());
+    }
+
+    @Test
+    public void provisioningNotRequiredWhenAppNotFound() {
+        setupForRequiredProvisioning();
+        when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+            .thenReturn(null);
+        mEnMgr.updateConfiguration(new TetheringConfiguration(mContext, mLog));
+        assertFalse(mEnMgr.isTetherProvisioningRequired());
+        when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+            .thenReturn(new String[] {"malformedApp"});
+        mEnMgr.updateConfiguration(new TetheringConfiguration(mContext, mLog));
+        assertFalse(mEnMgr.isTetherProvisioningRequired());
+    }
+
+}