Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9f7f0b5b authored by lucaslin's avatar lucaslin
Browse files

Add capport info to WiFi details

Session expiration time and venue webpage can be obtained through the
captive portal API.

Test: make RunSettingsRoboTests \
      ROBOTEST_FILTER=WifiDetailPreferenceControllerTest
Bug: 139269711
Change-Id: Ie767a9b8eb17de2c1b70928a8f3cdf4cf2a1dbd1
parent e6704dc3
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -1755,6 +1755,12 @@
    <string name="wifi_band_5ghz">5 GHz</string>
    <!-- Wifi Sign in text for button [CHAR LIMIT = 40]-->
    <string name="wifi_sign_in_button_text">Sign in</string>
    <!-- Text for button to go to Wifi venue information webpage when Wifi is a captive portal [CHAR LIMIT=40]-->
    <string name="wifi_venue_website_button_text">Open site</string>
    <!-- Text shown in wifi settings indicating how much time is left on an internet access point that has a time limit for the session [CHAR LIMIT=40] -->
    <string name="wifi_time_remaining"><xliff:g id="Remaining time" example="1 day, 2 hours, 3 minutes">%1$s</xliff:g> left</string>
    <!-- Text shown in wifi settings indicating at what time the connection will expire on an internet access point that has a time limit for the session [CHAR LIMIT=40] -->
    <string name="wifi_expiry_time">Expires on <xliff:g id="Expiry time" example="2020/01/02 12:34PM">%1$s</xliff:g></string>
    <!-- Wifi Sign in CTA for wifi settings when captive portal auth is required [CHAR LIMIT = 50] -->
    <string name="wifi_tap_to_sign_in">Tap here to sign in to network</string>
    <!-- Transmit Link speed on Wifi Status screen [CHAR LIMIT=32] -->
+93 −8
Original line number Diff line number Diff line
@@ -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);
    }
+92 −4
Original line number Diff line number Diff line
@@ -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<OnClickListener> 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)