diff options
Diffstat (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeolocationTest.kt')
-rw-r--r-- | mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeolocationTest.kt | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeolocationTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeolocationTest.kt new file mode 100644 index 0000000000..4c51a4d65c --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeolocationTest.kt @@ -0,0 +1,294 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test +import android.content.Context +import android.location.LocationManager +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.lifecycle.* // ktlint-disable no-wildcard-imports +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.core.IsNot.not +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import org.mozilla.geckoview.Autofill +import org.mozilla.geckoview.GeckoResult +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule +import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.MockLocationProvider + +@RunWith(AndroidJUnit4::class) +@LargeTest +class GeolocationTest : BaseSessionTest() { + private val LOGTAG = "GeolocationTest" + private val activityRule = ActivityScenarioRule(GeckoViewTestActivity::class.java) + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var locManager: LocationManager + private lateinit var mockGpsProvider: MockLocationProvider + private lateinit var mockNetworkProvider: MockLocationProvider + + @get:Rule + override val rules: RuleChain = RuleChain.outerRule(activityRule).around(sessionRule) + + @Before + fun setup() { + activityRule.scenario.onActivity { activity -> + activity.view.setSession(mainSession) + // Prevents using the network provider for these tests + sessionRule.setPrefsUntilTestEnd(mapOf("geo.provider.testing" to false)) + locManager = activity.getSystemService(Context.LOCATION_SERVICE) as LocationManager + mockGpsProvider = sessionRule.MockLocationProvider(locManager, LocationManager.GPS_PROVIDER, 0.0, 0.0, true) + mockNetworkProvider = sessionRule.MockLocationProvider(locManager, LocationManager.NETWORK_PROVIDER, 0.0, 0.0, true) + } + } + + @After + fun cleanup() { + try { + activityRule.scenario.onActivity { activity -> + activity.view.releaseSession() + } + mockGpsProvider.removeMockLocationProvider() + mockNetworkProvider.removeMockLocationProvider() + } catch (e: Exception) {} + } + + private fun setEnableLocationPermissions() { + sessionRule.delegateDuringNextWait(object : GeckoSession.PermissionDelegate { + override fun onContentPermissionRequest( + session: GeckoSession, + perm: GeckoSession.PermissionDelegate.ContentPermission, + ): + GeckoResult<Int> { + return GeckoResult.fromValue(GeckoSession.PermissionDelegate.ContentPermission.VALUE_ALLOW) + } + override fun onAndroidPermissionsRequest( + session: GeckoSession, + permissions: Array<out String>?, + callback: GeckoSession.PermissionDelegate.Callback, + ) { + callback.grant() + } + }) + } + + private fun getCurrentPositionJS(maximumAge: Number = 0, timeout: Number = 3000, enableHighAccuracy: Boolean = false): JSONObject { + return mainSession.evaluatePromiseJS( + """ + new Promise((resolve, reject) => + window.navigator.geolocation.getCurrentPosition( + position => resolve( + {latitude: position.coords.latitude, + longitude: position.coords.longitude, + accuracy: position.coords.accuracy}), + error => reject(error.code), + {maximumAge: $maximumAge, + timeout: $timeout, + enableHighAccuracy: $enableHighAccuracy }))""", + ).value as JSONObject + } + + private fun getCurrentPositionJSWithWait(): JSONObject { + return mainSession.evaluatePromiseJS( + """ + new Promise((resolve, reject) => + setTimeout(() => { + window.navigator.geolocation.getCurrentPosition( + position => resolve( + {latitude: position.coords.latitude, longitude: position.coords.longitude})), + error => reject(error.code) + }, "750"))""", + ).value as JSONObject + } + + @GeckoSessionTestRule.NullDelegate(Autofill.Delegate::class) + // General test that location can be requested from JS and that the mock provider is providing location + @Test + fun jsContentRequestForLocation() { + val mockLat = 1.1111 + val mockLon = 2.2222 + mockGpsProvider.setMockLocation(mockLat, mockLon) + mockGpsProvider.setDoContinuallyPost(true) + mockGpsProvider.postLocation() + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + setEnableLocationPermissions() + + val position = getCurrentPositionJS() + mockGpsProvider.stopPostingLocation() + assertThat("Mocked latitude matches.", position["latitude"] as Number, equalTo(mockLat)) + assertThat("Mocked longitude matches.", position["longitude"] as Number, equalTo(mockLon)) + } + + @GeckoSessionTestRule.NullDelegate(Autofill.Delegate::class) + // Testing that more accurate location providers are selected without high accuracy enabled + @Test + fun accurateProviderSelected() { + val highAccuracy = .000001f + val highMockLat = 1.1111 + val highMockLon = 2.2222 + + // Lower accuracy should still be better than device provider ~20m + val lowAccuracy = 10.01f + val lowMockLat = 3.3333 + val lowMockLon = 4.4444 + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + setEnableLocationPermissions() + + // Test when lower accuracy is more recent + mockGpsProvider.setMockLocation(highMockLat, highMockLon, highAccuracy) + mockGpsProvider.setDoContinuallyPost(false) + mockGpsProvider.postLocation() + + // Sleep ensures the mocked locations have different clock times + Thread.sleep(10) + // Set inaccurate second, so that it is the most recent location + mockNetworkProvider.setMockLocation(lowMockLat, lowMockLon, lowAccuracy) + mockNetworkProvider.setDoContinuallyPost(false) + mockNetworkProvider.postLocation() + + val position = getCurrentPositionJS(0, 3000, false) + assertThat("Higher accuracy latitude is expected.", position["latitude"] as Number, equalTo(highMockLat)) + assertThat("Higher accuracy longitude is expected.", position["longitude"] as Number, equalTo(highMockLon)) + + // Test that higher accuracy becomes stale after 6 seconds + mockGpsProvider.postLocation() + Thread.sleep(6001) + mockNetworkProvider.postLocation() + val inaccuratePosition = getCurrentPositionJS(0, 3000, false) + assertThat("Lower accuracy latitude is expected.", inaccuratePosition["latitude"] as Number, equalTo(lowMockLat)) + assertThat("Lower accuracy longitude is expected.", inaccuratePosition["longitude"] as Number, equalTo(lowMockLon)) + } + + @GeckoSessionTestRule.NullDelegate(Autofill.Delegate::class) + // Testing that high accuracy requests a fresh location + @Test + fun highAccuracyTest() { + val accuracyMed = 4f + val accuracyHigh = .000001f + val latMedAcc = 1.1111 + val lonMedAcc = 2.2222 + val latHighAcc = 3.3333 + val lonHighAcc = 4.4444 + + // High accuracy usage requires HTTPS + mainSession.loadUri("https://example.com/") + mainSession.waitForPageStop() + setEnableLocationPermissions() + + // Have two location providers posting locations + mockNetworkProvider.setMockLocation(latMedAcc, lonMedAcc, accuracyMed) + mockNetworkProvider.setDoContinuallyPost(true) + mockNetworkProvider.postLocation() + + mockGpsProvider.setMockLocation(latHighAcc, lonHighAcc, accuracyHigh) + mockGpsProvider.setDoContinuallyPost(true) + mockGpsProvider.postLocation() + + val highAccuracyPosition = getCurrentPositionJS(0, 6001, true) + mockGpsProvider.stopPostingLocation() + mockNetworkProvider.stopPostingLocation() + + assertThat("High accuracy latitude is expected.", highAccuracyPosition["latitude"] as Number, equalTo(latHighAcc)) + assertThat("High accuracy longitude is expected.", highAccuracyPosition["longitude"] as Number, equalTo(lonHighAcc)) + } + + @GeckoSessionTestRule.NullDelegate(Autofill.Delegate::class) + // Checks that location services is reenabled after going to background + @Test + fun locationOnBackground() { + val beforePauseLat = 1.1111 + val beforePauseLon = 2.2222 + val afterPauseLat = 3.3333 + val afterPauseLon = 4.4444 + mockGpsProvider.setDoContinuallyPost(true) + + mainSession.loadTestPath(HELLO_HTML_PATH) + mainSession.waitForPageStop() + setEnableLocationPermissions() + + var actualResumeCount = 0 + var actualPauseCount = 0 + + // Monitor lifecycle changes + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + Log.i(LOGTAG, "onResume Event") + actualResumeCount++ + super.onResume(owner) + try { + mainSession.setActive(true) + // onResume is also called when starting too + if (actualResumeCount > 1) { + // Ensures the location has had time to post + Thread.sleep(3001) + val onResumeFromPausePosition = getCurrentPositionJS() + assertThat("Latitude after onPause matches.", onResumeFromPausePosition["latitude"] as Number, equalTo(afterPauseLat)) + assertThat("Longitude after onPause matches.", onResumeFromPausePosition["longitude"] as Number, equalTo(afterPauseLon)) + } + } catch (e: Exception) { + // Intermittent CI test issue where Activity is gone after resume occurs + assertThat("onResume count matches.", actualResumeCount, equalTo(2)) + assertThat("onPause count matches.", actualPauseCount, equalTo(1)) + try { + mockGpsProvider.removeMockLocationProvider() + } catch (e: Exception) { + // Cleanup could have already occurred + } + } + } + override fun onPause(owner: LifecycleOwner) { + Log.i(LOGTAG, "onPause Event") + actualPauseCount++ + super.onPause(owner) + try { + mockGpsProvider.setMockLocation(afterPauseLat, afterPauseLon) + mockGpsProvider.postLocation() + } catch (e: Exception) { + Log.w(LOGTAG, "onPause was called too late.") + // Potential situation where onPause is called too late + } + } + }) + + // Before onPause Event + mockGpsProvider.setMockLocation(beforePauseLat, beforePauseLon) + mockGpsProvider.postLocation() + val beforeOnPausePosition = getCurrentPositionJS() + assertThat("Latitude before onPause matches.", beforeOnPausePosition["latitude"] as Number, equalTo(beforePauseLat)) + assertThat("Longitude before onPause matches.", beforeOnPausePosition["longitude"] as Number, equalTo(beforePauseLon)) + + // Ensures a return to the foreground + Handler(Looper.getMainLooper()).postDelayed({ + sessionRule.requestActivityToForeground(context) + }, 1500) + + // Will cause onPause event to occur + sessionRule.simulatePressHome(context) + + // After/During onPause Event + val whilePausingPosition = getCurrentPositionJSWithWait() + mockGpsProvider.stopPostingLocation() + assertThat("Latitude after/during onPause matches.", whilePausingPosition["latitude"] as Number, equalTo(afterPauseLat)) + assertThat("Longitude after/during onPause matches.", whilePausingPosition["longitude"] as Number, equalTo(afterPauseLon)) + + assertThat("onResume count matches.", actualResumeCount, equalTo(2)) + assertThat("onPause count matches.", actualPauseCount, equalTo(1)) + } +} |