diff --git a/res/values/strings.xml b/res/values/strings.xml
index adf414f255b..f66c8b39179 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1755,6 +1755,12 @@
5 GHz
Sign in
+
+ Open site
+
+ %1$s left
+
+ Expires on %1$s
Tap here to sign in to network
diff --git a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java
index e9d2e08bb1a..a7c409de7f4 100644
--- a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java
+++ b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java
@@ -31,6 +31,7 @@ import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
+import android.net.CaptivePortalData;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.LinkAddress;
@@ -41,6 +42,7 @@ import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.NetworkUtils;
import android.net.RouteInfo;
+import android.net.Uri;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -75,6 +77,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnPause;
import com.android.settingslib.core.lifecycle.events.OnResume;
+import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.ActionButtonsPreference;
import com.android.settingslib.widget.LayoutPreference;
import com.android.settingslib.wifi.AccessPoint;
@@ -86,6 +89,10 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.util.StringJoiner;
import java.util.stream.Collectors;
@@ -189,6 +196,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
WifiDataUsageSummaryPreferenceController mSummaryHeaderController;
private final IconInjector mIconInjector;
+ private final Clock mClock;
private final IntentFilter mFilter;
// Passpoint information - cache it in case of losing these information after
@@ -229,6 +237,8 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
if (network.equals(mNetwork) && !lp.equals(mLinkProperties)) {
mLinkProperties = lp;
+ refreshEntityHeader();
+ refreshButtons();
refreshIpLayerInfo();
}
}
@@ -322,7 +332,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
MetricsFeatureProvider metricsFeatureProvider) {
return new WifiDetailPreferenceController(
accessPoint, connectivityManager, context, fragment, handler, lifecycle,
- wifiManager, metricsFeatureProvider, new IconInjector(context));
+ wifiManager, metricsFeatureProvider, new IconInjector(context), new Clock());
}
@VisibleForTesting
@@ -335,7 +345,8 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
Lifecycle lifecycle,
WifiManager wifiManager,
MetricsFeatureProvider metricsFeatureProvider,
- IconInjector injector) {
+ IconInjector injector,
+ Clock clock) {
super(context);
mAccessPoint = accessPoint;
@@ -347,6 +358,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
mWifiManager = wifiManager;
mMetricsFeatureProvider = metricsFeatureProvider;
mIconInjector = injector;
+ mClock = clock;
mFilter = new IntentFilter();
mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
@@ -404,9 +416,6 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
.setButton1Text(R.string.forget)
.setButton1Icon(R.drawable.ic_settings_delete)
.setButton1OnClickListener(view -> forgetNetwork())
- .setButton2Text(R.string.wifi_sign_in_button_text)
- .setButton2Icon(R.drawable.ic_settings_sign_in)
- .setButton2OnClickListener(view -> signIntoNetwork())
.setButton3Text(R.string.wifi_connect)
.setButton3Icon(R.drawable.ic_settings_wireless)
.setButton3OnClickListener(view -> connectNetwork())
@@ -414,6 +423,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
.setButton4Text(R.string.share)
.setButton4Icon(R.drawable.ic_qrcode_24dp)
.setButton4OnClickListener(view -> shareNetwork());
+ updateCaptivePortalButton();
if (isPasspointConfigurationR1Expired()) {
// Hide Connect button.
@@ -439,6 +449,42 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
mSecurityPref.setSummary(mAccessPoint.getSecurityString(/* concise */ false));
}
+ /**
+ * Update text, icon and listener of the captive portal button.
+ * @return True if the button should be shown.
+ */
+ private boolean updateCaptivePortalButton() {
+ final Uri venueInfoUrl = getCaptivePortalVenueInfoUrl();
+ if (venueInfoUrl == null) {
+ mButtonsPref.setButton2Text(R.string.wifi_sign_in_button_text)
+ .setButton2Icon(R.drawable.ic_settings_sign_in)
+ .setButton2OnClickListener(view -> signIntoNetwork());
+ return canSignIntoNetwork();
+ }
+
+ mButtonsPref.setButton2Text(R.string.wifi_venue_website_button_text)
+ .setButton2Icon(R.drawable.ic_settings_sign_in)
+ .setButton2OnClickListener(view -> {
+ final Intent infoIntent = new Intent(Intent.ACTION_VIEW);
+ infoIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ infoIntent.setData(venueInfoUrl);
+ mContext.startActivity(infoIntent);
+ });
+ return mAccessPoint.isActive();
+ }
+
+ private Uri getCaptivePortalVenueInfoUrl() {
+ final LinkProperties lp = mLinkProperties;
+ if (lp == null) {
+ return null;
+ }
+ final CaptivePortalData data = lp.getCaptivePortalData();
+ if (data == null) {
+ return null;
+ }
+ return data.getVenueInfoUrl();
+ }
+
private void setupEntityHeader(PreferenceScreen screen) {
LayoutPreference headerPref = screen.findPreference(KEY_HEADER);
@@ -464,6 +510,37 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
mEntityHeaderController.setLabel(mAccessPoint.getTitle());
}
+ private String getExpiryTimeSummary() {
+ if (mLinkProperties == null || mLinkProperties.getCaptivePortalData() == null) {
+ return null;
+ }
+
+ final long expiryTimeMillis = mLinkProperties.getCaptivePortalData().getExpiryTimeMillis();
+ if (expiryTimeMillis <= 0) {
+ return null;
+ }
+ final ZonedDateTime now = mClock.now();
+ final ZonedDateTime expiryTime = ZonedDateTime.ofInstant(
+ Instant.ofEpochMilli(expiryTimeMillis),
+ now.getZone());
+
+ if (now.isAfter(expiryTime)) {
+ return null;
+ }
+
+ if (now.plusDays(2).isAfter(expiryTime)) {
+ // Expiration within 2 days: show a duration
+ return mContext.getString(R.string.wifi_time_remaining, StringUtil.formatElapsedTime(
+ mContext,
+ Duration.between(now, expiryTime).getSeconds() * 1000,
+ false /* withSeconds */));
+ }
+
+ // For more than 2 days, show the expiry date
+ return mContext.getString(R.string.wifi_expiry_time,
+ DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(expiryTime));
+ }
+
private void refreshEntityHeader() {
if (usingDataUsageHeader(mContext)) {
mSummaryHeaderController.updateState(mDataUsageSummaryPref);
@@ -480,6 +557,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
mEntityHeaderController
.setSummary(summary)
+ .setSecondSummary(getExpiryTimeSummary())
.setRecyclerView(mFragment.getListView(), mLifecycle)
.done(mFragment.getActivity(), true /* rebind */);
}
@@ -766,16 +844,16 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
mIsEphemeral ? R.string.wifi_disconnect_button_text : R.string.forget);
boolean canForgetNetwork = canForgetNetwork();
- boolean canSignIntoNetwork = canSignIntoNetwork();
+ boolean showCaptivePortalButton = updateCaptivePortalButton();
boolean canConnectNetwork = canConnectNetwork() && !isPasspointConfigurationR1Expired();
boolean canShareNetwork = canShareNetwork();
mButtonsPref.setButton1Visible(canForgetNetwork);
- mButtonsPref.setButton2Visible(canSignIntoNetwork);
+ mButtonsPref.setButton2Visible(showCaptivePortalButton);
mButtonsPref.setButton3Visible(canConnectNetwork);
mButtonsPref.setButton4Visible(canShareNetwork);
mButtonsPref.setVisible(canForgetNetwork
- || canSignIntoNetwork
+ || showCaptivePortalButton
|| canConnectNetwork
|| canShareNetwork);
}
@@ -996,6 +1074,13 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController
}
}
+ @VisibleForTesting
+ static class Clock {
+ public ZonedDateTime now() {
+ return ZonedDateTime.now();
+ }
+ }
+
private boolean usingDataUsageHeader(Context context) {
return FeatureFlagUtils.isEnabled(context, FeatureFlags.WIFI_DETAILS_DATAUSAGE_HEADER);
}
diff --git a/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java
index b9908467260..11d5ece8037 100644
--- a/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java
@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
@@ -41,6 +40,7 @@ import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.net.CaptivePortalData;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.IpPrefix;
@@ -52,6 +52,7 @@ import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.RouteInfo;
+import android.net.Uri;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -75,6 +76,7 @@ import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.ActionButtonsPreference;
import com.android.settingslib.widget.LayoutPreference;
import com.android.settingslib.wifi.AccessPoint;
@@ -99,6 +101,10 @@ import org.robolectric.shadows.ShadowToast;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Collectors;
@@ -146,6 +152,8 @@ public class WifiDetailPreferenceControllerTest {
@Mock
private WifiDetailPreferenceController.IconInjector mockIconInjector;
@Mock
+ private WifiDetailPreferenceController.Clock mMockClock;
+ @Mock
private MacAddress mockMacAddress;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
@@ -286,7 +294,10 @@ public class WifiDetailPreferenceControllerTest {
// builder pattern
when(mockHeaderController.setRecyclerView(mockFragment.getListView(), mLifecycle))
.thenReturn(mockHeaderController);
- when(mockHeaderController.setSummary(anyString())).thenReturn(mockHeaderController);
+ when(mockHeaderController.setSummary(nullable(String.class)))
+ .thenReturn(mockHeaderController);
+ when(mockHeaderController.setSecondSummary(nullable(String.class)))
+ .thenReturn(mockHeaderController);
when(mockIconInjector.getIcon(anyInt())).thenReturn(new ColorDrawable());
setupMockedPreferenceScreen();
@@ -338,7 +349,8 @@ public class WifiDetailPreferenceControllerTest {
mLifecycle,
mockWifiManager,
mockMetricsFeatureProvider,
- mockIconInjector);
+ mockIconInjector,
+ mMockClock);
}
private void setupMockedPreferenceScreen() {
@@ -525,6 +537,54 @@ public class WifiDetailPreferenceControllerTest {
verify(mockHeaderController).setSummary(summary);
}
+ private void doShouldShowRemainingTimeTest(ZonedDateTime now, long timeRemainingMs) {
+ when(mMockClock.now()).thenReturn(now);
+ setUpForConnectedNetwork();
+ displayAndResume();
+
+ final CaptivePortalData data = new CaptivePortalData.Builder()
+ .setExpiryTime(now.toInstant().getEpochSecond() * 1000 + timeRemainingMs)
+ .build();
+ final LinkProperties lp = new LinkProperties();
+ lp.setCaptivePortalData(data);
+
+ updateLinkProperties(lp);
+ }
+
+ @Test
+ public void entityHeader_shouldShowShortRemainingTime() {
+ // Expires in 1h, 2min, 15sec
+ final long timeRemainingMs = (3600 + 2 * 60 + 15) * 1000;
+ final ZonedDateTime fakeNow = ZonedDateTime.of(2020, 1, 2, 3, 4, 5, 6,
+ ZoneId.of("Europe/London"));
+ doShouldShowRemainingTimeTest(fakeNow, timeRemainingMs);
+ final String expectedSummary = mContext.getString(R.string.wifi_time_remaining,
+ StringUtil.formatElapsedTime(mContext, timeRemainingMs, false /* withSeconds */));
+ final InOrder inOrder = inOrder(mockHeaderController);
+ inOrder.verify(mockHeaderController).setSecondSummary(expectedSummary);
+
+ updateLinkProperties(new LinkProperties());
+ inOrder.verify(mockHeaderController).setSecondSummary((String) null);
+ }
+
+ @Test
+ public void entityHeader_shouldShowExpiryDate() {
+ // Expires in 49h, 2min, 15sec
+ final long timeRemainingMs = (49 * 3600 + 2 * 60 + 15) * 1000;
+ final ZonedDateTime fakeNow = ZonedDateTime.of(2020, 1, 2, 3, 4, 5, 6,
+ ZoneId.of("Europe/London"));
+ doShouldShowRemainingTimeTest(fakeNow, timeRemainingMs);
+ final String expectedSummary = mContext.getString(
+ R.string.wifi_expiry_time,
+ DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(
+ fakeNow.plusNanos(timeRemainingMs * 1_000_000)));
+ final InOrder inOrder = inOrder(mockHeaderController);
+ inOrder.verify(mockHeaderController).setSecondSummary(expectedSummary);
+
+ updateLinkProperties(new LinkProperties());
+ inOrder.verify(mockHeaderController).setSecondSummary((String) null);
+ }
+
@Test
public void entityHeader_shouldConvertSavedAsDisconnected() {
setUpForDisconnectedNetwork();
@@ -1272,6 +1332,8 @@ public class WifiDetailPreferenceControllerTest {
nc.addCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
updateNetworkCapabilities(nc);
+
+ inOrder.verify(mockButtonsPref).setButton2Text(R.string.wifi_sign_in_button_text);
inOrder.verify(mockButtonsPref).setButton2Visible(true);
nc.removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
@@ -1279,6 +1341,31 @@ public class WifiDetailPreferenceControllerTest {
inOrder.verify(mockButtonsPref).setButton2Visible(false);
}
+ @Test
+ public void captivePortal_shouldShowVenueInfoButton() {
+ setUpForConnectedNetwork();
+
+ InOrder inOrder = inOrder(mockButtonsPref);
+
+ displayAndResume();
+
+ inOrder.verify(mockButtonsPref).setButton2Visible(false);
+
+ LinkProperties lp = new LinkProperties();
+ final CaptivePortalData data = new CaptivePortalData.Builder()
+ .setVenueInfoUrl(Uri.parse("https://example.com/info"))
+ .build();
+ lp.setCaptivePortalData(data);
+ updateLinkProperties(lp);
+
+ inOrder.verify(mockButtonsPref).setButton2Text(R.string.wifi_venue_website_button_text);
+ inOrder.verify(mockButtonsPref).setButton2Visible(true);
+
+ lp.setCaptivePortalData(null);
+ updateLinkProperties(lp);
+ inOrder.verify(mockButtonsPref).setButton2Visible(false);
+ }
+
@Test
public void testSignInButton_shouldStartCaptivePortalApp() {
setUpForConnectedNetwork();
@@ -1286,7 +1373,8 @@ public class WifiDetailPreferenceControllerTest {
displayAndResume();
ArgumentCaptor captor = ArgumentCaptor.forClass(OnClickListener.class);
- verify(mockButtonsPref).setButton2OnClickListener(captor.capture());
+ verify(mockButtonsPref, atLeastOnce()).setButton2OnClickListener(captor.capture());
+ // getValue() returns the last captured value
captor.getValue().onClick(null);
verify(mockConnectivityManager).startCaptivePortalApp(mockNetwork);
verify(mockMetricsFeatureProvider)