// Copyright 2015 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 android.location.Location;
import android.location.LocationManager;
import android.os.SystemClock;
import android.util.Base64;
import androidx.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.OmniboxTestUtils;
import org.chromium.chrome.test.util.browser.LocationSettingsTestUtil;
import org.chromium.components.browser_ui.site_settings.PermissionInfo;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.content_settings.SessionModel;
/** Tests for GeolocationHeader and GeolocationTracker. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@Batch(Batch.PER_CLASS)
public class GeolocationHeaderTest {
public @ClassRule static ChromeTabbedActivityTestRule sActivityTestRule =
new ChromeTabbedActivityTestRule();
public @Rule BlankCTATabInitialStateRule mInitialStateRule =
new BlankCTATabInitialStateRule(sActivityTestRule, true);
private OmniboxTestUtils mOmniboxTestUtils;
private static final String SEARCH_URL_1 = "https://www.google.com/search?q=potatoes";
private static final String SEARCH_URL_2 = "https://www.google.co.jp/webhp?#q=dinosaurs";
private static final String GOOGLE_BASE_URL_SWITCH = "google-base-url=https://www.google.com";
private static final double LOCATION_LAT = 20.3;
private static final double LOCATION_LONG = 155.8;
private static final float LOCATION_ACCURACY = 20f;
@Before
public void setUp() throws InterruptedException {
LocationSettingsTestUtil.setSystemLocationSettingEnabled(true);
mOmniboxTestUtils = new OmniboxTestUtils(sActivityTestRule.getActivity());
}
@Test
@SmallTest
@Feature({"Location"})
@CommandLineFlags.Add({GOOGLE_BASE_URL_SWITCH})
public void testConsistentHeader() {
setPermission(ContentSettingValues.ALLOW);
long now = setMockLocationNow();
// X-Geo should be sent for Google search results page URLs.
assertNonNullHeader(SEARCH_URL_1, false, now);
// But only the current CCTLD.
assertNullHeader(SEARCH_URL_2, false);
// X-Geo shouldn't be sent in incognito mode.
assertNullHeader(SEARCH_URL_1, true);
assertNullHeader(SEARCH_URL_2, true);
// X-Geo shouldn't be sent with URLs that aren't the Google search results page.
assertNullHeader("invalid$url", false);
assertNullHeader("https://www.chrome.fr/", false);
assertNullHeader("https://www.google.com/", false);
// X-Geo shouldn't be sent over HTTP.
assertNullHeader("http://www.google.com/search?q=potatoes", false);
assertNullHeader("http://www.google.com/webhp?#q=dinosaurs", false);
}
@Test
@SmallTest
@Feature({"Location"})
@CommandLineFlags.Add({GOOGLE_BASE_URL_SWITCH})
public void testConsistentHeaderForOneTimeGrant() {
setOneTimeGrant();
long now = setMockLocationNow();
// X-Geo should be sent for Google search results page URLs.
assertNonNullHeader(SEARCH_URL_1, false, now);
// But only the current CCTLD.
assertNullHeader(SEARCH_URL_2, false);
// X-Geo shouldn't be sent in incognito mode.
assertNullHeader(SEARCH_URL_1, true);
assertNullHeader(SEARCH_URL_2, true);
// X-Geo shouldn't be sent with URLs that aren't the Google search results page.
assertNullHeader("invalid$url", false);
assertNullHeader("https://www.chrome.fr/", false);
assertNullHeader("https://www.google.com/", false);
// X-Geo shouldn't be sent over HTTP.
assertNullHeader("http://www.google.com/search?q=potatoes", false);
assertNullHeader("http://www.google.com/webhp?#q=dinosaurs", false);
}
@Test
@SmallTest
@Feature({"Location"})
@CommandLineFlags.Add({GOOGLE_BASE_URL_SWITCH})
public void testPermissionWithoutAutogrant() {
long now = setMockLocationNow();
// X-Geo should be sent if DSE autogrant is enabled only if the user has explicitly allowed
// geolocation.
checkHeaderWithPermission(ContentSettingValues.ALLOW, now, false);
checkHeaderWithPermission(ContentSettingValues.BLOCK, now, true);
checkHeaderWithPermission(ContentSettingValues.DEFAULT, now, true);
}
@Test
@SmallTest
@Feature({"Location"})
public void testProtoEncoding() {
setPermission(ContentSettingValues.ALLOW);
long now = setMockLocationNow();
// X-Geo should be sent for Google search results page URLs using proto encoding.
assertNonNullHeader(SEARCH_URL_1, false, now);
}
@Test
@SmallTest
@Feature({"Location"})
public void testGpsFallback() {
setPermission(ContentSettingValues.ALLOW);
// Only GPS location, should be sent when flag is on.
long now = System.currentTimeMillis();
Location gpsLocation = generateMockLocation(LocationManager.GPS_PROVIDER, now);
GeolocationTracker.setLocationForTesting(null, gpsLocation);
assertNonNullHeader(SEARCH_URL_1, false, now);
}
@Test
@SmallTest
@Feature({"Location"})
public void testGpsFallbackYounger() {
setPermission(ContentSettingValues.ALLOW);
long now = System.currentTimeMillis();
// GPS location is younger.
Location gpsLocation = generateMockLocation(LocationManager.GPS_PROVIDER, now + 100);
// Network location is older
Location netLocation = generateMockLocation(LocationManager.NETWORK_PROVIDER, now);
GeolocationTracker.setLocationForTesting(netLocation, gpsLocation);
// The younger (GPS) should be used.
assertNonNullHeader(SEARCH_URL_1, false, now + 100);
}
@Test
@SmallTest
@Feature({"Location"})
public void testGpsFallbackOlder() {
setPermission(ContentSettingValues.ALLOW);
long now = System.currentTimeMillis();
// GPS location is older.
Location gpsLocation = generateMockLocation(LocationManager.GPS_PROVIDER, now - 100);
// Network location is younger.
Location netLocation = generateMockLocation(LocationManager.NETWORK_PROVIDER, now);
GeolocationTracker.setLocationForTesting(netLocation, gpsLocation);
// The younger (Network) should be used.
assertNonNullHeader(SEARCH_URL_1, false, now);
}
@Test
@SmallTest
@Feature({"Location"})
public void testGeolocationHeaderPrimingEnabledPermissionAllow() {
setPermission(ContentSettingValues.ALLOW);
checkHeaderPriming(/* shouldPrimeHeader= */ true);
}
@Test
@SmallTest
@Feature({"Location"})
public void testGeolocationHeaderPrimingDisabledPermissionBlock() {
setPermission(ContentSettingValues.BLOCK);
checkHeaderPriming(/* shouldPrimeHeader= */ false);
}
@Test
@SmallTest
@Feature({"Location"})
public void testGeolocationHeaderPrimingDisabledPermissionAsk() {
setPermission(ContentSettingValues.ASK);
checkHeaderPriming(/* shouldPrimeHeader= */ false);
}
@Test
@SmallTest
@Feature({"Location"})
@RequiresRestart(value = "Needs to reset cached geolocation from previous tests")
public void testGeolocationHeaderPrimingDisabledOSPermissionBlocked() {
setPermission(ContentSettingValues.ALLOW);
LocationSettingsTestUtil.setSystemLocationSettingEnabled(false);
checkHeaderPriming(/* shouldPrimeHeader= */ false);
}
private void checkHeaderWithPermission(
final @ContentSettingValues int httpsPermission,
final long locationTime,
final boolean shouldBeNull) {
ThreadUtils.runOnUiThreadBlocking(
() -> {
PermissionInfo infoHttps =
new PermissionInfo(
ContentSettingsType.GEOLOCATION,
SEARCH_URL_1,
null,
/* isEmbargo= */ false,
SessionModel.DURABLE);
infoHttps.setContentSetting(
ProfileManager.getLastUsedRegularProfile(), httpsPermission);
String header =
GeolocationHeader.getGeoHeader(
SEARCH_URL_1, sActivityTestRule.getActivity().getActivityTab());
assertHeaderState(header, locationTime, shouldBeNull);
});
}
private void checkHeaderWithLocation(final long locationTime, final boolean shouldBeNull) {
ThreadUtils.runOnUiThreadBlocking(
() -> {
setMockLocation(locationTime);
String header =
GeolocationHeader.getGeoHeader(
SEARCH_URL_1, sActivityTestRule.getActivity().getActivityTab());
assertHeaderState(header, locationTime, shouldBeNull);
});
}
private void checkHeaderPriming(boolean shouldPrimeHeader) {
sActivityTestRule.loadUrlInNewTab("about:blank", false);
mOmniboxTestUtils.requestFocus();
mOmniboxTestUtils.typeText("aaaaaaaaaa", false);
mOmniboxTestUtils.waitAnimationsComplete();
// We use the existance of the GeolocationHeader.sFirstLocation field to indicate whether
// there has been a location request yet.
if (shouldPrimeHeader) {
Assert.assertNotEquals(
Long.MAX_VALUE, GeolocationHeader.getFirstLocationTimeForTesting());
} else {
Assert.assertEquals(Long.MAX_VALUE, GeolocationHeader.getFirstLocationTimeForTesting());
}
}
private void assertHeaderState(String header, long locationTime, boolean shouldBeNull) {
if (shouldBeNull) {
Assert.assertNull(header);
} else {
assertHeaderEquals(locationTime, header);
}
}
private long setMockLocationNow() {
long now = System.currentTimeMillis();
setMockLocation(now);
return now;
}
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;
}
private void setMockLocation(long time) {
Location location = generateMockLocation(LocationManager.NETWORK_PROVIDER, time);
GeolocationTracker.setLocationForTesting(location, null);
}
private void assertNullHeader(final String url, final boolean isIncognito) {
final Tab tab = sActivityTestRule.loadUrlInNewTab("about:blank", isIncognito);
ThreadUtils.runOnUiThreadBlocking(
() -> {
Assert.assertNull(GeolocationHeader.getGeoHeader(url, tab));
});
}
private void assertNonNullHeader(
final String url, final boolean isIncognito, final long locationTime) {
final Tab tab = sActivityTestRule.loadUrlInNewTab("about:blank", isIncognito);
ThreadUtils.runOnUiThreadBlocking(
() -> {
assertHeaderEquals(locationTime, GeolocationHeader.getGeoHeader(url, tab));
});
}
private void assertHeaderEquals(long locationTime, String header) {
long timestamp = locationTime * 1000;
// Latitude times 1e7.
int latitudeE7 = (int) (LOCATION_LAT * 10000000);
// Longitude times 1e7.
int longitudeE7 = (int) (LOCATION_LONG * 10000000);
// Radius of 68% accuracy in mm.
int radius = (int) (LOCATION_ACCURACY * 1000);
// Create a LatLng for the coordinates.
PartnerLocationDescriptor.LatLng latlng =
PartnerLocationDescriptor.LatLng.newBuilder()
.setLatitudeE7(latitudeE7)
.setLongitudeE7(longitudeE7)
.build();
// Populate a LocationDescriptor with the LatLng.
PartnerLocationDescriptor.LocationDescriptor locationDescriptor =
PartnerLocationDescriptor.LocationDescriptor.newBuilder()
.setLatlng(latlng)
// Include role, producer, timestamp and radius.
.setRole(PartnerLocationDescriptor.LocationRole.CURRENT_LOCATION)
.setProducer(PartnerLocationDescriptor.LocationProducer.DEVICE_LOCATION)
.setTimestamp(timestamp)
.setRadius((float) radius)
.build();
String locationProto =
Base64.encodeToString(
locationDescriptor.toByteArray(), Base64.NO_WRAP | Base64.URL_SAFE);
String expectedHeader = "X-Geo: w " + locationProto;
Assert.assertEquals(expectedHeader, header);
}
private void setPermission(final @ContentSettingValues int setting) {
setPermission(setting, SessionModel.DURABLE);
}
private void setOneTimeGrant() {
setPermission(ContentSettingValues.ALLOW, SessionModel.ONE_TIME);
}
private void setPermission(
final @ContentSettingValues int setting, @SessionModel.EnumType int sessionModel) {
PermissionInfo infoHttps =
new PermissionInfo(
ContentSettingsType.GEOLOCATION,
SEARCH_URL_1,
/* embedder= */ null,
/* isEmbargo= */ false,
sessionModel);
ThreadUtils.runOnUiThreadBlocking(
() -> {
infoHttps.setContentSetting(
ProfileManager.getLastUsedRegularProfile(), setting);
});
CriteriaHelper.pollUiThread(
() -> {
return infoHttps.getContentSetting(ProfileManager.getLastUsedRegularProfile())
== setting;
});
}
}