Compare commits

..

No commits in common. "d27dc1c5539a33c72a995d658e1c8d6b4eea86f5" and "7f1118a2432b0c04a1966bc7316fe79926c18555" have entirely different histories.

13 changed files with 96 additions and 301 deletions

View file

@ -3,18 +3,7 @@
All notable changes to Tane are recorded here. Human-readable, newest All notable changes to Tane are recorded here. Human-readable, newest
first. Dates are ISO (YYYY-MM-DD). first. Dates are ISO (YYYY-MM-DD).
## [0.1.1] — 2026-07-17 ## [Unreleased] — Block 1 beta prep
### Changed
- **Fully Google-free build.** The optional "use my approximate location"
button (market setup) now talks to Android's own location service instead
of a plugin that embedded Google Play Services code. Same behavior — the
fix is still reduced on-device to a coarse area code — and the APK now
contains no proprietary classes, paving the way for F-Droid distribution.
- Release builds no longer embed Google's encrypted dependency-metadata
block in the signature.
## [0.1.0] — 2026-07-16 — Block 1 beta
Block 1 is the offline, encrypted seed inventory — useful with zero network. Block 1 is the offline, encrypted seed inventory — useful with zero network.

View file

@ -56,13 +56,6 @@ android {
} }
} }
dependenciesInfo {
// No Google-encrypted dependency-metadata block in the APK/AAB
// signature: F-Droid rejects it, and Play does not require it.
includeInApk = false
includeInBundle = false
}
buildTypes { buildTypes {
release { release {
// Sign with the real release keystore when key.properties is present, // Sign with the real release keystore when key.properties is present,

View file

@ -1,172 +1,5 @@
package org.comunes.tane package org.comunes.tane
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.CancellationSignal
import android.os.Handler
import android.os.Looper
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
/** class MainActivity : FlutterActivity()
* Hosts the "coarse location" platform channel backed by Android's own
* LocationManager. Deliberately no Google Play Services: the APK must stay
* free of proprietary classes (F-Droid). Mirrors the old plugin's behavior:
* ask permission if needed, try a single coarse fix with a timeout, fall
* back to the last known position, and reply null on any failure never
* throw across the channel.
*/
class MainActivity : FlutterActivity() {
private var pendingPermissionResult: MethodChannel.Result? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"getCoarseLatLon" -> getCoarseLatLon(result)
else -> result.notImplemented()
}
}
}
private fun getCoarseLatLon(result: MethodChannel.Result) {
if (hasLocationPermission()) {
fetchCoarseLocation(result)
return
}
if (pendingPermissionResult != null) {
// A permission prompt is already on screen; don't stack requests.
result.success(null)
return
}
pendingPermissionResult = result
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
PERMISSION_REQUEST_CODE,
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode != PERMISSION_REQUEST_CODE) return
val result = pendingPermissionResult ?: return
pendingPermissionResult = null
if (hasLocationPermission()) {
fetchCoarseLocation(result)
} else {
result.success(null)
}
}
private fun hasLocationPermission(): Boolean =
ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
private fun fetchCoarseLocation(result: MethodChannel.Result) {
val reply = SingleReply(result)
try {
val lm = getSystemService(Context.LOCATION_SERVICE) as LocationManager
val provider = pickProvider(lm)
if (provider == null) {
reply.send(lastKnownLocation(lm))
return
}
val handler = Handler(Looper.getMainLooper())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val cancel = CancellationSignal()
handler.postDelayed({
if (!reply.sent) {
cancel.cancel()
reply.send(lastKnownLocation(lm))
}
}, FIX_TIMEOUT_MS)
lm.getCurrentLocation(provider, cancel, mainExecutor) { location ->
reply.send(location ?: lastKnownLocation(lm))
}
} else {
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
reply.send(location)
}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(
provider: String?,
status: Int,
extras: android.os.Bundle?,
) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
handler.postDelayed({
if (!reply.sent) {
try {
lm.removeUpdates(listener)
} catch (_: Exception) {
}
reply.send(lastKnownLocation(lm))
}
}, FIX_TIMEOUT_MS)
@Suppress("DEPRECATION")
lm.requestSingleUpdate(provider, listener, Looper.getMainLooper())
}
} catch (_: Exception) {
// SecurityException, disabled providers, anything: degrade quietly.
reply.send(null)
}
}
/** Coarse first: the network provider is enough for a ~2 km geohash. */
private fun pickProvider(lm: LocationManager): String? = when {
lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ->
LocationManager.NETWORK_PROVIDER
lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ->
LocationManager.GPS_PROVIDER
else -> null
}
private fun lastKnownLocation(lm: LocationManager): Location? = try {
lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
?: lm.getLastKnownLocation(LocationManager.GPS_PROVIDER)
?: lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
} catch (_: Exception) {
null
}
/** Answers a MethodChannel.Result exactly once (fix vs. timeout race). */
private class SingleReply(private val result: MethodChannel.Result) {
var sent = false
private set
fun send(location: Location?) {
if (sent) return
sent = true
result.success(
location?.let { mapOf("lat" to it.latitude, "lon" to it.longitude) },
)
}
}
private companion object {
const val CHANNEL = "org.comunes.tane/coarse_location"
const val PERMISSION_REQUEST_CODE = 4711
const val FIX_TIMEOUT_MS = 12_000L
}
}

View file

@ -1 +0,0 @@
The app is now fully Google-free: the optional "use my location" button uses Android's own location service, with no Google Play Services code in the app. Paves the way for F-Droid distribution.

View file

@ -1 +0,0 @@
La aplicación ya es totalmente libre de Google: el botón opcional «usar mi ubicación» usa el servicio de ubicación propio de Android, sin código de Google Play Services en la app. Allana el camino para su distribución en F-Droid.

View file

@ -86,7 +86,7 @@ class _BootstrapState extends State<Bootstrap> {
getIt.isRegistered<SocialService>() ? getIt<SocialService>() : null, getIt.isRegistered<SocialService>() ? getIt<SocialService>() : null,
socialSettings: getIt<SocialSettings>(), socialSettings: getIt<SocialSettings>(),
connection: connection, connection: connection,
location: const NativeCoarseLocation(), location: const GeolocatorCoarseLocation(),
outbox: getIt<OfferOutbox>(), outbox: getIt<OfferOutbox>(),
messageStore: getIt<MessageStore>(), messageStore: getIt<MessageStore>(),
profileStore: getIt<ProfileStore>(), profileStore: getIt<ProfileStore>(),

View file

@ -1,40 +1,50 @@
import 'package:flutter/foundation.dart'; import 'package:geolocator/geolocator.dart';
import 'package:flutter/services.dart';
/// Supplies the device's *approximate* location (or null when unavailable or /// Supplies the device's *approximate* location (or null when unavailable or
/// denied). Behind an interface so the UI is fakeable in tests and the concrete /// denied). Behind an interface so the UI is fakeable in tests and the concrete
/// platform code stays at the composition root. The caller immediately reduces /// platform plugin stays at the composition root. The caller immediately reduces
/// the result to a low-precision geohash a precise fix never leaves the device. /// the result to a low-precision geohash a precise fix never leaves the device.
abstract interface class CoarseLocationProvider { abstract interface class CoarseLocationProvider {
Future<({double lat, double lon})?> currentCoarseLatLon(); Future<({double lat, double lon})?> currentCoarseLatLon();
} }
/// Platform-channel implementation backed by Android's own LocationManager — /// [geolocator]-backed implementation. Requests low accuracy only, and returns
/// deliberately no Google Play Services, so the APK stays free of proprietary /// null on any denial/error rather than throwing, so the UI can degrade quietly.
/// classes (F-Droid inclusion). The native side handles the permission prompt, class GeolocatorCoarseLocation implements CoarseLocationProvider {
/// a coarse single fix with a timeout, and a last-known-position fallback. const GeolocatorCoarseLocation();
/// Returns null on any denial/error rather than throwing, so the UI can
/// degrade quietly. Android only; other platforms return null.
class NativeCoarseLocation implements CoarseLocationProvider {
const NativeCoarseLocation();
static const _channel = MethodChannel('org.comunes.tane/coarse_location');
@override @override
Future<({double lat, double lon})?> currentCoarseLatLon() async { Future<({double lat, double lon})?> currentCoarseLatLon() async {
if (defaultTargetPlatform != TargetPlatform.android) return null;
try { try {
// Safety net over the native 12s fix timeout; the permission prompt can var permission = await Geolocator.checkPermission();
// legitimately keep the call open for a while, hence the wide margin. if (permission == LocationPermission.denied) {
final result = await _channel permission = await Geolocator.requestPermission();
.invokeMapMethod<String, double>('getCoarseLatLon') }
.timeout(const Duration(seconds: 60)); // Denied (or turned off): return null quietly the UI shows an actionable
final lat = result?['lat']; // message. We don't yank the user into system settings.
final lon = result?['lon']; if (permission == LocationPermission.denied ||
if (lat == null || lon == null) return null; permission == LocationPermission.deniedForever) {
return (lat: lat, lon: lon); return null;
}
if (!await Geolocator.isLocationServiceEnabled()) return null;
// A fresh coarse fix can take a while (or never come indoors); fall back
// to the last known position so the button stays responsive.
Position? position;
try {
position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.low,
timeLimit: Duration(seconds: 12),
),
);
} catch (_) {
position = await Geolocator.getLastKnownPosition();
}
if (position == null) return null;
return (lat: position.latitude, lon: position.longitude);
} catch (_) { } catch (_) {
return null; // denied, timeout, channel error return null; // plugin unsupported (desktop), etc.
} }
} }
} }

View file

