chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeaderUnitTest.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.omnibox.geo;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.location.Location;
import android.os.SystemClock;

import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.Granularity;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.LooperMode;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.omnibox.geo.VisibleNetworks.VisibleCell;
import org.chromium.chrome.browser.omnibox.geo.VisibleNetworks.VisibleWifi;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridgeJni;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.embedder_support.util.UrlUtilitiesJni;
import org.chromium.components.omnibox.OmniboxFeatureList;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.content_public.browser.WebContents;

import java.util.Arrays;
import java.util.HashSet;

/** Robolectric tests for {@link GeolocationHeader}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@LooperMode(LooperMode.Mode.LEGACY)
public class GeolocationHeaderUnitTest {
    private static final String SEARCH_URL = "https://www.google.com/search?q=potatoes";

    private static final double LOCATION_LAT = 20.3;
    private static final double LOCATION_LONG = 155.8;
    private static final float LOCATION_ACCURACY = 20f;
    private static final long LOCATION_TIME = 400;
    // Encoded location for LOCATION_LAT, LOCATION_LONG, LOCATION_ACCURACY and LOCATION_TIME.
    private static final String ENCODED_PROTO_LOCATION = "CAEQDBiAtRgqCg3AiBkMFYAx3Vw9AECcRg==";

    private static final VisibleWifi VISIBLE_WIFI1 =
            VisibleWifi.create("ssid1", "11:11:11:11:11:11", -1, 10L);
    private static final VisibleWifi VISIBLE_WIFI_NO_LEVEL =
            VisibleWifi.create("ssid1", "11:11:11:11:11:11", null, 10L);
    private static final VisibleWifi VISIBLE_WIFI2 =
            VisibleWifi.create("ssid2", "11:11:11:11:11:12", -10, 20L);
    private static final VisibleWifi VISIBLE_WIFI3 =
            VisibleWifi.create("ssid3", "11:11:11:11:11:13", -30, 30L);
    private static final VisibleWifi VISIBLE_WIFI_NOMAP =
            VisibleWifi.create("ssid1_nomap", "11:11:11:11:11:11", -1, 10L);
    private static final VisibleWifi VISIBLE_WIFI_OPTOUT =
            VisibleWifi.create("ssid1_optout", "11:11:11:11:11:11", -1, 10L);
    private static final VisibleCell VISIBLE_CELL1 =
            VisibleCell.builder(VisibleCell.RadioType.CDMA)
                    .setCellId(10)
                    .setLocationAreaCode(11)
                    .setMobileCountryCode(12)
                    .setMobileNetworkCode(13)
                    .setTimestamp(10L)
                    .build();
    private static final VisibleCell VISIBLE_CELL2 =
            VisibleCell.builder(VisibleCell.RadioType.GSM)
                    .setCellId(20)
                    .setLocationAreaCode(21)
                    .setMobileCountryCode(22)
                    .setMobileNetworkCode(23)
                    .setTimestamp(20L)
                    .build();
    // Encoded proto location for VISIBLE_WIFI1 connected, VISIBLE_WIFI3 not connected,
    // VISIBLE_CELL1 connected, VISIBLE_CELL2 not connected.
    private static final String ENCODED_PROTO_VISIBLE_NETWORKS =
            "CAEQDLoBJAoeChExMToxMToxMToxMToxMToxMRD___________8BGAEgCroBJAoeChExMToxMToxMToxMTox"
                + "MToxMxDi__________8BGAAgHroBEBIKCAMQChgLIAwoDRgBIAq6ARASCggBEBQYFSAWKBcYACAU";

    private static int sRefreshVisibleNetworksRequests;
    private static int sRefreshLastKnownLocation;

    @Rule public JniMocker mocker = new JniMocker();

    @Mock UrlUtilities.Natives mUrlUtilitiesJniMock;

    @Mock WebsitePreferenceBridge.Natives mWebsitePreferenceBridgeJniMock;

    @Mock Profile mProfileMock;

    @Mock private Tab mTab;

    @Mock WebContents mWebContentsMock;

    @Mock TemplateUrlService mTemplateUrlServiceMock;
    @Mock FusedLocationProviderClient mLocationProviderClient;
    @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor;
    @Captor private ArgumentCaptor<LocationRequest> mLocationRequestCaptor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mocker.mock(UrlUtilitiesJni.TEST_HOOKS, mUrlUtilitiesJniMock);
        mocker.mock(WebsitePreferenceBridgeJni.TEST_HOOKS, mWebsitePreferenceBridgeJniMock);
        GeolocationTracker.setLocationAgeForTesting(null);
        GeolocationHeader.setLocationSourceForTesting(
                GeolocationHeader.LocationSource.HIGH_ACCURACY);
        GeolocationHeader.setAppPermissionGrantedForTesting(true);
        when(mTab.isIncognito()).thenReturn(false);
        when(mTab.getProfile()).thenReturn(mProfileMock);
        when(mTab.getWebContents()).thenReturn(mWebContentsMock);
        when(mWebsitePreferenceBridgeJniMock.getPermissionSettingForOrigin(
                        any(BrowserContextHandle.class), eq(ContentSettingsType.GEOLOCATION),
                        anyString(), anyString()))
                .thenReturn(ContentSettingValues.ALLOW);
        when(mWebsitePreferenceBridgeJniMock.isDSEOrigin(
                        any(BrowserContextHandle.class), anyString()))
                .thenReturn(true);
        when(mUrlUtilitiesJniMock.isGoogleSearchUrl(anyString())).thenReturn(true);
        when(mProfileMock.isOffTheRecord()).thenReturn(false);
        when(mTemplateUrlServiceMock.getUrlForSearchQuery(anyString()))
                .thenReturn("https://example.com/");
        sRefreshVisibleNetworksRequests = 0;
        sRefreshLastKnownLocation = 0;
        ShadowLocationServices.sFusedLocationProviderClient = mLocationProviderClient;
    }

    @Test
    public void testEncodeProtoLocation() {
        Location location = generateMockLocation("should_not_matter", LOCATION_TIME);
        String encodedProtoLocation = GeolocationHeader.encodeProtoLocation(location);
        assertEquals(ENCODED_PROTO_LOCATION, encodedProtoLocation);
    }

    @Test
    public void voidtestTrimVisibleNetworks() {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VISIBLE_WIFI_NO_LEVEL,
                        VISIBLE_CELL1,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI1, VISIBLE_WIFI2, VISIBLE_WIFI3)),
                        new HashSet<>(Arrays.asList(VISIBLE_CELL1, VISIBLE_CELL2)));

        // We expect trimming to replace connected Wifi (since it will have level), and select only
        // the visible wifi different from the connected one, with strongest level.
        VisibleNetworks expectedTrimmed =
                VisibleNetworks.create(
                        VISIBLE_WIFI1,
                        VISIBLE_CELL1,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI3)),
                        new HashSet<>(Arrays.asList(VISIBLE_CELL2)));

        VisibleNetworks trimmed = GeolocationHeader.trimVisibleNetworks(visibleNetworks);
        assertEquals(expectedTrimmed, trimmed);
    }

    @Test
    public void testTrimVisibleNetworksEmptyOrNull() {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VisibleWifi.create("whatever", null, null, null),
                        null,
                        new HashSet<>(),
                        new HashSet<>());
        assertNull(GeolocationHeader.trimVisibleNetworks(visibleNetworks));
        assertNull(GeolocationHeader.trimVisibleNetworks(null));
    }

    @Test
    public void testEncodeProtoVisibleNetworks() {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VISIBLE_WIFI1,
                        VISIBLE_CELL1,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI3)),
                        new HashSet<>(Arrays.asList(VISIBLE_CELL2)));
        String encodedProtoLocation = GeolocationHeader.encodeProtoVisibleNetworks(visibleNetworks);
        assertEquals(ENCODED_PROTO_VISIBLE_NETWORKS, encodedProtoLocation);
    }

    @Test
    public void testEncodeProtoVisibleNetworksEmptyOrNull() {
        assertNull(GeolocationHeader.encodeProtoVisibleNetworks(null));
        assertNull(
                GeolocationHeader.encodeProtoVisibleNetworks(
                        VisibleNetworks.create(null, null, null, null)));
        assertNull(
                GeolocationHeader.encodeProtoVisibleNetworks(
                        VisibleNetworks.create(null, null, new HashSet<>(), new HashSet<>())));
        assertNotNull(
                GeolocationHeader.encodeProtoVisibleNetworks(
                        VisibleNetworks.create(
                                null, null, null, new HashSet<>(Arrays.asList(VISIBLE_CELL2)))));
    }

    @Test
    public void testEncodeProtoVisibleNetworksExcludeNoMapOrOptout() {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VISIBLE_WIFI_NOMAP,
                        null,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI_OPTOUT)),
                        new HashSet<>());
        String encodedProtoLocation = GeolocationHeader.encodeProtoVisibleNetworks(visibleNetworks);
        assertNull(encodedProtoLocation);
    }

    @Test
    public void testGetGeoHeaderFreshLocation() {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VISIBLE_WIFI1,
                        VISIBLE_CELL1,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI3)),
                        new HashSet<>(Arrays.asList(VISIBLE_CELL2)));
        VisibleNetworksTracker.setVisibleNetworksForTesting(visibleNetworks);
        Location location = generateMockLocation("should_not_matter", LOCATION_TIME);
        GeolocationTracker.setLocationForTesting(location, null);
        // 1 minute should be good enough and not require visible networks.
        GeolocationTracker.setLocationAgeForTesting(1 * 60 * 1000L);
        String header = GeolocationHeader.getGeoHeader(SEARCH_URL, mTab);
        assertEquals("X-Geo: w " + ENCODED_PROTO_LOCATION, header);
    }

    @Test
    public void testGetGeoHeaderLocationMissing() {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VISIBLE_WIFI1,
                        VISIBLE_CELL1,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI3)),
                        new HashSet<>(Arrays.asList(VISIBLE_CELL2)));
        VisibleNetworksTracker.setVisibleNetworksForTesting(visibleNetworks);
        GeolocationTracker.setLocationForTesting(null, null);
        String header = GeolocationHeader.getGeoHeader(SEARCH_URL, mTab);
        assertEquals("X-Geo: w " + ENCODED_PROTO_VISIBLE_NETWORKS, header);
    }

    @Test
    public void testGetGeoHeaderOldLocationHighAccuracy() {
        GeolocationHeader.setLocationSourceForTesting(
                GeolocationHeader.LocationSource.HIGH_ACCURACY);
        // Visible networks should be included
        checkOldLocation(
                "X-Geo: w " + ENCODED_PROTO_LOCATION + " w " + ENCODED_PROTO_VISIBLE_NETWORKS);
    }

    @Test
    public void testGetGeoHeaderOldLocationBatterySaving() {
        GeolocationHeader.setLocationSourceForTesting(
                GeolocationHeader.LocationSource.BATTERY_SAVING);
        checkOldLocation(
                "X-Geo: w " + ENCODED_PROTO_LOCATION + " w " + ENCODED_PROTO_VISIBLE_NETWORKS);
    }

    @Test
    public void testGetGeoHeaderOldLocationGpsOnly() {
        GeolocationHeader.setLocationSourceForTesting(GeolocationHeader.LocationSource.GPS_ONLY);
        // In GPS only mode, networks should never be included.
        checkOldLocation("X-Geo: w " + ENCODED_PROTO_LOCATION);
    }

    @Test
    public void testGetGeoHeaderOldLocationLocationOff() {
        GeolocationHeader.setLocationSourceForTesting(
                GeolocationHeader.LocationSource.LOCATION_OFF);
        // If the location switch is off, networks should never be included (old location might).
        checkOldLocation("X-Geo: w " + ENCODED_PROTO_LOCATION);
    }

    @Test
    public void testGetGeoHeaderOldLocationAppPermissionDenied() {
        GeolocationHeader.setLocationSourceForTesting(
                GeolocationHeader.LocationSource.HIGH_ACCURACY);
        GeolocationHeader.setAppPermissionGrantedForTesting(false);
        // Nothing should be included when app permission is missing.
        checkOldLocation(null);
    }

    @Test
    @Config(shadows = {ShadowVisibleNetworksTracker.class, ShadowGeolocationTracker.class})
    public void testPrimeLocationForGeoHeader() {
        GeolocationHeader.primeLocationForGeoHeaderIfEnabled(mProfileMock, mTemplateUrlServiceMock);
        assertEquals(1, sRefreshLastKnownLocation);
        assertEquals(1, sRefreshVisibleNetworksRequests);
    }

    @Test
    @Config(shadows = {ShadowVisibleNetworksTracker.class, ShadowGeolocationTracker.class})
    public void testPrimeLocationForGeoHeaderPermissionOff() {
        GeolocationHeader.setAppPermissionGrantedForTesting(false);
        GeolocationHeader.primeLocationForGeoHeaderIfEnabled(mProfileMock, mTemplateUrlServiceMock);
        assertEquals(0, sRefreshLastKnownLocation);
        assertEquals(0, sRefreshVisibleNetworksRequests);
    }

    @Test
    @Config(shadows = {ShadowVisibleNetworksTracker.class, ShadowGeolocationTracker.class})
    public void testPrimeLocationForGeoHeaderDSEAutograntOff() {
        when(mWebsitePreferenceBridgeJniMock.getPermissionSettingForOrigin(
                        any(BrowserContextHandle.class), eq(ContentSettingsType.GEOLOCATION),
                        anyString(), anyString()))
                .thenReturn(ContentSettingValues.ASK);
        GeolocationHeader.primeLocationForGeoHeaderIfEnabled(mProfileMock, mTemplateUrlServiceMock);
        assertEquals(0, sRefreshLastKnownLocation);
        assertEquals(0, sRefreshVisibleNetworksRequests);
    }

    @Test
    @EnableFeatures(OmniboxFeatureList.USE_FUSED_LOCATION_PROVIDER)
    @Config(
            shadows = {
                ShadowVisibleNetworksTracker.class,
                ShadowGeolocationTracker.class,
                ShadowLocationServices.class
            })
    public void testFusedLocationProvider() {
        GeolocationHeader.primeLocationForGeoHeaderIfEnabled(mProfileMock, mTemplateUrlServiceMock);
        verify(mLocationProviderClient)
                .requestLocationUpdates(
                        mLocationRequestCaptor.capture(),
                        mLocationListenerCaptor.capture(),
                        eq(null));

        LocationRequest actualRequest = mLocationRequestCaptor.getValue();
        assertEquals(GeolocationHeader.REFRESH_LOCATION_AGE, actualRequest.getMaxUpdateAgeMillis());
        assertEquals(
                GeolocationHeader.LOCATION_REQUEST_UPDATE_INTERVAL,
                actualRequest.getMinUpdateIntervalMillis());
        assertEquals(Granularity.GRANULARITY_PERMISSION_LEVEL, actualRequest.getGranularity());

        Location mockLocation = generateMockLocation("network", LOCATION_TIME);
        mLocationListenerCaptor.getValue().onLocationChanged(mockLocation);
        assertEquals(mockLocation, GeolocationHeader.getLastKnownLocation());
        assertEquals(0, sRefreshLastKnownLocation);
        assertEquals(1, sRefreshVisibleNetworksRequests);

        GeolocationHeader.stopListeningForLocationUpdates();
        verify(mLocationProviderClient).removeLocationUpdates(mLocationListenerCaptor.getValue());

        doThrow(new RuntimeException())
                .when(mLocationProviderClient)
                .requestLocationUpdates(
                        any(LocationRequest.class), any(LocationListener.class), eq(null));
        GeolocationHeader.primeLocationForGeoHeaderIfEnabled(mProfileMock, mTemplateUrlServiceMock);

        assertEquals(1, sRefreshLastKnownLocation);
        assertEquals(2, sRefreshVisibleNetworksRequests);
    }

    private void checkOldLocation(String expectedHeader) {
        VisibleNetworks visibleNetworks =
                VisibleNetworks.create(
                        VISIBLE_WIFI1,
                        VISIBLE_CELL1,
                        new HashSet<>(Arrays.asList(VISIBLE_WIFI3)),
                        new HashSet<>(Arrays.asList(VISIBLE_CELL2)));
        VisibleNetworksTracker.setVisibleNetworksForTesting(visibleNetworks);
        Location location = generateMockLocation("should_not_matter", LOCATION_TIME);
        GeolocationTracker.setLocationForTesting(location, null);
        // 6 minutes should hit the age limit, but the feature is off.
        GeolocationTracker.setLocationAgeForTesting(6 * 60 * 1000L);
        String header = GeolocationHeader.getGeoHeader(SEARCH_URL, mTab);
        assertEquals(expectedHeader, header);
    }

    private Location generateMockLocation(String provider, long time) {
        Location location = new Location(provider);
        location.setLatitude(LOCATION_LAT);
        location.setLongitude(LOCATION_LONG);
        location.setAccuracy(LOCATION_ACCURACY);
        location.setTime(time);
        location.setElapsedRealtimeNanos(
                SystemClock.elapsedRealtimeNanos() + 1000000 * (time - System.currentTimeMillis()));
        return location;
    }

    /** Shadow for VisibleNetworksTracker */
    @Implements(VisibleNetworksTracker.class)
    public static class ShadowVisibleNetworksTracker {
        @Implementation
        public static void refreshVisibleNetworks(final Context context) {
            sRefreshVisibleNetworksRequests++;
        }
    }

    /** Shadow for GeolocationTracker */
    @Implements(GeolocationTracker.class)
    public static class ShadowGeolocationTracker {
        @Implementation
        public static void refreshLastKnownLocation(Context context, long maxAge) {
            sRefreshLastKnownLocation++;
        }
    }

    /** Shadow for LocationServices */
    @Implements(LocationServices.class)
    public static class ShadowLocationServices {
        static FusedLocationProviderClient sFusedLocationProviderClient;

        @Implementation
        public static FusedLocationProviderClient getFusedLocationProviderClient(Context context) {
            return sFusedLocationProviderClient;
        }
    }
}