diff --git a/README.md b/README.md index 740f942c..dbdbe7fb 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ class _NavigationSampleState extends State { ); } // Note: make sure user has also granted location permissions before starting navigation session. - await GoogleMapsNavigator.initializeNavigationSession(); + await GoogleMapsNavigator.initializeNavigationSession(taskRemovedBehavior: TaskRemovedBehavior.continueService); setState(() { _navigationSessionInitialized = true; }); @@ -158,6 +158,15 @@ class _NavigationSampleState extends State { } ``` +#### Task Removed Behavior + +The `taskRemovedBehavior` parameter of navigation session initialization defines how the navigation should behave when a task is removed from the recent apps list on Android. It can either: + + - `TaskRemovedBehavior.continueService`: Continue running in the background. (default) + - `TaskRemovedBehavior.quitService`: Shut down immediately. + +This parameter has only an effect on Android. + ### Add a map view ```dart diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/Convert.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/Convert.kt index ae580f08..83c67eff 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/Convert.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/Convert.kt @@ -45,6 +45,7 @@ import com.google.android.libraries.navigation.NavigationRoadStretchRenderingDat import com.google.android.libraries.navigation.NavigationTrafficData import com.google.android.libraries.navigation.Navigator import com.google.android.libraries.navigation.Navigator.AudioGuidance +import com.google.android.libraries.navigation.Navigator.TaskRemovedBehavior import com.google.android.libraries.navigation.RouteSegment import com.google.android.libraries.navigation.RoutingOptions import com.google.android.libraries.navigation.RoutingOptions.RoutingStrategy @@ -1055,4 +1056,14 @@ object Convert { ImageDescriptorDto() } } + + fun taskRemovedBehaviorDtoToTaskRemovedBehavior( + behavior: TaskRemovedBehaviorDto? + ): @TaskRemovedBehavior Int { + return when (behavior) { + TaskRemovedBehaviorDto.CONTINUESERVICE -> TaskRemovedBehavior.CONTINUE_SERVICE + TaskRemovedBehaviorDto.QUITSERVICE -> TaskRemovedBehavior.QUIT_SERVICE + else -> TaskRemovedBehavior.CONTINUE_SERVICE + } + } } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt index c80449d5..c2832028 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionManager.kt @@ -31,6 +31,7 @@ import com.google.android.libraries.navigation.NavigationApi.NavigatorListener import com.google.android.libraries.navigation.NavigationUpdatesOptions import com.google.android.libraries.navigation.NavigationUpdatesOptions.GeneratedStepImagesType import com.google.android.libraries.navigation.Navigator +import com.google.android.libraries.navigation.Navigator.TaskRemovedBehavior import com.google.android.libraries.navigation.RoadSnappedLocationProvider import com.google.android.libraries.navigation.RouteSegment import com.google.android.libraries.navigation.RoutingOptions @@ -43,7 +44,6 @@ import com.google.android.libraries.navigation.TermsAndConditionsUIParams import com.google.android.libraries.navigation.TimeAndDistance import com.google.android.libraries.navigation.Waypoint import com.google.maps.flutter.navigation.Convert.convertTravelModeFromDto -import com.google.maps.flutter.navigation.GoogleMapsNavigationSessionManager.Companion.navigationReadyListener import io.flutter.plugin.common.BinaryMessenger import java.lang.ref.WeakReference @@ -117,6 +117,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven private var weakActivity: WeakReference? = null private var turnByTurnEventsEnabled: Boolean = false private var weakLifecycleOwner: WeakReference? = null + private var taskRemovedBehavior: @TaskRemovedBehavior Int = 0 override fun onCreate(owner: LifecycleOwner) { weakLifecycleOwner = WeakReference(owner) @@ -170,6 +171,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven /** Creates Navigator instance. */ fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, + behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { if (navigator != null) { @@ -180,6 +182,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven callback(Result.success(Unit)) return } + taskRemovedBehavior = Convert.taskRemovedBehaviorDtoToTaskRemovedBehavior(behavior) // Align API behavior with iOS: // If the terms haven't yet been accepted throw an error. @@ -202,6 +205,7 @@ private constructor(private val navigationSessionEventApi: NavigationSessionEven object : NavigatorListener { override fun onNavigatorReady(newNavigator: Navigator) { navigator = newNavigator + navigator?.setTaskRemovedBehavior(taskRemovedBehavior) registerNavigationListeners() isNavigationSessionInitialized = true navigationReadyListener?.onNavigationReady(true) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt index 219097f0..b443bfee 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationSessionMessageHandler.kt @@ -27,9 +27,10 @@ class GoogleMapsNavigationSessionMessageHandler : NavigationSessionApi { override fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, + behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) { - manager().createNavigationSession(abnormalTerminationReportingEnabled, callback) + manager().createNavigationSession(abnormalTerminationReportingEnabled, behavior, callback) } override fun isInitialized(): Boolean { diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt index 71014ecc..48205558 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/messages.g.kt @@ -507,6 +507,26 @@ enum class LaneShapeDto(val raw: Int) { } } +/** Determines how application should behave when a application task is removed. */ +enum class TaskRemovedBehaviorDto(val raw: Int) { + /** + * The default state, indicating that navigation guidance, location updates, and notification + * should persist after user removes the application task. + */ + CONTINUESERVICE(0), + /** + * Indicates that navigation guidance, location updates, and notification should shut down + * immediately when the user removes the application task. + */ + QUITSERVICE(1); + + companion object { + fun ofRaw(raw: Int): TaskRemovedBehaviorDto? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** * Object containing map options used to initialize Google Map view. * @@ -5022,6 +5042,7 @@ interface NavigationSessionApi { /** General. */ fun createNavigationSession( abnormalTerminationReportingEnabled: Boolean, + behavior: TaskRemovedBehaviorDto, callback: (Result) -> Unit, ) @@ -5137,7 +5158,8 @@ interface NavigationSessionApi { channel.setMessageHandler { message, reply -> val args = message as List val abnormalTerminationReportingEnabledArg = args[0] as Boolean - api.createNavigationSession(abnormalTerminationReportingEnabledArg) { + val behaviorArg = TaskRemovedBehaviorDto.ofRaw(args[1] as Int)!! + api.createNavigationSession(abnormalTerminationReportingEnabledArg, behaviorArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { diff --git a/android/src/test/kotlin/com/google/maps/flutter/navigation/ConvertTest.kt b/android/src/test/kotlin/com/google/maps/flutter/navigation/ConvertTest.kt index 64d88cf0..b0a7a35a 100644 --- a/android/src/test/kotlin/com/google/maps/flutter/navigation/ConvertTest.kt +++ b/android/src/test/kotlin/com/google/maps/flutter/navigation/ConvertTest.kt @@ -29,6 +29,7 @@ import com.google.android.libraries.navigation.AlternateRoutesStrategy import com.google.android.libraries.navigation.NavigationRoadStretchRenderingData import com.google.android.libraries.navigation.NavigationTrafficData import com.google.android.libraries.navigation.Navigator.AudioGuidance +import com.google.android.libraries.navigation.Navigator.TaskRemovedBehavior import com.google.android.libraries.navigation.RoutingOptions.RoutingStrategy import com.google.android.libraries.navigation.SpeedAlertSeverity import com.google.android.libraries.navigation.TimeAndDistance @@ -605,4 +606,20 @@ internal class ConvertTest { assertEquals(10.0, imageDescriptor.width) assertEquals(20.0, imageDescriptor.height) } + + @Test + fun taskRemovedBehaviorDtoToTaskRemovedBehavior_returnsExpectedValue() { + assertEquals( + TaskRemovedBehavior.QUIT_SERVICE, + Convert.taskRemovedBehaviorDtoToTaskRemovedBehavior(TaskRemovedBehaviorDto.QUITSERVICE), + ) + assertEquals( + TaskRemovedBehavior.CONTINUE_SERVICE, + Convert.taskRemovedBehaviorDtoToTaskRemovedBehavior(TaskRemovedBehaviorDto.CONTINUESERVICE), + ) + assertEquals( + TaskRemovedBehavior.CONTINUE_SERVICE, + Convert.taskRemovedBehaviorDtoToTaskRemovedBehavior(null), + ) + } } diff --git a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift index 69d6db1d..a432afeb 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/GoogleMapsNavigationSessionMessageHandler.swift @@ -57,6 +57,9 @@ class GoogleMapsNavigationSessionMessageHandler: NavigationSessionApi { } func createNavigationSession(abnormalTerminationReportingEnabled: Bool, + // taskRemovedBehaviourValue is Android only value and not used on + // iOS. + behavior: TaskRemovedBehaviorDto, completion: @escaping (Result) -> Void) { do { try GoogleMapsNavigationSessionManager.shared diff --git a/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift b/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift index 0a7ccf18..52369124 100644 --- a/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift +++ b/ios/google_navigation_flutter/Sources/google_navigation_flutter/messages.g.swift @@ -372,6 +372,16 @@ enum LaneShapeDto: Int { case uTurnRight = 9 } +/// Determines how application should behave when a application task is removed. +enum TaskRemovedBehaviorDto: Int { + /// The default state, indicating that navigation guidance, + /// location updates, and notification should persist after user removes the application task. + case continueService = 0 + /// Indicates that navigation guidance, location updates, and notification should shut down + /// immediately when the user removes the application task. + case quitService = 1 +} + /// Object containing map options used to initialize Google Map view. /// /// Generated class from Pigeon that represents data sent in messages. @@ -4618,6 +4628,7 @@ class NavigationSessionApiCodec: FlutterStandardMessageCodec { protocol NavigationSessionApi { /// General. func createNavigationSession(abnormalTerminationReportingEnabled: Bool, + behavior: TaskRemovedBehaviorDto, completion: @escaping (Result) -> Void) func isInitialized() throws -> Bool func cleanup() throws @@ -4694,17 +4705,18 @@ enum NavigationSessionApiSetup { createNavigationSessionChannel.setMessageHandler { message, reply in let args = message as! [Any?] let abnormalTerminationReportingEnabledArg = args[0] as! Bool - api - .createNavigationSession( - abnormalTerminationReportingEnabled: abnormalTerminationReportingEnabledArg - ) { result in - switch result { - case .success: - reply(wrapResult(nil)) - case let .failure(error): - reply(wrapError(error)) - } + let behaviorArg = TaskRemovedBehaviorDto(rawValue: args[1] as! Int)! + api.createNavigationSession( + abnormalTerminationReportingEnabled: abnormalTerminationReportingEnabledArg, + behavior: behaviorArg + ) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case let .failure(error): + reply(wrapError(error)) } + } } } else { createNavigationSessionChannel.setMessageHandler(nil) diff --git a/lib/src/method_channel/convert/navigation.dart b/lib/src/method_channel/convert/navigation.dart index c7cb6ce3..e310ebc9 100644 --- a/lib/src/method_channel/convert/navigation.dart +++ b/lib/src/method_channel/convert/navigation.dart @@ -206,3 +206,14 @@ extension ConvertNavigationViewOptions on NavigationViewOptions { return NavigationViewOptionsDto(navigationUIEnabledPreference: preference); } } + +extension ConvertTaskRemovedBehavior on TaskRemovedBehavior { + TaskRemovedBehaviorDto toDto() { + switch (this) { + case TaskRemovedBehavior.continueService: + return TaskRemovedBehaviorDto.continueService; + case TaskRemovedBehavior.quitService: + return TaskRemovedBehaviorDto.quitService; + } + } +} diff --git a/lib/src/method_channel/messages.g.dart b/lib/src/method_channel/messages.g.dart index 529abc5f..ad7ad4ea 100644 --- a/lib/src/method_channel/messages.g.dart +++ b/lib/src/method_channel/messages.g.dart @@ -430,6 +430,16 @@ enum LaneShapeDto { uTurnRight, } +/// Determines how application should behave when a application task is removed. +enum TaskRemovedBehaviorDto { + /// The default state, indicating that navigation guidance, + /// location updates, and notification should persist after user removes the application task. + continueService, + + /// Indicates that navigation guidance, location updates, and notification should shut down immediately when the user removes the application task. + quitService, +} + /// Object containing map options used to initialize Google Map view. class MapOptionsDto { MapOptionsDto({ @@ -5346,8 +5356,8 @@ class NavigationSessionApi { _NavigationSessionApiCodec(); /// General. - Future createNavigationSession( - bool abnormalTerminationReportingEnabled) async { + Future createNavigationSession(bool abnormalTerminationReportingEnabled, + TaskRemovedBehaviorDto behavior) async { const String __pigeon_channelName = 'dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.createNavigationSession'; final BasicMessageChannel __pigeon_channel = @@ -5356,8 +5366,9 @@ class NavigationSessionApi { pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([abnormalTerminationReportingEnabled]) as List?; + final List? __pigeon_replyList = await __pigeon_channel.send( + [abnormalTerminationReportingEnabled, behavior.index]) + as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { diff --git a/lib/src/method_channel/session_api.dart b/lib/src/method_channel/session_api.dart index 4b6a4182..3d58bbcc 100644 --- a/lib/src/method_channel/session_api.dart +++ b/lib/src/method_channel/session_api.dart @@ -48,14 +48,14 @@ class NavigationSessionAPIImpl { } /// Creates navigation session in the native platform and returns navigation session controller. - Future createNavigationSession( - bool abnormalTerminationReportingEnabled) async { + Future createNavigationSession(bool abnormalTerminationReportingEnabled, + TaskRemovedBehavior taskRemovedBehavior) async { // Setup session API streams. ensureSessionAPISetUp(); try { // Create native navigation session manager. - await _sessionApi - .createNavigationSession(abnormalTerminationReportingEnabled); + await _sessionApi.createNavigationSession( + abnormalTerminationReportingEnabled, taskRemovedBehavior.toDto()); } on PlatformException catch (e) { switch (e.code) { case 'notAuthorized': diff --git a/lib/src/navigator/google_navigation_flutter_navigator.dart b/lib/src/navigator/google_navigation_flutter_navigator.dart index eb6bf508..24ef78ff 100644 --- a/lib/src/navigator/google_navigation_flutter_navigator.dart +++ b/lib/src/navigator/google_navigation_flutter_navigator.dart @@ -50,9 +50,12 @@ class GoogleMapsNavigator { /// reporting abnormal SDK terminations such as the app crashes while the SDK is still running. /// static Future initializeNavigationSession( - {bool abnormalTerminationReportingEnabled = true}) async { + {bool abnormalTerminationReportingEnabled = true, + TaskRemovedBehavior taskRemovedBehavior = + TaskRemovedBehavior.continueService}) async { await GoogleMapsNavigationPlatform.instance.navigationSessionAPI - .createNavigationSession(abnormalTerminationReportingEnabled); + .createNavigationSession( + abnormalTerminationReportingEnabled, taskRemovedBehavior); // Enable road-snapped location updates if there are subscriptions to them. if ((_roadSnappedLocationUpdatedController?.hasListener ?? false) || diff --git a/lib/src/types/navigation_initialization_params.dart b/lib/src/types/navigation_initialization_params.dart new file mode 100644 index 00000000..ec962205 --- /dev/null +++ b/lib/src/types/navigation_initialization_params.dart @@ -0,0 +1,26 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Determines how application should behave when a application task is removed. +/// +/// Android only. +/// {@category Navigation} +enum TaskRemovedBehavior { + /// The default state, indicating that navigation guidance, + /// location updates, and notification should persist after user removes the application task. + continueService, + + /// Indicates that navigation guidance, location updates, and notification should shut down immediately when the user removes the application task. + quitService, +} diff --git a/lib/src/types/types.dart b/lib/src/types/types.dart index 3be5f36e..8a3fd312 100644 --- a/lib/src/types/types.dart +++ b/lib/src/types/types.dart @@ -18,6 +18,7 @@ export 'lat_lng.dart'; export 'lat_lng_bounds.dart'; export 'markers.dart'; export 'navigation_destinations.dart'; +export 'navigation_initialization_params.dart'; export 'navigation_view_types.dart'; export 'navinfo.dart'; export 'polygons.dart'; diff --git a/pigeons/messages.dart b/pigeons/messages.dart index a7c7ac4a..2d30811f 100644 --- a/pigeons/messages.dart +++ b/pigeons/messages.dart @@ -1182,11 +1182,22 @@ class NavInfoDto { final int? timeToNextDestinationSeconds; } +/// Determines how application should behave when a application task is removed. +enum TaskRemovedBehaviorDto { + /// The default state, indicating that navigation guidance, + /// location updates, and notification should persist after user removes the application task. + continueService, + + /// Indicates that navigation guidance, location updates, and notification should shut down immediately when the user removes the application task. + quitService, +} + @HostApi(dartHostTestHandler: 'TestNavigationSessionApi') abstract class NavigationSessionApi { /// General. @async - void createNavigationSession(bool abnormalTerminationReportingEnabled); + void createNavigationSession(bool abnormalTerminationReportingEnabled, + TaskRemovedBehaviorDto behavior); bool isInitialized(); void cleanup(); @async diff --git a/test/google_navigation_flutter_test.dart b/test/google_navigation_flutter_test.dart index 29fd3e30..99a186b2 100644 --- a/test/google_navigation_flutter_test.dart +++ b/test/google_navigation_flutter_test.dart @@ -695,8 +695,8 @@ void main() { // Initialize session and session controller. await GoogleMapsNavigator.initializeNavigationSession( abnormalTerminationReportingEnabled: false); - VerificationResult result = - verify(sessionMockApi.createNavigationSession(captureAny)); + VerificationResult result = verify( + sessionMockApi.createNavigationSession(captureAny, captureAny)); expect(result.captured[0] as bool, false); // Start/stop guidance. diff --git a/test/google_navigation_flutter_test.mocks.dart b/test/google_navigation_flutter_test.mocks.dart index 882cf015..cfe8c5df 100644 --- a/test/google_navigation_flutter_test.mocks.dart +++ b/test/google_navigation_flutter_test.mocks.dart @@ -105,11 +105,16 @@ class MockTestNavigationSessionApi extends _i1.Mock @override _i4.Future createNavigationSession( - bool? abnormalTerminationReportingEnabled) => + bool? abnormalTerminationReportingEnabled, + _i2.TaskRemovedBehaviorDto? behavior, + ) => (super.noSuchMethod( Invocation.method( #createNavigationSession, - [abnormalTerminationReportingEnabled], + [ + abnormalTerminationReportingEnabled, + behavior, + ], ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), diff --git a/test/messages_test.g.dart b/test/messages_test.g.dart index 8f54c954..e8d2c30b 100644 --- a/test/messages_test.g.dart +++ b/test/messages_test.g.dart @@ -3703,8 +3703,8 @@ abstract class TestNavigationSessionApi { _TestNavigationSessionApiCodec(); /// General. - Future createNavigationSession( - bool abnormalTerminationReportingEnabled); + Future createNavigationSession(bool abnormalTerminationReportingEnabled, + TaskRemovedBehaviorDto behavior); bool isInitialized(); @@ -3808,9 +3808,14 @@ abstract class TestNavigationSessionApi { (args[0] as bool?); assert(arg_abnormalTerminationReportingEnabled != null, 'Argument for dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.createNavigationSession was null, expected non-null bool.'); + final TaskRemovedBehaviorDto? arg_behavior = args[1] == null + ? null + : TaskRemovedBehaviorDto.values[args[1]! as int]; + assert(arg_behavior != null, + 'Argument for dev.flutter.pigeon.google_navigation_flutter.NavigationSessionApi.createNavigationSession was null, expected non-null TaskRemovedBehaviorDto.'); try { await api.createNavigationSession( - arg_abnormalTerminationReportingEnabled!); + arg_abnormalTerminationReportingEnabled!, arg_behavior!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e);