@ -1,7 +1,7 @@
name: tane name: tane
description: "Tane — local-first, encrypted, decentralized traditional-seed inventory and market." description: "Tane — local-first, encrypted, decentralized traditional-seed inventory and market."
publish_to: 'none' publish_to: 'none'
version: 0.1.1+3 version: 0.1.0+2
environment: environment:
sdk: ^3.11.5 sdk: ^3.11.5
@ -67,10 +67,10 @@ dependencies:
material_symbols_icons: ^4.2951.0 material_symbols_icons: ^4.2951.0
url_launcher: ^6.3.2 url_launcher: ^6.3.2
package_info_plus: ^9.0.1 package_info_plus: ^9.0.1
# Optional coarse device location goes through a small platform channel to # Optional coarse device location (BSD-3) to set the sharing area without
# Android's own LocationManager (lib/services/coarse_location.dart) — no # typing a code. Low accuracy only; the value is immediately reduced to a
# location plugin, and deliberately no Google Play Services, so the APK # low-precision geohash. Behind an interface so it's fakeable and pluggable.
# stays free of proprietary classes (F-Droid). geolocator: ^13.0.1
# Network reachability (BSD-3) to show a global "you're offline" banner — the # Network reachability (BSD-3) to show a global "you're offline" banner — the
# social layer needs a connection; the inventory works regardless. # social layer needs a connection; the inventory works regardless.
connectivity_plus: ^6.1.0 connectivity_plus: ^6.1.0

View file

@ -1,59 +0,0 @@
import 'package:tane/services/coarse_location.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('org.comunes.tane/coarse_location');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
tearDown(() {
messenger.setMockMethodCallHandler(channel, null);
debugDefaultTargetPlatformOverride = null;
});
test('returns the lat/lon the platform channel provides', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
messenger.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'getCoarseLatLon');
return {'lat': 41.39, 'lon': 2.16};
});
final loc = await const NativeCoarseLocation().currentCoarseLatLon();
expect(loc, isNotNull);
expect(loc!.lat, 41.39);
expect(loc.lon, 2.16);
});
test('returns null when the platform reports no fix (denied/off)', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
messenger.setMockMethodCallHandler(channel, (call) async => null);
expect(await const NativeCoarseLocation().currentCoarseLatLon(), isNull);
});
test('returns null instead of throwing on a channel error', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
messenger.setMockMethodCallHandler(channel, (call) async {
throw PlatformException(code: 'boom');
});
expect(await const NativeCoarseLocation().currentCoarseLatLon(), isNull);
});
test('returns null off Android without touching the channel', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.linux;
var invoked = false;
messenger.setMockMethodCallHandler(channel, (call) async {
invoked = true;
return {'lat': 1.0, 'lon': 1.0};
});
expect(await const NativeCoarseLocation().currentCoarseLatLon(), isNull);
expect(invoked, isFalse);
});
}

View file

@ -9,6 +9,7 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <geolocator_windows/geolocator_windows.h>
#include <sqlcipher_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlcipher_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@ -19,6 +20,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar( Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
file_selector_windows file_selector_windows
flutter_secure_storage_windows flutter_secure_storage_windows
geolocator_windows
sqlcipher_flutter_libs sqlcipher_flutter_libs
url_launcher_windows url_launcher_windows
) )

View file

@ -37,29 +37,8 @@ Builds:
- export PATH="/opt/flutter/bin:$PATH" - export PATH="/opt/flutter/bin:$PATH"
- flutter build apk --release - flutter build apk --release
- versionName: 0.1.1
versionCode: 3
commit: v0.1.1
subdir: apps/app_seeds
sudo:
- apt-get update
- apt-get install -y git unzip xz-utils
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
- chown -R vagrant:vagrant /opt/flutter
output: build/app/outputs/flutter-apk/app-release.apk
prebuild:
- export PATH="/opt/flutter/bin:$PATH"
- git config --global --add safe.directory /opt/flutter
- flutter config --no-analytics
- flutter pub get
- dart run slang
- dart run build_runner build --delete-conflicting-outputs
build:
- export PATH="/opt/flutter/bin:$PATH"
- flutter build apk --release
AutoUpdateMode: Version AutoUpdateMode: Version
UpdateCheckMode: Tags v[\d.]+ UpdateCheckMode: Tags v[\d.]+
UpdateCheckData: apps/app_seeds/pubspec.yaml|version:\s*[\d.]+\+(\d+)|apps/app_seeds/pubspec.yaml|version:\s*([\d.]+)\+ UpdateCheckData: apps/app_seeds/pubspec.yaml|version:\s*[\d.]+\+(\d+)|apps/app_seeds/pubspec.yaml|version:\s*([\d.]+)\+
CurrentVersion: 0.1.1 CurrentVersion: 0.1.0
CurrentVersionCode: 3 CurrentVersionCode: 2

View file

@ -567,6 +567,54 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
geolocator:
dependency: transitive
description:
name: geolocator
sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2
url: "https://pub.dev"
source: hosted
version: "13.0.4"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
url: "https://pub.dev"
source: hosted
version: "4.6.2"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
url: "https://pub.dev"
source: hosted
version: "2.3.14"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
url: "https://pub.dev"
source: hosted
version: "4.2.8"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: "19e485a0f8d6a88abcf9c53cba3a4105e14b7435ed8ac1c108c067b938fe8429"
url: "https://pub.dev"
source: hosted
version: "4.1.4"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
get_it: get_it:
dependency: transitive dependency: transitive
description: description: