[Tether04] Migrate EntitlementManager into module

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

Change-Id: Ifdfc6cd95377351c37946a146b60896f07ece59d
Merged-In: Ifdfc6cd95377351c37946a146b60896f07ece59d
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index ca69c18..2bfe287 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -21,6 +21,7 @@
         "src/**/*.java",
         ":framework-tethering-shared-srcs",
         ":services-tethering-shared-srcs",
+        ":servicescore-tethering-src",
     ],
     static_libs: [
         "androidx.annotation_annotation",
@@ -67,9 +68,17 @@
 
 // This group will be removed when tethering migration is done.
 filegroup {
-    name: "tethering-services-srcs",
+    name: "tethering-servicescore-srcs",
     srcs: [
+        "src/com/android/server/connectivity/tethering/EntitlementManager.java",
         "src/com/android/server/connectivity/tethering/TetheringConfiguration.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",
diff --git a/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java b/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
new file mode 100644
index 0000000..6b0f1de
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/EntitlementManager.java
@@ -0,0 +1,678 @@
+/*
+ * 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_RUN_PROVISION;
+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.TETHER_ERROR_ENTITLEMENT_UNKONWN;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_PROVISION_FAILED;
+
+import static com.android.internal.R.string.config_wifi_tether_enable;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.net.util.SharedLog;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.util.ArraySet;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.StateMachine;
+import com.android.server.connectivity.MockableSystemProperties;
+
+import java.io.PrintWriter;
+
+/**
+ * Re-check tethering provisioning for enabled downstream tether types.
+ * Reference ConnectivityManager.TETHERING_{@code *} for each tether type.
+ *
+ * All methods of this class must be accessed from the thread of tethering
+ * state machine.
+ * @hide
+ */
+public class EntitlementManager {
+    private static final String TAG = EntitlementManager.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    @VisibleForTesting
+    protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning";
+    private static final String ACTION_PROVISIONING_ALARM =
+            "com.android.server.connectivity.tethering.PROVISIONING_RECHECK_ALARM";
+    private static final String EXTRA_SUBID = "subId";
+
+    // {@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));
+    private static final int MS_PER_HOUR = 60 * 60 * 1000;
+    private static final int EVENT_START_PROVISIONING = 0;
+    private static final int EVENT_STOP_PROVISIONING = 1;
+    private static final int EVENT_UPSTREAM_CHANGED = 2;
+    private static final int EVENT_MAYBE_RUN_PROVISIONING = 3;
+    private static final int EVENT_GET_ENTITLEMENT_VALUE = 4;
+
+    // The ArraySet contains enabled downstream types, ex:
+    // {@link ConnectivityManager.TETHERING_WIFI}
+    // {@link ConnectivityManager.TETHERING_USB}
+    // {@link ConnectivityManager.TETHERING_BLUETOOTH}
+    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;
+    private final StateMachine mTetherMasterSM;
+    // Key: ConnectivityManager.TETHERING_*(downstream).
+    // Value: ConnectivityManager.TETHER_ERROR_{NO_ERROR or PROVISION_FAILED}(provisioning result).
+    private final SparseIntArray mCellularPermitted;
+    private PendingIntent mProvisioningRecheckAlarm;
+    private boolean mCellularUpstreamPermitted = true;
+    private boolean mUsingCellularAsUpstream = false;
+    private boolean mNeedReRunProvisioningUi = false;
+    private OnUiEntitlementFailedListener mListener;
+    private TetheringConfigurationFetcher mFetcher;
+
+    public EntitlementManager(Context ctx, StateMachine tetherMasterSM, SharedLog log,
+            int permissionChangeMessageCode, MockableSystemProperties systemProperties) {
+        mContext = ctx;
+        mLog = log.forSubComponent(TAG);
+        mCurrentTethers = new ArraySet<Integer>();
+        mCellularPermitted = new SparseIntArray();
+        mSystemProperties = systemProperties;
+        mEntitlementCacheValue = new SparseIntArray();
+        mTetherMasterSM = tetherMasterSM;
+        mPermissionChangeMessageCode = permissionChangeMessageCode;
+        final Handler masterHandler = tetherMasterSM.getHandler();
+        // Create entitlement's own handler which is associated with TetherMaster thread
+        // let all entitlement processes run in the same thread.
+        mHandler = new EntitlementHandler(masterHandler.getLooper());
+        mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
+                null, mHandler);
+    }
+
+    public void setOnUiEntitlementFailedListener(final OnUiEntitlementFailedListener listener) {
+        mListener = listener;
+    }
+
+    /** Callback fired when UI entitlement failed. */
+    public interface OnUiEntitlementFailedListener {
+        /**
+         * Ui entitlement check fails in |downstream|.
+         *
+         * @param downstream tethering type from ConnectivityManager.TETHERING_{@code *}.
+         */
+        void onUiEntitlementFailed(int downstream);
+    }
+
+    public void setTetheringConfigurationFetcher(final TetheringConfigurationFetcher fetcher) {
+        mFetcher = fetcher;
+    }
+
+    /** Interface to fetch TetheringConfiguration. */
+    public interface TetheringConfigurationFetcher {
+        /**
+         * Fetch current tethering configuration. This will be called to ensure whether entitlement
+         * check is needed.
+         * @return TetheringConfiguration instance.
+         */
+        TetheringConfiguration fetchTetheringConfiguration();
+    }
+
+    /**
+     * Check if cellular upstream is permitted.
+     */
+    public boolean isCellularUpstreamPermitted() {
+        // If provisioning is required and EntitlementManager don't know any downstream,
+        // cellular upstream should not be allowed.
+        final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+        if (mCurrentTethers.size() == 0 && isTetherProvisioningRequired(config)) {
+            return false;
+        }
+        return mCellularUpstreamPermitted;
+    }
+
+    /**
+     * This is called when tethering starts.
+     * Launch provisioning app if upstream is cellular.
+     *
+     * @param downstreamType tethering type from ConnectivityManager.TETHERING_{@code *}
+     * @param showProvisioningUi a boolean indicating whether to show the
+     *        provisioning app UI if there is one.
+     */
+    public void startProvisioningIfNeeded(int downstreamType, boolean showProvisioningUi) {
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_START_PROVISIONING,
+                downstreamType, encodeBool(showProvisioningUi)));
+    }
+
+    private void handleStartProvisioningIfNeeded(int type, boolean showProvisioningUi) {
+        if (!isValidDownstreamType(type)) return;
+
+        if (!mCurrentTethers.contains(type)) mCurrentTethers.add(type);
+
+        final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+        if (isTetherProvisioningRequired(config)) {
+            // If provisioning is required and the result is not available yet,
+            // cellular upstream should not be allowed.
+            if (mCellularPermitted.size() == 0) {
+                mCellularUpstreamPermitted = false;
+            }
+            // If upstream is not cellular, provisioning app would not be launched
+            // till upstream change to cellular.
+            if (mUsingCellularAsUpstream) {
+                if (showProvisioningUi) {
+                    runUiTetherProvisioning(type, config.subId);
+                } else {
+                    runSilentTetherProvisioning(type, config.subId);
+                }
+                mNeedReRunProvisioningUi = false;
+            } else {
+                mNeedReRunProvisioningUi |= showProvisioningUi;
+            }
+        } else {
+            mCellularUpstreamPermitted = true;
+        }
+    }
+
+    /**
+     * Tell EntitlementManager that a given type of tethering has been disabled
+     *
+     * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
+     */
+    public void stopProvisioningIfNeeded(int type) {
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_STOP_PROVISIONING, type, 0));
+    }
+
+    private void handleStopProvisioningIfNeeded(int type) {
+        if (!isValidDownstreamType(type)) return;
+
+        mCurrentTethers.remove(type);
+        // There are lurking bugs where the notion of "provisioning required" or
+        // "tethering supported" may change without without tethering being notified properly.
+        // Remove the mapping all the time no matter provisioning is required or not.
+        removeDownstreamMapping(type);
+    }
+
+    /**
+     * Notify EntitlementManager if upstream is cellular or not.
+     *
+     * @param isCellular whether tethering upstream is cellular.
+     */
+    public void notifyUpstream(boolean isCellular) {
+        mHandler.sendMessage(mHandler.obtainMessage(
+                EVENT_UPSTREAM_CHANGED, encodeBool(isCellular), 0));
+    }
+
+    private void handleNotifyUpstream(boolean isCellular) {
+        if (DBG) {
+            mLog.i("notifyUpstream: " + isCellular
+                    + ", mCellularUpstreamPermitted: " + mCellularUpstreamPermitted
+                    + ", mNeedReRunProvisioningUi: " + mNeedReRunProvisioningUi);
+        }
+        mUsingCellularAsUpstream = isCellular;
+
+        if (mUsingCellularAsUpstream) {
+            final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+            handleMaybeRunProvisioning(config);
+        }
+    }
+
+    /** Run provisioning if needed */
+    public void maybeRunProvisioning() {
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_MAYBE_RUN_PROVISIONING));
+    }
+
+    private void handleMaybeRunProvisioning(final TetheringConfiguration config) {
+        if (mCurrentTethers.size() == 0 || !isTetherProvisioningRequired(config)) {
+            return;
+        }
+
+        // Whenever any entitlement value changes, all downstreams will re-evaluate whether they
+        // are allowed. Therefore even if the silent check here ends in a failure and the UI later
+        // yields success, then the downstream that got a failure will re-evaluate as a result of
+        // the change and get the new correct value.
+        for (Integer downstream : mCurrentTethers) {
+            if (mCellularPermitted.indexOfKey(downstream) < 0) {
+                if (mNeedReRunProvisioningUi) {
+                    mNeedReRunProvisioningUi = false;
+                    runUiTetherProvisioning(downstream, config.subId);
+                } else {
+                    runSilentTetherProvisioning(downstream, config.subId);
+                }
+            }
+        }
+    }
+
+    /**
+     * Check if the device requires a provisioning check in order to enable tethering.
+     *
+     * @param config an object that encapsulates the various tethering configuration elements.
+     * @return a boolean - {@code true} indicating tether provisioning is required by the carrier.
+     */
+    @VisibleForTesting
+    protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
+        if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
+                || config.provisioningApp.length == 0) {
+            return false;
+        }
+        if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+            return false;
+        }
+        return (config.provisioningApp.length == 2);
+    }
+
+    /**
+     * Re-check tethering provisioning for all enabled tether types.
+     * Reference ConnectivityManager.TETHERING_{@code *} for each tether type.
+     *
+     * @param config an object that encapsulates the various tethering configuration elements.
+     * Note: this method is only called from TetherMaster on the handler thread.
+     * If there are new callers from different threads, the logic should move to
+     * masterHandler to avoid race conditions.
+     */
+    public void reevaluateSimCardProvisioning(final TetheringConfiguration config) {
+        if (DBG) mLog.i("reevaluateSimCardProvisioning");
+
+        if (!mHandler.getLooper().isCurrentThread()) {
+            // Except for test, this log should not appear in normal flow.
+            mLog.log("reevaluateSimCardProvisioning() don't run in TetherMaster thread");
+        }
+        mEntitlementCacheValue.clear();
+        mCellularPermitted.clear();
+
+        // TODO: refine provisioning check to isTetherProvisioningRequired() ??
+        if (!config.hasMobileHotspotProvisionApp()
+                || carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+            evaluateCellularPermission(config);
+            return;
+        }
+
+        if (mUsingCellularAsUpstream) {
+            handleMaybeRunProvisioning(config);
+        }
+    }
+
+    /**
+     * Get carrier configuration bundle.
+     * @param config an object that encapsulates the various tethering configuration elements.
+     * */
+    public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
+        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        if (configManager == null) return null;
+
+        final PersistableBundle carrierConfig = configManager.getConfigForSubId(config.subId);
+
+        if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
+            return carrierConfig;
+        }
+
+        return null;
+    }
+
+    // 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(
+            final TetheringConfiguration config) {
+        // Check carrier config for entitlement checks
+        final PersistableBundle carrierConfig = getCarrierConfig(config);
+        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;
+    }
+
+    /**
+     * Run no UI tethering provisioning check.
+     * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
+     * @param subId default data subscription ID.
+     */
+    @VisibleForTesting
+    protected void runSilentTetherProvisioning(int type, int subId) {
+        if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
+        // For silent provisioning, settings would stop tethering when entitlement fail.
+        ResultReceiver receiver = buildProxyReceiver(type, false/* notifyFail */, null);
+
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+        intent.putExtra(EXTRA_RUN_PROVISION, true);
+        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+        intent.putExtra(EXTRA_SUBID, subId);
+        intent.setComponent(TETHER_SERVICE);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.startServiceAsUser(intent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void runUiTetherProvisioning(int type, int subId) {
+        ResultReceiver receiver = buildProxyReceiver(type, true/* notifyFail */, null);
+        runUiTetherProvisioning(type, subId, receiver);
+    }
+
+    /**
+     * Run the UI-enabled tethering provisioning check.
+     * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
+     * @param subId default data subscription ID.
+     * @param receiver to receive entitlement check result.
+     */
+    @VisibleForTesting
+    protected void runUiTetherProvisioning(int type, int subId, ResultReceiver receiver) {
+        if (DBG) mLog.i("runUiTetherProvisioning: " + type);
+
+        Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING);
+        intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
+        intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
+        intent.putExtra(EXTRA_SUBID, subId);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    // Not needed to check if this don't run on the handler thread because it's private.
+    private void scheduleProvisioningRechecks(final TetheringConfiguration config) {
+        if (mProvisioningRecheckAlarm == null) {
+            final int period = config.provisioningCheckPeriod;
+            if (period <= 0) return;
+
+            Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
+            mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+            AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
+                    Context.ALARM_SERVICE);
+            long periodMs = period * MS_PER_HOUR;
+            long firstAlarmTime = SystemClock.elapsedRealtime() + periodMs;
+            alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, firstAlarmTime, periodMs,
+                    mProvisioningRecheckAlarm);
+        }
+    }
+
+    private void cancelTetherProvisioningRechecks() {
+        if (mProvisioningRecheckAlarm != null) {
+            AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
+                    Context.ALARM_SERVICE);
+            alarmManager.cancel(mProvisioningRecheckAlarm);
+            mProvisioningRecheckAlarm = null;
+        }
+    }
+
+    private void evaluateCellularPermission(final TetheringConfiguration config) {
+        final boolean oldPermitted = mCellularUpstreamPermitted;
+        mCellularUpstreamPermitted = (!isTetherProvisioningRequired(config)
+                || mCellularPermitted.indexOfValue(TETHER_ERROR_NO_ERROR) > -1);
+
+        if (DBG) {
+            mLog.i("Cellular permission change from " + oldPermitted
+                    + " to " + mCellularUpstreamPermitted);
+        }
+
+        if (mCellularUpstreamPermitted != oldPermitted) {
+            mLog.log("Cellular permission change: " + mCellularUpstreamPermitted);
+            mTetherMasterSM.sendMessage(mPermissionChangeMessageCode);
+        }
+        // Only schedule periodic re-check when tether is provisioned
+        // and the result is ok.
+        if (mCellularUpstreamPermitted && mCellularPermitted.size() > 0) {
+            scheduleProvisioningRechecks(config);
+        } else {
+            cancelTetherProvisioningRechecks();
+        }
+    }
+
+    /**
+     * Add the mapping between provisioning result and tethering type.
+     * Notify UpstreamNetworkMonitor if Cellular permission changes.
+     *
+     * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
+     * @param resultCode Provisioning result
+     */
+    protected void addDownstreamMapping(int type, int resultCode) {
+        mLog.i("addDownstreamMapping: " + type + ", result: " + resultCode
+                + " ,TetherTypeRequested: " + mCurrentTethers.contains(type));
+        if (!mCurrentTethers.contains(type)) return;
+
+        mCellularPermitted.put(type, resultCode);
+        final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+        evaluateCellularPermission(config);
+    }
+
+    /**
+     * Remove the mapping for input tethering type.
+     * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
+     */
+    protected void removeDownstreamMapping(int type) {
+        mLog.i("removeDownstreamMapping: " + type);
+        mCellularPermitted.delete(type);
+        final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+        evaluateCellularPermission(config);
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_PROVISIONING_ALARM.equals(intent.getAction())) {
+                mLog.log("Received provisioning alarm");
+                final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+                reevaluateSimCardProvisioning(config);
+            }
+        }
+    };
+
+    private class EntitlementHandler extends Handler {
+        EntitlementHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_START_PROVISIONING:
+                    handleStartProvisioningIfNeeded(msg.arg1, toBool(msg.arg2));
+                    break;
+                case EVENT_STOP_PROVISIONING:
+                    handleStopProvisioningIfNeeded(msg.arg1);
+                    break;
+                case EVENT_UPSTREAM_CHANGED:
+                    handleNotifyUpstream(toBool(msg.arg1));
+                    break;
+                case EVENT_MAYBE_RUN_PROVISIONING:
+                    final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+                    handleMaybeRunProvisioning(config);
+                    break;
+                case EVENT_GET_ENTITLEMENT_VALUE:
+                    handleGetLatestTetheringEntitlementValue(msg.arg1, (ResultReceiver) msg.obj,
+                            toBool(msg.arg2));
+                    break;
+                default:
+                    mLog.log("Unknown event: " + msg.what);
+                    break;
+            }
+        }
+    }
+
+    private static boolean toBool(int encodedBoolean) {
+        return encodedBoolean != 0;
+    }
+
+    private static int encodeBool(boolean b) {
+        return b ? 1 : 0;
+    }
+
+    private static boolean isValidDownstreamType(int type) {
+        switch (type) {
+            case TETHERING_BLUETOOTH:
+            case TETHERING_USB:
+            case TETHERING_WIFI:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Dump the infromation of EntitlementManager.
+     * @param pw {@link PrintWriter} is used to print formatted
+     */
+    public void dump(PrintWriter pw) {
+        pw.print("mCellularUpstreamPermitted: ");
+        pw.println(mCellularUpstreamPermitted);
+        for (Integer type : mCurrentTethers) {
+            pw.print("Type: ");
+            pw.print(typeString(type));
+            if (mCellularPermitted.indexOfKey(type) > -1) {
+                pw.print(", Value: ");
+                pw.println(errorString(mCellularPermitted.get(type)));
+            } else {
+                pw.println(", Value: empty");
+            }
+        }
+    }
+
+    private static String typeString(int type) {
+        switch (type) {
+            case TETHERING_BLUETOOTH: return "TETHERING_BLUETOOTH";
+            case TETHERING_INVALID: return "TETHERING_INVALID";
+            case TETHERING_USB: return "TETHERING_USB";
+            case TETHERING_WIFI: return "TETHERING_WIFI";
+            default:
+                return String.format("TETHERING UNKNOWN TYPE (%d)", type);
+        }
+    }
+
+    private static String errorString(int value) {
+        switch (value) {
+            case TETHER_ERROR_ENTITLEMENT_UNKONWN: return "TETHER_ERROR_ENTITLEMENT_UNKONWN";
+            case TETHER_ERROR_NO_ERROR: return "TETHER_ERROR_NO_ERROR";
+            case TETHER_ERROR_PROVISION_FAILED: return "TETHER_ERROR_PROVISION_FAILED";
+            default:
+                return String.format("UNKNOWN ERROR (%d)", value);
+        }
+    }
+
+    private ResultReceiver buildProxyReceiver(int type, boolean notifyFail,
+            final ResultReceiver receiver) {
+        ResultReceiver rr = new ResultReceiver(mHandler) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                int updatedCacheValue = updateEntitlementCacheValue(type, resultCode);
+                addDownstreamMapping(type, updatedCacheValue);
+                if (updatedCacheValue == TETHER_ERROR_PROVISION_FAILED && notifyFail) {
+                    mListener.onUiEntitlementFailed(type);
+                }
+                if (receiver != null) receiver.send(updatedCacheValue, null);
+            }
+        };
+
+        return writeToParcel(rr);
+    }
+
+    // Instances of ResultReceiver need to be public classes for remote processes to be able
+    // to load them (otherwise, ClassNotFoundException). For private classes, this method
+    // performs a trick : round-trip parceling any instance of ResultReceiver will return a
+    // vanilla instance of ResultReceiver sharing the binder token with the original receiver.
+    // The binder token has a reference to the original instance of the private class and will
+    // still call its methods, and can be sent over. However it cannot be used for anything
+    // else than sending over a Binder call.
+    // While round-trip parceling is not great, there is currently no other way of generating
+    // a vanilla instance of ResultReceiver because all its fields are private.
+    private ResultReceiver writeToParcel(final ResultReceiver receiver) {
+        Parcel parcel = Parcel.obtain();
+        receiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        return receiverForSending;
+    }
+
+    /**
+     * Update the last entitlement value to internal cache
+     *
+     * @param type tethering type from ConnectivityManager.TETHERING_{@code *}
+     * @param resultCode last entitlement value
+     * @return the last updated entitlement value
+     */
+    private int updateEntitlementCacheValue(int type, int resultCode) {
+        if (DBG) {
+            mLog.i("updateEntitlementCacheValue: " + type + ", result: " + resultCode);
+        }
+        if (resultCode == TETHER_ERROR_NO_ERROR) {
+            mEntitlementCacheValue.put(type, resultCode);
+            return resultCode;
+        } else {
+            mEntitlementCacheValue.put(type, TETHER_ERROR_PROVISION_FAILED);
+            return TETHER_ERROR_PROVISION_FAILED;
+        }
+    }
+
+    /** Get the last value of the tethering entitlement check. */
+    public void getLatestTetheringEntitlementResult(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) {
+        final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
+        if (!isTetherProvisioningRequired(config)) {
+            receiver.send(TETHER_ERROR_NO_ERROR, null);
+            return;
+        }
+
+        final int cacheValue = mEntitlementCacheValue.get(
+                downstream, TETHER_ERROR_ENTITLEMENT_UNKONWN);
+        if (cacheValue == TETHER_ERROR_NO_ERROR || !showEntitlementUi) {
+            receiver.send(cacheValue, null);
+        } else {
+            ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver);
+            runUiTetherProvisioning(downstream, config.subId, proxy);
+        }
+    }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index da62107..5564bd6 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -17,7 +17,10 @@
 android_test {
     name: "TetheringTests",
     certificate: "platform",
-    srcs: ["src/**/*.java"],
+    srcs: [
+        ":servicescore-tethering-src",
+        "src/**/*.java",
+    ],
     test_suites: ["device-tests"],
     static_libs: [
         "androidx.test.rules",
@@ -42,6 +45,7 @@
 filegroup {
     name: "tethering-tests-src",
     srcs: [
+        "src/com/android/server/connectivity/tethering/EntitlementManagerTest.java",
         "src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java",
         "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
         "src/android/net/ip/IpServerTest.java",
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
new file mode 100644
index 0000000..5217e26
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/EntitlementManagerTest.java
@@ -0,0 +1,508 @@
+/*
+ * 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.TETHERING_BLUETOOTH;
+import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHER_ERROR_ENTITLEMENT_UNKONWN;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_PROVISION_FAILED;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+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.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.times;
+import static org.mockito.Mockito.verify;
+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.Bundle;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.internal.util.State;
+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;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@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"};
+    private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
+
+    @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;
+
+    // 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 Context mMockContext;
+    private MockContentResolver mContentResolver;
+
+    private TestStateMachine mSM;
+    private WrappedEntitlementManager mEnMgr;
+    private TetheringConfiguration mConfig;
+
+    private class MockContext extends BroadcastInterceptingContext {
+        MockContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Resources getResources() {
+            return mResources;
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+    }
+
+    public class WrappedEntitlementManager extends EntitlementManager {
+        public int fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKONWN;
+        public int uiProvisionCount = 0;
+        public int silentProvisionCount = 0;
+
+        public WrappedEntitlementManager(Context ctx, StateMachine target,
+                SharedLog log, int what, MockableSystemProperties systemProperties) {
+            super(ctx, target, log, what, systemProperties);
+        }
+
+        public void reset() {
+            fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKONWN;
+            uiProvisionCount = 0;
+            silentProvisionCount = 0;
+        }
+
+        @Override
+        protected void runUiTetherProvisioning(int type, int subId, ResultReceiver receiver) {
+            uiProvisionCount++;
+            receiver.send(fakeEntitlementResult, null);
+        }
+
+        @Override
+        protected void runSilentTetherProvisioning(int type, int subId) {
+            silentProvisionCount++;
+            addDownstreamMapping(type, fakeEntitlementResult);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        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);
+
+        mContentResolver = new MockContentResolver();
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        mMockContext = new MockContext(mContext);
+        mSM = new TestStateMachine();
+        mEnMgr = new WrappedEntitlementManager(mMockContext, mSM, mLog, EVENT_EM_UPDATE,
+                mSystemProperties);
+        mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
+        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        mEnMgr.setTetheringConfigurationFetcher(() -> {
+            return mConfig;
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mSM != null) {
+            mSM.quit();
+            mSM = null;
+        }
+    }
+
+    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);
+        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);
+        when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
+        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+    }
+
+    @Test
+    public void canRequireProvisioning() {
+        setupForRequiredProvisioning();
+        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+    }
+
+    @Test
+    public void toleratesCarrierConfigManagerMissing() {
+        setupForRequiredProvisioning();
+        when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
+            .thenReturn(null);
+        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
+        // Therefore provisioning still be required.
+        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+    }
+
+    @Test
+    public void toleratesCarrierConfigMissing() {
+        setupForRequiredProvisioning();
+        when(mCarrierConfigManager.getConfig()).thenReturn(null);
+        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        // We still have a provisioning app configured, so still require provisioning.
+        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+    }
+
+    @Test
+    public void toleratesCarrierConfigNotLoaded() {
+        setupForRequiredProvisioning();
+        mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
+        // We still have a provisioning app configured, so still require provisioning.
+        assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
+    }
+
+    @Test
+    public void provisioningNotRequiredWhenAppNotFound() {
+        setupForRequiredProvisioning();
+        when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+            .thenReturn(null);
+        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
+        when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
+            .thenReturn(new String[] {"malformedApp"});
+        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
+    }
+
+    @Test
+    public void testGetLastEntitlementCacheValue() throws Exception {
+        final CountDownLatch mCallbacklatch = new CountDownLatch(1);
+        // 1. Entitlement check is not required.
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        ResultReceiver receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+
+        setupForRequiredProvisioning();
+        // 2. No cache value and don't need to run entitlement check.
+        receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_ENTITLEMENT_UNKONWN, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+        // 3. No cache value and ui entitlement check is needed.
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_PROVISION_FAILED, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(1, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+        // 4. Cache value is TETHER_ERROR_PROVISION_FAILED and don't need to run entitlement check.
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_PROVISION_FAILED, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+        // 5. Cache value is TETHER_ERROR_PROVISION_FAILED and ui entitlement check is needed.
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(1, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+        // 6. Cache value is TETHER_ERROR_NO_ERROR.
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+        // 7. Test get value for other downstream type.
+        receiver = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                assertEquals(TETHER_ERROR_ENTITLEMENT_UNKONWN, resultCode);
+                mCallbacklatch.countDown();
+            }
+        };
+        mEnMgr.getLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
+        mLooper.dispatchAll();
+        callbackTimeoutHelper(mCallbacklatch);
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        mEnMgr.reset();
+    }
+
+    void callbackTimeoutHelper(final CountDownLatch latch) throws Exception {
+        if (!latch.await(1, TimeUnit.SECONDS)) {
+            fail("Timout, fail to receive callback");
+        }
+    }
+
+    @Test
+    public void verifyPermissionResult() {
+        setupForRequiredProvisioning();
+        mEnMgr.notifyUpstream(true);
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        mLooper.dispatchAll();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+        mLooper.dispatchAll();
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        mLooper.dispatchAll();
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+    }
+
+    @Test
+    public void verifyPermissionIfAllNotApproved() {
+        setupForRequiredProvisioning();
+        mEnMgr.notifyUpstream(true);
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        mLooper.dispatchAll();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+        mLooper.dispatchAll();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
+        mLooper.dispatchAll();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+    }
+
+    @Test
+    public void verifyPermissionIfAnyApproved() {
+        setupForRequiredProvisioning();
+        mEnMgr.notifyUpstream(true);
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        mLooper.dispatchAll();
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+        mLooper.dispatchAll();
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+        mLooper.dispatchAll();
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+        mLooper.dispatchAll();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+    }
+
+    @Test
+    public void verifyPermissionWhenProvisioningNotStarted() {
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+        setupForRequiredProvisioning();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+    }
+
+    @Test
+    public void testRunTetherProvisioning() {
+        setupForRequiredProvisioning();
+        // 1. start ui provisioning, upstream is mobile
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+        mLooper.dispatchAll();
+        assertEquals(1, mEnMgr.uiProvisionCount);
+        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.reset();
+        // 2. start no-ui provisioning
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false);
+        mLooper.dispatchAll();
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        assertEquals(1, mEnMgr.silentProvisionCount);
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.reset();
+        // 3. tear down mobile, then start ui provisioning
+        mEnMgr.notifyUpstream(false);
+        mLooper.dispatchAll();
+        mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
+        mLooper.dispatchAll();
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        assertEquals(0, mEnMgr.silentProvisionCount);
+        mEnMgr.reset();
+        // 4. switch upstream back to mobile
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        assertEquals(1, mEnMgr.uiProvisionCount);
+        assertEquals(0, mEnMgr.silentProvisionCount);
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+        mEnMgr.reset();
+        // 5. tear down mobile, then switch SIM
+        mEnMgr.notifyUpstream(false);
+        mLooper.dispatchAll();
+        mEnMgr.reevaluateSimCardProvisioning(mConfig);
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        assertEquals(0, mEnMgr.silentProvisionCount);
+        mEnMgr.reset();
+        // 6. switch upstream back to mobile again
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        assertEquals(0, mEnMgr.uiProvisionCount);
+        assertEquals(3, mEnMgr.silentProvisionCount);
+        mEnMgr.reset();
+    }
+
+    @Test
+    public void testCallStopTetheringWhenUiProvisioningFail() {
+        setupForRequiredProvisioning();
+        verify(mEntitlementFailedListener, times(0)).onUiEntitlementFailed(TETHERING_WIFI);
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISION_FAILED;
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
+        mLooper.dispatchAll();
+        assertEquals(1, mEnMgr.uiProvisionCount);
+        verify(mEntitlementFailedListener, times(1)).onUiEntitlementFailed(TETHERING_WIFI);
+    }
+
+    public class TestStateMachine extends StateMachine {
+        public final ArrayList<Message> messages = new ArrayList<>();
+        private final State
+                mLoggingState = new EntitlementManagerTest.TestStateMachine.LoggingState();
+
+        class LoggingState extends State {
+            @Override public void enter() {
+                messages.clear();
+            }
+
+            @Override public void exit() {
+                messages.clear();
+            }
+
+            @Override public boolean processMessage(Message msg) {
+                messages.add(msg);
+                return false;
+            }
+        }
+
+        public TestStateMachine() {
+            super("EntitlementManagerTest.TestStateMachine", mLooper.getLooper());
+            addState(mLoggingState);
+            setInitialState(mLoggingState);
+            super.start();
+        }
+    }
+}