diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc7778..492a155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.1.0] + +- **Breaking change :** `Geolocation.requestLocationPermission` now takes a named parameter for permission +- **Breaking change :** New `GeolocationResultErrorType.permissionNotGranted` type. Previous meaning for `permissionDenied` is now divided in two different states: + - `permissionNotGranted`: User didn't accept nor decline the locationn permission request yet + - `permissionDenied`: User specifically declined the permission request +- Ability to open settings when requesting permission, and user already declined the permission previously: `Geolocation.requestLocationPermission(openSettingsIfDenied: true)` (opening the settings as fallback is now the default behaviour). +- Fix background pause/resume on iOS +- Refactor iOS internal structure + ## [1.0.2] - Fix `Accuracy.nearestTenMeters` on iOS diff --git a/README.md b/README.md index 974ce20..77102d4 100644 --- a/README.md +++ b/README.md @@ -22,29 +22,13 @@ The plugin is under active development and the following features are planned so | :----------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------: | | ![](https://github.com/loup-v/geolocation/blob/master/doc/android_screenshot.jpg?raw=true) | ![](https://github.com/loup-v/geolocation/blob/master/doc/ios_screenshot.jpg?raw=true) | -## Installation +### Installation -Add geolocation to your pubspec.yaml: +Follow the instructions: https://pub.dev/packages/geolocation#-installing-tab- -```yaml -dependencies: - geolocation: ^1.0.2 -``` - -## Import +#### Android -Package is called geolocation, with Geolocation being the base class. - -```dart -import 'package:geolocation/geolocation.dart'; -``` - -**Note:** There is a known issue for integrating swift written plugin into Flutter project created with Objective-C template. -See issue [Flutter#16049](https://github.com/flutter/flutter/issues/16049) for help on integration. - -### AndroidX Requirement - -You may need to updated your '/android/gradle.properties' to include use of AndroidX. +Geolocation is dependent on AndroidX. Make sure to include the following settings to 'android/gradle.properties': ``` android.useAndroidX=true @@ -99,13 +83,13 @@ Note that `ACCESS_FINE_LOCATION` permission includes `ACCESS_COARSE_LOCATION`. ## API For more complete documentation on all usage, check the API documentation: -https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/geolocation-library.html +https://pub.dartlang.org/documentation/geolocation/latest/geolocation/geolocation-library.html You can also check the example project that showcase a comprehensive usage of Geolocation plugin. ### Check if location service is operational -API documentation: https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/Geolocation/isLocationOperational.html +API documentation: https://pub.dartlang.org/documentation/geolocation/latest/geolocation/Geolocation/isLocationOperational.html ```dart final GeolocationResult result = await Geolocation.isLocationOperational(); @@ -118,24 +102,27 @@ if(result.isSuccessful) { ### Request location permission -On Android (api 23+) and iOS, geolocation needs to request permission at runtime. +On Android (api 23+) and iOS, apps need to request location permission at runtime. _Note: You are not required to request permission manually. Geolocation plugin will request permission automatically if it's needed, when you make a location request._ -API documentation: https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/Geolocation/requestLocationPermission.html +API documentation: https://pub.dartlang.org/documentation/geolocation/latest/geolocation/Geolocation/requestLocationPermission.html ```dart -final GeolocationResult result = await Geolocation.requestLocationPermission(const LocationPermission( - android: LocationPermissionAndroid.fine, - ios: LocationPermissionIOS.always, -)); +final GeolocationResult result = await Geolocation.requestLocationPermission( + const LocationPermission( + android: LocationPermissionAndroid.fine, + ios: LocationPermissionIOS.always, + ), + openSettingsIfDenied: true, +); if(result.isSuccessful) { // location permission is granted (or was already granted before making the request) } else { // location permission is not granted - // user might have denied, but it's also possible that location service is not enabled, restricted, and user never saw the permission request dialog + // user might have denied, but it's also possible that location service is not enabled, restricted, and user never saw the permission request dialog. Check the result.error.type for details. } ``` @@ -144,11 +131,11 @@ if(result.isSuccessful) { Geolocation offers three methods: - Last known location (best on Android): - https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/Geolocation/lastKnownLocation.html + https://pub.dartlang.org/documentation/geolocation/latest/geolocation/Geolocation/lastKnownLocation.html - Single location update (best on iOS): - https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/Geolocation/singleLocationUpdate.html + https://pub.dartlang.org/documentation/geolocation/latest/geolocation/Geolocation/singleLocationUpdate.html - Current location (best of both worlds, tries to retrieve last known location on Android, otherwise requests a single location update): - https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/Geolocation/currentLocation.html + https://pub.dartlang.org/documentation/geolocation/latest/geolocation/Geolocation/currentLocation.html ```dart // get last known location, which is a future rather than a stream (best for android) @@ -170,7 +157,7 @@ StreamSubscription subscription = Geolocation.currentLocation(ac ### Continuous location updates -API documentation: https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/Geolocation/locationUpdates.html +API documentation: https://pub.dartlang.org/documentation/geolocation/latest/geolocation/Geolocation/locationUpdates.html ```dart StreamSubscription subscription = Geolocation.locationUpdates( @@ -193,7 +180,7 @@ subscription.cancel(); Location request return either a `LocationResult` future or a stream of `LocationResult`. -API documentation: https://pub.dartlang.org/documentation/geolocation/0.2.1/geolocation/LocationResult-class.html +API documentation: https://pub.dartlang.org/documentation/geolocation/latest/geolocation/LocationResult-class.html ```dart LocationResult result = await Geolocation.lastKnownLocation(); @@ -214,10 +201,14 @@ if (result.isSuccessful) { // location services disabled on device // might be that GPS is turned off, or parental control (android) break; + case GeolocationResultErrorType.permissionNotGranted: + // location has not been requested yet + // app must request permission in order to access the location + break; case GeolocationResultErrorType.permissionDenied: - // user denied location permission request - // rejection is final on iOS, and can be on Android - // user will need to manually allow the app from the settings + // user denied the location permission for the app + // rejection is final on iOS, and can be on Android if user checks `don't ask again` + // user will need to manually allow the app from the settings, see requestLocationPermission(openSettingsIfDenied: true) break; case GeolocationResultErrorType.playServicesUnavailable: // android only diff --git a/android/.gitignore b/android/.gitignore index ba98deb..c6cbe56 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -6,5 +6,3 @@ .DS_Store /build /captures -.project -.settings \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index d4b335f..5cef8ca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,15 +1,15 @@ -group 'io.intheloup.geolocation' +group 'app.loup.geolocation' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.41' + ext.kotlin_version = '1.3.61' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -40,10 +40,10 @@ android { } dependencies { - implementation 'androidx.core:core:1.0.2' + implementation 'androidx.core:core:1.1.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC2' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0-RC2" api "com.google.android.gms:play-services-location:17.0.0" implementation 'com.squareup.moshi:moshi:1.5.0' -} \ No newline at end of file +} diff --git a/android/gradle.properties b/android/gradle.properties index 4d3226a..38c8d45 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.enableR8=true android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..019065d --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1ed28a4..c806982 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="app.loup.geolocation"> diff --git a/android/src/main/kotlin/io/intheloup/geolocation/Codec.kt b/android/src/main/kotlin/app/loup/geolocation/Codec.kt similarity index 77% rename from android/src/main/kotlin/io/intheloup/geolocation/Codec.kt rename to android/src/main/kotlin/app/loup/geolocation/Codec.kt index 501c2cd..64ae46f 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/Codec.kt +++ b/android/src/main/kotlin/app/loup/geolocation/Codec.kt @@ -1,13 +1,10 @@ // Copyright (c) 2018 Loup Inc. // Licensed under Apache License v2.0 -package io.intheloup.geolocation +package app.loup.geolocation +import app.loup.geolocation.data.* import com.squareup.moshi.Moshi -import io.intheloup.geolocation.data.LocationUpdatesRequest -import io.intheloup.geolocation.data.Permission -import io.intheloup.geolocation.data.Priority -import io.intheloup.geolocation.data.Result object Codec { @@ -29,4 +26,7 @@ object Codec { fun decodeLocationUpdatesRequest(arguments: Any?): LocationUpdatesRequest = moshi.adapter(LocationUpdatesRequest::class.java).fromJson(arguments!! as String)!! + fun decodePermissionRequest(arguments: Any?): PermissionRequest = + moshi.adapter(PermissionRequest::class.java).fromJson(arguments!! as String)!! + } diff --git a/android/src/main/kotlin/app/loup/geolocation/GeolocationPlugin.kt b/android/src/main/kotlin/app/loup/geolocation/GeolocationPlugin.kt new file mode 100644 index 0000000..9630b88 --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/GeolocationPlugin.kt @@ -0,0 +1,140 @@ +package app.loup.geolocation + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.os.Bundle +import androidx.annotation.NonNull +import app.loup.geolocation.helper.log +import app.loup.geolocation.location.LocationClient +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry.Registrar + +public class GeolocationPlugin : FlutterPlugin, ActivityAware, Application.ActivityLifecycleCallbacks { + + companion object { + @JvmStatic + fun registerWith(registrar: Registrar) { + val instance = GeolocationPlugin() + register(instance, registrar.activeContext(), registrar.messenger()) + + instance.locationClient.activity = registrar.activity() + + registrar.addRequestPermissionsResultListener(instance.locationClient.permissionResultListener) + registrar.addActivityResultListener(instance.locationClient.activityResultListener) + registrar.activity().application.registerActivityLifecycleCallbacks(instance) + } + + private fun register(instance: GeolocationPlugin, context: Context, binaryMessenger: BinaryMessenger) { + instance.locationClient = LocationClient(context) + instance.handler = Handler(instance.locationClient) + + val methodChannel = MethodChannel(binaryMessenger, "geolocation/location") + val eventChannel = EventChannel(binaryMessenger, "geolocation/locationUpdates") + + methodChannel.setMethodCallHandler(instance.handler) + eventChannel.setStreamHandler(instance.handler) + } + } + + private lateinit var locationClient: LocationClient + private lateinit var handler: Handler + private var activityBinding: ActivityPluginBinding? = null + + private fun attachToActivity(binding: ActivityPluginBinding) { + if (activityBinding != null) { + detachFromActivity() + } + activityBinding = binding + + locationClient.activity = binding.activity + + binding.addRequestPermissionsResultListener(locationClient.permissionResultListener) + binding.addActivityResultListener(locationClient.activityResultListener) + binding.activity.application.registerActivityLifecycleCallbacks(this) + } + + private fun detachFromActivity() { + val binding = activityBinding ?: return + + locationClient.activity = null + + binding.removeRequestPermissionsResultListener(locationClient.permissionResultListener) + binding.removeActivityResultListener(locationClient.activityResultListener) + binding.activity.application.unregisterActivityLifecycleCallbacks(this) + + activityBinding = null + } + + + // FlutterPlugin + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + register(this, flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + } + + + // ActivityAware + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + attachToActivity(binding) + } + + override fun onDetachedFromActivity() { + detachFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + + // Application.ActivityLifecycleCallbacks + + override fun onActivityPaused(activity: Activity?) { + locationClient.pause() + } + + override fun onActivityResumed(activity: Activity?) { + locationClient.resume() + } + + override fun onActivityStarted(activity: Activity?) { + + } + + override fun onActivityDestroyed(activity: Activity?) { + + } + + override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { + + } + + override fun onActivityStopped(activity: Activity?) { + + } + + override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { + + } + + + object Intents { + const val LocationPermissionRequestId = 12234 + const val LocationPermissionSettingsRequestId = 12230 + const val EnableLocationSettingsRequestId = 12237 + } +} diff --git a/android/src/main/kotlin/app/loup/geolocation/Handler.kt b/android/src/main/kotlin/app/loup/geolocation/Handler.kt new file mode 100644 index 0000000..2453759 --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/Handler.kt @@ -0,0 +1,80 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation + +import app.loup.geolocation.data.LocationUpdatesRequest +import app.loup.geolocation.data.Permission +import app.loup.geolocation.data.PermissionRequest +import app.loup.geolocation.location.LocationClient +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + + +class Handler(private val locationClient: LocationClient) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler { + + + private fun isLocationOperational(permission: Permission, result: MethodChannel.Result) { + result.success(Codec.encodeResult(locationClient.isLocationOperational(permission))) + } + + private fun requestLocationPermission(permission: PermissionRequest, result: MethodChannel.Result) { + GlobalScope.launch(Dispatchers.Main) { + result.success(Codec.encodeResult(locationClient.requestLocationPermission(permission))) + } + } + + private fun enableLocationSettings(result: MethodChannel.Result) { + GlobalScope.launch(Dispatchers.Main) { + result.success(Codec.encodeResult(locationClient.enableLocationServices())) + } + } + + private fun lastKnownLocation(permission: Permission, result: MethodChannel.Result) { + GlobalScope.launch(Dispatchers.Main) { + result.success(Codec.encodeResult(locationClient.lastKnownLocation(permission))) + } + } + + private fun addLocationUpdatesRequest(request: LocationUpdatesRequest) { + GlobalScope.launch(Dispatchers.Main) { + locationClient.addLocationUpdatesRequest(request) + } + } + + private fun removeLocationUpdatesRequest(id: Int) { + locationClient.removeLocationUpdatesRequest(id) + } + + + // MethodChannel.MethodCallHandler + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "isLocationOperational" -> isLocationOperational(Codec.decodePermission(call.arguments), result) + "requestLocationPermission" -> requestLocationPermission(Codec.decodePermissionRequest(call.arguments), result) + "lastKnownLocation" -> lastKnownLocation(Codec.decodePermission(call.arguments), result) + "addLocationUpdatesRequest" -> addLocationUpdatesRequest(Codec.decodeLocationUpdatesRequest(call.arguments)) + "removeLocationUpdatesRequest" -> removeLocationUpdatesRequest(Codec.decodeInt(call.arguments)) + "enableLocationServices" -> enableLocationSettings(result) + else -> result.notImplemented() + } + } + + + // EventChannel.StreamHandler + + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + locationClient.registerLocationUpdatesCallback { result -> + events.success(Codec.encodeResult(result)) + } + } + + override fun onCancel(arguments: Any?) { + locationClient.deregisterLocationUpdatesCallback() + } +} diff --git a/android/src/main/kotlin/app/loup/geolocation/data/LocationData.kt b/android/src/main/kotlin/app/loup/geolocation/data/LocationData.kt new file mode 100644 index 0000000..e1f1069 --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/LocationData.kt @@ -0,0 +1,22 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + +import android.location.Location +import android.os.Build + +class LocationData(val latitude: Double, + val longitude: Double, + val altitude: Double, + val isMocked: Boolean +) { + companion object { + fun from(location: Location) = LocationData( + latitude = location.latitude, + longitude = location.longitude, + altitude = location.altitude, + isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) location.isFromMockProvider else false + ) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/app/loup/geolocation/data/LocationUpdatesRequest.kt b/android/src/main/kotlin/app/loup/geolocation/data/LocationUpdatesRequest.kt new file mode 100644 index 0000000..7ec3e4a --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/LocationUpdatesRequest.kt @@ -0,0 +1,39 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson + + +class LocationUpdatesRequest(val id: Int, + val strategy: Strategy, + val permission: Permission, + val accuracy: Priority, + val inBackground: Boolean, + val displacementFilter: Float, + val options: Options) { + + + enum class Strategy { + Current, Single, Continuous; + + class Adapter { + @FromJson + fun fromJson(json: String): Strategy = + valueOf(json.capitalize()) + + @ToJson + fun toJson(value: Strategy): String = + value.toString().toLowerCase() + } + } + + class Options(val interval: Long?, + val fastestInterval: Long?, + val expirationTime: Long?, + val expirationDuration: Long?, + val maxWaitTime: Long?, + val numUpdates: Int?) +} \ No newline at end of file diff --git a/android/src/main/kotlin/app/loup/geolocation/data/Param.kt b/android/src/main/kotlin/app/loup/geolocation/data/Param.kt new file mode 100644 index 0000000..fc3911e --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/Param.kt @@ -0,0 +1,11 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + +object Param { + + data class SingleLocationParam(val accuracy: Facet) { + class Facet(val android: String) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/app/loup/geolocation/data/Permission.kt b/android/src/main/kotlin/app/loup/geolocation/data/Permission.kt new file mode 100644 index 0000000..c139da9 --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/Permission.kt @@ -0,0 +1,28 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + +import android.Manifest +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson + +enum class Permission { + Coarse, Fine; + + val manifestValue + get() = when (this) { + Fine -> Manifest.permission.ACCESS_FINE_LOCATION + Coarse -> Manifest.permission.ACCESS_COARSE_LOCATION + } + + class Adapter { + @FromJson + fun fromJson(json: String): Permission = + Permission.valueOf(json.capitalize()) + + @ToJson + fun toJson(value: Permission): String = + value.toString().toLowerCase() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/app/loup/geolocation/data/PermissionRequest.kt b/android/src/main/kotlin/app/loup/geolocation/data/PermissionRequest.kt new file mode 100644 index 0000000..c68c5bf --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/PermissionRequest.kt @@ -0,0 +1,8 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + + +class PermissionRequest(val value: Permission, + val openSettingsIfDenied: Boolean) \ No newline at end of file diff --git a/android/src/main/kotlin/app/loup/geolocation/data/Priority.kt b/android/src/main/kotlin/app/loup/geolocation/data/Priority.kt new file mode 100644 index 0000000..125c59d --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/Priority.kt @@ -0,0 +1,30 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + +import com.google.android.gms.location.LocationRequest +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson + +enum class Priority { + NoPower, Low, Balanced, High; + + val androidValue + get() = when (this) { + NoPower -> LocationRequest.PRIORITY_NO_POWER + Low -> LocationRequest.PRIORITY_LOW_POWER + Balanced -> LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY + High -> LocationRequest.PRIORITY_HIGH_ACCURACY + } + + class Adapter { + @FromJson + fun fromJson(json: String): Priority = + Priority.valueOf(json.capitalize()) + + @ToJson + fun toJson(value: Priority): String = + value.toString().toLowerCase() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/app/loup/geolocation/data/Result.kt b/android/src/main/kotlin/app/loup/geolocation/data/Result.kt new file mode 100644 index 0000000..a97c5a0 --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/data/Result.kt @@ -0,0 +1,57 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.data + +import com.google.android.gms.common.ConnectionResult + +data class Result(val isSuccessful: Boolean, + val data: Any? = null, + val error: Error? = null +) { + companion object { + fun success(data: Any) = Result(isSuccessful = true, data = data) + + fun failure(type: String, playServices: String? = null, message: String? = null, fatal: Boolean = false) = Result( + isSuccessful = false, + error = Error( + type = type, + playServices = playServices, + message = message, + fatal = fatal + ) + ) + } + + data class Error(val type: String, + val playServices: String?, + val message: String?, + val fatal: Boolean) { + + object Type { + const val Runtime = "runtime" + const val LocationNotFound = "locationNotFound" + const val PermissionNotGranted = "permissionNotGranted" + const val PermissionDenied = "permissionDenied" + const val ServiceDisabled = "serviceDisabled" + const val PlayServicesUnavailable = "playServicesUnavailable" + } + + object PlayServices { + const val Missing = "missing" + const val Updating = "updating" + const val VersionUpdateRequired = "versionUpdateRequired" + const val Disabled = "disabled" + const val Invalid = "invalid" + + fun fromConnectionResult(value: Int) = when (value) { + ConnectionResult.SERVICE_MISSING -> Missing + ConnectionResult.SERVICE_UPDATING -> Updating + ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED -> VersionUpdateRequired + ConnectionResult.SERVICE_DISABLED -> Disabled + ConnectionResult.SERVICE_INVALID -> Invalid + else -> throw IllegalStateException("unknown ConnectionResult: $value") + } + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/helper/Logger.kt b/android/src/main/kotlin/app/loup/geolocation/helper/Logger.kt similarity index 71% rename from android/src/main/kotlin/io/intheloup/geolocation/helper/Logger.kt rename to android/src/main/kotlin/app/loup/geolocation/helper/Logger.kt index 57624d6..d8d035b 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/helper/Logger.kt +++ b/android/src/main/kotlin/app/loup/geolocation/helper/Logger.kt @@ -1,4 +1,4 @@ -package io.intheloup.geolocation.helper +package app.loup.geolocation.helper private const val tag = "geolocation" diff --git a/android/src/main/kotlin/app/loup/geolocation/location/LocationClient.kt b/android/src/main/kotlin/app/loup/geolocation/location/LocationClient.kt new file mode 100644 index 0000000..80ca092 --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/location/LocationClient.kt @@ -0,0 +1,479 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.location + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.Location +import android.net.Uri +import android.provider.Settings +import androidx.core.app.ActivityCompat +import app.loup.geolocation.GeolocationPlugin +import app.loup.geolocation.data.* +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.ResolvableApiException +import com.google.android.gms.location.* +import io.flutter.plugin.common.PluginRegistry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + + +@SuppressLint("MissingPermission") +class LocationClient(private val context: Context) { + + var activity: Activity? = null + + val permissionResultListener: PluginRegistry.RequestPermissionsResultListener = PluginRegistry.RequestPermissionsResultListener { id, _, grantResults -> + if (id == GeolocationPlugin.Intents.LocationPermissionRequestId) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + permissionCallbacks.forEach { it.success(Unit) } + } else { + permissionCallbacks.forEach { it.failure(Unit) } + } + permissionCallbacks.clear() + return@RequestPermissionsResultListener true + } + + return@RequestPermissionsResultListener false + } + + val activityResultListener: PluginRegistry.ActivityResultListener = PluginRegistry.ActivityResultListener { id, resultCode, _ -> + when (id) { + GeolocationPlugin.Intents.LocationPermissionSettingsRequestId -> { + permissionSettingsCallback?.invoke() + permissionSettingsCallback = null + return@ActivityResultListener true + } + + GeolocationPlugin.Intents.EnableLocationSettingsRequestId -> { + if (resultCode == Activity.RESULT_OK) { + locationSettingsCallbacks.forEach { it.success(Unit) } + } else { + locationSettingsCallbacks.forEach { it.failure(Unit) } + } + locationSettingsCallbacks.clear() + + return@ActivityResultListener true + } + } + + return@ActivityResultListener false + } + + private val providerClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + private val permissionCallbacks = ArrayList>() + private var permissionSettingsCallback: (() -> Unit)? = null + private val locationSettingsCallbacks = ArrayList>() + + private var locationUpdatesCallback: ((Result) -> Unit)? = null + private val locationUpdatesRequests = ArrayList() + private var currentLocationRequest: LocationRequest? = null + + private val hasLocationRequest get() = currentLocationRequest != null + private val hasInBackgroundLocationRequest get() = locationUpdatesRequests.any { it.inBackground } + + private var isPaused = false + + private val locationCallback: LocationCallback = object : LocationCallback() { + override fun onLocationResult(result: LocationResult) { + onLocationUpdatesResult(Result.success(result.locations.map { LocationData.from(it) })) + } + } + + suspend fun enableLocationServices(): Result { + return if (LocationHelper.isLocationEnabled(context)) { + Result.success(true) + } else { + Result(requestEnablingLocation()) + } + } + + // One shot API + + fun isLocationOperational(permission: Permission): Result { + val status = currentServiceStatus(permission) + return if (status.isReady) { + Result.success(true) + } else { + status.failure!! + } + } + + suspend fun requestLocationPermission(permission: PermissionRequest): Result { + val validity = validateServiceStatus(permission) + return if (validity.isValid) { + Result.success(true) + } else { + validity.failure!! + } + } + + suspend fun lastKnownLocation(permission: Permission): Result { + val validity = validateServiceStatus(permission) + if (!validity.isValid) { + return validity.failure!! + } + + val location = try { + lastLocation() + } catch (e: Exception) { + return Result.failure(Result.Error.Type.Runtime, message = e.message) + } + + return if (location != null) { + Result.success(arrayOf(LocationData.from(location))) + } else { + Result.failure(Result.Error.Type.LocationNotFound) + } + } + + + // Updates API + + suspend fun addLocationUpdatesRequest(request: LocationUpdatesRequest) { + val validity = validateServiceStatus(request.permission) + if (!validity.isValid) { + onLocationUpdatesResult(validity.failure!!) + return + } + + locationUpdatesRequests.add(request) + updateLocationRequest() + } + + fun removeLocationUpdatesRequest(id: Int) { + locationUpdatesRequests.removeAll { it.id == id } + updateLocationRequest() + } + + fun registerLocationUpdatesCallback(callback: (Result) -> Unit) { + check(locationUpdatesCallback == null) { "trying to register a 2nd location updates callback" } + locationUpdatesCallback = callback + } + + fun deregisterLocationUpdatesCallback() { + check(locationUpdatesCallback != null) { "trying to deregister a non-existent location updates callback" } + locationUpdatesCallback = null + } + + + // Lifecycle API + + fun resume() { + if (!hasLocationRequest || !isPaused) { + return + } + + isPaused = false + updateLocationRequest() + } + + fun pause() { + if (!hasLocationRequest || isPaused || hasInBackgroundLocationRequest) { + return + } + + isPaused = true + updateLocationRequest() + providerClient.removeLocationUpdates(locationCallback) + } + + + // Location updates logic + + private fun onLocationUpdatesResult(result: Result) { + locationUpdatesCallback?.invoke(result) + } + + private fun updateLocationRequest() { + GlobalScope.launch(Dispatchers.Main) { + if (locationUpdatesRequests.isEmpty()) { + currentLocationRequest = null + isPaused = false + providerClient.removeLocationUpdates(locationCallback) + return@launch + } + + if (currentLocationRequest != null) { + providerClient.removeLocationUpdates(locationCallback) + } + + if (isPaused) { + return@launch + } + + val hasCurrentRequest = locationUpdatesRequests.any { it.strategy == LocationUpdatesRequest.Strategy.Current } + if (hasCurrentRequest) { + val lastLocationResult = lastLocationIfAvailable() + if (lastLocationResult != null) { + onLocationUpdatesResult(lastLocationResult) + + val hasOnlyCurrentRequest = locationUpdatesRequests.all { it.strategy == LocationUpdatesRequest.Strategy.Current } + if (hasOnlyCurrentRequest) { + return@launch + } + } + } + + val locationRequest = LocationRequest.create() + + locationRequest.priority = locationUpdatesRequests.map { it.accuracy.androidValue } + .sortedWith(Comparator { o1, o2 -> + when (o1) { + o2 -> 0 + LocationHelper.getBestPriority(o1, o2) -> 1 + else -> -1 + } + }) + .first() + + locationUpdatesRequests.map { it.displacementFilter } + .min()!! + .takeIf { it > 0 } + ?.let { locationRequest.smallestDisplacement = it } + + locationUpdatesRequests + .filter { it.options.interval != null } + .map { it.options.interval!! } + .min() + ?.let { locationRequest.interval = it } + + locationUpdatesRequests + .filter { it.options.fastestInterval != null } + .map { it.options.fastestInterval!! } + .min() + ?.let { locationRequest.fastestInterval = it } + + locationUpdatesRequests + .filter { it.options.expirationTime != null } + .map { it.options.expirationTime!! } + .min() + ?.let { locationRequest.expirationTime = it } + + locationUpdatesRequests + .filter { it.options.expirationDuration != null } + .map { it.options.expirationDuration!! } + .min() + ?.let { locationRequest.setExpirationDuration(it) } + + locationUpdatesRequests + .filter { it.options.maxWaitTime != null } + .map { it.options.maxWaitTime!! } + .min() + ?.let { locationRequest.maxWaitTime = it } + + if (locationUpdatesRequests.any { it.strategy == LocationUpdatesRequest.Strategy.Continuous }) { + locationUpdatesRequests + .filter { it.options.numUpdates != null } + .map { it.options.numUpdates!! } + .max() + ?.let { locationRequest.numUpdates = it } + } else { + locationRequest.numUpdates = 1 + } + + currentLocationRequest = locationRequest + + if (!isPaused) { + providerClient.requestLocationUpdates(currentLocationRequest!!, locationCallback, null) + } + } + } + + private suspend fun lastLocationIfAvailable(): Result? { + return try { + val availability = locationAvailability() + if (availability.isLocationAvailable) { + val location = lastLocation() + if (location != null) { + Result.success(arrayOf(LocationData.from(location))) + } else { + null + } + } else { + null + } + } catch (e: Exception) { + Result.failure(Result.Error.Type.Runtime, message = e.message) + } +// +// if (result != null) { +// onLocationUpdatesResult(result) +// } +// +// return result != null + } + + + // Service status + + private suspend fun validateServiceStatus(permission: Permission): ValidateServiceStatus { + return validateServiceStatus(PermissionRequest(permission, openSettingsIfDenied = false)) + } + + private suspend fun validateServiceStatus(permission: PermissionRequest): ValidateServiceStatus { + val status = currentServiceStatus(permission.value) + if (status.isReady) return ValidateServiceStatus(true) + + return if (status.needsAuthorization) { + if (requestPermission(permission.value)) { + ValidateServiceStatus(true) + } else { + ValidateServiceStatus(false, Result.failure(Result.Error.Type.PermissionDenied)) + } + } else if (status.failure!!.error!!.type == Result.Error.Type.PermissionDenied && permission.openSettingsIfDenied && tryShowSettings()) { + val refreshedStatus = currentServiceStatus(permission.value) + return if (refreshedStatus.isReady) { + ValidateServiceStatus(true) + } else { + ValidateServiceStatus(false, refreshedStatus.failure) + } + } else { + ValidateServiceStatus(false, status.failure!!) + } + } + + + private fun currentServiceStatus(permission: Permission): ServiceStatus { + val connectionResult = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) + if (connectionResult != ConnectionResult.SUCCESS) { + return ServiceStatus(isReady = false, needsAuthorization = false, + failure = Result.failure(Result.Error.Type.PlayServicesUnavailable, + playServices = Result.Error.PlayServices.fromConnectionResult(connectionResult))) + } + + if (!LocationHelper.isLocationEnabled(context)) { + return ServiceStatus(isReady = false, needsAuthorization = false, failure = Result.failure(Result.Error.Type.ServiceDisabled)) + } + + if (!LocationHelper.isPermissionDeclared(context, permission)) { + return ServiceStatus(isReady = false, needsAuthorization = false, failure = Result.failure(Result.Error.Type.Runtime, message = "Missing location permission in AndroidManifest.xml. You need to add one of ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION. See readme for details.", fatal = true)) + } + + if (activity != null && LocationHelper.isPermissionDeclined(activity!!, permission)) { + return ServiceStatus(isReady = false, needsAuthorization = false, failure = Result.failure(Result.Error.Type.PermissionDenied)) + } + + if (!LocationHelper.isPermissionGranted(context)) { + return ServiceStatus(isReady = false, needsAuthorization = true, failure = Result.failure(Result.Error.Type.PermissionNotGranted)) + } + + return ServiceStatus(true) + } + + + // Permission + + private suspend fun tryShowSettings(): Boolean = suspendCoroutine { cont -> + if (activity == null) { + cont.resume(false) + return@suspendCoroutine + } + + permissionSettingsCallback = { + cont.resume(true) + } + + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null)) + activity!!.startActivityForResult(intent, GeolocationPlugin.Intents.LocationPermissionSettingsRequestId) + } + + private suspend fun requestPermission(permission: Permission): Boolean = suspendCoroutine { cont -> + if (activity == null) { + cont.resume(false) + return@suspendCoroutine + } + + val callback = Callback( + success = { cont.resume(true) }, + failure = { cont.resume(false) } + ) + permissionCallbacks.add(callback) + + ActivityCompat.requestPermissions(activity!!, arrayOf(permission.manifestValue), GeolocationPlugin.Intents.LocationPermissionRequestId) + } + + private suspend fun requestEnablingLocation(): Boolean = suspendCoroutine { cont -> + val callback = Callback( + success = { cont.resume(true) }, + failure = { cont.resume(false) } + ) + + val request = LocationRequest() + request.priority = LocationRequest.PRIORITY_HIGH_ACCURACY + + LocationServices + .getSettingsClient(context) + .checkLocationSettings( + LocationSettingsRequest + .Builder() + .addLocationRequest(request) + .setAlwaysShow(true) + .build() + ).addOnSuccessListener { + callback.success(Unit) + }.addOnFailureListener { exception -> + when (exception) { + is ApiException -> when (exception.statusCode) { + LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> { + if (activity == null) { + callback.failure(Unit) + return@addOnFailureListener + } + + try { + val resolvable = exception as ResolvableApiException + resolvable.startResolutionForResult(activity, GeolocationPlugin.Intents.EnableLocationSettingsRequestId) + locationSettingsCallbacks.add(callback) + } catch (ignore: java.lang.Exception) { + callback.failure(Unit) + } + } + + else -> { + callback.failure(Unit) + } + } + + else -> { + callback.failure(Unit) + } + } + } + } + + // Location helpers + + private suspend fun locationAvailability(): LocationAvailability = suspendCoroutine { cont -> + providerClient.locationAvailability + .addOnSuccessListener { cont.resume(it) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + private suspend fun lastLocation(): Location? = suspendCoroutine { cont -> + providerClient.lastLocation + .addOnSuccessListener { location: Location? -> cont.resume(location) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + + // Structures + + private class Callback(val success: (T) -> Unit, val failure: (E) -> Unit) + + private class ServiceStatus(val isReady: Boolean, + val needsAuthorization: Boolean = false, + val failure: Result? = null) + + private class ValidateServiceStatus(val isValid: Boolean, val failure: Result? = null) +} diff --git a/android/src/main/kotlin/app/loup/geolocation/location/LocationHelper.kt b/android/src/main/kotlin/app/loup/geolocation/location/LocationHelper.kt new file mode 100644 index 0000000..8ab3e6b --- /dev/null +++ b/android/src/main/kotlin/app/loup/geolocation/location/LocationHelper.kt @@ -0,0 +1,61 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package app.loup.geolocation.location + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.provider.Settings +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import app.loup.geolocation.data.Permission +import com.google.android.gms.location.LocationRequest + +object LocationHelper { + + fun isPermissionDeclared(context: Context, permission: Permission): Boolean { + val permissions = context.packageManager + .getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + .requestedPermissions + + return permissions.any { it == permission.manifestValue } + } + + fun isLocationEnabled(context: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return true + } + + val locationMode = try { + Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE) + } catch (e: Settings.SettingNotFoundException) { + Settings.Secure.LOCATION_MODE_OFF + } + + return locationMode != Settings.Secure.LOCATION_MODE_OFF + } + + fun isPermissionDeclined(activity: Activity, permission: Permission) = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission.manifestValue) + + /** + * Android doesn't make any difference between coarse and fine for permission. + */ + fun isPermissionGranted(context: Context) = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED + + fun getBestPriority(p1: Int, p2: Int) = when { + p1 == LocationRequest.PRIORITY_HIGH_ACCURACY -> p1 + p2 == LocationRequest.PRIORITY_HIGH_ACCURACY -> p2 + p1 == LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY -> p1 + p2 == LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY -> p2 + p1 == LocationRequest.PRIORITY_LOW_POWER -> p1 + p2 == LocationRequest.PRIORITY_LOW_POWER -> p2 + p1 == LocationRequest.PRIORITY_NO_POWER -> p1 + p2 == LocationRequest.PRIORITY_NO_POWER -> p2 + else -> throw IllegalArgumentException("Unknown priority: $p1 vs $p2") + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt b/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt deleted file mode 100644 index dd4cb7a..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation - -import android.app.Activity -import android.app.Application -import android.os.Bundle -import io.flutter.plugin.common.PluginRegistry.Registrar -import io.intheloup.geolocation.location.LocationClient - -class GeolocationPlugin(val registrar: Registrar) { - - private val locationClient = LocationClient(registrar.activity()) - private val locationChannel = LocationChannel(locationClient) - - init { - registrar.addRequestPermissionsResultListener(locationClient.permissionResultListener) - registrar.addActivityResultListener(locationClient.activityResultListener); - registrar.activity().application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { - override fun onActivityPaused(activity: Activity?) { - locationClient.pause() - } - - override fun onActivityResumed(activity: Activity?) { - locationClient.resume() - } - - override fun onActivityStarted(activity: Activity?) { - - } - - override fun onActivityDestroyed(activity: Activity?) { - - } - - override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { - - } - - override fun onActivityStopped(activity: Activity?) { - - } - - override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { - - } - }) - - locationChannel.register(this) - } - - companion object { - - @JvmStatic - fun registerWith(registrar: Registrar) { - val plugin = GeolocationPlugin(registrar) - } - } - - object Intents { - const val LocationPermissionRequestId = 12234 - const val EnableLocationSettingsRequestId = 12237 - } -} diff --git a/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt b/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt deleted file mode 100644 index f6a1368..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation - -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.intheloup.geolocation.data.LocationUpdatesRequest -import io.intheloup.geolocation.data.Permission -import io.intheloup.geolocation.location.LocationClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - - -class LocationChannel(private val locationClient: LocationClient) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler { - - fun register(plugin: GeolocationPlugin) { - val methodChannel = MethodChannel(plugin.registrar.messenger(), "geolocation/location") - methodChannel.setMethodCallHandler(this) - - val eventChannel = EventChannel(plugin.registrar.messenger(), "geolocation/locationUpdates") - eventChannel.setStreamHandler(this) - } - - private fun isLocationOperational(permission: Permission, result: MethodChannel.Result) { - result.success(Codec.encodeResult(locationClient.isLocationOperational(permission))) - } - - private fun requestLocationPermission(permission: Permission, result: MethodChannel.Result) { - GlobalScope.launch(Dispatchers.Main) { - result.success(Codec.encodeResult(locationClient.requestLocationPermission(permission))) - } - } - - private fun enableLocationSettings(result: MethodChannel.Result) { - GlobalScope.launch(Dispatchers.Main) { - result.success(Codec.encodeResult(locationClient.enableLocationServices())) - } - } - - private fun lastKnownLocation(permission: Permission, result: MethodChannel.Result) { - GlobalScope.launch(Dispatchers.Main) { - result.success(Codec.encodeResult(locationClient.lastKnownLocation(permission))) - } - } - - private fun addLocationUpdatesRequest(request: LocationUpdatesRequest) { - GlobalScope.launch(Dispatchers.Main) { - locationClient.addLocationUpdatesRequest(request) - } - } - - private fun removeLocationUpdatesRequest(id: Int) { - locationClient.removeLocationUpdatesRequest(id) - } - - - // MethodChannel.MethodCallHandler - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - "isLocationOperational" -> isLocationOperational(Codec.decodePermission(call.arguments), result) - "requestLocationPermission" -> requestLocationPermission(Codec.decodePermission(call.arguments), result) - "lastKnownLocation" -> lastKnownLocation(Codec.decodePermission(call.arguments), result) - "addLocationUpdatesRequest" -> addLocationUpdatesRequest(Codec.decodeLocationUpdatesRequest(call.arguments)) - "removeLocationUpdatesRequest" -> removeLocationUpdatesRequest(Codec.decodeInt(call.arguments)) - "enableLocationServices" -> enableLocationSettings(result) - else -> result.notImplemented() - } - } - - - // EventChannel.StreamHandler - - override fun onListen(arguments: Any?, events: EventChannel.EventSink) { - locationClient.registerLocationUpdatesCallback { result -> - events.success(Codec.encodeResult(result)) - } - } - - override fun onCancel(arguments: Any?) { - locationClient.deregisterLocationUpdatesCallback() - } -} diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/LocationData.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/LocationData.kt deleted file mode 100644 index 540ebb2..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/data/LocationData.kt +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.data - -import android.location.Location - -class LocationData(val latitude: Double, - val longitude: Double, - val altitude: Double, - val isMocked: Boolean -) { - companion object { - fun from(location: Location) = LocationData( - latitude = location.latitude, - longitude = location.longitude, - altitude = location.altitude, - isMocked = location.isFromMockProvider() - ) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/LocationUpdatesRequest.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/LocationUpdatesRequest.kt deleted file mode 100644 index 9d4dfd6..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/data/LocationUpdatesRequest.kt +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.data - -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson - - -class LocationUpdatesRequest(val id: Int, - val strategy: Strategy, - val permission: Permission, - val accuracy: Priority, - val inBackground: Boolean, - val displacementFilter: Float, - val options: Options) { - - - enum class Strategy { - Current, Single, Continuous; - - class Adapter { - @FromJson - fun fromJson(json: String): Strategy = - valueOf(json.capitalize()) - - @ToJson - fun toJson(value: Strategy): String = - value.toString().toLowerCase() - } - } - - class Options(val interval: Long?, - val fastestInterval: Long?, - val expirationTime: Long?, - val expirationDuration: Long?, - val maxWaitTime: Long?, - val numUpdates: Int?) -} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/Param.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/Param.kt deleted file mode 100644 index 883e4c2..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/data/Param.kt +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.data - -object Param { - - data class SingleLocationParam(val accuracy: Facet) { - class Facet(val android: String) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/Permission.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/Permission.kt deleted file mode 100644 index e02225a..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/data/Permission.kt +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.data - -import android.Manifest -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson - -enum class Permission { - Coarse, Fine; - - val manifestValue get() = when(this) { - Fine -> Manifest.permission.ACCESS_FINE_LOCATION - Coarse -> Manifest.permission.ACCESS_COARSE_LOCATION - } - - class Adapter { - @FromJson - fun fromJson(json: String): Permission = - Permission.valueOf(json.capitalize()) - - @ToJson - fun toJson(value: Permission): String = - value.toString().toLowerCase() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/Priority.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/Priority.kt deleted file mode 100644 index 5c303db..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/data/Priority.kt +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.data - -import com.google.android.gms.location.LocationRequest -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson - -enum class Priority { - NoPower, Low, Balanced, High; - - val androidValue - get() = when (this) { - NoPower -> LocationRequest.PRIORITY_NO_POWER - Low -> LocationRequest.PRIORITY_LOW_POWER - Balanced -> LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY - High -> LocationRequest.PRIORITY_HIGH_ACCURACY - } - - class Adapter { - @FromJson - fun fromJson(json: String): Priority = - Priority.valueOf(json.capitalize()) - - @ToJson - fun toJson(value: Priority): String = - value.toString().toLowerCase() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/Result.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/Result.kt deleted file mode 100644 index d0229a3..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/data/Result.kt +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.data - -import com.google.android.gms.common.ConnectionResult - -data class Result(val isSuccessful: Boolean, - val data: Any? = null, - val error: Error? = null -) { - companion object { - fun success(data: Any) = Result(isSuccessful = true, data = data) - - fun failure(type: String, playServices: String? = null, message: String? = null, fatal: Boolean = false) = Result( - isSuccessful = false, - error = Result.Error( - type = type, - playServices = playServices, - message = message, - fatal = fatal - ) - ) - } - - data class Error(val type: String, - val playServices: String?, - val message: String?, - val fatal: Boolean) { - - object Type { - const val Runtime = "runtime" - const val LocationNotFound = "locationNotFound" - const val PermissionDenied = "permissionDenied" - const val ServiceDisabled = "serviceDisabled" - const val PlayServicesUnavailable = "playServicesUnavailable" - } - - object PlayServices { - const val Missing = "missing" - const val Updating = "updating" - const val VersionUpdateRequired = "versionUpdateRequired" - const val Disabled = "disabled" - const val Invalid = "invalid" - - fun fromConnectionResult(value: Int) = when (value) { - ConnectionResult.SERVICE_MISSING -> Missing - ConnectionResult.SERVICE_UPDATING -> Updating - ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED -> VersionUpdateRequired - ConnectionResult.SERVICE_DISABLED -> Disabled - ConnectionResult.SERVICE_INVALID -> Invalid - else -> throw IllegalStateException("unknown ConnectionResult: $value") - } - } - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt deleted file mode 100644 index e216650..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt +++ /dev/null @@ -1,423 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.location - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.pm.PackageManager -import android.location.Location -import androidx.core.app.ActivityCompat -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.common.api.ResolvableApiException -import com.google.android.gms.location.* -import io.flutter.plugin.common.PluginRegistry -import io.intheloup.geolocation.GeolocationPlugin -import io.intheloup.geolocation.data.LocationData -import io.intheloup.geolocation.data.LocationUpdatesRequest -import io.intheloup.geolocation.data.Permission -import io.intheloup.geolocation.data.Result -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlin.coroutines.experimental.suspendCoroutine -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.LocationSettingsRequest -import com.google.android.gms.location.LocationRequest - - -@SuppressLint("MissingPermission") -class LocationClient(private val activity: Activity) { - - val permissionResultListener: PluginRegistry.RequestPermissionsResultListener = PluginRegistry.RequestPermissionsResultListener { id, _, grantResults -> - if (id == GeolocationPlugin.Intents.LocationPermissionRequestId) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - permissionCallbacks.forEach { it.success(Unit) } - } else { - permissionCallbacks.forEach { it.failure(Unit) } - } - permissionCallbacks.clear() - return@RequestPermissionsResultListener true - } - - return@RequestPermissionsResultListener false - } - - val activityResultListener: PluginRegistry.ActivityResultListener = PluginRegistry.ActivityResultListener { id, resultCode, intent -> - if (id == GeolocationPlugin.Intents.EnableLocationSettingsRequestId) { - if (resultCode == Activity.RESULT_OK) { - locationSettingsCallbacks.forEach { it.success(Unit) } - } else { - locationSettingsCallbacks.forEach { it.failure(Unit) } - } - locationSettingsCallbacks.clear() - - return@ActivityResultListener true - } - return@ActivityResultListener false - } - - - private val providerClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(activity) - private val permissionCallbacks = ArrayList>() - private val locationSettingsCallbacks = ArrayList>() - - private var locationUpdatesCallback: ((Result) -> Unit)? = null - private val locationUpdatesRequests = ArrayList() - private var currentLocationRequest: LocationRequest? = null - - private val hasLocationRequest get() = currentLocationRequest != null - private val hasInBackgroundLocationRequest get() = locationUpdatesRequests.any { it.inBackground } - - private var isPaused = false - - private val locationCallback: LocationCallback = object : LocationCallback() { - override fun onLocationResult(result: LocationResult) { - onLocationUpdatesResult(Result.success(result.locations.map { LocationData.from(it) })) - } - } - - suspend fun enableLocationServices(): Result { - return if (LocationHelper.isLocationEnabled(activity)) { - Result.success(true) - } else { - Result(requestEnablingLocation()) - } - } - - // One shot API - - fun isLocationOperational(permission: Permission): Result { - val status = currentServiceStatus(permission) - return if (status.isReady) { - Result.success(true) - } else { - status.failure!! - } - } - - suspend fun requestLocationPermission(permission: Permission): Result { - val validity = validateServiceStatus(permission) - return if (validity.isValid) { - Result.success(true) - } else { - validity.failure!! - } - } - - suspend fun lastKnownLocation(permission: Permission): Result { - val validity = validateServiceStatus(permission) - if (!validity.isValid) { - return validity.failure!! - } - - val location = try { - lastLocation() - } catch (e: Exception) { - return Result.failure(Result.Error.Type.Runtime, message = e.message) - } - - return if (location != null) { - Result.success(arrayOf(LocationData.from(location))) - } else { - Result.failure(Result.Error.Type.LocationNotFound) - } - } - - - // Updates API - - suspend fun addLocationUpdatesRequest(request: LocationUpdatesRequest) { - val validity = validateServiceStatus(request.permission) - if (!validity.isValid) { - onLocationUpdatesResult(validity.failure!!) - return - } - - locationUpdatesRequests.add(request) - updateLocationRequest() - } - - fun removeLocationUpdatesRequest(id: Int) { - locationUpdatesRequests.removeAll { it.id == id } - updateLocationRequest() - } - - fun registerLocationUpdatesCallback(callback: (Result) -> Unit) { - check(locationUpdatesCallback == null, { "trying to register a 2nd location updates callback" }) - locationUpdatesCallback = callback - } - - fun deregisterLocationUpdatesCallback() { - check(locationUpdatesCallback != null, { "trying to deregister a non-existent location updates callback" }) - locationUpdatesCallback = null - } - - - // Lifecycle API - - fun resume() { - if (!hasLocationRequest || !isPaused) { - return - } - - isPaused = false - updateLocationRequest() - } - - fun pause() { - if (!hasLocationRequest || isPaused || hasInBackgroundLocationRequest) { - return - } - - isPaused = true - updateLocationRequest() - providerClient.removeLocationUpdates(locationCallback) - } - - - // Location updates logic - - private fun onLocationUpdatesResult(result: Result) { - locationUpdatesCallback?.invoke(result) - } - - private fun updateLocationRequest() { - GlobalScope.launch(Dispatchers.Main){ - if (locationUpdatesRequests.isEmpty()) { - currentLocationRequest = null - isPaused = false - providerClient.removeLocationUpdates(locationCallback) - return@launch - } - - if (currentLocationRequest != null) { - providerClient.removeLocationUpdates(locationCallback) - } - - if (isPaused) { - return@launch - } - - val hasCurrentRequest = locationUpdatesRequests.any { it.strategy == LocationUpdatesRequest.Strategy.Current } - if (hasCurrentRequest) { - val lastLocationResult = lastLocationIfAvailable() - if (lastLocationResult != null) { - onLocationUpdatesResult(lastLocationResult) - - val hasOnlyCurrentRequest = locationUpdatesRequests.all { it.strategy == LocationUpdatesRequest.Strategy.Current } - if (hasOnlyCurrentRequest) { - return@launch - } - } - } - - val locationRequest = LocationRequest.create() - - locationRequest.priority = locationUpdatesRequests.map { it.accuracy.androidValue } - .sortedWith(Comparator { o1, o2 -> - when (o1) { - o2 -> 0 - LocationHelper.getBestPriority(o1, o2) -> 1 - else -> -1 - } - }) - .first() - - locationUpdatesRequests.map { it.displacementFilter } - .min()!! - .takeIf { it > 0 } - ?.let { locationRequest.smallestDisplacement = it } - - locationUpdatesRequests - .filter { it.options.interval != null } - .map { it.options.interval!! } - .min() - ?.let { locationRequest.interval = it } - - locationUpdatesRequests - .filter { it.options.fastestInterval != null } - .map { it.options.fastestInterval!! } - .min() - ?.let { locationRequest.fastestInterval = it } - - locationUpdatesRequests - .filter { it.options.expirationTime != null } - .map { it.options.expirationTime!! } - .min() - ?.let { locationRequest.expirationTime = it } - - locationUpdatesRequests - .filter { it.options.expirationDuration != null } - .map { it.options.expirationDuration!! } - .min() - ?.let { locationRequest.setExpirationDuration(it) } - - locationUpdatesRequests - .filter { it.options.maxWaitTime != null } - .map { it.options.maxWaitTime!! } - .min() - ?.let { locationRequest.maxWaitTime = it } - - if (locationUpdatesRequests.any { it.strategy == LocationUpdatesRequest.Strategy.Continuous }) { - locationUpdatesRequests - .filter { it.options.numUpdates != null } - .map { it.options.numUpdates!! } - .max() - ?.let { locationRequest.numUpdates = it } - } else { - locationRequest.numUpdates = 1 - } - - currentLocationRequest = locationRequest - - if (!isPaused) { - providerClient.requestLocationUpdates(currentLocationRequest!!, locationCallback, null) - } - } - } - - private suspend fun lastLocationIfAvailable(): Result? { - return try { - val availability = locationAvailability() - if (availability.isLocationAvailable) { - val location = lastLocation() - if (location != null) { - Result.success(arrayOf(LocationData.from(location))) - } else { - null - } - } else { - null - } - } catch (e: Exception) { - Result.failure(Result.Error.Type.Runtime, message = e.message) - } -// -// if (result != null) { -// onLocationUpdatesResult(result) -// } -// -// return result != null - } - - - // Service status - - private suspend fun validateServiceStatus(permission: Permission): ValidateServiceStatus { - val status = currentServiceStatus(permission) - if (status.isReady) return ValidateServiceStatus(true) - - return if (status.needsAuthorization) { - if (requestPermission(permission)) { - ValidateServiceStatus(true) - } else { - ValidateServiceStatus(false, Result.failure(Result.Error.Type.PermissionDenied)) - } - } else { - ValidateServiceStatus(false, status.failure!!) - } - } - - private fun currentServiceStatus(permission: Permission): ServiceStatus { - val connectionResult = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) - if (connectionResult != ConnectionResult.SUCCESS) { - return ServiceStatus(false, false, - Result.failure(Result.Error.Type.PlayServicesUnavailable, - playServices = Result.Error.PlayServices.fromConnectionResult(connectionResult))) - } - - if (!LocationHelper.isLocationEnabled(activity)) { - return ServiceStatus(false, false, Result.failure(Result.Error.Type.ServiceDisabled)) - } - - if (!LocationHelper.isPermissionDeclared(activity, permission)) { - return ServiceStatus(false, false, Result.failure(Result.Error.Type.Runtime, message = "Missing location permission in AndroidManifest.xml. You need to add one of ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION. See readme for details.", fatal = true)) - } - - if (!LocationHelper.hasLocationPermission(activity)) { - return ServiceStatus(false, true, Result.failure(Result.Error.Type.PermissionDenied)) - } - - return ServiceStatus(true) - } - - - // Permission - - private suspend fun requestPermission(permission: Permission): Boolean = suspendCoroutine { cont -> - val callback = Callback( - success = { _ -> cont.resume(true) }, - failure = { _ -> cont.resume(false) } - ) - permissionCallbacks.add(callback) - - ActivityCompat.requestPermissions(activity, arrayOf(permission.manifestValue), GeolocationPlugin.Intents.LocationPermissionRequestId) - } - - private suspend fun requestEnablingLocation(): Boolean = suspendCoroutine { cont -> - val callback = Callback( - success = { _ -> cont.resume(true) }, - failure = { _ -> cont.resume(false) } - ) - val mLocationRequest = LocationRequest() - mLocationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY - - LocationServices - .getSettingsClient(activity) - .checkLocationSettings( - LocationSettingsRequest - .Builder() - .addLocationRequest(mLocationRequest) - .setAlwaysShow(true) - .build() - ).addOnSuccessListener { - callback.success - }.addOnFailureListener { ex -> - val exception: ApiException = ex as ApiException - when (exception.statusCode) { - LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> { - try { - val resolvable = exception as ResolvableApiException - resolvable.startResolutionForResult( - activity, - GeolocationPlugin.Intents.EnableLocationSettingsRequestId - ) - locationSettingsCallbacks.add(callback) - - return@addOnFailureListener - } catch (ignore: java.lang.Exception) { - } - } - } - callback.failure - } - } - - // Location helpers - - private suspend fun locationAvailability(): LocationAvailability = suspendCoroutine { cont -> - providerClient.locationAvailability - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun lastLocation(): Location? = suspendCoroutine { cont -> - providerClient.lastLocation - .addOnSuccessListener { location: Location? -> cont.resume(location) } - .addOnFailureListener { cont.resumeWithException(it) } - } - - - // Structures - - private class Callback(val success: (T) -> Unit, val failure: (E) -> Unit) - - private class ServiceStatus(val isReady: Boolean, - val needsAuthorization: Boolean = false, - val failure: Result? = null) - - private class ValidateServiceStatus(val isValid: Boolean, val failure: Result? = null) -} diff --git a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationHelper.kt b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationHelper.kt deleted file mode 100644 index 9332f7d..0000000 --- a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationHelper.kt +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 - -package io.intheloup.geolocation.location - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import android.provider.Settings -import androidx.core.content.ContextCompat -import com.google.android.gms.location.LocationRequest -import io.intheloup.geolocation.data.Permission - -object LocationHelper { - - fun isPermissionDeclared(context: Context, permission: Permission): Boolean { - val permissions = context.packageManager - .getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) - .requestedPermissions - - return when { - permissions.count { it == Manifest.permission.ACCESS_FINE_LOCATION } > 0 -> true - permissions.count { it == Manifest.permission.ACCESS_COARSE_LOCATION } > 0 && permission == Permission.Coarse -> true - else -> false - } - } - - fun isLocationEnabled(context: Context): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - return true - } - - val locationMode = try { - Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE) - } catch (e: Settings.SettingNotFoundException) { - Settings.Secure.LOCATION_MODE_OFF - } - - return locationMode != Settings.Secure.LOCATION_MODE_OFF - } - - fun hasLocationPermission(context: Context) = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - - fun getBestPriority(p1: Int, p2: Int) = when { - p1 == LocationRequest.PRIORITY_HIGH_ACCURACY -> p1 - p2 == LocationRequest.PRIORITY_HIGH_ACCURACY -> p2 - p1 == LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY -> p1 - p2 == LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY -> p2 - p1 == LocationRequest.PRIORITY_LOW_POWER -> p1 - p2 == LocationRequest.PRIORITY_LOW_POWER -> p2 - p1 == LocationRequest.PRIORITY_NO_POWER -> p1 - p2 == LocationRequest.PRIORITY_NO_POWER -> p2 - else -> throw IllegalArgumentException("Unknown priority: $p1 vs $p2") - } -} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index f9c12cb..ae1f183 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,12 +1,37 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ -.packages -.pub/ +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ -build/ +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ .flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ -.vscode +# Web related +lib/generated_plugin_registrant.dart +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..5d1241e --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md index a135626..50555d4 100644 --- a/example/README.md +++ b/example/README.md @@ -1,6 +1,6 @@ -# example +# geolocation_example -A new Flutter project. +Demonstrates how to use the geolocation plugin. ## Getting Started diff --git a/example/android/.gitignore b/example/android/.gitignore index 9764db5..bc2100d 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -1,10 +1,7 @@ -*.iml -*.class -.gradle +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat /local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -GeneratedPluginRegistrant.java \ No newline at end of file +GeneratedPluginRegistrant.java diff --git a/example/android/app/.classpath b/example/android/app/.classpath deleted file mode 100644 index eb19361..0000000 --- a/example/android/app/.classpath +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/example/android/app/.project b/example/android/app/.project deleted file mode 100644 index ac485d7..0000000 --- a/example/android/app/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - app - Project app created by Buildship. - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.buildship.core.gradleprojectnature - - diff --git a/example/android/app/.settings/org.eclipse.buildship.core.prefs b/example/android/app/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index b1886ad..0000000 --- a/example/android/app/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,2 +0,0 @@ -connection.project.dir=.. -eclipse.preferences.version=1 diff --git a/example/android/app/.settings/org.eclipse.jdt.core.prefs b/example/android/app/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 35068d9..0000000 --- a/example/android/app/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.source=1.8 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 9425582..f690a89 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -22,23 +22,28 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 28 + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + lintOptions { disable 'InvalidPackage' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.intheloup.geolocation_example" + applicationId "app.loup.geolocation_example" minSdkVersion 16 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -55,7 +60,8 @@ flutter { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index d7d9276..7a48a45 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="app.loup.geolocation_example"> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 91315f0..0ed41ef 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,36 +1,28 @@ + package="app.loup.geolocation_example"> - - - - + + diff --git a/example/android/app/src/main/java/com/example/example/MainActivity.java b/example/android/app/src/main/java/com/example/example/MainActivity.java deleted file mode 100644 index fc1e332..0000000 --- a/example/android/app/src/main/java/com/example/example/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.intheloup.geolocation_example; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/example/android/app/src/main/kotlin/app/loup/geolocation_example/MainActivity.kt b/example/android/app/src/main/kotlin/app/loup/geolocation_example/MainActivity.kt new file mode 100644 index 0000000..abb69ce --- /dev/null +++ b/example/android/app/src/main/kotlin/app/loup/geolocation_example/MainActivity.kt @@ -0,0 +1,12 @@ +package app.loup.geolocation_example + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index d7d9276..7a48a45 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="app.loup.geolocation_example"> diff --git a/example/android/build.gradle b/example/android/build.gradle index 541636c..232bc0d 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,11 +1,13 @@ buildscript { + ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1441b1d..38c8d45 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M - android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 2819f02..296b146 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/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-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/example/ios/.gitignore b/example/ios/.gitignore index ca7d1da..e96ef60 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -16,6 +16,7 @@ xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework +Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/app.flx Flutter/app.zip @@ -29,9 +30,3 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 - -# Fastlane -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots -fastlane/test_output diff --git a/example/ios/Podfile b/example/ios/Podfile index d6df24e..b30a428 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,8 +1,5 @@ -# Using a CDN with CocoaPods 1.7.2 or later can save a lot of time on pod installation, but it's experimental rather than the default. -# source 'https://cdn.cocoapods.org/' - # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +# platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -18,51 +15,67 @@ def parse_KV_file(file, separator='=') if !File.exists? file_abs_path return []; end - pods_ary = [] + generated_key_values = {} skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) { |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - pods_ary.push({:name => podname, :path => podpath}); - else - puts "Invalid plugin specification: #{line}" - end - } - return pods_ary + File.foreach(file_abs_path) do |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + generated_key_values[podname] = podpath + else + puts "Invalid plugin specification: #{line}" + end + end + generated_key_values end target 'Runner' do use_frameworks! + use_modular_headers! + + # Flutter Pod - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') + copied_flutter_dir = File.join(__dir__, 'Flutter') + copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') + copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') + unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) + # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. + # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. + # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - # Flutter Pods - generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') - if generated_xcode_build_settings.empty? - puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first." - end - generated_xcode_build_settings.map { |p| - if p[:name] == 'FLUTTER_FRAMEWORK_DIR' - symlink = File.join('.symlinks', 'flutter') - File.symlink(File.dirname(p[:path]), symlink) - pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) + generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') + unless File.exist?(generated_xcode_build_settings_path) + raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - } + generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) + cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; + + unless File.exist?(copied_framework_path) + FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) + end + unless File.exist?(copied_podspec_path) + FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) + end + end + + # Keep pod path relative so it can be checked into Podfile.lock. + pod 'Flutter', :path => 'Flutter' # Plugin Pods + + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + system('rm -rf .symlinks') + system('mkdir -p .symlinks/plugins') plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.map { |p| - symlink = File.join('.symlinks', 'plugins', p[:name]) - File.symlink(p[:path], symlink) - pod p[:name], :path => File.join(symlink, 'ios') - } + plugin_pods.each do |name, path| + symlink = File.join('.symlinks', 'plugins', name) + File.symlink(path, symlink) + pod name, :path => File.join(symlink, 'ios') + end end # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4c4f602..a2766af 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,19 +1,19 @@ PODS: - Flutter (1.0.0) - - geolocation (1.0.0): + - geolocation (0.0.1): - Flutter - streams_channel - streams_channel (0.2.1): - Flutter DEPENDENCIES: - - Flutter (from `.symlinks/flutter/ios`) + - Flutter (from `Flutter`) - geolocation (from `.symlinks/plugins/geolocation/ios`) - streams_channel (from `.symlinks/plugins/streams_channel/ios`) EXTERNAL SOURCES: Flutter: - :path: ".symlinks/flutter/ios" + :path: Flutter geolocation: :path: ".symlinks/plugins/geolocation/ios" streams_channel: @@ -21,9 +21,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: 0e3d915762c693b495b44d77113d4970485de6ec - geolocation: c90e491e495d98a7ac995cf241c020fb5d68e61e + geolocation: c194aabfd0f48cd7d6041afa8ba6542aa5714dc2 streams_channel: 733858967fb8b62e0d63a1d03f6b8f0cb7d3bfc2 -PODFILE CHECKSUM: 2ef0a40072d836b2e62fb7fcb125a5e514afe953 +PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83 COCOAPODS: 1.8.4 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 1df6bd1..689ecd2 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,11 +11,10 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 56B6427D0CFAC931233FEC7D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD5FBE4F2E53CE060C8270CE /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 7E20F6643F6BF9CB67EEB02C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF82DF53D15FD7E0D2799576 /* Pods_Runner.framework */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -39,13 +38,14 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 186311665D77F1132AF68B65 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 284F18F571AE0C013301C3F2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 4D973CD13B7EF4B7C1450560 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6FC20E83E014748F94F7AF7D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 81A939CB5DB3053087F07505 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; @@ -54,8 +54,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C9709B4C58A2AA611D80446B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - FF82DF53D15FD7E0D2799576 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD5FBE4F2E53CE060C8270CE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -65,28 +64,27 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 7E20F6643F6BF9CB67EEB02C /* Pods_Runner.framework in Frameworks */, + 56B6427D0CFAC931233FEC7D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 363A91072394481A87F6EC94 /* Pods */ = { + 2CFB1C0A4F89D85DB5CE529E /* Pods */ = { isa = PBXGroup; children = ( - C9709B4C58A2AA611D80446B /* Pods-Runner.debug.xcconfig */, - 186311665D77F1132AF68B65 /* Pods-Runner.release.xcconfig */, - 81A939CB5DB3053087F07505 /* Pods-Runner.profile.xcconfig */, + 284F18F571AE0C013301C3F2 /* Pods-Runner.debug.xcconfig */, + 4D973CD13B7EF4B7C1450560 /* Pods-Runner.release.xcconfig */, + 6FC20E83E014748F94F7AF7D /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; - 7CDE9D9CFFE00FDEF1393E23 /* Frameworks */ = { + 7DA2150B0254667CFBB28F48 /* Frameworks */ = { isa = PBXGroup; children = ( - FF82DF53D15FD7E0D2799576 /* Pods_Runner.framework */, + DD5FBE4F2E53CE060C8270CE /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -110,8 +108,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 363A91072394481A87F6EC94 /* Pods */, - 7CDE9D9CFFE00FDEF1393E23 /* Frameworks */, + 2CFB1C0A4F89D85DB5CE529E /* Pods */, + 7DA2150B0254667CFBB28F48 /* Frameworks */, ); sourceTree = ""; }; @@ -153,14 +151,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - AB7481ABE9FDF74140265198 /* [CP] Check Pods Manifest.lock */, + E1392B1526720DCF994ACF8C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 02C7DA88F48E0AD8CF1A7926 /* [CP] Embed Pods Frameworks */, + B1244165708ECC323D130A55 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -182,7 +180,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 0910; + LastSwiftMigration = 1100; }; }; }; @@ -211,7 +209,6 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -220,50 +217,50 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 02C7DA88F48E0AD8CF1A7926 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + B1244165708ECC323D130A55 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run Script"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - AB7481ABE9FDF74140265198 /* [CP] Check Pods Manifest.lock */ = { + E1392B1526720DCF994ACF8C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -361,9 +358,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -387,10 +385,10 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.intheloup.geolocation_example; + PRODUCT_BUNDLE_IDENTIFIER = app.loup.geolocationExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -443,7 +441,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -493,9 +491,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -520,11 +519,11 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.intheloup.geolocation_example; + PRODUCT_BUNDLE_IDENTIFIER = app.loup.geolocationExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -547,10 +546,10 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.intheloup.geolocation_example; + PRODUCT_BUNDLE_IDENTIFIER = app.loup.geolocationExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 919434a..1d526a1 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "group:Runner.xcodeproj"> diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 3cafb16..85ee63d 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,12 +2,8 @@ - NSLocationWhenInUseUsageDescription - Reason why app needs location - NSLocationAlwaysAndWhenInUseUsageDescription - Reason why app needs location CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,7 +11,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - geolocation_example + Geolocation CFBundlePackageType APPL CFBundleShortVersionString @@ -45,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + NSLocationWhenInUseUsageDescription + Reason why app needs location + NSLocationAlwaysAndWhenInUseUsageDescription + Reason why app needs location diff --git a/example/lib/tab_location.dart b/example/lib/tab_location.dart index a036044..943ccf9 100644 --- a/example/lib/tab_location.dart +++ b/example/lib/tab_location.dart @@ -216,8 +216,7 @@ class _Item extends StatelessWidget { String text; if (data.result.isSuccessful) { text = - 'Lat: ${data.result.location.latitude} - Lng: ${data.result.location - .longitude}'; + 'Lat: ${data.result.location.latitude} - Lng: ${data.result.location.longitude}'; } else { switch (data.result.error.type) { case GeolocationResultErrorType.runtime: @@ -229,6 +228,9 @@ class _Item extends StatelessWidget { case GeolocationResultErrorType.serviceDisabled: text = 'Service disabled'; break; + case GeolocationResultErrorType.permissionNotGranted: + text = 'Permission not granted'; + break; case GeolocationResultErrorType.permissionDenied: text = 'Permission denied'; break; @@ -250,8 +252,7 @@ class _Item extends StatelessWidget { height: 3.0, ), new Text( - 'Elapsed time: ${data.elapsedTimeSeconds == 0 ? '< 1' : data - .elapsedTimeSeconds}s', + 'Elapsed time: ${data.elapsedTimeSeconds == 0 ? '< 1' : data.elapsedTimeSeconds}s', style: const TextStyle(fontSize: 12.0, color: Colors.grey), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/example/lib/tab_settings.dart b/example/lib/tab_settings.dart index 4c8f6ad..aafe01c 100644 --- a/example/lib/tab_settings.dart +++ b/example/lib/tab_settings.dart @@ -16,14 +16,9 @@ class _TabSettingsState extends State { GeolocationResult _locationOperationalResult; GeolocationResult _requestPermissionResult; - @override - initState() { - super.initState(); - _isLocationOperationalPressed(); - } - - _isLocationOperationalPressed() async { + _checkLocationOperational() async { final GeolocationResult result = await Geolocation.isLocationOperational(); + if (mounted) { setState(() { _locationOperationalResult = result; @@ -31,12 +26,16 @@ class _TabSettingsState extends State { } } - _requestLocationPermissionPressed() async { + _requestPermission() async { final GeolocationResult result = - await Geolocation.requestLocationPermission(const LocationPermission( - android: LocationPermissionAndroid.fine, - ios: LocationPermissionIOS.always, - )); + await Geolocation.requestLocationPermission( + permission: const LocationPermission( + android: LocationPermissionAndroid.fine, + ios: LocationPermissionIOS.always, + ), + openSettingsIfDenied: true, + ); + if (mounted) { setState(() { _requestPermissionResult = result; @@ -53,14 +52,16 @@ class _TabSettingsState extends State { body: new ListView( children: ListTile.divideTiles(context: context, tiles: [ new _Item( - isPermissionRequest: false, + title: 'Is location operational', + successLabel: 'Yes', result: _locationOperationalResult, - onPressed: _isLocationOperationalPressed, + onPressed: _checkLocationOperational, ), new _Item( - isPermissionRequest: true, + title: 'Request permission', + successLabel: 'Granted', result: _requestPermissionResult, - onPressed: _requestLocationPermissionPressed, + onPressed: _requestPermission, ), ]).toList(), ), @@ -69,42 +70,48 @@ class _TabSettingsState extends State { } class _Item extends StatelessWidget { - _Item({@required this.isPermissionRequest, this.result, this.onPressed}); - - final bool isPermissionRequest; + _Item({ + @required this.title, + @required this.successLabel, + @required this.result, + @required this.onPressed, + }); + + final String title; + final String successLabel; final GeolocationResult result; final VoidCallback onPressed; @override Widget build(BuildContext context) { - String text; + String value; String status; Color color; if (result != null) { if (result.isSuccessful) { - text = isPermissionRequest - ? 'Location permission granted' - : 'Location is operational'; - + value = successLabel; status = 'success'; color = Colors.green; } else { switch (result.error.type) { case GeolocationResultErrorType.runtime: - text = 'Failure: ${result.error.message}'; + value = 'Failure: ${result.error.message}'; break; case GeolocationResultErrorType.locationNotFound: - text = 'Location not found'; + value = 'Location not found'; break; case GeolocationResultErrorType.serviceDisabled: - text = 'Service disabled'; + value = 'Service disabled'; + break; + case GeolocationResultErrorType.permissionNotGranted: + value = 'Permission not granted'; break; case GeolocationResultErrorType.permissionDenied: - text = 'Permission denied'; + value = 'Permission denied'; break; case GeolocationResultErrorType.playServicesUnavailable: - text = 'Play services unavailable: ${result.error.additionalInfo}'; + value = 'Play services unavailable: ${result.error.additionalInfo}'; break; } @@ -112,27 +119,26 @@ class _Item extends StatelessWidget { color = Colors.red; } } else { - text = 'Is ${isPermissionRequest - ? 'permission granted' - : 'location operational'}?'; - + value = 'Unknown'; status = 'undefined'; color = Colors.blueGrey; } + final text = '$title: $value'; + final List content = [ - new Text( + Text( text, - style: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500), - maxLines: 1, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + maxLines: 2, overflow: TextOverflow.ellipsis, ), - new SizedBox( - height: 3.0, + const SizedBox( + height: 3, ), - new Text( - 'Tap to request', - style: const TextStyle(fontSize: 12.0, color: Colors.grey), + Text( + 'Tap to trigger', + style: const TextStyle(fontSize: 12, color: Colors.grey), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -143,7 +149,7 @@ class _Item extends StatelessWidget { child: new Container( color: Colors.white, child: new SizedBox( - height: 56.0, + height: 80, child: new Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: new Row( diff --git a/example/lib/tab_track.dart b/example/lib/tab_track.dart index 838fbc4..53e09c6 100644 --- a/example/lib/tab_track.dart +++ b/example/lib/tab_track.dart @@ -41,13 +41,11 @@ class _TabTrackState extends State { }); _subscriptionStartedTimestamp = new DateTime.now().millisecondsSinceEpoch; - _subscription = Geolocation - .locationUpdates( + _subscription = Geolocation.locationUpdates( accuracy: LocationAccuracy.best, displacementFilter: 0.0, inBackground: false, - ) - .listen((result) { + ).listen((result) { final location = new _LocationData( result: result, elapsedTimeSeconds: (new DateTime.now().millisecondsSinceEpoch - @@ -159,8 +157,7 @@ class _Item extends StatelessWidget { if (data.result.isSuccessful) { text = - 'Lat: ${data.result.location.latitude} - Lng: ${data.result.location - .longitude}'; + 'Lat: ${data.result.location.latitude} - Lng: ${data.result.location.longitude}'; status = 'success'; color = Colors.green; } else { @@ -174,6 +171,9 @@ class _Item extends StatelessWidget { case GeolocationResultErrorType.serviceDisabled: text = 'Service disabled'; break; + case GeolocationResultErrorType.permissionNotGranted: + text = 'Permission not granted'; + break; case GeolocationResultErrorType.permissionDenied: text = 'Permission denied'; break; @@ -198,8 +198,7 @@ class _Item extends StatelessWidget { height: 3.0, ), new Text( - 'Elapsed time: ${data.elapsedTimeSeconds == 0 ? '< 1' : data - .elapsedTimeSeconds}s', + 'Elapsed time: ${data.elapsedTimeSeconds == 0 ? '< 1' : data.elapsedTimeSeconds}s', style: const TextStyle(fontSize: 12.0, color: Colors.grey), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 707ff35..8125a58 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,17 +1,7 @@ name: geolocation_example -description: A new Flutter project. - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +description: Geolocation example version: 1.0.0+1 +publish_to: "none" environment: sdk: ">=2.1.0 <3.0.0" @@ -20,55 +10,11 @@ dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 dev_dependencies: - flutter_test: - sdk: flutter - geolocation: path: ../ -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/geolocation.iml b/geolocation.iml index 73e7ebd..429df7d 100644 --- a/geolocation.iml +++ b/geolocation.iml @@ -8,11 +8,11 @@ - + diff --git a/geolocation_android.iml b/geolocation_android.iml deleted file mode 100644 index ac5d744..0000000 --- a/geolocation_android.iml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/.gitignore b/ios/.gitignore index 710ec6c..aa479fd 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -34,3 +34,4 @@ Icon? .tags* /Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/ios/Classes/Channel/Codec.swift b/ios/Classes/Channel/Codec.swift index 12fa58c..ed5d903 100644 --- a/ios/Classes/Channel/Codec.swift +++ b/ios/Classes/Channel/Codec.swift @@ -24,4 +24,8 @@ struct Codec { static func decodeLocationUpdatesRequest(from arguments: Any?) -> LocationUpdatesRequest { return try! jsonDecoder.decode(LocationUpdatesRequest.self, from: (arguments as! String).data(using: .utf8)!) } + + static func decodePermissionRequest(from arguments: Any?) -> PermissionRequest { + return try! jsonDecoder.decode(PermissionRequest.self, from: (arguments as! String).data(using: .utf8)!) + } } diff --git a/ios/Classes/Channel/LocationChannels.swift b/ios/Classes/Channel/Handler.swift similarity index 76% rename from ios/Classes/Channel/LocationChannels.swift rename to ios/Classes/Channel/Handler.swift index 7296c14..90403d1 100644 --- a/ios/Classes/Channel/LocationChannels.swift +++ b/ios/Classes/Channel/Handler.swift @@ -7,30 +7,22 @@ import Foundation import CoreLocation import streams_channel -class LocationChannels { - +class Handler { + private let locationClient: LocationClient - private let locationUpdatesHandler: LocationUpdatesHandler + public let locationUpdatesHandler: LocationUpdatesHandler init(locationClient: LocationClient) { self.locationClient = locationClient self.locationUpdatesHandler = LocationUpdatesHandler(locationClient: locationClient) } - func register(on plugin: SwiftGeolocationPlugin) { - let methodChannel = FlutterMethodChannel(name: "geolocation/location", binaryMessenger: plugin.registrar.messenger()) - methodChannel.setMethodCallHandler(handleMethodCall(_:result:)) - - let eventChannel = FlutterEventChannel(name: "geolocation/locationUpdates", binaryMessenger: plugin.registrar.messenger()) - eventChannel.setStreamHandler(locationUpdatesHandler) - } - - private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + public func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "isLocationOperational": isLocationOperational(permission: Codec.decodePermission(from: call.arguments), on: result) case "requestLocationPermission": - requestLocationPermission(permission: Codec.decodePermission(from: call.arguments), on: result) + requestLocationPermission(permission: Codec.decodePermissionRequest(from: call.arguments), on: result) case "lastKnownLocation": lastKnownLocation(permission: Codec.decodePermission(from: call.arguments), on: result) case "addLocationUpdatesRequest": @@ -46,7 +38,7 @@ class LocationChannels { flutterResult(Codec.encode(result: locationClient.isLocationOperational(with: permission))) } - private func requestLocationPermission(permission: Permission, on flutterResult: @escaping FlutterResult) { + private func requestLocationPermission(permission: PermissionRequest, on flutterResult: @escaping FlutterResult) { locationClient.requestLocationPermission(with: permission) { result in flutterResult(Codec.encode(result: result)) } diff --git a/ios/Classes/Data/PermissionRequest.swift b/ios/Classes/Data/PermissionRequest.swift new file mode 100644 index 0000000..4d7ee32 --- /dev/null +++ b/ios/Classes/Data/PermissionRequest.swift @@ -0,0 +1,11 @@ +// +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 +// + +import Foundation + +struct PermissionRequest: Codable { + let value: Permission + let openSettingsIfDenied: Bool +} diff --git a/ios/Classes/Data/Result.swift b/ios/Classes/Data/Result.swift index c8c4f97..6c8bd5e 100644 --- a/ios/Classes/Data/Result.swift +++ b/ios/Classes/Data/Result.swift @@ -27,6 +27,7 @@ struct ResultError: Codable { enum Kind: String, Codable { case runtime = "runtime" case locationNotFound = "locationNotFound" + case permissionNotGranted = "permissionNotGranted" case permissionDenied = "permissionDenied" case serviceDisabled = "serviceDisabled" } diff --git a/ios/Classes/GeolocationPlugin.h b/ios/Classes/GeolocationPlugin.h index f4c7692..9a07b05 100644 --- a/ios/Classes/GeolocationPlugin.h +++ b/ios/Classes/GeolocationPlugin.h @@ -1,8 +1,3 @@ -// -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 -// - #import @interface GeolocationPlugin : NSObject diff --git a/ios/Classes/GeolocationPlugin.m b/ios/Classes/GeolocationPlugin.m index b1235e2..0ec9cb9 100644 --- a/ios/Classes/GeolocationPlugin.m +++ b/ios/Classes/GeolocationPlugin.m @@ -1,10 +1,12 @@ -// -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 -// - #import "GeolocationPlugin.h" +#if __has_include() #import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "geolocation-Swift.h" +#endif @implementation GeolocationPlugin + (void)registerWithRegistrar:(NSObject*)registrar { diff --git a/ios/Classes/Location/LocationClient.swift b/ios/Classes/Location/LocationClient.swift index 9b1671b..e4fc901 100644 --- a/ios/Classes/Location/LocationClient.swift +++ b/ios/Classes/Location/LocationClient.swift @@ -10,6 +10,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() private var permissionCallbacks: Array> = [] + private var permissionSettingsCallback: (() -> Void)? = nil private var locationUpdatesCallback: LocationUpdatesCallback? = nil private var locationUpdatesRequests: Array = [] @@ -36,7 +37,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { return status.isReady ? Result.success(with: true) : status.failure! } - func requestLocationPermission(with permission: Permission, _ callback: @escaping (Result) -> Void) { + func requestLocationPermission(with permission: PermissionRequest, _ callback: @escaping (Result) -> Void) { runWithValidServiceStatus(with: permission, success: { callback(Result.success(with: true)) }, failure: { result in @@ -67,7 +68,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { } func removeLocationUpdates(requestId: Int) { - guard let index = locationUpdatesRequests.index(where: { $0.id == requestId }) else { + guard let index = locationUpdatesRequests.firstIndex(where: { $0.id == requestId }) else { return } @@ -89,6 +90,11 @@ class LocationClient : NSObject, CLLocationManagerDelegate { // Lifecycle API func resume() { + if let callback = permissionSettingsCallback { + callback() + permissionSettingsCallback = nil + } + guard hasLocationRequest && isPaused else { return } @@ -145,7 +151,12 @@ class LocationClient : NSObject, CLLocationManagerDelegate { // Service status private func runWithValidServiceStatus(with permission: Permission, success: @escaping () -> Void, failure: @escaping (Result) -> Void) { - let status: ServiceStatus = currentServiceStatus(with: permission) + let permissionRequest = PermissionRequest(value: permission, openSettingsIfDenied: false) + runWithValidServiceStatus(with: permissionRequest, success: success, failure: failure) + } + + private func runWithValidServiceStatus(with permission: PermissionRequest, success: @escaping () -> Void, failure: @escaping (Result) -> Void) { + let status: ServiceStatus = currentServiceStatus(with: permission.value) if status.isReady { success() @@ -158,7 +169,23 @@ class LocationClient : NSObject, CLLocationManagerDelegate { permissionCallbacks.append(callback) locationManager.requestAuthorization(for: permission) } else { - failure(status.failure!) + if #available(iOS 10.0, *), + status.failure!.error!.type == .permissionDenied, + permission.openSettingsIfDenied, + let appSettingURl = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(appSettingURl) { + permissionSettingsCallback = { + let refreshedStatus: ServiceStatus = self.currentServiceStatus(with: permission.value) + if refreshedStatus.isReady { + success() + } else { + failure(refreshedStatus.failure!) + } + } + UIApplication.shared.openURL(appSettingURl) + } else { + failure(status.failure!) + } } } } @@ -174,13 +201,15 @@ class LocationClient : NSObject, CLLocationManagerDelegate { return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .runtime, message: "Missing location usage description values in Info.plist. See readme for details.", fatal: true)) } - return ServiceStatus(isReady: false, needsAuthorization: permission, failure: Result.failure(of: .permissionDenied)) + return ServiceStatus(isReady: false, needsAuthorization: permission, failure: Result.failure(of: .permissionNotGranted)) case .denied: return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .permissionDenied)) case .restricted: return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .serviceDisabled)) case .authorizedWhenInUse, .authorizedAlways: return ServiceStatus(isReady: true, needsAuthorization: nil, failure: nil) + @unknown default: + fatalError("Unknown CLLocationManager.authorizationStatus(): \(CLLocationManager.authorizationStatus())") } } diff --git a/ios/Classes/Location/LocationHelper.swift b/ios/Classes/Location/LocationHelper.swift index 36a470e..3a3e1b1 100644 --- a/ios/Classes/Location/LocationHelper.swift +++ b/ios/Classes/Location/LocationHelper.swift @@ -19,6 +19,6 @@ struct LocationHelper { ] static func betterAccuracy(between a1: CLLocationAccuracy, and a2: CLLocationAccuracy) -> CLLocationAccuracy { - return accuracies.index(of: a1)! > accuracies.index(of: a2)! ? a1 : a2 + return accuracies.firstIndex(of: a1)! > accuracies.firstIndex(of: a2)! ? a1 : a2 } } diff --git a/ios/Classes/SwiftGeolocationPlugin.swift b/ios/Classes/SwiftGeolocationPlugin.swift index 1b9a77b..dbdbc8a 100644 --- a/ios/Classes/SwiftGeolocationPlugin.swift +++ b/ios/Classes/SwiftGeolocationPlugin.swift @@ -1,8 +1,3 @@ -// -// Copyright (c) 2018 Loup Inc. -// Licensed under Apache License v2.0 -// - import Flutter import UIKit import CoreLocation @@ -10,25 +5,28 @@ import CoreLocation @available(iOS 9.0, *) public class SwiftGeolocationPlugin: NSObject, FlutterPlugin, UIApplicationDelegate { - internal let registrar: FlutterPluginRegistrar private let locationClient = LocationClient() - private let locationChannels: LocationChannels + private let handler: Handler - init(registrar: FlutterPluginRegistrar) { - self.registrar = registrar - self.locationChannels = LocationChannels(locationClient: locationClient) + override init() { + self.handler = Handler(locationClient: locationClient) super.init() - - registrar.addApplicationDelegate(self) - locationChannels.register(on: self) } public static func register(with registrar: FlutterPluginRegistrar) { - _ = SwiftGeolocationPlugin(registrar: registrar) + let methodChannel = FlutterMethodChannel(name: "geolocation/location", binaryMessenger: registrar.messenger()) + let eventChannel = FlutterEventChannel(name: "geolocation/locationUpdates", binaryMessenger: registrar.messenger()) + + let instance = SwiftGeolocationPlugin() + + registrar.addApplicationDelegate(instance) + registrar.addMethodCallDelegate(instance, channel: methodChannel) + eventChannel.setStreamHandler(instance.handler.locationUpdatesHandler) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + handler.handleMethodCall(call, result: result) } - - - // UIApplicationDelegate public func applicationDidBecomeActive(_ application: UIApplication) { locationClient.resume() @@ -38,4 +36,3 @@ public class SwiftGeolocationPlugin: NSObject, FlutterPlugin, UIApplicationDeleg locationClient.pause() } } - diff --git a/ios/geolocation.podspec b/ios/geolocation.podspec index 842cad9..216e81b 100644 --- a/ios/geolocation.podspec +++ b/ios/geolocation.podspec @@ -1,19 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint geolocation.podspec' to validate before publishing. +# Pod::Spec.new do |s| s.name = 'geolocation' - s.version = '1.0.0' - s.summary = 'Geolocation plugin for iOS and Android.' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' s.description = <<-DESC -Geolocation plugin for iOS and Android. +Flutter Geolocation plugin for iOS and Android. DESC s.homepage = 'https://github.com/loup-v/geolocation' s.license = { :file => '../LICENSE' } - s.author = { 'Loup Inc.' => 'hello@intheloup.io' } + s.author = { 'Loup Inc.' => 'hello@loup.app' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' s.dependency 'streams_channel' s.frameworks = 'CoreLocation' - s.ios.deployment_target = '9.0' -end \ No newline at end of file + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.swift_version = '5.0' +end diff --git a/lib/channel/codec.dart b/lib/channel/codec.dart index ddcbf28..d99b6b1 100644 --- a/lib/channel/codec.dart +++ b/lib/channel/codec.dart @@ -11,7 +11,7 @@ class _Codec { _JsonCodec.locationResultFromJson(json.decode(data)); static String encodeLocationPermission(LocationPermission permission) => - platformSpecific( + _Codec.platformSpecific( android: _Codec.encodeEnum(permission.android), ios: _Codec.encodeEnum(permission.ios), ); @@ -19,11 +19,14 @@ class _Codec { static String encodeLocationUpdatesRequest(_LocationUpdatesRequest request) => json.encode(_JsonCodec.locationUpdatesRequestToJson(request)); + static String encodePermissionRequest(_PermissionRequest request) => + json.encode(_JsonCodec.permissionRequestToJson(request)); + // see: https://stackoverflow.com/questions/49611724/dart-how-to-json-decode-0-as-double static double parseJsonNumber(dynamic value) { return value.runtimeType == int ? (value as int).toDouble() : value; } - + static bool parseJsonBoolean(dynamic value) { return value.toString() == 'true'; } @@ -129,4 +132,11 @@ class _JsonCodec { ios: request.iosOptions, ), }; + + static Map permissionRequestToJson( + _PermissionRequest request) => + { + 'value': _Codec.encodeLocationPermission(request.value), + 'openSettingsIfDenied': request.openSettingsIfDenied, + }; } diff --git a/lib/channel/location_channel.dart b/lib/channel/location_channel.dart index d5f188c..54e340d 100644 --- a/lib/channel/location_channel.dart +++ b/lib/channel/location_channel.dart @@ -36,18 +36,23 @@ class _LocationChannel { Future isLocationOperational( LocationPermission permission) async { - final response = await _invokeChannelMethod(_loggingTag, _channel, - 'isLocationOperational', _Codec.encodeLocationPermission(permission)); + final response = await _invokeChannelMethod( + _loggingTag, + _channel, + 'isLocationOperational', + _Codec.encodeLocationPermission(permission), + ); return _Codec.decodeResult(response); } Future requestLocationPermission( - LocationPermission permission) async { + _PermissionRequest request) async { final response = await _invokeChannelMethod( - _loggingTag, - _channel, - 'requestLocationPermission', - _Codec.encodeLocationPermission(permission)); + _loggingTag, + _channel, + 'requestLocationPermission', + _Codec.encodePermissionRequest(request), + ); return _Codec.decodeResult(response); } @@ -59,8 +64,12 @@ class _LocationChannel { Future lastKnownLocation( LocationPermission permission) async { - final response = await _invokeChannelMethod(_loggingTag, _channel, - 'lastKnownLocation', _Codec.encodeLocationPermission(permission)); + final response = await _invokeChannelMethod( + _loggingTag, + _channel, + 'lastKnownLocation', + _Codec.encodeLocationPermission(permission), + ); return _Codec.decodeLocationResult(response); } diff --git a/lib/channel/param.dart b/lib/channel/param.dart index b26c1b2..784d099 100644 --- a/lib/channel/param.dart +++ b/lib/channel/param.dart @@ -27,3 +27,12 @@ class _LocationUpdatesRequest { } enum _LocationUpdateStrategy { current, single, continuous } + +class _PermissionRequest { + _PermissionRequest( + this.value, { + @required this.openSettingsIfDenied, + }); + final LocationPermission value; + final bool openSettingsIfDenied; +} diff --git a/lib/data/result.dart b/lib/data/result.dart index f22ff8e..ff4b72c 100644 --- a/lib/data/result.dart +++ b/lib/data/result.dart @@ -53,6 +53,8 @@ class GeolocationResultError { switch (type) { case GeolocationResultErrorType.locationNotFound: return 'location not found'; + case GeolocationResultErrorType.permissionNotGranted: + return 'permission not granted'; case GeolocationResultErrorType.permissionDenied: return 'permission denied'; case GeolocationResultErrorType.serviceDisabled: @@ -69,6 +71,7 @@ class GeolocationResultError { enum GeolocationResultErrorType { runtime, locationNotFound, + permissionNotGranted, permissionDenied, serviceDisabled, playServicesUnavailable, @@ -80,6 +83,8 @@ GeolocationResultErrorType _mapResultErrorTypeJson(String jsonValue) { return GeolocationResultErrorType.runtime; case 'locationNotFound': return GeolocationResultErrorType.locationNotFound; + case 'permissionNotGranted': + return GeolocationResultErrorType.permissionNotGranted; case 'permissionDenied': return GeolocationResultErrorType.permissionDenied; case 'serviceDisabled': diff --git a/lib/geolocation.dart b/lib/geolocation.dart index 013e57d..9e5b4ec 100644 --- a/lib/geolocation.dart +++ b/lib/geolocation.dart @@ -57,26 +57,32 @@ class Geolocation { /// Requests the location [permission], if needed. /// /// If location permission is already granted, it returns successfully. - /// If location is not operational, the request will fail without asking the permission. + /// If location is not operational (location disabled, google play services unavailable on Android, etc), the request will fail without asking the permission. /// - /// You don't need to call this method manually. - /// Every [Geolocation] method requiring the location permission will request it automatically if needed. - /// Automatic permission request always request [LocationPermissionAndroid.fine] and [LocationPermissionIOS.whenInUse]. - /// If you want another permission request, you have to request it manually. - /// Also it's a common practice to request the permission early in the application flow (like during an on boarding flow). + /// If the user denied the permission before, requesting it again won't show the dialog again on iOS. + /// On Android, it happens when user declines and checks `don't ask again`. + /// In this situation, [openSettingsIfDenied] will show the system settings where the user can manually enable location for the app. /// - /// Request permission must also be declared in `Info.plist` for iOS and `AndroidManifest.xml` for Android. - /// If required declaration is missing, location will not work. - /// Throws a [GeolocationException] if missing, to help you catch this mistake. + /// You don't need to call this method manually before requesting a location. + /// Every [Geolocation] location request will also request the permission automatically if needed. + /// + /// This method is useful to request the permission earlier in the application flow, like during an on boarding. + /// + /// Requested permission must be declared in `Info.plist` for iOS and `AndroidManifest.xml` for Android. + /// Throws a [GeolocationException] if the associated declaration is missing. /// /// See also: /// /// * [LocationPermission], which describes what are the available permissions /// * [GeolocationResult], the result you can expect from this request. - static Future requestLocationPermission([ + static Future requestLocationPermission({ LocationPermission permission = const LocationPermission(), - ]) => - _locationChannel.requestLocationPermission(permission); + bool openSettingsIfDenied = true, + }) => + _locationChannel.requestLocationPermission(_PermissionRequest( + permission, + openSettingsIfDenied: openSettingsIfDenied, + )); /// Retrieves the most recent [Location] currently available. /// Automatically request location [permission] beforehand if not granted. @@ -124,7 +130,7 @@ class Geolocation { LocationOptionsAndroid.defaultSingle, LocationOptionsIOS iosOptions = const LocationOptionsIOS(), }) => - _locationChannel.locationUpdates(new _LocationUpdatesRequest( + _locationChannel.locationUpdates(_LocationUpdatesRequest( _LocationUpdateStrategy.single, permission, accuracy, @@ -163,7 +169,7 @@ class Geolocation { LocationOptionsAndroid.defaultSingle, LocationOptionsIOS iosOptions = const LocationOptionsIOS(), }) => - _locationChannel.locationUpdates(new _LocationUpdatesRequest( + _locationChannel.locationUpdates(_LocationUpdatesRequest( _LocationUpdateStrategy.current, permission, accuracy, @@ -199,7 +205,7 @@ class Geolocation { LocationOptionsAndroid.defaultContinuous, LocationOptionsIOS iosOptions = const LocationOptionsIOS(), }) => - _locationChannel.locationUpdates(new _LocationUpdatesRequest( + _locationChannel.locationUpdates(_LocationUpdatesRequest( _LocationUpdateStrategy.continuous, permission, accuracy, diff --git a/pubspec.yaml b/pubspec.yaml index 8906c2d..e6d9261 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,12 @@ name: geolocation -description: Location plugin for iOS and Android. -version: 1.0.2 +description: >- + Flutter plugin for location / geolocation / GPS. Supports iOS and Android. + Multiple settings for speed, precision, battery optimization, continuous updates in background, etc. +version: 1.1.0 homepage: https://github.com/loup-v/geolocation/ environment: - sdk: ">=1.19.0 <3.0.0" + sdk: ">=2.1.0 <3.0.0" dependencies: flutter: @@ -14,5 +16,9 @@ dependencies: flutter: plugin: - androidPackage: io.intheloup.geolocation - pluginClass: GeolocationPlugin + platforms: + android: + package: app.loup.geolocation + pluginClass: GeolocationPlugin + ios: + pluginClass: GeolocationPlugin diff --git a/test/geolocation_test.dart b/test/geolocation_test.dart new file mode 100644 index 0000000..84032b2 --- /dev/null +++ b/test/geolocation_test.dart @@ -0,0 +1,23 @@ +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:geolocation/geolocation.dart'; + +// void main() { +// const MethodChannel channel = MethodChannel('geolocation'); + +// TestWidgetsFlutterBinding.ensureInitialized(); + +// setUp(() { +// channel.setMockMethodCallHandler((MethodCall methodCall) async { +// return '42'; +// }); +// }); + +// tearDown(() { +// channel.setMockMethodCallHandler(null); +// }); + +// test('getPlatformVersion', () async { +// expect(await Geolocation.platformVersion, '42'); +// }); +// }