Baseflow / flutter-geolocator

Android and iOS Geolocation plugin for Flutter
https://baseflow.com/
MIT License
1.25k stars 662 forks source link

🚀 WearOS support #1197

Open masus04 opened 1 year ago

masus04 commented 1 year ago

🚀 WearOs Support Feature Request

As WearOS is based on android and works quite similar, it would be great to have geolocator support it. So far none of the Flutter location packages are supporting WearOS and this could be a great argument to use geolocator.

Contextualize the feature

Geolocator supports most relevant platforms on which location is required. The only exception being Apple watchOS and Android WearOS. This feature request seeks to address one of these. Doing so would also make geolocator the first flutter plugin that supports location integration on wearOS devices.

Describe the feature

Ideally the full feature set that is currently supported on mobile android should be supported. If there are limitations, the key features would be a great start.

I suspect the fact that geolocator does not currently work on wearOS, even though it works great on android is mostly due to different permission / service requirements.

Platforms affected (mark all that apply)

Test Application

I have set up a very minimal test application to showcase the current behaviour: https://github.com/masus04/Flutter-WearOS-Location/blob/main/README.md

The same behaviour can be observed when running the application on an emulator in Android Studio or a physical device.

When running this application on Android mobile / wearOS, the following log is printed:

Android Mobile

Launching lib/main.dart on sdk gphone64 x86 64 in debug mode...
Running Gradle task 'assembleDebug'...
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
D/FlutterGeolocator( 7053): Attaching Geolocator to activity
D/FlutterGeolocator( 7053): Creating service.
D/FlutterGeolocator( 7053): Binding to location service.
D/FlutterGeolocator( 7053): Geolocator foreground service connected
D/FlutterGeolocator( 7053): Initializing Geolocator services
D/FlutterGeolocator( 7053): Flutter engine connected. Connected engine count 1
Debug service listening on ws://127.0.0.1:36951/LqjelzE7zxs=/ws
Syncing files to device sdk gphone64 x86 64...
I/wearos_location( 7053): Compiler allocated 4533KB to compile void android.view.ViewRootImpl.performTraversals()
I/flutter ( 7053): Initializing GeoLocator
E/FlutterGeolocator( 7053): Geolocator position updates started
E/SurfaceSyncer( 7053): Failed to find sync for id=0
W/Parcel  ( 7053): Expecting binder but got null!
D/EGL_emulation( 7053): app_time_stats: avg=21.08ms min=3.63ms max=89.87ms count=20
D/EGL_emulation( 7053): app_time_stats: avg=7.85ms min=1.73ms max=29.29ms count=24
D/EGL_emulation( 7053): app_time_stats: avg=6.34ms min=1.55ms max=32.13ms count=23
D/EGL_emulation( 7053): app_time_stats: avg=5.31ms min=1.67ms max=27.66ms count=28
I/flutter ( 7053): Current Speed: 1.3889987468719482 m/s
I/flutter ( 7053): Current Speed: 1.3889987468719482 m/s
D/EGL_emulation( 7053): app_time_stats: avg=151.05ms min=1.17ms max=1868.30ms count=13
I/flutter ( 7053): Current Speed: 1.3889987468719482 m/s
D/EGL_emulation( 7053): app_time_stats: avg=2997.02ms min=2997.02ms max=2997.02ms count=1
I/flutter ( 7053): Current Speed: 1.3889987468719482 m/s
D/EGL_emulation( 7053): app_time_stats: avg=2985.55ms min=2985.55ms max=2985.55ms count=1

WearOS

Launching lib/main.dart on sdk gwear x86 in debug mode...
Running Gradle task 'assembleDebug'...
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk...
D/FlutterGeolocator( 4135): Attaching Geolocator to activity
D/FlutterGeolocator( 4135): Creating service.
D/FlutterGeolocator( 4135): Binding to location service.
D/FlutterGeolocator( 4135): Geolocator foreground service connected
D/FlutterGeolocator( 4135): Initializing Geolocator services
D/FlutterGeolocator( 4135): Flutter engine connected. Connected engine count 1
Debug service listening on ws://127.0.0.1:39289/2bRo_AjON9Q=/ws
Syncing files to device sdk gwear x86...
I/flutter ( 4135): Initializing GeoLocator
E/FlutterGeolocator( 4135): Geolocator position updates started
I/wearos_locatio( 4135): Waiting for a blocking GC ProfileSaver
I/flutter ( 4135): The following error occurred: The location service on the device is disabled.
I/flutter ( 4135): #0      GeolocatorAndroid.getPositionStream.<anonymous closure> (package:geolocator_android/src/geolocator_android.dart:187:9)
I/flutter ( 4135): (elided 26 frames from dart:async)

This is with the latest Flutter & dependencies at the time of this issue

masus04 commented 1 year ago

This is a follow up to #785

talhazengin commented 1 year ago

So, no updates?

masus04 commented 1 year ago

Any updates on this? There is still not a single Location package that supports WearOS.

masus04 commented 8 months ago

While wearOS is still not officially supported by flutter, most use cases can be covered by third party packages like this one and I personally have multiple apps productive on the play store.

However, there is still no location package available for wearOS applications, signifficantly reducing the appeal of using Flutter for wearOS. Supporting this use case could be huge for Flutter on WearOS.

masus04 commented 8 months ago

I am considering implementing this feature in the near future. Can you point me to what would have to be done in order to do so?

This woul dbe the first time I work on Flutter plugins, so I would really appreciate the help.

Also, would a wearOS implementation be part of the geolocator_android federated plugin, or would it be implement as a separate federated plugin?

SkylL3r commented 8 months ago

It would be so great to see geolocator working on WearOS ! I just started a WearOS project on my own with geolocator as its key feature. I was surprised to see no location packages works on WearOS so far...

As of the WearOS implementation for geolocator, I guess it could be part of geolocator_android plugin, but might not be the easiest way though.

If any help is necessary I would be glad to join, even if I'm in the same situation as you are regarding Flutter packages.

tgritter commented 8 months ago

@masus04 @SkylL3r I need this too for my project and would also being willing to chip in. Yeah I believe the geolocator_android is the place to start. I was able to build a local bridge between android and flutter to get it working on my emulator (can't see to get it working my device though), that's where I added my code.

tgritter commented 8 months ago

@masus04 @SkylL3r I was able to get the location working on WearOS writing my own code (not using this package). The trick is to use the FusedLocationProviderClient instead of the regular location provider (a smartwatch gets its GPS data differently than a phone). Let me know if you want my code, and/or if we should add it to this package for future users.

SkylL3r commented 8 months ago

@tgritter Indeed, I also was able to get the location on WearOs using FusedLocationClientProvider last night on my own plugin. But I admit my own is pretty standard and maybe not quite strong enough on permissions checks and stuff based on what geolocator_android does.

So I'd be interested in seeing your code as a comparison and/or show you mine.

I also keep thinking it would be a great addition to this package for future users. 👍🏽

tgritter commented 8 months ago

@SkylL3r Here is my code:

build.gradle

implementation 'com.google.android.gms:play-services-location:18.0.0'

MainActivity.kt

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import android.location.Location
import android.os.Bundle
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import io.flutter.plugin.common.EventChannel
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationRequest
import java.util.concurrent.TimeUnit

class MainActivity : FlutterActivity() {
    private val CHANNEL = "location_permission"
    private val LOCATION_CHANNEL = "location_updates"
    private val PERMISSION_REQUEST_CODE = 123
    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private var eventSink: EventChannel.EventSink? = null
    private lateinit var locationCallback: LocationCallback // Declare locationCallback as a class property

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
    }

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "requestPermission" -> {
                        requestLocationPermission(result)
                    }
                    "checkPermission" -> {
                        result.success(checkLocationPermission())
                    }
                    else -> result.notImplemented()
                }
            }

        EventChannel(flutterEngine.dartExecutor.binaryMessenger, LOCATION_CHANNEL)
            .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(arguments: Any?, eventSink: EventChannel.EventSink?) {
                    this@MainActivity.eventSink = eventSink
                    startLocationUpdates()
                }

                override fun onCancel(arguments: Any?) {
                    stopLocationUpdates()
                }
            })
    }

    private fun requestLocationPermission(result: MethodChannel.Result) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.ACCESS_FINE_LOCATION
                ) == PackageManager.PERMISSION_GRANTED
            ) {
                result.success(true)
                return
            }
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                PERMISSION_REQUEST_CODE
            )
        }
    }

    private fun checkLocationPermission(): Boolean {
        return ActivityCompat.checkSelfPermission(
            this,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun startLocationUpdates() {
        if (checkLocationPermission()) {
            val locationRequest = getLocationRequest()
            locationCallback = object : LocationCallback() {
                override fun onLocationResult(locationResult: LocationResult) {
                    for (location in locationResult.locations) {
                        eventSink?.success("${location.latitude} ${location.longitude}")
                    }
                }
            }
            fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
        }
    }

    private fun stopLocationUpdates() {
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

    private fun getLocationRequest(): LocationRequest {
        return LocationRequest.create().apply {
            interval = TimeUnit.SECONDS.toMillis(1)
            fastestInterval = TimeUnit.SECONDS.toMillis(1)
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                eventSink?.success(true)
            } else {
                eventSink?.success(false)
            }
        }
    }
}

Then you can call it in Flutter like so:

Future<void> _startLocationUpdates() async {
    try {
      await platform.invokeMethod('requestPermission');
      final bool result = await platform.invokeMethod('checkPermission');
      if (result) {
        eventChannel.receiveBroadcastStream().listen((event) {
          List<double> coords = parseCoordinates(event);
          print("Coordinates: $coords");
        });
      }
    } on PlatformException catch (e) {
      print("Failed to start location updates: '${e.message}'.");
    }
  }
tgritter commented 8 months ago

And yeah would be great to get this package working for future users. Although it's probably not as simple as just copying and pasting this code 😟

momo21584 commented 1 month ago

Any news here ? I'm also willing to help in this case.

tgritter commented 1 month ago

@momo21584 I haven't made a PR for this change, no. If you need GPS for the watch I would advise building it yourself using a bridge modal. The code I posted above ^ was mainly working for me. The key difference between the watch and phone is using the com.google.android.gms.location.FusedLocationProviderClient package for the watch.

masus04 commented 1 month ago

If it's not that big of a change, would it be possible to open a PR for it?

tgritter commented 1 month ago

@masus04 I think it would be a big change unfortunately. There would be a lot of device based logic you'd have to be very careful about. You'd probably need to test on various devices too, different watches handle GPS differently I've found. There may be other device based problems encountered too.

Unless someone really wants to take this on, I think writing a bridging module is going to be much easier for getting Flutter GPS on WearOS. A good experience too, I learned a lot about bridging writing mine!