Merge "Allow to exempt from entitlement check"
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 23b8be1..3c6e8d8 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -80,6 +80,7 @@
     // {@link TetheringManager.TETHERING_USB}
     // {@link TetheringManager.TETHERING_BLUETOOTH}
     private final BitSet mCurrentDownstreams;
+    private final BitSet mExemptedDownstreams;
     private final Context mContext;
     private final SharedLog mLog;
     private final SparseIntArray mEntitlementCacheValue;
@@ -100,6 +101,7 @@
         mContext = ctx;
         mLog = log.forSubComponent(TAG);
         mCurrentDownstreams = new BitSet();
+        mExemptedDownstreams = new BitSet();
         mCurrentEntitlementResults = new SparseIntArray();
         mEntitlementCacheValue = new SparseIntArray();
         mPermissionChangeCallback = callback;
@@ -150,14 +152,30 @@
     private boolean isCellularUpstreamPermitted(final TetheringConfiguration config) {
         if (!isTetherProvisioningRequired(config)) return true;
 
-        // If provisioning is required and EntitlementManager doesn't know any downstreams,
-        // cellular upstream should not be allowed.
-        if (mCurrentDownstreams.isEmpty()) return false;
+        // If provisioning is required and EntitlementManager doesn't know any downstreams, cellular
+        // upstream should not be enabled. Enable cellular upstream for exempted downstreams only
+        // when there is no non-exempted downstream.
+        if (mCurrentDownstreams.isEmpty()) return !mExemptedDownstreams.isEmpty();
 
         return mCurrentEntitlementResults.indexOfValue(TETHER_ERROR_NO_ERROR) > -1;
     }
 
     /**
+     * Set exempted downstream type. If there is only exempted downstream type active,
+     * corresponding entitlement check will not be run and cellular upstream will be permitted
+     * by default. If a privileged app enables tethering without a provisioning check, and then
+     * another app enables tethering of the same type but does not disable the provisioning check,
+     * then the downstream immediately loses exempt status and a provisioning check is run.
+     * If any non-exempted downstream type is active, the cellular upstream will be gated by the
+     * result of entitlement check from non-exempted downstreams. If entitlement check is still
+     * in progress on non-exempt downstreams, ceullar upstream would default be disabled. When any
+     * non-exempted downstream gets positive entitlement result, ceullar upstream will be enabled.
+     */
+    public void setExemptedDownstreamType(final int type) {
+        mExemptedDownstreams.set(type, true);
+    }
+
+    /**
      * This is called when tethering starts.
      * Launch provisioning app if upstream is cellular.
      *
@@ -170,6 +188,8 @@
 
         mCurrentDownstreams.set(downstreamType, true);
 
+        mExemptedDownstreams.set(downstreamType, false);
+
         final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
         if (!isTetherProvisioningRequired(config)) return;
 
@@ -200,6 +220,7 @@
         // "tethering supported" may change without without tethering being notified properly.
         // Remove the mapping all the time no matter provisioning is required or not.
         removeDownstreamMapping(downstreamType);
+        mExemptedDownstreams.set(downstreamType, false);
     }
 
     /**
@@ -505,6 +526,13 @@
         if (!mWaiting.block(DUMP_TIMEOUT)) {
             pw.println("... dump timed out after " + DUMP_TIMEOUT + "ms");
         }
+        pw.print("Exempted: [");
+        for (int type = mExemptedDownstreams.nextSetBit(0); type >= 0;
+                type = mExemptedDownstreams.nextSetBit(type + 1)) {
+            pw.print(typeString(type));
+            pw.print(", ");
+        }
+        pw.println("]");
     }
 
     private static String typeString(int type) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 55e9c90..d1440a7 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -516,8 +516,12 @@
             }
             mActiveTetheringRequests.put(request.tetheringType, request);
 
-            mEntitlementMgr.startProvisioningIfNeeded(request.tetheringType,
-                    request.showProvisioningUi);
+            if (request.exemptFromEntitlementCheck) {
+                mEntitlementMgr.setExemptedDownstreamType(request.tetheringType);
+            } else {
+                mEntitlementMgr.startProvisioningIfNeeded(request.tetheringType,
+                        request.showProvisioningUi);
+            }
             enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
         });
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index c82e2be..af349f2 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -16,6 +16,8 @@
 
 package com.android.networkstack.tethering;
 
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.TETHER_PRIVILEGED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
@@ -151,7 +153,12 @@
         @Override
         public void startTethering(TetheringRequestParcel request, String callerPkg,
                 String callingAttributionTag, IIntResultListener listener) {
-            if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
+            if (checkAndNotifyCommonError(callerPkg,
+                    callingAttributionTag,
+                    request.exemptFromEntitlementCheck /* onlyAllowPrivileged */,
+                    listener)) {
+                return;
+            }
 
             mTethering.startTethering(request, listener);
         }
@@ -179,7 +186,7 @@
         public void registerTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
             try {
-                if (!mService.hasTetherAccessPermission()) {
+                if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
                     return;
                 }
@@ -191,7 +198,7 @@
         public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
                 String callerPkg) {
             try {
-                if (!mService.hasTetherAccessPermission()) {
+                if (!hasTetherAccessPermission()) {
                     callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
                     return;
                 }
@@ -226,10 +233,18 @@
             mTethering.dump(fd, writer, args);
         }
 
-        private boolean checkAndNotifyCommonError(String callerPkg, String callingAttributionTag,
-                IIntResultListener listener) {
+        private boolean checkAndNotifyCommonError(final String callerPkg,
+                final String callingAttributionTag, final IIntResultListener listener) {
+            return checkAndNotifyCommonError(callerPkg, callingAttributionTag,
+                    false /* onlyAllowPrivileged */, listener);
+        }
+
+        private boolean checkAndNotifyCommonError(final String callerPkg,
+                final String callingAttributionTag, final boolean onlyAllowPrivileged,
+                final IIntResultListener listener) {
             try {
-                if (!mService.hasTetherChangePermission(callerPkg, callingAttributionTag)) {
+                if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
+                        onlyAllowPrivileged)) {
                     listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
                     return true;
                 }
@@ -244,9 +259,10 @@
             return false;
         }
 
-        private boolean checkAndNotifyCommonError(String callerPkg, String callingAttributionTag,
-                ResultReceiver receiver) {
-            if (!mService.hasTetherChangePermission(callerPkg, callingAttributionTag)) {
+        private boolean checkAndNotifyCommonError(final String callerPkg,
+                final String callingAttributionTag, final ResultReceiver receiver) {
+            if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
+                    false /* onlyAllowPrivileged */)) {
                 receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
                 return true;
             }
@@ -258,6 +274,30 @@
             return false;
         }
 
+        private boolean hasTetherPrivilegedPermission() {
+            return mService.checkCallingOrSelfPermission(TETHER_PRIVILEGED) == PERMISSION_GRANTED;
+        }
+
+        private boolean hasTetherChangePermission(final String callerPkg,
+                final String callingAttributionTag, final boolean onlyAllowPrivileged) {
+            if (hasTetherPrivilegedPermission()) return true;
+
+            if (onlyAllowPrivileged || mTethering.isTetherProvisioningRequired()) return false;
+
+            int uid = Binder.getCallingUid();
+            // If callerPkg's uid is not same as Binder.getCallingUid(),
+            // checkAndNoteWriteSettingsOperation will return false and the operation will be
+            // denied.
+            return TetheringService.checkAndNoteWriteSettingsOperation(mService, uid, callerPkg,
+                    callingAttributionTag, false /* throwException */);
+        }
+
+        private boolean hasTetherAccessPermission() {
+            if (hasTetherPrivilegedPermission()) return true;
+
+            return mService.checkCallingOrSelfPermission(
+                    ACCESS_NETWORK_STATE) == PERMISSION_GRANTED;
+        }
     }
 
     // if ro.tether.denied = true we default to no tethering
@@ -274,26 +314,6 @@
         return tetherEnabledInSettings && mTethering.hasTetherableConfiguration();
     }
 
-    private boolean hasTetherChangePermission(String callerPkg, String callingAttributionTag) {
-        if (checkCallingOrSelfPermission(
-                android.Manifest.permission.TETHER_PRIVILEGED) == PERMISSION_GRANTED) {
-            return true;
-        }
-
-        if (mTethering.isTetherProvisioningRequired()) return false;
-
-
-        int uid = Binder.getCallingUid();
-        // If callerPkg's uid is not same as Binder.getCallingUid(),
-        // checkAndNoteWriteSettingsOperation will return false and the operation will be denied.
-        if (checkAndNoteWriteSettingsOperation(mContext, uid, callerPkg,
-                callingAttributionTag, false /* throwException */)) {
-            return true;
-        }
-
-        return false;
-    }
-
     /**
      * Check if the package is a allowed to write settings. This also accounts that such an access
      * happened.
@@ -308,21 +328,6 @@
                 throwException);
     }
 
-    private boolean hasTetherAccessPermission() {
-        if (checkCallingOrSelfPermission(
-                android.Manifest.permission.TETHER_PRIVILEGED) == PERMISSION_GRANTED) {
-            return true;
-        }
-
-        if (checkCallingOrSelfPermission(
-                android.Manifest.permission.ACCESS_NETWORK_STATE) == PERMISSION_GRANTED) {
-            return true;
-        }
-
-        return false;
-    }
-
-
     /**
      * An injection method for testing.
      */
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index a692935..cdd0e24 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -543,4 +543,33 @@
         assertEquals(1, mEnMgr.uiProvisionCount);
         verify(mEntitlementFailedListener, times(1)).onUiEntitlementFailed(TETHERING_WIFI);
     }
