diff --git a/README.md b/README.md
index 02894d5b..74afd659 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,16 @@ guarantee a bounty currently :/
Currently there are still some core features and rewrites open I want to do involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot.
+## Custom dependencies
+
+Vendored packages are tracked in `dependencies.yaml`. Set `custom_changes: true`
+on a dependency or an individual subpackage when it contains local modifications.
+`dependencies.py` preserves these folders during normal and `--cache-only`
+updates, and keeps their local versions and paths in the managed `pubspec.yaml`
+sections. Update these packages manually and merge upstream changes with the
+local modifications. Missing custom packages must be restored from the
+`dependencies` repository.
+
## Signing Keys
When you download the app **via GitHub or F-Droid** you can verify the signing keys using for example the [AppVerifyer](https://github.com/soupslurpr/AppVerifier) and the following SHA-256 fingerprint of the signing certificate.
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7bc8a4ff..b41ec1ca 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -40,6 +40,7 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [appName: "twonly"]
}
signingConfigs {
@@ -75,6 +76,8 @@ flutter {
}
dependencies {
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'junit:junit:4.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation platform('com.google.firebase:firebase-bom:34.9.0')
implementation 'com.google.firebase:firebase-messaging'
diff --git a/android/app/src/androidTest/kotlin/eu/twonly/location/NativeLocationTest.kt b/android/app/src/androidTest/kotlin/eu/twonly/location/NativeLocationTest.kt
new file mode 100644
index 00000000..45af657e
--- /dev/null
+++ b/android/app/src/androidTest/kotlin/eu/twonly/location/NativeLocationTest.kt
@@ -0,0 +1,121 @@
+package eu.twonly.location
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.location.Criteria
+import android.location.Location
+import android.location.LocationManager
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import org.json.JSONArray
+import org.junit.After
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+
+/** Run on an emulator: the mock providers are removed after every test. */
+@SdkSuppress(minSdkVersion = 31)
+class NativeLocationTest {
+ companion object {
+ private var activityStarted = false
+ }
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val context = instrumentation.targetContext
+ private val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ private val providers = listOf("fused", LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
+
+ @Before
+ fun setUp() {
+ instrumentation.uiAutomation.grantRuntimePermission(context.packageName, Manifest.permission.ACCESS_COARSE_LOCATION)
+ instrumentation.uiAutomation.grantRuntimePermission(context.packageName, Manifest.permission.ACCESS_FINE_LOCATION)
+ if (!activityStarted) {
+ instrumentation.startActivitySync(
+ Intent(context, LocationTestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ )
+ activityStarted = true
+ }
+ instrumentation.uiAutomation.executeShellCommand(
+ "appops set ${context.packageName} android:mock_location allow",
+ ).use { android.os.ParcelFileDescriptor.AutoCloseInputStream(it).readBytes() }
+ for (provider in providers) {
+ @Suppress("DEPRECATION")
+ manager.addTestProvider(provider, false, false, false, false, true, true, true,
+ Criteria.POWER_LOW, Criteria.ACCURACY_FINE)
+ manager.setTestProviderEnabled(provider, true)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ for (provider in providers) {
+ runCatching { manager.removeTestProvider(provider) }
+ }
+ instrumentation.uiAutomation.executeShellCommand(
+ "appops set ${context.packageName} android:mock_location default",
+ ).use { android.os.ParcelFileDescriptor.AutoCloseInputStream(it).readBytes() }
+ }
+
+ private fun fix(provider: String, ageMillis: Long = 0, accuracyMeters: Float = 10f): Location =
+ Location(provider).apply {
+ latitude = 52.52
+ longitude = 13.405
+ accuracy = accuracyMeters
+ time = System.currentTimeMillis() - ageMillis
+ elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() - ageMillis * 1_000_000
+ }
+
+ private fun assertFix(value: String?) {
+ assertNotNull("A recent precise fix should be returned", value)
+ val result = JSONArray(value!!)
+ assertEquals(52.52, result.getDouble(0), 0.000001)
+ assertEquals(13.405, result.getDouble(1), 0.000001)
+ assertEquals(10.0, result.getDouble(2), 0.000001)
+ }
+
+ @Test
+ fun recentGpsFixIsReturnedWithoutWaitingForAnUpdate() {
+ manager.setTestProviderLocation(LocationManager.GPS_PROVIDER, fix(LocationManager.GPS_PROVIDER))
+ assertFix(NativeLocation.current(500))
+ }
+
+ @Test
+ fun recentFusedFixIsReturnedWithoutWaitingForGps() {
+ manager.setTestProviderLocation("fused", fix("fused"))
+ assertFix(NativeLocation.current(500))
+ }
+
+ @Test
+ fun staleCachedFixIsNotStoredAsTheCaptureLocation() {
+ manager.setTestProviderLocation(LocationManager.GPS_PROVIDER, fix(LocationManager.GPS_PROVIDER, 240_000))
+ assertNull(NativeLocation.current(200))
+ }
+
+ @Test
+ fun liveFusedUpdateIsUsedWhenThereIsNoCachedFix() {
+ manager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, false)
+ manager.setTestProviderEnabled(LocationManager.NETWORK_PROVIDER, false)
+ Handler(Looper.getMainLooper()).postDelayed({
+ manager.setTestProviderLocation("fused", fix("fused"))
+ }, 100)
+ assertFix(NativeLocation.current(2_000))
+ }
+
+ @Test
+ fun bestAvailableFixIsReturnedAtTheDeadline() {
+ manager.setTestProviderLocation(LocationManager.NETWORK_PROVIDER,
+ fix(LocationManager.NETWORK_PROVIDER, accuracyMeters = 100f))
+ val value = NativeLocation.current(200)
+ assertNotNull(value)
+ assertEquals(100.0, JSONArray(value!!).getDouble(2), 0.000001)
+ }
+
+ @Test
+ fun disabledProvidersReturnWithoutARequest() {
+ for (provider in providers) manager.setTestProviderEnabled(provider, false)
+ assertNull(NativeLocation.current(200))
+ }
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 1fcfe704..3430f5d4 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -5,6 +5,7 @@
-->
+