diff --git a/client/android/app/build.gradle b/client/android/app/build.gradle index 9e9f2b028..5fea8634e 100644 --- a/client/android/app/build.gradle +++ b/client/android/app/build.gradle @@ -27,6 +27,12 @@ android { compileSdkVersion flutter.compileSdkVersion ndkVersion "25.1.8937393" + packagingOptions { + jniLibs { + useLegacyPackaging true + } + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -42,11 +48,10 @@ android { defaultConfig { applicationId "com.appveyor.flet" - minSdkVersion flutter.minSdkVersion + minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - minSdkVersion 23 } buildTypes { @@ -60,4 +65,4 @@ flutter { source '../..' } -dependencies {} \ No newline at end of file +dependencies {} diff --git a/client/android/app/src/main/AndroidManifest.xml b/client/android/app/src/main/AndroidManifest.xml index 8e5c26ce2..d7c4db353 100644 --- a/client/android/app/src/main/AndroidManifest.xml +++ b/client/android/app/src/main/AndroidManifest.xml @@ -1,38 +1,58 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.appveyor.flet"> - <uses-permission android:name="android.permission.INTERNET" /> - <!-- Media access permissions. - Android 13 or higher. - https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions --> - <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> - <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> - <!-- Storage access permissions. Android 12 or lower. --> - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.RECORD_AUDIO" /> - <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> - <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> - <application android:label="Flet" android:name="${applicationName}" - android:icon="@mipmap/ic_launcher"> - <meta-data - android:name="io.flutter.embedding.android.EnableImpeller" - android:value="true"/> - <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" - android:theme="@style/LaunchTheme" - android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" - android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> - <!-- Specifies an Android theme to apply to this Activity as soon as + <uses-permission android:name="android.permission.INTERNET" /> + <!-- Media access permissions. + Android 13 or higher. + https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions --> + <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> + <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> + <!-- Storage access permissions. Android 12 or lower. --> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <!-- Google TV --> + <uses-feature android:name="android.software.leanback" android:required="false" /> + <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> + + <application + android:label="Flet" + android:name="${applicationName}" + android:enableOnBackInvokedCallback="true" + android:icon="@mipmap/ic_launcher"> + <meta-data + android:name="io.flutter.embedding.android.EnableImpeller" + android:value="false"/> + <activity + android:name=".MainActivity" + android:exported="true" + android:launchMode="singleTop" + android:theme="@style/LaunchTheme" + android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" + android:hardwareAccelerated="true" + android:windowSoftInputMode="adjustResize"> + <!-- Specifies an Android theme to apply to this Activity as soon as the Android process has started. This theme is visible to the user while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> - <meta-data android:name="io.flutter.embedding.android.NormalTheme" - android:resource="@style/NormalTheme" /> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - </activity> - <!-- Don't delete the meta-data below. + <meta-data + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> <!-- Google TV --> + </intent-filter> + </activity> + <!-- Don't delete the meta-data below. This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> - <meta-data android:name="flutterEmbedding" android:value="2" /> - </application> + <meta-data + android:name="flutterEmbedding" + android:value="2" /> + </application> </manifest> \ No newline at end of file diff --git a/client/android/build.gradle b/client/android/build.gradle index 2dcf86f90..52d31f376 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.9.24' repositories { google() mavenCentral() @@ -27,4 +27,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b..a35eb1fa3 100644 --- a/client/android/gradle/wrapper/gradle-wrapper.properties +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip diff --git a/client/android/settings.gradle b/client/android/settings.gradle index 45d92d220..b64b2eb73 100644 --- a/client/android/settings.gradle +++ b/client/android/settings.gradle @@ -23,7 +23,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version "8.3.1" apply false } -include ":app" \ No newline at end of file +include ":app" diff --git a/packages/flet_geolocator/lib/src/geolocator.dart b/packages/flet_geolocator/lib/src/geolocator.dart index 9653e90ca..353a97d44 100644 --- a/packages/flet_geolocator/lib/src/geolocator.dart +++ b/packages/flet_geolocator/lib/src/geolocator.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:convert'; import 'package:flet/flet.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -22,6 +24,8 @@ class GeolocatorControl extends StatefulWidget { class _GeolocatorControlState extends State<GeolocatorControl> with FletStoreMixin { + StreamSubscription<Position>? _positionStream; + @override void initState() { super.initState(); @@ -41,10 +45,81 @@ class _GeolocatorControlState extends State<GeolocatorControl> super.deactivate(); } + void _onPosition(Position position) { + debugPrint("Geolocator onPosition: $position"); + final jsonData = jsonEncode({ + "latitude": position.latitude, + "longitude": position.longitude, + }); + widget.backend.triggerControlEvent(widget.control.id, "position", jsonData); + } + + Future<bool> _enableLocationService() async { + late LocationSettings locationSettings; + if (defaultTargetPlatform == TargetPlatform.android) { + locationSettings = AndroidSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + forceLocationManager: true, + intervalDuration: const Duration(seconds: 30), + // Needs this or when app goes in background, background service stops working + foregroundNotificationConfig: const ForegroundNotificationConfig( + notificationText: + "Location Updates", + notificationTitle: "Running in Background", + enableWakeLock: true, + ) + ); + } else if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { + locationSettings = AppleSettings( + accuracy: LocationAccuracy.bestForNavigation, + activityType: ActivityType.automotiveNavigation, + distanceFilter: 0, + pauseLocationUpdatesAutomatically: false, + showBackgroundLocationIndicator: true, + allowBackgroundLocationUpdates: true, + ); + } else if (kIsWeb) { + locationSettings = WebSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + // maximumAge: Duration(minutes: 5) + maximumAge: Duration.zero, + ); + } else { + locationSettings = LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 0, + ); + } + + _positionStream = Geolocator.getPositionStream(locationSettings: locationSettings, + ).listen( + (Position? position) { + if (position != null) { + _onPosition(position); + debugPrint('Geolocator: ${position.latitude}, ${position.longitude}, ${position}'); + } else { + debugPrint('Geolocator: Position is null.'); + } + }, + onError: (e) { + debugPrint('Geolocator: Error getting stream position: $e'); + }, + ); + return true; + } + + Future<bool> _disableLocationService() async { + await _positionStream?.cancel(); + return true; + } + @override Widget build(BuildContext context) { debugPrint( "Geolocator build: ${widget.control.id} (${widget.control.hashCode})"); + bool onPosition = widget.control.attrBool("onPosition", false)!; () async { widget.backend.subscribeMethods(widget.control.id, @@ -59,6 +134,15 @@ class _GeolocatorControlState extends State<GeolocatorControl> case "is_location_service_enabled": var serviceEnabled = await Geolocator.isLocationServiceEnabled(); return serviceEnabled.toString(); + case "service_enable": + var serviceEnabled = false; + if (onPosition) { + serviceEnabled = await _enableLocationService(); + } + return serviceEnabled.toString(); + case "service_disable": + var serviceDisabled = await _disableLocationService(); + return serviceDisabled.toString(); case "open_app_settings": if (!kIsWeb) { var opened = await Geolocator.openAppSettings(); diff --git a/packages/flet_geolocator/pubspec.yaml b/packages/flet_geolocator/pubspec.yaml index 3d9c40bab..d10a279ad 100644 --- a/packages/flet_geolocator/pubspec.yaml +++ b/packages/flet_geolocator/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: flutter: sdk: flutter - geolocator: ^11.0.0 + geolocator: ^13.0.1 flet: path: ../flet/ @@ -20,4 +20,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 \ No newline at end of file + flutter_lints: ^2.0.0 diff --git a/packages/flet_permission_handler/pubspec.yaml b/packages/flet_permission_handler/pubspec.yaml index a39b7abb9..98375f8d4 100644 --- a/packages/flet_permission_handler/pubspec.yaml +++ b/packages/flet_permission_handler/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: collection: ^1.16.0 permission_handler: ^11.3.1 + permission_handler_html: ^0.1.3+2 flet: path: ../flet/ @@ -21,4 +22,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 \ No newline at end of file + flutter_lints: ^2.0.0 diff --git a/sdk/python/packages/flet-core/src/flet_core/geolocator.py b/sdk/python/packages/flet-core/src/flet_core/geolocator.py index 5f6c7bab3..c53ec6a9a 100644 --- a/sdk/python/packages/flet-core/src/flet_core/geolocator.py +++ b/sdk/python/packages/flet-core/src/flet_core/geolocator.py @@ -1,10 +1,16 @@ -import json from dataclasses import dataclass, field from enum import Enum from typing import Any, Optional from flet_core.control import Control from flet_core.ref import Ref +from flet_core.types import ( + OptionalEventCallable, + OptionalControlEventCallable, +) +from flet_core.event_handler import EventHandler +from flet_core.control_event import ControlEvent +import json class GeolocatorPositionAccuracy(Enum): @@ -58,12 +64,17 @@ def __init__( # ref: Optional[Ref] = None, data: Any = None, + on_position: OptionalEventCallable["PositionEvent"] = None, + ): Control.__init__( self, ref=ref, data=data, ) + self.__on_position = EventHandler(lambda e: PositionEvent(e)) + self._add_event_handler("position", self.__on_position.get_handler()) + self.on_position = on_position def _get_control_name(self): return "geolocator" @@ -192,7 +203,7 @@ async def request_permission_async( def is_location_service_enabled(self, wait_timeout: Optional[float] = 10) -> bool: enabled = self.invoke_method( - "request_permission", + "is_location_service_enabled", wait_for_result=True, wait_timeout=wait_timeout, ) @@ -245,3 +256,56 @@ async def open_location_settings_async( wait_timeout=wait_timeout, ) return opened == "true" + + def service_enable(self, wait_timeout: Optional[float] = 10) -> bool: + opened = self.invoke_method( + "service_enable", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + return opened == "true" + + async def service_enable_async( + self, wait_timeout: Optional[float] = 10 + ) -> bool: + opened = await self.invoke_method_async( + "service_enable", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + return opened == "true" + + def service_disable(self, wait_timeout: Optional[float] = 10) -> bool: + opened = self.invoke_method( + "service_disable", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + return opened == "true" + + async def service_disable_async( + self, wait_timeout: Optional[float] = 10 + ) -> bool: + opened = await self.invoke_method_async( + "service_disable", + wait_for_result=True, + wait_timeout=wait_timeout, + ) + return opened == "true" + + @property + def on_position(self) -> OptionalEventCallable["PositionEvent"]: + return self.__on_position.handler + + @on_position.setter + def on_position(self, handler: OptionalEventCallable["PositionEvent"]): + self.__on_position.handler = handler + self._set_attr("onPosition", True if handler is not None else None) + + +class PositionEvent(ControlEvent): + def __init__(self, e: ControlEvent): + super().__init__(e.target, e.name, e.data, e.control, e.page) + d = json.loads(e.data) + self.latitude: float = d.get("latitude") + self.longitude: float = d.get("longitude")