+
+    @Test
+    public void testsetExemptedDownstreamType() throws Exception {
+        setupForRequiredProvisioning();
+        // Cellular upstream is not permitted when no entitlement result.
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+        // If there is exempted downstream and no other non-exempted downstreams, cellular is
+        // permitted.
+        mEnMgr.setExemptedDownstreamType(TETHERING_WIFI);
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+
+        // If second downstream run entitlement check fail, cellular upstream is not permitted.
+        mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
+        mEnMgr.notifyUpstream(true);
+        mLooper.dispatchAll();
+        mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+        mLooper.dispatchAll();
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+
+        // When second downstream is down, exempted downstream can use cellular upstream.
+        assertEquals(1, mEnMgr.uiProvisionCount);
+        verify(mEntitlementFailedListener).onUiEntitlementFailed(TETHERING_USB);
+        mEnMgr.stopProvisioningIfNeeded(TETHERING_USB);
+        assertTrue(mEnMgr.isCellularUpstreamPermitted());
+
+        mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
+        assertFalse(mEnMgr.isCellularUpstreamPermitted());
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index a67a4b6..85c2f2b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -57,6 +57,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.any;
@@ -172,6 +173,8 @@
     private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
     private static final String TEST_NCM_IFNAME = "test_ncm0";
     private static final String TETHERING_NAME = "Tethering";
+    private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
+    private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
 
     private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
 
@@ -539,16 +542,16 @@
     }
 
     private TetheringRequestParcel createTetheringRequestParcel(final int type) {
-        return createTetheringRequestParcel(type, null, null);
+        return createTetheringRequestParcel(type, null, null, false);
     }
 
     private TetheringRequestParcel createTetheringRequestParcel(final int type,
-            final LinkAddress serverAddr, final LinkAddress clientAddr) {
+            final LinkAddress serverAddr, final LinkAddress clientAddr, final boolean exempt) {
         final TetheringRequestParcel request = new TetheringRequestParcel();
         request.tetheringType = type;
         request.localIPv4Address = serverAddr;
         request.staticClientAddress = clientAddr;
-        request.exemptFromEntitlementCheck = false;
+        request.exemptFromEntitlementCheck = exempt;
         request.showProvisioningUi = false;
 
         return request;
@@ -1659,7 +1662,7 @@
 
         // Enable USB tethering and check that Tethering starts USB.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  null, null), firstResult);
+                  null, null, false), firstResult);
         mLooper.dispatchAll();
         firstResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
@@ -1667,7 +1670,7 @@
 
         // Enable USB tethering again with the same request and expect no change to USB.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  null, null), secondResult);
+                  null, null, false), secondResult);
         mLooper.dispatchAll();
         secondResult.assertHasResult();
         verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -1676,7 +1679,7 @@
         // Enable USB tethering with a different request and expect that USB is stopped and
         // started.
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr), thirdResult);
+                  serverLinkAddr, clientLinkAddr, false), thirdResult);
         mLooper.dispatchAll();
         thirdResult.assertHasResult();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -1700,7 +1703,7 @@
         final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
                 ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
-                  serverLinkAddr, clientLinkAddr), null);
+                  serverLinkAddr, clientLinkAddr, false), null);
         mLooper.dispatchAll();
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
         mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
@@ -1762,6 +1765,69 @@
         mLooper.stopAutoDispatch();
     }
 
+    @Test
+    public void testExemptFromEntitlementCheck() throws Exception {
+        setupForRequiredProvisioning();
+        final TetheringRequestParcel wifiNotExemptRequest =
+                createTetheringRequestParcel(TETHERING_WIFI, null, null, false);
+        mTethering.startTethering(wifiNotExemptRequest, null);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
+        verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
+        assertFalse(mEntitleMgr.isCellularUpstreamPermitted());
+        mTethering.stopTethering(TETHERING_WIFI);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
+        reset(mEntitleMgr);
+
+        setupForRequiredProvisioning();
+        final TetheringRequestParcel wifiExemptRequest =
+                createTetheringRequestParcel(TETHERING_WIFI, null, null, true);
+        mTethering.startTethering(wifiExemptRequest, null);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
+        verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
+        assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
+        mTethering.stopTethering(TETHERING_WIFI);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
+        reset(mEntitleMgr);
+
+        // If one app enables tethering without provisioning check first, then another app enables
+        // tethering of the same type but does not disable the provisioning check.
+        setupForRequiredProvisioning();
+        mTethering.startTethering(wifiExemptRequest, null);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
+        verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
+        assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
+        reset(mEntitleMgr);
+        setupForRequiredProvisioning();
+        mTethering.startTethering(wifiNotExemptRequest, null);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
+        verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
+        assertFalse(mEntitleMgr.isCellularUpstreamPermitted());
+        mTethering.stopTethering(TETHERING_WIFI);
+        mLooper.dispatchAll();
+        verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
+        reset(mEntitleMgr);
+    }
+
+    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);
+        // 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);
+        sendConfigurationChanged();
+    }
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }