/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtPositioning module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include "qgeopositioninfosource_android_p.h" #include "qgeosatelliteinfosource_android_p.h" #include "jnipositioning.h" static JavaVM *javaVM = nullptr; static jclass positioningClass; static jmethodID providerListMethodId; static jmethodID lastKnownPositionMethodId; static jmethodID startUpdatesMethodId; static jmethodID stopUpdatesMethodId; static jmethodID requestUpdateMethodId; static jmethodID startSatelliteUpdatesMethodId; static const char logTag[] = "QtPositioning"; static const char classErrorMsg[] = "Can't find class \"%s\""; static const char methodErrorMsg[] = "Can't find method \"%s%s\""; namespace AndroidPositioning { typedef QMap PositionSourceMap; typedef QMap SatelliteSourceMap; Q_GLOBAL_STATIC(PositionSourceMap, idToPosSource) Q_GLOBAL_STATIC(SatelliteSourceMap, idToSatSource) struct AttachedJNIEnv { AttachedJNIEnv() { attached = false; if (javaVM && javaVM->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6) < 0) { if (javaVM->AttachCurrentThread(&jniEnv, nullptr) < 0) { __android_log_print(ANDROID_LOG_ERROR, logTag, "AttachCurrentThread failed"); jniEnv = nullptr; return; } attached = true; } } ~AttachedJNIEnv() { if (attached) javaVM->DetachCurrentThread(); } bool attached; JNIEnv *jniEnv; }; int registerPositionInfoSource(QObject *obj) { static bool firstInit = true; if (firstInit) { firstInit = false; } int key = -1; if (obj->inherits("QGeoPositionInfoSource")) { QGeoPositionInfoSourceAndroid *src = qobject_cast(obj); Q_ASSERT(src); do { key = qAbs(int(QRandomGenerator::global()->generate())); } while (idToPosSource()->contains(key)); idToPosSource()->insert(key, src); } else if (obj->inherits("QGeoSatelliteInfoSource")) { QGeoSatelliteInfoSourceAndroid *src = qobject_cast(obj); Q_ASSERT(src); do { key = qAbs(int(QRandomGenerator::global()->generate())); } while (idToSatSource()->contains(key)); idToSatSource()->insert(key, src); } return key; } void unregisterPositionInfoSource(int key) { idToPosSource()->remove(key); idToSatSource()->remove(key); } enum PositionProvider { PROVIDER_GPS = 0, PROVIDER_NETWORK = 1, PROVIDER_PASSIVE = 2 }; QGeoPositionInfoSource::PositioningMethods availableProviders() { QGeoPositionInfoSource::PositioningMethods ret = QGeoPositionInfoSource::NoPositioningMethods; AttachedJNIEnv env; if (!env.jniEnv) return ret; jintArray jProviders = static_cast(env.jniEnv->CallStaticObjectMethod( positioningClass, providerListMethodId)); jint *providers = env.jniEnv->GetIntArrayElements(jProviders, nullptr); const int size = env.jniEnv->GetArrayLength(jProviders); for (int i = 0; i < size; i++) { switch (providers[i]) { case PROVIDER_GPS: ret |= QGeoPositionInfoSource::SatellitePositioningMethods; break; case PROVIDER_NETWORK: ret |= QGeoPositionInfoSource::NonSatellitePositioningMethods; break; case PROVIDER_PASSIVE: //we ignore as Qt doesn't have interface for it right now break; default: __android_log_print(ANDROID_LOG_INFO, logTag, "Unknown positioningMethod"); } } env.jniEnv->ReleaseIntArrayElements(jProviders, providers, 0); env.jniEnv->DeleteLocalRef(jProviders); return ret; } //caching originally taken from corelib/kernel/qjni.cpp typedef QHash JMethodIDHash; Q_GLOBAL_STATIC(JMethodIDHash, cachedMethodID) static jmethodID getCachedMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { jmethodID id = nullptr; uint offset_name = qstrlen(name); uint offset_signal = qstrlen(sig); QByteArray key(int(offset_name + offset_signal), Qt::Uninitialized); memcpy(key.data(), name, offset_name); memcpy(key.data()+offset_name, sig, offset_signal); QHash::iterator it = cachedMethodID->find(key); if (it == cachedMethodID->end()) { id = env->GetMethodID(clazz, name, sig); if (env->ExceptionCheck()) { id = nullptr; #ifdef QT_DEBUG env->ExceptionDescribe(); #endif // QT_DEBUG env->ExceptionClear(); } cachedMethodID->insert(key, id); } else { id = it.value(); } return id; } QGeoPositionInfo positionInfoFromJavaLocation(JNIEnv * jniEnv, const jobject &location) { QGeoPositionInfo info; jclass thisClass = jniEnv->GetObjectClass(location); if (!thisClass) return QGeoPositionInfo(); jmethodID mid = getCachedMethodID(jniEnv, thisClass, "getLatitude", "()D"); jdouble latitude = jniEnv->CallDoubleMethod(location, mid); mid = getCachedMethodID(jniEnv, thisClass, "getLongitude", "()D"); jdouble longitude = jniEnv->CallDoubleMethod(location, mid); QGeoCoordinate coordinate(latitude, longitude); //altitude mid = getCachedMethodID(jniEnv, thisClass, "hasAltitude", "()Z"); jboolean attributeExists = jniEnv->CallBooleanMethod(location, mid); if (attributeExists) { mid = getCachedMethodID(jniEnv, thisClass, "getAltitude", "()D"); jdouble value = jniEnv->CallDoubleMethod(location, mid); coordinate.setAltitude(value); } info.setCoordinate(coordinate); //time stamp mid = getCachedMethodID(jniEnv, thisClass, "getTime", "()J"); jlong timestamp = jniEnv->CallLongMethod(location, mid); info.setTimestamp(QDateTime::fromMSecsSinceEpoch(timestamp, Qt::UTC)); //horizontal accuracy mid = getCachedMethodID(jniEnv, thisClass, "hasAccuracy", "()Z"); attributeExists = jniEnv->CallBooleanMethod(location, mid); if (attributeExists) { mid = getCachedMethodID(jniEnv, thisClass, "getAccuracy", "()F"); jfloat accuracy = jniEnv->CallFloatMethod(location, mid); info.setAttribute(QGeoPositionInfo::HorizontalAccuracy, qreal(accuracy)); } //vertical accuracy mid = getCachedMethodID(jniEnv, thisClass, "hasVerticalAccuracy", "()Z"); if (mid) { attributeExists = jniEnv->CallBooleanMethod(location, mid); if (attributeExists) { mid = getCachedMethodID(jniEnv, thisClass, "getVerticalAccuracyMeters", "()F"); if (mid) { jfloat accuracy = jniEnv->CallFloatMethod(location, mid); info.setAttribute(QGeoPositionInfo::VerticalAccuracy, qreal(accuracy)); } } } if (!mid) jniEnv->ExceptionClear(); //ground speed mid = getCachedMethodID(jniEnv, thisClass, "hasSpeed", "()Z"); attributeExists = jniEnv->CallBooleanMethod(location, mid); if (attributeExists) { mid = getCachedMethodID(jniEnv, thisClass, "getSpeed", "()F"); jfloat speed = jniEnv->CallFloatMethod(location, mid); info.setAttribute(QGeoPositionInfo::GroundSpeed, qreal(speed)); } //bearing mid = getCachedMethodID(jniEnv, thisClass, "hasBearing", "()Z"); attributeExists = jniEnv->CallBooleanMethod(location, mid); if (attributeExists) { mid = getCachedMethodID(jniEnv, thisClass, "getBearing", "()F"); jfloat bearing = jniEnv->CallFloatMethod(location, mid); info.setAttribute(QGeoPositionInfo::Direction, qreal(bearing)); } jniEnv->DeleteLocalRef(thisClass); return info; } QList satelliteInfoFromJavaLocation(JNIEnv *jniEnv, jobjectArray satellites, QList* usedInFix) { QList sats; jsize length = jniEnv->GetArrayLength(satellites); for (int i = 0; iGetObjectArrayElement(satellites, i); if (jniEnv->ExceptionOccurred()) { qWarning() << "Cannot process all satellite data due to exception."; break; } jclass thisClass = jniEnv->GetObjectClass(element); if (!thisClass) continue; QGeoSatelliteInfo info; //signal strength jmethodID mid = getCachedMethodID(jniEnv, thisClass, "getSnr", "()F"); jfloat snr = jniEnv->CallFloatMethod(element, mid); info.setSignalStrength(int(snr)); //ignore any satellite with no signal whatsoever if (qFuzzyIsNull(snr)) continue; //prn mid = getCachedMethodID(jniEnv, thisClass, "getPrn", "()I"); jint prn = jniEnv->CallIntMethod(element, mid); info.setSatelliteIdentifier(prn); if (prn >= 1 && prn <= 32) info.setSatelliteSystem(QGeoSatelliteInfo::GPS); else if (prn >= 65 && prn <= 96) info.setSatelliteSystem(QGeoSatelliteInfo::GLONASS); //azimuth mid = getCachedMethodID(jniEnv, thisClass, "getAzimuth", "()F"); jfloat azimuth = jniEnv->CallFloatMethod(element, mid); info.setAttribute(QGeoSatelliteInfo::Azimuth, qreal(azimuth)); //elevation mid = getCachedMethodID(jniEnv, thisClass, "getElevation", "()F"); jfloat elevation = jniEnv->CallFloatMethod(element, mid); info.setAttribute(QGeoSatelliteInfo::Elevation, qreal(elevation)); //used in a fix mid = getCachedMethodID(jniEnv, thisClass, "usedInFix", "()Z"); jboolean inFix = jniEnv->CallBooleanMethod(element, mid); sats.append(info); if (inFix) usedInFix->append(info); jniEnv->DeleteLocalRef(thisClass); jniEnv->DeleteLocalRef(element); } return sats; } QGeoPositionInfo lastKnownPosition(bool fromSatellitePositioningMethodsOnly) { AttachedJNIEnv env; if (!env.jniEnv) return QGeoPositionInfo(); if (!requestionPositioningPermissions(env.jniEnv)) return {}; jobject location = env.jniEnv->CallStaticObjectMethod(positioningClass, lastKnownPositionMethodId, fromSatellitePositioningMethodsOnly); if (location == nullptr) return QGeoPositionInfo(); const QGeoPositionInfo info = positionInfoFromJavaLocation(env.jniEnv, location); env.jniEnv->DeleteLocalRef(location); return info; } inline int positioningMethodToInt(QGeoPositionInfoSource::PositioningMethods m) { int providerSelection = 0; if (m & QGeoPositionInfoSource::SatellitePositioningMethods) providerSelection |= 1; if (m & QGeoPositionInfoSource::NonSatellitePositioningMethods) providerSelection |= 2; return providerSelection; } QGeoPositionInfoSource::Error startUpdates(int androidClassKey) { AttachedJNIEnv env; if (!env.jniEnv) return QGeoPositionInfoSource::UnknownSourceError; QGeoPositionInfoSourceAndroid *source = AndroidPositioning::idToPosSource()->value(androidClassKey); if (source) { if (!requestionPositioningPermissions(env.jniEnv)) return QGeoPositionInfoSource::AccessError; int errorCode = env.jniEnv->CallStaticIntMethod(positioningClass, startUpdatesMethodId, androidClassKey, positioningMethodToInt(source->preferredPositioningMethods()), source->updateInterval()); switch (errorCode) { case 0: case 1: case 2: case 3: return static_cast(errorCode); default: break; } } return QGeoPositionInfoSource::UnknownSourceError; } //used for stopping regular and single updates void stopUpdates(int androidClassKey) { AttachedJNIEnv env; if (!env.jniEnv) return; env.jniEnv->CallStaticVoidMethod(positioningClass, stopUpdatesMethodId, androidClassKey); } QGeoPositionInfoSource::Error requestUpdate(int androidClassKey) { AttachedJNIEnv env; if (!env.jniEnv) return QGeoPositionInfoSource::UnknownSourceError; QGeoPositionInfoSourceAndroid *source = AndroidPositioning::idToPosSource()->value(androidClassKey); if (source) { if (!requestionPositioningPermissions(env.jniEnv)) return QGeoPositionInfoSource::AccessError; int errorCode = env.jniEnv->CallStaticIntMethod(positioningClass, requestUpdateMethodId, androidClassKey, positioningMethodToInt(source->preferredPositioningMethods())); switch (errorCode) { case 0: case 1: case 2: case 3: return static_cast(errorCode); default: break; } } return QGeoPositionInfoSource::UnknownSourceError; } QGeoSatelliteInfoSource::Error startSatelliteUpdates(int androidClassKey, bool isSingleRequest, int requestTimeout) { AttachedJNIEnv env; if (!env.jniEnv) return QGeoSatelliteInfoSource::UnknownSourceError; QGeoSatelliteInfoSourceAndroid *source = AndroidPositioning::idToSatSource()->value(androidClassKey); if (source) { if (!requestionPositioningPermissions(env.jniEnv)) return QGeoSatelliteInfoSource::AccessError; int interval = source->updateInterval(); if (isSingleRequest) interval = requestTimeout; int errorCode = env.jniEnv->CallStaticIntMethod(positioningClass, startSatelliteUpdatesMethodId, androidClassKey, interval, isSingleRequest); switch (errorCode) { case -1: case 0: case 1: case 2: return static_cast(errorCode); default: qWarning() << "startSatelliteUpdates: Unknown error code " << errorCode; break; } } return QGeoSatelliteInfoSource::UnknownSourceError; } bool requestionPositioningPermissions(JNIEnv *env) { using namespace QtAndroidPrivate; if (androidSdkVersion() < 23) return true; // Android v23+ requires runtime permission check and requests QString permission(QLatin1String("android.permission.ACCESS_FINE_LOCATION")); if (checkPermission(permission) == PermissionsResult::Denied) { const QHash results = requestPermissionsSync(env, QStringList() << permission); if (!results.contains(permission) || results[permission] == PermissionsResult::Denied) { qWarning() << "Position data not available due to missing permission " << permission; return false; } } return true; } } static void positionUpdated(JNIEnv *env, jobject /*thiz*/, jobject location, jint androidClassKey, jboolean isSingleUpdate) { QGeoPositionInfo info = AndroidPositioning::positionInfoFromJavaLocation(env, location); QGeoPositionInfoSourceAndroid *source = AndroidPositioning::idToPosSource()->value(androidClassKey); if (!source) { qWarning("positionUpdated: source == 0"); return; } //we need to invoke indirectly as the Looper thread is likely to be not the same thread if (!isSingleUpdate) QMetaObject::invokeMethod(source, "processPositionUpdate", Qt::AutoConnection, Q_ARG(QGeoPositionInfo, info)); else QMetaObject::invokeMethod(source, "processSinglePositionUpdate", Qt::AutoConnection, Q_ARG(QGeoPositionInfo, info)); } static void locationProvidersDisabled(JNIEnv *env, jobject /*thiz*/, jint androidClassKey) { Q_UNUSED(env); QObject *source = AndroidPositioning::idToPosSource()->value(androidClassKey); if (!source) source = AndroidPositioning::idToSatSource()->value(androidClassKey); if (!source) { qWarning("locationProvidersDisabled: source == 0"); return; } QMetaObject::invokeMethod(source, "locationProviderDisabled", Qt::AutoConnection); } static void locationProvidersChanged(JNIEnv *env, jobject /*thiz*/, jint androidClassKey) { Q_UNUSED(env); QObject *source = AndroidPositioning::idToPosSource()->value(androidClassKey); if (!source) { qWarning("locationProvidersChanged: source == 0"); return; } QMetaObject::invokeMethod(source, "locationProvidersChanged", Qt::AutoConnection); } static void satelliteUpdated(JNIEnv *env, jobject /*thiz*/, jobjectArray satellites, jint androidClassKey, jboolean isSingleUpdate) { QList inUse; QList sats = AndroidPositioning::satelliteInfoFromJavaLocation(env, satellites, &inUse); QGeoSatelliteInfoSourceAndroid *source = AndroidPositioning::idToSatSource()->value(androidClassKey); if (!source) { qWarning("satelliteUpdated: source == 0"); return; } QMetaObject::invokeMethod(source, "processSatelliteUpdateInView", Qt::AutoConnection, Q_ARG(QList, sats), Q_ARG(bool, isSingleUpdate)); QMetaObject::invokeMethod(source, "processSatelliteUpdateInUse", Qt::AutoConnection, Q_ARG(QList, inUse), Q_ARG(bool, isSingleUpdate)); } #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ clazz = env->FindClass(CLASS_NAME); \ if (!clazz) { \ __android_log_print(ANDROID_LOG_FATAL, logTag, classErrorMsg, CLASS_NAME); \ return JNI_FALSE; \ } #define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetStaticMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, logTag, methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \ return JNI_FALSE; \ } static JNINativeMethod methods[] = { {"positionUpdated", "(Landroid/location/Location;IZ)V", (void *)positionUpdated}, {"locationProvidersDisabled", "(I)V", (void *) locationProvidersDisabled}, {"satelliteUpdated", "([Landroid/location/GpsSatellite;IZ)V", (void *)satelliteUpdated}, {"locationProvidersChanged", "(I)V", (void *) locationProvidersChanged} }; static bool registerNatives(JNIEnv *env) { jclass clazz; FIND_AND_CHECK_CLASS("org/qtproject/qt/android/positioning/QtPositioning"); positioningClass = static_cast(env->NewGlobalRef(clazz)); if (env->RegisterNatives(positioningClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { __android_log_print(ANDROID_LOG_FATAL, logTag, "RegisterNatives failed"); return JNI_FALSE; } GET_AND_CHECK_STATIC_METHOD(providerListMethodId, positioningClass, "providerList", "()[I"); GET_AND_CHECK_STATIC_METHOD(lastKnownPositionMethodId, positioningClass, "lastKnownPosition", "(Z)Landroid/location/Location;"); GET_AND_CHECK_STATIC_METHOD(startUpdatesMethodId, positioningClass, "startUpdates", "(III)I"); GET_AND_CHECK_STATIC_METHOD(stopUpdatesMethodId, positioningClass, "stopUpdates", "(I)V"); GET_AND_CHECK_STATIC_METHOD(requestUpdateMethodId, positioningClass, "requestUpdate", "(II)I"); GET_AND_CHECK_STATIC_METHOD(startSatelliteUpdatesMethodId, positioningClass, "startSatelliteUpdates", "(IIZ)I"); return true; } Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) { static bool initialized = false; if (initialized) return JNI_VERSION_1_6; initialized = true; typedef union { JNIEnv *nativeEnvironment; void *venv; } UnionJNIEnvToVoid; __android_log_print(ANDROID_LOG_INFO, logTag, "Positioning start"); UnionJNIEnvToVoid uenv; uenv.venv = nullptr; javaVM = nullptr; if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) { __android_log_print(ANDROID_LOG_FATAL, logTag, "GetEnv failed"); return -1; } JNIEnv *env = uenv.nativeEnvironment; if (!registerNatives(env)) { __android_log_print(ANDROID_LOG_FATAL, logTag, "registerNatives failed"); return -1; } javaVM = vm; return JNI_VERSION_1_6; }