diff --git a/framework/src/android/net/CaptivePortalData.java b/framework/src/android/net/CaptivePortalData.java index 53aa1b92ed..06cefb588b 100644 --- a/framework/src/android/net/CaptivePortalData.java +++ b/framework/src/android/net/CaptivePortalData.java @@ -50,7 +50,9 @@ public final class CaptivePortalData implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"CAPTIVE_PORTAL_DATA_SOURCE_"}, value = { CAPTIVE_PORTAL_DATA_SOURCE_OTHER, - CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT}) + CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN + }) public @interface CaptivePortalDataSource {} /** @@ -63,6 +65,17 @@ public final class CaptivePortalData implements Parcelable { */ public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; + // TODO : Remove this, replace with an explicit boolean for opting in. Better yet, when the + // feature is publicly rolled out, remove without a replacement. Because the network stack and + // connectivity modules are not yet merged, adding anything in this class would require + // public API, which would have to be maintained forever and would force very long time-to- + // market timelines, so this is used instead. + /** + * Source of information : Capport, and capport opts in to custom tabs. + * @hide + */ + public static final int CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN = 2; + private CaptivePortalData(long refreshTimeMillis, Uri userPortalUrl, Uri venueInfoUrl, boolean isSessionExtendable, long byteLimit, long expiryTimeMillis, boolean captive, CharSequence venueFriendlyName, int venueInfoUrlSource, int userPortalUrlSource) { diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt index 67a523c14f..92d0f5669f 100644 --- a/tests/common/java/android/net/CaptivePortalDataTest.kt +++ b/tests/common/java/android/net/CaptivePortalDataTest.kt @@ -57,20 +57,35 @@ class CaptivePortalDataTest { .apply { if (SdkLevel.isAtLeastS()) { setVenueFriendlyName("venue friendly name") - setUserPortalUrl(Uri.parse("https://tc.example.com/passpoint"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) - setVenueInfoUrl(Uri.parse("https://venue.example.com/passpoint"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT + ) + setVenueInfoUrl( + Uri.parse("https://venue.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT + ) } } .build() + private val optInData = CaptivePortalData.Builder().apply { + setCaptive(true) + if (SdkLevel.isAtLeastS()) { + setUserPortalUrl( + Uri.parse("https://tc.example.passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN + ) + } + }.build() + private fun makeBuilder() = CaptivePortalData.Builder(data) @Test fun testParcelUnparcel() { assertParcelingIsLossless(data) assertParcelingIsLossless(dataFromPasspoint) + if (SdkLevel.isAtLeastS()) assertParcelingIsLossless(optInData) assertParcelingIsLossless(makeBuilder().setUserPortalUrl(null).build()) assertParcelingIsLossless(makeBuilder().setVenueInfoUrl(null).build()) @@ -96,24 +111,35 @@ class CaptivePortalDataTest { assertEquals(dataFromPasspoint, CaptivePortalData.Builder(dataFromPasspoint).build()) assertNotEqualsAfterChange { it.setUserPortalUrl( - Uri.parse("https://tc.example.com/passpoint")) } + Uri.parse("https://tc.example.com/passpoint") + ) } assertNotEqualsAfterChange { it.setUserPortalUrl( Uri.parse("https://tc.example.com/passpoint"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER + ) } assertNotEqualsAfterChange { it.setUserPortalUrl( Uri.parse("https://tc.example.com/other"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) } + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT + ) } assertNotEqualsAfterChange { it.setUserPortalUrl( - Uri.parse("https://tc.example.com/passpoint"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER + ) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN + ) } assertNotEqualsAfterChange { it.setVenueInfoUrl( - Uri.parse("https://venue.example.com/passpoint")) } + Uri.parse("https://venue.example.com/passpoint") + ) } assertNotEqualsAfterChange { it.setVenueInfoUrl( Uri.parse("https://venue.example.com/other"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) } + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT + ) } assertNotEqualsAfterChange { it.setVenueInfoUrl( Uri.parse("https://venue.example.com/passpoint"), - CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER + ) } } } @@ -164,18 +190,32 @@ class CaptivePortalDataTest { @Test @IgnoreUpTo(Build.VERSION_CODES.R) fun testGetVenueInfoUrlSource() { - assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, - data.venueInfoUrlSource) - assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, - dataFromPasspoint.venueInfoUrlSource) + assertEquals( + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + data.venueInfoUrlSource + ) + assertEquals( + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + dataFromPasspoint.venueInfoUrlSource + ) } @Test @IgnoreUpTo(Build.VERSION_CODES.R) fun testGetUserPortalUrlSource() { - assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, - data.userPortalUrlSource) - assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, - dataFromPasspoint.userPortalUrlSource) + assertEquals( + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + data.userPortalUrlSource + ) + assertEquals( + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + dataFromPasspoint.userPortalUrlSource + ) + if (SdkLevel.isAtLeastS()) { + assertEquals( + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN, + optInData.userPortalUrlSource + ) + } } private fun CaptivePortalData.mutate(mutator: (CaptivePortalData.Builder) -> Unit) = @@ -184,4 +224,4 @@ class CaptivePortalDataTest { private fun assertNotEqualsAfterChange(mutator: (CaptivePortalData.Builder) -> Unit) { assertNotEquals(data, data.mutate(mutator)) } -} \ No newline at end of file +} diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt index 563ced89e5..d9eb23e71a 100644 --- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt +++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt @@ -24,7 +24,10 @@ import android.content.Context.BIND_AUTO_CREATE import android.content.Context.BIND_IMPORTANT import android.content.Intent import android.content.ServiceConnection +import android.content.pm.PackageManager import android.content.res.Resources +import android.net.CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN +import android.net.CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER import android.net.ConnectivityManager import android.net.IDnsResolver import android.net.INetd @@ -236,7 +239,7 @@ class ConnectivityServiceIntegrationTest { } private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService( - context, + context, dnsResolver, log, netd, @@ -253,7 +256,7 @@ class ConnectivityServiceIntegrationTest { override fun getBpfNetMaps( context: Context?, netd: INetd?, - interfaceTracker: InterfaceTracker? + interfaceTracker: InterfaceTracker? ) = mock(BpfNetMaps::class.java) .also { doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt()) @@ -284,7 +287,7 @@ class ConnectivityServiceIntegrationTest { tm: TelephonyManager, requestRestrictedWifiEnabled: Boolean, listener: BiConsumer, - handler: Handler + handler: Handler, ): CarrierPrivilegeAuthenticator { return CarrierPrivilegeAuthenticator( context, @@ -363,8 +366,44 @@ class ConnectivityServiceIntegrationTest { } } + private fun getModuleVersion(): Long { + val captivePortalActivity = Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN) + .resolveActivity(context.packageManager) + val captivePortalInfo = context.packageManager.getPackageInfo( + captivePortalActivity.packageName, + PackageManager.GET_ACTIVITIES + ) + return captivePortalInfo.longVersionCode + } + + val OPT_IN = true + var OPT_OUT = false + @Test - fun testCapportApi() { + fun testCapportApiWithoutOptInToCustomTabs() = testCapportApi( + optInToCustomTabsString = null, + expectedOptIn = OPT_OUT + ) + + @Test + fun testCapportApiWithFullOptInToCustomTabs() = testCapportApi( + optInToCustomTabsString = "true", + expectedOptIn = OPT_IN + ) + + @Test + fun testCapportApiWithHighVersionOptInToCustomTabs() = testCapportApi( + optInToCustomTabsString = (getModuleVersion() + 10).toString(), + expectedOptIn = OPT_OUT + ) + + @Test + fun testCapportApiWithLowVersionOptInToCustomTabs() = testCapportApi( + optInToCustomTabsString = (getModuleVersion() - 10).toString(), + expectedOptIn = OPT_IN + ) + + fun testCapportApi(optInToCustomTabsString: String?, expectedOptIn: Boolean) { val request = NetworkRequest.Builder() .clearCapabilities() .addCapability(NET_CAPABILITY_INTERNET) @@ -373,11 +412,17 @@ class ConnectivityServiceIntegrationTest { val apiUrl = "https://capport.android.com" cm.registerNetworkCallback(request, testCb) + val optInString = if (optInToCustomTabsString != null) { + "\"x-android-use-custom-tabs\": $optInToCustomTabsString," + } else { + "" + } nsInstrumentation.addHttpResponse(HttpResponse( apiUrl, """ |{ | "captive": true, + | $optInString | "user-portal-url": "https://login.capport.android.com", | "venue-info-url": "https://venueinfo.capport.android.com" |} @@ -412,6 +457,14 @@ class ConnectivityServiceIntegrationTest { Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl ) + assertEquals( + if (expectedOptIn == OPT_IN) { + CAPTIVE_PORTAL_DATA_SOURCE_CAPPORT_WITH_CUSTOM_TABS_OPTIN + } else { + CAPTIVE_PORTAL_DATA_SOURCE_OTHER + }, + capportData.userPortalUrlSource + ) testCb.expectCaps(na, TEST_TIMEOUT_MS) { it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&