From 63600a99489634e7f585efe7fcb2c849472693a0 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Sun, 19 May 2024 17:48:14 +0200 Subject: [PATCH] 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 3269fc7567..7cc64c122d 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 0000000000..b3e939d0cb --- /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 4a26fb2def..8662a91a01 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 0000000000..b6e5417767 --- /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 0000000000..5fb1454721 --- /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 0fb68dc33a..fc8a026707 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 9d9baa4245..039273e9b4 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 a1b29e942c..4de37f510d 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 0b4e7adc88..c83f1ababa 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 {