fix(android): drop geolocator for a native LocationManager channel
Removes the geolocator dependency, which transitively embedded com.google.android.gms.* (play-services-location) — proprietary classes that F-Droid's scanner rejects. The optional coarse-location button now talks to Android's own LocationManager over a small platform channel (org.comunes.tane/coarse_location): permission prompt, single coarse fix with a 12s timeout, last-known fallback, null on any failure. Behavior is unchanged and the CoarseLocationProvider interface is preserved, so the UI, i18n and widget tests need no changes. Also sets dependenciesInfo.includeInApk/Bundle = false so the AGP dependency-metadata block (also flagged by F-Droid) is not embedded. Neither change affects the Google Play build.
This commit is contained in:
parent
7f1118a243
commit
d0fabb80a9
9 changed files with 264 additions and 93 deletions
|
|
@ -56,6 +56,13 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,172 @@
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 GeolocatorCoarseLocation(),
|
location: const NativeCoarseLocation(),
|
||||||
outbox: getIt<OfferOutbox>(),
|
outbox: getIt<OfferOutbox>(),
|
||||||
messageStore: getIt<MessageStore>(),
|
messageStore: getIt<MessageStore>(),
|
||||||
profileStore: getIt<ProfileStore>(),
|
profileStore: getIt<ProfileStore>(),
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,40 @@
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:flutter/foundation.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 plugin stays at the composition root. The caller immediately reduces
|
/// platform code 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [geolocator]-backed implementation. Requests low accuracy only, and returns
|
/// Platform-channel implementation backed by Android's own LocationManager —
|
||||||
/// null on any denial/error rather than throwing, so the UI can degrade quietly.
|
/// deliberately no Google Play Services, so the APK stays free of proprietary
|
||||||
class GeolocatorCoarseLocation implements CoarseLocationProvider {
|
/// classes (F-Droid inclusion). The native side handles the permission prompt,
|
||||||
const GeolocatorCoarseLocation();
|
/// a coarse single fix with a timeout, and a last-known-position fallback.
|
||||||
|
/// 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 {
|
||||||
var permission = await Geolocator.checkPermission();
|
// Safety net over the native 12s fix timeout; the permission prompt can
|
||||||
if (permission == LocationPermission.denied) {
|
// legitimately keep the call open for a while, hence the wide margin.
|
||||||
permission = await Geolocator.requestPermission();
|
final result = await _channel
|
||||||
}
|
.invokeMapMethod<String, double>('getCoarseLatLon')
|
||||||
// Denied (or turned off): return null quietly — the UI shows an actionable
|
.timeout(const Duration(seconds: 60));
|
||||||
// message. We don't yank the user into system settings.
|
final lat = result?['lat'];
|
||||||
if (permission == LocationPermission.denied ||
|
final lon = result?['lon'];
|
||||||
permission == LocationPermission.deniedForever) {
|
if (lat == null || lon == null) return null;
|
||||||
return null;
|
return (lat: lat, lon: lon);
|
||||||
}
|
|
||||||
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 (_) {
|
} catch (_) {
|
||||||
position = await Geolocator.getLastKnownPosition();
|
return null; // denied, timeout, channel error…
|
||||||
}
|
|
||||||
if (position == null) return null;
|
|
||||||
return (lat: position.latitude, lon: position.longitude);
|
|
||||||
} catch (_) {
|
|
||||||
return null; // plugin unsupported (desktop), etc.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.0+2
|
version: 0.1.1+3
|
||||||
|
|
||||||
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 (BSD-3) to set the sharing area without
|
# Optional coarse device location goes through a small platform channel to
|
||||||
# typing a code. Low accuracy only; the value is immediately reduced to a
|
# Android's own LocationManager (lib/services/coarse_location.dart) — no
|
||||||
# low-precision geohash. Behind an interface so it's fakeable and pluggable.
|
# location plugin, and deliberately no Google Play Services, so the APK
|
||||||
geolocator: ^13.0.1
|
# stays free of proprietary classes (F-Droid).
|
||||||
# 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
|
||||||
|
|
|
||||||
59
apps/app_seeds/test/services/coarse_location_test.dart
Normal file
59
apps/app_seeds/test/services/coarse_location_test.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
#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>
|
||||||
|
|
||||||
|
|
@ -20,8 +19,6 @@ 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(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
48
pubspec.lock
48
pubspec.lock
|
|
@ -567,54 +567,6 @@ 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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue