From 7fa229a51c5db156773bf61f8416207656d2a918 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Sun, 19 May 2024 17:48:14 +0200 Subject: [PATCH 1/4] Initial support for fused location provider --- app/CMakeLists.txt | 14 +- .../co/lutraconsulting/MMAndroidPosition.java | 190 +++++++++++++ app/position/positionkit.cpp | 23 +- .../providers/androidpositionprovider.cpp | 258 ++++++++++++++++++ .../providers/androidpositionprovider.h | 53 ++++ .../providers/internalpositionprovider.cpp | 2 +- .../providers/positionprovidersmodel.cpp | 19 ++ app/qml/settings/MMSettingsPage.qml | 2 +- cmake_templates/build.gradle.in | 1 + 9 files changed, 550 insertions(+), 12 deletions(-) create mode 100644 app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java create mode 100644 app/position/providers/androidpositionprovider.cpp create mode 100644 app/position/providers/androidpositionprovider.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 3590c862a..b96c77c27 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -252,12 +252,18 @@ if (IOS) endif () if (ANDROID) - set(MM_HDRS ${MM_HDRS} position/tracking/androidtrackingbackend.h - position/tracking/androidtrackingbroadcast.h + set(MM_HDRS + ${MM_HDRS} + position/tracking/androidtrackingbackend.h + position/tracking/androidtrackingbroadcast.h + position/providers/androidpositionprovider.h ) - set(MM_SRCS ${MM_SRCS} position/tracking/androidtrackingbackend.cpp - position/tracking/androidtrackingbroadcast.cpp + set(MM_SRCS + ${MM_SRCS} + position/tracking/androidtrackingbackend.cpp + position/tracking/androidtrackingbroadcast.cpp + position/providers/androidpositionprovider.cpp ) endif () diff --git a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java new file mode 100644 index 000000000..b3e939d0c --- /dev/null +++ b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java @@ -0,0 +1,190 @@ +package uk.co.lutraconsulting; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.GnssStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Looper; +import android.os.Handler; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.Priority; + + + +public class MMAndroidPosition { + + static public abstract class Callback { + public void onPositionChanged(@NonNull Location location, GnssStatus gnssStatus) { + } + } + + private static native void jniOnPositionUpdated(int instanceId, Location location, GnssStatus gnssStatus); + + static public MMAndroidPosition createWithJniCallback(Context context, boolean useFused, int instanceId) { + Log.i("CPP", "[java] createWithJniCallback"); + + MMAndroidPosition.Callback callback = new MMAndroidPosition.Callback() { + @Override + public void onPositionChanged(@NonNull Location location, GnssStatus gnssStatus) { + jniOnPositionUpdated(instanceId, location, gnssStatus); + } + }; + + return new MMAndroidPosition(context, callback, useFused); + } + + private final Context mContext; + private final LocationManager mLocationManager; + private final boolean mUseFused; + private FusedLocationProviderClient mFusedLocationClient = null; + private final LocationCallback mLocationCallback; + private final LocationListener mLocationManagerCallback; + private final GnssStatus.Callback mGnssStatusCallback; + private final MMAndroidPosition.Callback mClientCallback; + private boolean mFusedAvailable = false; + private boolean mGpsProviderAvailable = false; + private boolean mIsStarted = false; + private String mErrorMessage; + private GnssStatus mLastGnssStatus; + + public MMAndroidPosition(Context context, MMAndroidPosition.Callback clientCallback, boolean useFused) { + mContext = context; + mClientCallback = clientCallback; + mUseFused = useFused; + + Log.i("CPP", "[java] constructor!"); + + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + if (mUseFused) { + GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); + mFusedAvailable = googleApiAvailability.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS; + Log.i("CPP", "[java] fused available: " + mFusedAvailable); + if (mFusedAvailable) { + mFusedLocationClient = LocationServices.getFusedLocationProviderClient(context); + } + } else { + mGpsProviderAvailable = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + Log.i("CPP", "[java] gps provider available: " + mGpsProviderAvailable); + } + + mLocationCallback = new LocationCallback() { + @Override + public void onLocationResult(@NonNull LocationResult locationResult) { + for (Location location : locationResult.getLocations()) { + Log.i("CPP", "[java] FLP " + location.getLatitude() + " " + location.getLongitude()); + + // call the native function! + mClientCallback.onPositionChanged(location, mLastGnssStatus); + } + } + }; + + mGnssStatusCallback = new GnssStatus.Callback() { + @Override + public void onSatelliteStatusChanged(@NonNull GnssStatus status) { + //Log.v("LOC", "GNSS Status: " + status.getSatelliteCount() + " satellites."); + + // store the satellite info + mLastGnssStatus = status; + } + }; + + mLocationManagerCallback = new LocationListener() { + @Override + public void onLocationChanged(@NonNull Location location) { + Log.i("CPP", "[java] GPS " + location.getLatitude() + " " + location.getLongitude()); + + mClientCallback.onPositionChanged(location, mLastGnssStatus); + } + }; + + Log.i("CPP", "[java] constructor end"); + + } + + public String errorMessage() { + return mErrorMessage; + } + + public boolean start() { + Log.i("CPP", "[java] start()"); + + if (mIsStarted) + return false; + + Log.e("CPP", "[java] here 0"); + + if (mUseFused && !mFusedAvailable) { + mErrorMessage = "FUSED_NOT_AVAILABLE"; + Log.e("CPP", "[java] FUSED_NOT_AVAILABLE"); + return false; + } + + if (!mUseFused && !mGpsProviderAvailable) { + mErrorMessage = "GPS_NOT_AVAILABLE"; + Log.e("CPP", "[java] GPS_NOT_AVAILABLE"); + return false; + } + + if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + mErrorMessage = "MISSING_PERMISSIONS"; + Log.e("CPP", "[java] MISSING_PERMISSIONS"); + return false; + } + + Log.e("CPP", "[java] here 1"); + + if (mUseFused) { + LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000).build(); + + mFusedLocationClient.requestLocationUpdates(locationRequest, mLocationCallback, Looper.getMainLooper()); + } + else { + mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 0.F, mLocationManagerCallback, Looper.getMainLooper()); + Log.e("CPP", "[java] here 2"); + } + + mLocationManager.registerGnssStatusCallback(mGnssStatusCallback, new Handler(Looper.getMainLooper())); + + Log.i("CPP", "[java] started!"); + + mIsStarted = true; + return true; + } + + public boolean stop() { + Log.i("CPP", "[java] stop()"); + + if (!mIsStarted) + return false; + + if (mUseFused) { + mFusedLocationClient.removeLocationUpdates(mLocationCallback); + } else { + mLocationManager.removeUpdates(mLocationManagerCallback); + } + + mLocationManager.unregisterGnssStatusCallback(mGnssStatusCallback); + + Log.i("CPP", "[java] stopped!"); + + mIsStarted = false; + return true; + } +} diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 4a26fb2de..8662a91a0 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -18,6 +18,10 @@ #include "position/providers/internalpositionprovider.h" #include "position/providers/simulatedpositionprovider.h" +#ifdef ANDROID +#include "position/providers/androidpositionprovider.h" +#include +#endif #include "appsettings.h" #include "inpututils.h" @@ -105,6 +109,16 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } +#ifdef ANDROID + else if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) + { + bool fused = ( id == QStringLiteral( "android_fused" ) ); + __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); + AbstractPositionProvider *provider = new AndroidPositionProvider( fused ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } +#endif else // id == devicegps { AbstractPositionProvider *provider = new InternalPositionProvider(); @@ -132,13 +146,10 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app return constructProvider( QStringLiteral( "internal" ), QStringLiteral( "simulated" ) ); } } - else if ( providerId == QStringLiteral( "devicegps" ) ) - { - return constructProvider( QStringLiteral( "internal" ), QStringLiteral( "devicegps" ) ); - } - else if ( providerId == QStringLiteral( "simulated" ) ) + else if ( providerId == QStringLiteral( "devicegps" ) || providerId == QStringLiteral( "simulated" ) || + providerId == QStringLiteral( "android_fused" ) || providerId == QStringLiteral( "android_gps" ) ) { - return constructProvider( QStringLiteral( "internal" ), QStringLiteral( "simulated" ) ); + return constructProvider( QStringLiteral( "internal" ), providerId ); } else { diff --git a/app/position/providers/androidpositionprovider.cpp b/app/position/providers/androidpositionprovider.cpp new file mode 100644 index 000000000..b6e541776 --- /dev/null +++ b/app/position/providers/androidpositionprovider.cpp @@ -0,0 +1,258 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "androidpositionprovider.h" +#include "coreutils.h" + +#include "qgis.h" + +#include +#include + +#include + +#include + + +int AndroidPositionProvider::sLastInstanceId = 0; +QMap AndroidPositionProvider::sInstances; + + +void jniOnPositionUpdated( JNIEnv *env, jclass clazz, jint instanceId, jobject locationObj, jobject gnssStatusObj ) +{ + AndroidPositionProvider *inst = AndroidPositionProvider::sInstances[instanceId]; + if ( !inst ) + { + __android_log_print( ANDROID_LOG_ERROR, "CPP", "[c++] unknown instance! %d", instanceId ); + return; + } + + QJniObject location( locationObj ); + if ( !location.isValid() ) + { + __android_log_print( ANDROID_LOG_ERROR, "CPP", "[c++] invalid location obj" ); + return; + } + + const jdouble latitude = location.callMethod( "getLatitude" ); + const jdouble longitude = location.callMethod( "getLongitude" ); + const jlong timestamp = location.callMethod( "getTime" ); + + GeoPosition pos; + pos.latitude = latitude; + pos.longitude = longitude; + pos.utcDateTime = QDateTime::fromMSecsSinceEpoch( timestamp, QTimeZone::UTC ); + + if ( location.callMethod( "hasAltitude" ) ) + { + const jdouble value = location.callMethod( "getAltitude" ); + if ( !qFuzzyIsNull( value ) ) + pos.elevation = value; + } + + // TODO: we are getting ellipsoid elevation here. From API level 34 (Android 14), + // there is AltitudeConverter() class in Java that can be used to add MSL altitude + // to Location object. How to deal with this correctly? (we could also convert + // to MSL (orthometric) altitude ourselves if we add geoid model to our APK + + // horizontal accuracy + if ( location.callMethod( "hasAccuracy" ) ) + { + const jfloat accuracy = location.callMethod( "getAccuracy" ); + if ( !qFuzzyIsNull( accuracy ) ) + pos.hacc = accuracy; + } + + // vertical accuracy (available since API Level 26 (Android 8.0)) + if ( QNativeInterface::QAndroidApplication::sdkVersion() >= 26 ) + { + if ( location.callMethod( "hasVerticalAccuracy" ) ) + { + const jfloat accuracy = location.callMethod( "getVerticalAccuracyMeters" ); + if ( !qFuzzyIsNull( accuracy ) ) + pos.vacc = accuracy; + } + } + + // ground speed + if ( location.callMethod( "hasSpeed" ) ) + { + const jfloat speed = location.callMethod( "getSpeed" ); + if ( !qFuzzyIsNull( speed ) ) + pos.speed = speed * 3.6; // convert from m/s to km/h + + // could also use getSpeedAccuracyMetersPerSecond() since API level 26 (Android 8.0) + } + + // bearing + if ( location.callMethod( "hasBearing" ) ) + { + const jfloat bearing = location.callMethod( "getBearing" ); + if ( !qFuzzyIsNull( bearing ) ) + pos.direction = bearing; + + // could also use getBearingAccuracyDegrees() since API level 26 (Android 8.0) + } + + // could also use isMock() to detect if location is mocked + // (may useful to check if 3rd party app is setting it for external GNSS receiver) + + // could also use getExtras() to get further details from mocked location + // (the key/value pairs are vendor-specific, and could include things like DOP, + // info about corrections, geoid undulation, receiver model) + + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] pos %f %f", latitude, longitude ); + + QJniObject gnssStatus( gnssStatusObj ); + if ( gnssStatus.isValid() ) + { + int satellitesUsed = 0; + const int satellitesCount = gnssStatus.callMethod( "getSatelliteCount" ); + for ( int i = 0; i < satellitesCount; ++i ) + { + if ( gnssStatus.callMethod( "usedInFix", i ) ) + ++satellitesUsed; + + // we could get more info here (ID, azimuth, elevation, signal strength, ...) + // but we are not using that anywhere + } + + pos.satellitesVisible = satellitesCount; + pos.satellitesUsed = satellitesUsed; + } + + QMetaObject::invokeMethod( inst, "positionChanged", + Qt::AutoConnection, Q_ARG( GeoPosition, pos ) ); + +} + + +AndroidPositionProvider::AndroidPositionProvider( bool fused, QObject *parent ) + : AbstractPositionProvider( fused ? QStringLiteral( "android_fused" ) : QStringLiteral( "android_gps" ), + QStringLiteral( "internal" ), + fused ? tr( "Android (fused)" ) : tr( "Android (gps)" ), parent ) + , mFused( fused ) + , mInstanceId( ++sLastInstanceId ) +{ + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] CONSTRUCT" ); + + Q_ASSERT( !sInstances.contains( mInstanceId ) ); + sInstances[mInstanceId] = this; + + // register the native methods + + JNINativeMethod methods[] + { + { + "jniOnPositionUpdated", + "(ILandroid/location/Location;Landroid/location/GnssStatus;)V", + reinterpret_cast( jniOnPositionUpdated ) + } + }; + + QJniEnvironment javaenv; + + javaenv.registerNativeMethods( "uk/co/lutraconsulting/MMAndroidPosition", methods, 1 ); + + // create the Java object + + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] create Java object" ); + + jobject context = QNativeInterface::QAndroidApplication::context(); + + mAndroidPos = QJniObject::callStaticObjectMethod( "uk/co/lutraconsulting/MMAndroidPosition", "createWithJniCallback", + "(Landroid/content/Context;ZI)Luk/co/lutraconsulting/MMAndroidPosition;", context, mFused, mInstanceId ); + + // Request permissions if needed + + QLocationPermission perm; + perm.setAccuracy( QLocationPermission::Precise ); + if ( qApp->checkPermission( perm ) != Qt::PermissionStatus::Granted ) + { + // if user previously completely denied location, the permissions request dialog + // may not even show up and we get denied response again. + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] going to request permissions" ); + qApp->requestPermission( perm, [this]( const QPermission & p ) + { + if ( p.status() == Qt::PermissionStatus::Granted ) + { + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] permissions granted!" ); + this->startUpdates(); + } + else + { + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] permissions denied :-(" ); + // User may have granted permission just for approximate location, + // so it would be good to detect that and warn about that: approximate + // location has intentionally only ~2km accuracy - too low for any data collection + QLocationPermission permApprox; + permApprox.setAccuracy( QLocationPermission::Approximate ); + if ( qApp->checkPermission( permApprox ) == Qt::PermissionStatus::Granted ) + this->setState( tr( "Approximate location only!" ), State::NoConnection ); + else + this->setState( tr( "No location permissions" ), State::NoConnection ); + } + } ); + return; + } + + // TODO: this should not be needed? + AndroidPositionProvider::startUpdates(); +} + +AndroidPositionProvider::~AndroidPositionProvider() +{ + __android_log_print( ANDROID_LOG_INFO, "CPP", "DESTRUCT" ); + + Q_ASSERT( sInstances[mInstanceId] == this ); + sInstances.remove( mInstanceId ); + +} + +void AndroidPositionProvider::startUpdates() +{ + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] start updates" ); + + jboolean res = mAndroidPos.callMethod( "start", "()Z" ); + + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] start updates res: %d", res ); + + if ( !res ) + { + QJniObject errMsgJni = mAndroidPos.callObjectMethod( "errorMessage", "()Ljava/lang/String;" ); + QString errMsg = errMsgJni.toString(); + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] error: %s", errMsg.toUtf8().constData() ); + if ( errMsg == "MISSING_PERMISSIONS" ) + setState( tr( "No location permissions" ), State::NoConnection ); + else if ( errMsg == "FUSED_NOT_AVAILABLE" ) + setState( tr( "Fused location not available" ), State::NoConnection ); + else + setState( errMsg, State::NoConnection ); + return; + } + + setState( tr( "Waiting for fix..." ), State::Connected ); +} + +void AndroidPositionProvider::stopUpdates() +{ + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] stop updates" ); + + jboolean res = mAndroidPos.callMethod( "stop", "()Z" ); + + __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] stop updates res: %d", res ); + +} + +void AndroidPositionProvider::closeProvider() +{ + stopUpdates(); + + mAndroidPos = QJniObject(); +} diff --git a/app/position/providers/androidpositionprovider.h b/app/position/providers/androidpositionprovider.h new file mode 100644 index 000000000..5fb145472 --- /dev/null +++ b/app/position/providers/androidpositionprovider.h @@ -0,0 +1,53 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef ANDROIDPOSITIONPROVIDER_H +#define ANDROIDPOSITIONPROVIDER_H + +#include "inputconfig.h" +#include "abstractpositionprovider.h" + +#include + +/** + * AndroidPositionProvider uses Android's LocationManager API (when fused=false) + * or Fused Location Provider from Google Play Services (when fused=true). + * + * Compared to Qt Positioning, it can use Fused Location Provider and it is + * potentially more flexible becuase we are not going through a generic + * positioning API. + */ +class AndroidPositionProvider : public AbstractPositionProvider +{ + Q_OBJECT + + public: + explicit AndroidPositionProvider( bool fused, QObject *parent = nullptr ); + virtual ~AndroidPositionProvider() override; + + virtual void startUpdates() override; + virtual void stopUpdates() override; + virtual void closeProvider() override; + + public slots: + + private: + bool mFused; + int mInstanceId; + QJniObject mAndroidPos; + + public: + // Multiple PositionProvider instances may exist at a time (because a new provider + // gets created before the old one gets deleted), and our JNI callback method needs + // to know to which instance to deliver a location update. + static QMap sInstances; + static int sLastInstanceId; +}; + +#endif // ANDROIDPOSITIONPROVIDER_H diff --git a/app/position/providers/internalpositionprovider.cpp b/app/position/providers/internalpositionprovider.cpp index 0fb68dc33..fc8a02670 100644 --- a/app/position/providers/internalpositionprovider.cpp +++ b/app/position/providers/internalpositionprovider.cpp @@ -13,7 +13,7 @@ #include "qgis.h" InternalPositionProvider::InternalPositionProvider( QObject *parent ) - : AbstractPositionProvider( QStringLiteral( "devicegps" ), QStringLiteral( "internal" ), QStringLiteral( "Internal" ), parent ) + : AbstractPositionProvider( QStringLiteral( "devicegps" ), QStringLiteral( "internal" ), tr( "Internal" ), parent ) { mGpsPositionSource = std::unique_ptr( QGeoPositionInfoSource::createDefaultSource( nullptr ) ); diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index 9d9baa424..039273e9b 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -20,6 +20,9 @@ PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractLis mProviders.push_front( simulated ); } + // Keep the names of position providers in sync with names + // used in the constructors of the providers... + PositionProvider internal; internal.name = tr( "Internal" ); internal.description = tr( "GPS receiver of this device" ); @@ -27,6 +30,22 @@ PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractLis internal.providerId = "devicegps"; mProviders.push_front( internal ); + +#ifdef ANDROID + PositionProvider internalFused; + internalFused.name = tr( "Android (fused)" ); + internalFused.description = tr( "Using GPS, Wifi and sensors" ); + internalFused.providerType = "internal"; + internalFused.providerId = "android_fused"; + mProviders.push_front( internalFused ); + + PositionProvider internalGps; + internalGps.name = tr( "Android (gps)" ); + internalGps.description = tr( "Using GPS only" ); + internalGps.providerType = "internal"; + internalGps.providerId = "android_gps"; + mProviders.push_front( internalGps ); +#endif } PositionProvidersModel::~PositionProvidersModel() = default; diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index a1b29e942..4de37f510 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -76,7 +76,7 @@ MMPage { MMSettingsComponents.MMSettingsItem { width: parent.width title: qsTr("Manage GPS receivers") - value: "Internal" + value: __positionKit.positionProvider.name() onClicked: root.manageGpsClicked() } diff --git a/cmake_templates/build.gradle.in b/cmake_templates/build.gradle.in index 0b4e7adc8..c83f1abab 100644 --- a/cmake_templates/build.gradle.in +++ b/cmake_templates/build.gradle.in @@ -33,6 +33,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.0-beta02' implementation "androidx.exifinterface:exifinterface:1.3.3" implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.google.android.gms:play-services-location:21.2.0' } android { From 5a34d28537659805f6412f8431008ce179319cbe Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Tue, 4 Jun 2024 15:03:46 +0200 Subject: [PATCH 2/4] Check for google play services, fall back to non-fused (qt), disable android non-fused implementation for now --- .../co/lutraconsulting/MMAndroidPosition.java | 16 +++++++++++-- app/position/positionkit.cpp | 9 ++++++++ .../providers/androidpositionprovider.cpp | 23 +++++++++++++++++-- .../providers/androidpositionprovider.h | 6 +++++ .../providers/positionprovidersmodel.cpp | 18 ++++++++++++--- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java index b3e939d0c..8ed92c6db 100644 --- a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java +++ b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java @@ -34,6 +34,19 @@ public void onPositionChanged(@NonNull Location location, GnssStatus gnssStatus) private static native void jniOnPositionUpdated(int instanceId, Location location, GnssStatus gnssStatus); + // find out whether fused provider could be actually used + static public boolean isFusedLocationProviderAvailable(Context context) { + GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); + return googleApiAvailability.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS; + } + + // get more details why FLP is not available (e.g. play services missing, disabled, updating...) + static public String fusedLocationProviderErrorString(Context context) { + GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); + return googleApiAvailability.getErrorString(googleApiAvailability.isGooglePlayServicesAvailable(context)); + } + + // called from C++ code static public MMAndroidPosition createWithJniCallback(Context context, boolean useFused, int instanceId) { Log.i("CPP", "[java] createWithJniCallback"); @@ -71,8 +84,7 @@ public MMAndroidPosition(Context context, MMAndroidPosition.Callback clientCallb mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); if (mUseFused) { - GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); - mFusedAvailable = googleApiAvailability.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS; + mFusedAvailable = isFusedLocationProviderAvailable(context); Log.i("CPP", "[java] fused available: " + mFusedAvailable); if (mFusedAvailable) { mFusedLocationClient = LocationServices.getFusedLocationProviderClient(context); diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 8662a91a0..6510cb464 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -113,6 +113,15 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c else if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) { bool fused = ( id == QStringLiteral( "android_fused" ) ); + if ( fused && !AndroidPositionProvider::isFusedAvailable() ) + { + // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? + + // fallback to the default - at this point the Qt Positioning implementation + AbstractPositionProvider *provider = new InternalPositionProvider(); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); AbstractPositionProvider *provider = new AndroidPositionProvider( fused ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); diff --git a/app/position/providers/androidpositionprovider.cpp b/app/position/providers/androidpositionprovider.cpp index b6e541776..4bd52d362 100644 --- a/app/position/providers/androidpositionprovider.cpp +++ b/app/position/providers/androidpositionprovider.cpp @@ -136,7 +136,7 @@ void jniOnPositionUpdated( JNIEnv *env, jclass clazz, jint instanceId, jobject l AndroidPositionProvider::AndroidPositionProvider( bool fused, QObject *parent ) : AbstractPositionProvider( fused ? QStringLiteral( "android_fused" ) : QStringLiteral( "android_gps" ), QStringLiteral( "internal" ), - fused ? tr( "Android (fused)" ) : tr( "Android (gps)" ), parent ) + fused ? tr( "Internal (fused)" ) : tr( "Internal (gps)" ), parent ) , mFused( fused ) , mInstanceId( ++sLastInstanceId ) { @@ -215,6 +215,25 @@ AndroidPositionProvider::~AndroidPositionProvider() } +bool AndroidPositionProvider::isFusedAvailable() +{ + jobject context = QNativeInterface::QAndroidApplication::context(); + + return QJniObject::callStaticMethod( "uk/co/lutraconsulting/MMAndroidPosition", "isFusedLocationProviderAvailable", + "(Landroid/content/Context;)Z", context ); +} + +QString AndroidPositionProvider::fusedErrorString() +{ + jobject context = QNativeInterface::QAndroidApplication::context(); + + QJniObject str = QJniObject::callStaticObjectMethod( "uk/co/lutraconsulting/MMAndroidPosition", "fusedLocationProviderErrorString", + "(Landroid/content/Context;)Ljava/lang/String;", context ); + + return str.toString(); +} + + void AndroidPositionProvider::startUpdates() { __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] start updates" ); @@ -237,7 +256,7 @@ void AndroidPositionProvider::startUpdates() return; } - setState( tr( "Waiting for fix..." ), State::Connected ); + setState( tr( "Connected" ), State::Connected ); } void AndroidPositionProvider::stopUpdates() diff --git a/app/position/providers/androidpositionprovider.h b/app/position/providers/androidpositionprovider.h index 5fb145472..fa1772334 100644 --- a/app/position/providers/androidpositionprovider.h +++ b/app/position/providers/androidpositionprovider.h @@ -35,6 +35,12 @@ class AndroidPositionProvider : public AbstractPositionProvider virtual void stopUpdates() override; virtual void closeProvider() override; + //! Checks whether the fused location provider can be used (i.e. Google Play services are present) + static bool isFusedAvailable(); + //! If fused provider is not available, returns error string that could be presented to users. + //! It is not very human friendly, but at least something (e.g. "SERVICE_DISABLED") + static QString fusedErrorString(); + public slots: private: diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index 039273e9b..e070e4576 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -11,6 +11,10 @@ #include "inpututils.h" #include "coreutils.h" +#ifdef ANDROID +#include "position/providers/androidpositionprovider.h" +#endif + PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractListModel( parent ) { if ( !InputUtils::isMobilePlatform() ) @@ -33,19 +37,27 @@ PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractLis #ifdef ANDROID PositionProvider internalFused; - internalFused.name = tr( "Android (fused)" ); - internalFused.description = tr( "Using GPS, Wifi and sensors" ); + internalFused.name = tr( "Internal (fused)" ); + if ( AndroidPositionProvider::isFusedAvailable() ) + internalFused.description = tr( "Using GPS, Wifi and sensors" ); + else + internalFused.description = tr( "Not available (%1)" ).arg( AndroidPositionProvider::fusedErrorString() ); internalFused.providerType = "internal"; internalFused.providerId = "android_fused"; mProviders.push_front( internalFused ); + // This one should have pretty much the same behavior as the provider + // implemented using Qt Positioning, so let's skip it. When we ditch + // Qt's implementation on Android, this can be used as a fallback. +#if 0 PositionProvider internalGps; - internalGps.name = tr( "Android (gps)" ); + internalGps.name = tr( "Internal (gps)" ); internalGps.description = tr( "Using GPS only" ); internalGps.providerType = "internal"; internalGps.providerId = "android_gps"; mProviders.push_front( internalGps ); #endif +#endif } PositionProvidersModel::~PositionProvidersModel() = default; From 785cd31146faaf124c1d6f0fa86e640b66bf507f Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Tue, 4 Jun 2024 15:09:12 +0200 Subject: [PATCH 3/4] Remove commented line --- app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java index 8ed92c6db..16049eaa5 100644 --- a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java +++ b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java @@ -109,7 +109,6 @@ public void onLocationResult(@NonNull LocationResult locationResult) { mGnssStatusCallback = new GnssStatus.Callback() { @Override public void onSatelliteStatusChanged(@NonNull GnssStatus status) { - //Log.v("LOC", "GNSS Status: " + status.getSatelliteCount() + " satellites."); // store the satellite info mLastGnssStatus = status; From adfa992fa4f7df3ea0001d35223ba71773463601 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Sun, 9 Jun 2024 17:22:54 +0200 Subject: [PATCH 4/4] Request location permissions only once needed (i.e. when project is loaded, not at the startup when position provider is created) --- .../co/lutraconsulting/MMAndroidPosition.java | 5 --- .../providers/androidpositionprovider.cpp | 45 +++++-------------- app/qml/main.qml | 18 ++------ 3 files changed, 15 insertions(+), 53 deletions(-) diff --git a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java index 16049eaa5..f2731f7aa 100644 --- a/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java +++ b/app/android/src/uk/co/lutraconsulting/MMAndroidPosition.java @@ -138,8 +138,6 @@ public boolean start() { if (mIsStarted) return false; - Log.e("CPP", "[java] here 0"); - if (mUseFused && !mFusedAvailable) { mErrorMessage = "FUSED_NOT_AVAILABLE"; Log.e("CPP", "[java] FUSED_NOT_AVAILABLE"); @@ -159,8 +157,6 @@ public boolean start() { return false; } - Log.e("CPP", "[java] here 1"); - if (mUseFused) { LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000).build(); @@ -168,7 +164,6 @@ public boolean start() { } else { mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 0.F, mLocationManagerCallback, Looper.getMainLooper()); - Log.e("CPP", "[java] here 2"); } mLocationManager.registerGnssStatusCallback(mGnssStatusCallback, new Handler(Looper.getMainLooper())); diff --git a/app/position/providers/androidpositionprovider.cpp b/app/position/providers/androidpositionprovider.cpp index 4bd52d362..277776d14 100644 --- a/app/position/providers/androidpositionprovider.cpp +++ b/app/position/providers/androidpositionprovider.cpp @@ -169,40 +169,6 @@ AndroidPositionProvider::AndroidPositionProvider( bool fused, QObject *parent ) mAndroidPos = QJniObject::callStaticObjectMethod( "uk/co/lutraconsulting/MMAndroidPosition", "createWithJniCallback", "(Landroid/content/Context;ZI)Luk/co/lutraconsulting/MMAndroidPosition;", context, mFused, mInstanceId ); - // Request permissions if needed - - QLocationPermission perm; - perm.setAccuracy( QLocationPermission::Precise ); - if ( qApp->checkPermission( perm ) != Qt::PermissionStatus::Granted ) - { - // if user previously completely denied location, the permissions request dialog - // may not even show up and we get denied response again. - __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] going to request permissions" ); - qApp->requestPermission( perm, [this]( const QPermission & p ) - { - if ( p.status() == Qt::PermissionStatus::Granted ) - { - __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] permissions granted!" ); - this->startUpdates(); - } - else - { - __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] permissions denied :-(" ); - // User may have granted permission just for approximate location, - // so it would be good to detect that and warn about that: approximate - // location has intentionally only ~2km accuracy - too low for any data collection - QLocationPermission permApprox; - permApprox.setAccuracy( QLocationPermission::Approximate ); - if ( qApp->checkPermission( permApprox ) == Qt::PermissionStatus::Granted ) - this->setState( tr( "Approximate location only!" ), State::NoConnection ); - else - this->setState( tr( "No location permissions" ), State::NoConnection ); - } - } ); - return; - } - - // TODO: this should not be needed? AndroidPositionProvider::startUpdates(); } @@ -238,6 +204,17 @@ void AndroidPositionProvider::startUpdates() { __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] start updates" ); + // permissions are currently being requested in main.qml, so here + // we only check that we have the permissions we need. + QLocationPermission perm; + perm.setAccuracy( QLocationPermission::Precise ); + if ( qApp->checkPermission( perm ) != Qt::PermissionStatus::Granted ) + { + __android_log_print( ANDROID_LOG_ERROR, "CPP", "[c++] no location permissions - not starting!" ); + setState( tr( "No location permissions" ), State::NoConnection ); + return; + } + jboolean res = mAndroidPos.callMethod( "start", "()Z" ); __android_log_print( ANDROID_LOG_INFO, "CPP", "[c++] start updates res: %d", res ); diff --git a/app/qml/main.qml b/app/qml/main.qml index 2051c4439..6232857e1 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -244,20 +244,6 @@ ApplicationWindow { LocationPermission { id: locationPermission accuracy: LocationPermission.Precise - - function requestPermissionAsync() { - if ( locationPermission.status === Qt.Granted ) { - return true; - } - else if ( locationPermission.status === Qt.Undetermined ) { - locationPermission.request() - } - else if ( locationPermission.status === Qt.Denied ) { - __inputUtils.log("Permissions", "Location permission is denied") - __notificationModel.addInfo( qsTr( "Location permission is required to show your location on map. Please enable it in system settings." ) ); - } - return false; - } } MMToolbar { @@ -948,6 +934,10 @@ ApplicationWindow { // check location permission if ( locationPermission.status === Qt.Undetermined ) { + // This is the place where we actually request permissions. + // When the system's request permissions dialog get closed, + // we get a notification that our application is active again, + // and PositionKit::appStateChanged() will try to start updates. locationPermission.request(); } else if ( locationPermission.status === Qt.Denied ) {