diff --git a/CMakeLists.txt b/CMakeLists.txt index ddebcab..2b80b0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,9 @@ project(waterLevelSensor VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTORCC ON) -find_package(Qt6 REQUIRED COMPONENTS Quick Network) +find_package(Qt6 REQUIRED COMPONENTS Quick Network CorePrivate) -qt_standard_project_setup(REQUIRES 6.9) +qt_standard_project_setup(REQUIRES 6.11) qt_add_executable(appwaterLevelSensor main.cpp @@ -41,8 +41,8 @@ set_property(TARGET service APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR target_link_libraries(service PRIVATE Qt::Core - Qt::CorePrivate Qt::Network + Qt::CorePrivate Qt::Quick ) qt_add_qml_module(appwaterLevelSensor diff --git a/Main.qml b/Main.qml index 0f60281..7a705dd 100644 --- a/Main.qml +++ b/Main.qml @@ -634,19 +634,29 @@ Window { } } Timer { + property int count: 0 id: init_timer running: true - repeat: false + repeat: true interval: 1000 onTriggered: { - console.log("Timer triggered saving config, hope everything is loaded by now"); - let current_min_reading = Math.max(0, (parseFloat(min_thres_input.text))); - let effective_max_reading = Math.max(0, (parseFloat(max_thres_input.text)) - current_min_reading); - let alert = parseFloat(alert_thres_input.text); - let alert_effective_reading = (effective_max_reading * (100 - alert) / 100) + current_min_reading; - console.log("Setting initial configs: ", alert_effective_reading, config.flipBits , config.mode, config.url); - listener.setConfig(alert_effective_reading, config.flipBits , config.mode, config.url); - http_url_input.cursorPosition = 0; + if(count === 0) { + listener.startService(); + count++; + return; + } + else { + console.log("Timer triggered saving config, hope everything is loaded by now"); + let current_min_reading = Math.max(0, (parseFloat(min_thres_input.text))); + let effective_max_reading = Math.max(0, (parseFloat(max_thres_input.text)) - current_min_reading); + let alert = parseFloat(alert_thres_input.text); + let alert_effective_reading = (effective_max_reading * (100 - alert) / 100) + current_min_reading; + console.log("Setting initial configs: ", alert_effective_reading, config.flipBits , config.mode, config.url); + listener.setConfig(alert_effective_reading, config.flipBits , config.mode, config.url); + http_url_input.cursorPosition = 0; + init_timer.repeat = false; + init_timer.stop(); + } } } @@ -658,9 +668,18 @@ Window { error_text.error_string = err; } - onLastUpdatedChanged: (update) => { + onLastUpdatedChanged: (update, warn) => { console.log("QML Last Updated: ", update); - last_updated_text.last_updated = update; + if(!warn) + { + last_updated_text.last_updated = update; + last_updated_text.color = root.accent_color; + } + else + { + last_updated_text.last_updated = update; + last_updated_text.color = root.danger_color; + } } onDistanceChanged: { @@ -722,9 +741,12 @@ Window { } } - onClosing:(close)=> { + onClosing: (close)=> { if(Qt.platform.os === "android"){ - close.accepted = false + // close.accepted = false + // listener.moveAppToBackground(); + // console.log("Moving to backgound"); + close.accepted = true; } else { close.accepted = true } diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 7c065f1..ead6548 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -29,7 +29,9 @@ + android:exported="true" + android:process=":water_service" + android:stopWithTask="false"> diff --git a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java index 79c4728..2d3709f 100644 --- a/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java +++ b/android/src/org/qtproject/example/appwaterLevelSensor/WaterLevelService.java @@ -32,22 +32,49 @@ public class WaterLevelService extends Service { private boolean bits_flipped = false; private String mode = ""; private String url = ""; + private Thread udpThread; + private Thread cmdThread; + private boolean resetBinds = false; + + public synchronized boolean checkBound() { + return resetBinds; + } + + public synchronized void setBound(boolean value) { + resetBinds = value; + } public class UDPCommandClient implements Runnable { private DatagramSocket socket_cmd_recv; private DatagramSocket socket_cmd_send; + private boolean received_cmd_first = false; + @Override public void run() { Log.d("UDP Service", "Command thread initialising"); try { - socket_cmd_recv = new DatagramSocket(8178, InetAddress.getByName("localhost")); - socket_cmd_send = new DatagramSocket(8179); - + socket_cmd_recv = new DatagramSocket(null); + socket_cmd_send = new DatagramSocket(null); + socket_cmd_recv.setSoTimeout(4000); + socket_cmd_recv.setReuseAddress(true); + socket_cmd_send.setReuseAddress(true); + socket_cmd_recv.bind(new InetSocketAddress(8178)); + socket_cmd_send.bind(new InetSocketAddress(8179)); Log.d("UDP Service", "Created command thread sockets"); while (!Thread.currentThread().isInterrupted()) { + if(socket_cmd_recv.isClosed()) { + socket_cmd_recv.bind(new InetSocketAddress(8178)); + } + if(socket_cmd_send.isClosed()) { + socket_cmd_send.bind(new InetSocketAddress(8179)); + } byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); - socket_cmd_recv.receive(packet); + try { + socket_cmd_recv.receive(packet); + } catch(java.net.SocketTimeoutException e) { + continue; + } String str = new String(buffer, StandardCharsets.UTF_8); Log.d("UDP Service ", "Received command packet " + str); if(str.startsWith("", ""); alert_thres_distance = Float.parseFloat(str); + { + if(!checkBound()) + { + setBound(true); + Log.d("UDP Service ", "Set Bound to True"); + } + else { + Log.d("UDP Service ", "Set Bound is False"); + } + + } } else if(str.startsWith(" serviceClass, Context context) { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } + + private static int getServicePid(Class serviceClass, Context context) { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return service.pid; + } + } + return 0; + } + public static void startService(Context context) { - Log.d("UDP Service", "Starting service via QtActivity instance"); + // Log.d("UDP Service", "Starting service via QtActivity instance"); - Intent intent = new Intent(context, WaterLevelService.class); + // if(!isMyServiceRunning(WaterLevelService.class, context)) { + // Intent intent = new Intent(context, WaterLevelService.class); + // context.startForegroundService(intent); + // } - context.startForegroundService(intent); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (!isMyServiceRunning(WaterLevelService.class, context)) { + Log.d("UDP Service", "Starting service safely on Main Looper"); + Intent intent = new Intent(context, WaterLevelService.class); + context.startForegroundService(intent); + } + else { + Log.d("UDP Service", "Service already running"); + } + Log.d("UDP Service", "Background service running with pid: " + getServicePid(WaterLevelService.class, context)); + } + }); } } diff --git a/configinterface.cpp b/configinterface.cpp index ab97ecb..73ae20a 100644 --- a/configinterface.cpp +++ b/configinterface.cpp @@ -23,9 +23,9 @@ ConfigInterface::ConfigInterface(QObject *parent) QJsonObject top = QJsonDocument::fromJson(data).object(); QString max, min, alert, mode, url; bool flipbits; - max = top["max_threshold"].toString("100"); - min = top["min_threshold"].toString("20"); - alert = top["alert_threshold"].toString("60"); + max = top["max_threshold"].toString("110"); + min = top["min_threshold"].toString("10"); + alert = top["alert_threshold"].toString("30"); flipbits = top["flip_bits"].toBool(true); mode = top["mode"].toString("http"); url = top["url"].toString("https://unrealdragon.in/server/waterlevel"); @@ -39,9 +39,9 @@ ConfigInterface::ConfigInterface(QObject *parent) else { qDebug() << "File does not exist creating one..."; QJsonObject top; - top["max_threshold"] = "125"; - top["min_threshold"] = "25"; - top["alert_threshold"] = "100"; + top["max_threshold"] = "110"; + top["min_threshold"] = "10"; + top["alert_threshold"] = "30"; top["flip_bits"] = true; top["mode"] = "http"; top["url"] = "https://unrealdragon.in/server/waterlevel"; @@ -54,9 +54,9 @@ ConfigInterface::ConfigInterface(QObject *parent) config.write(data); config.flush(); config.close(); - setMax("125"); - setMin("25"); - setAlert("100"); + setMax("110"); + setMin("10"); + setAlert("30"); setFlipBits(true); setMode("http"); setUrl("https://unrealdragon.in/server/waterlevel"); diff --git a/service.cpp b/service.cpp index 6b819b0..f48e455 100644 --- a/service.cpp +++ b/service.cpp @@ -11,7 +11,6 @@ Q_DECLARE_JNI_CLASS(ComponentName, "android/content/ComponentName") Q_DECLARE_JNI_CLASS(WaterLevelService, "org/qtproject/example/appwaterLevelSensor/WaterLevelService") - static volatile float level = -1; static void handleWaterLevelUpdate(JNIEnv *env, jobject /*obj*/, jbyteArray data) { @@ -29,87 +28,95 @@ static void handleWaterLevelUpdate(JNIEnv *env, jobject /*obj*/, jbyteArray data env->ReleaseByteArrayElements(data, bytes, JNI_ABORT); } +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + + // Register your native method ONCE when the library loads + jclass javaClass = env->FindClass("org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver"); + if (javaClass) { + static JNINativeMethod methods[] = { + {"updateWaterLevel", "([B)V", (void *)handleWaterLevelUpdate} + }; + env->RegisterNatives(javaClass, methods, 1); + } + + return JNI_VERSION_1_6; +} + + service::service(QObject *parent) : QObject{parent} { if(QSysInfo::productType() == "android") { qDebug() << "Running android service"; + // QJniEnvironment env; + // // if (env->ExceptionCheck()) env->ExceptionClear(); + // // static bool nativesRegistered = false; - using namespace QtJniTypes; - using namespace QNativeInterface; + // // if(!nativesRegistered) { + // // // jclass javaClass = env.findClass("org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver"); + // // // if (javaClass) { + // // // JNINativeMethod methods[] = {{"updateWaterLevel", "([B)V", (void *)handleWaterLevelUpdate}}; + // // // if (env->RegisterNatives(javaClass, methods, 1) < 0) { + // // // qDebug() << "RegisterNatives failed!"; + // // // } else { + // // // nativesRegistered = true; + // // // } + // // // } else { + // // // qDebug() << "Could not find WaterLevelReceiver class"; + // // // } + // // // if (env->ExceptionCheck()) env->ExceptionClear(); + // // } + // using namespace QtJniTypes; + // using namespace QNativeInterface; - JNINativeMethod methods[] = { - {"updateWaterLevel", "([B)V", (void *)handleWaterLevelUpdate} - }; - // auto *androidApp = qGuiApp->nativeInterface(); - // Context context = androidApp->context(); - QJniObject context = QNativeInterface::QAndroidApplication::context(); - - QJniEnvironment env; - auto serviceClass = env.findClass(Traits::className()); - if (!serviceClass) { - qDebug() << "FAILED to find WaterLevelService class!"; - env->ExceptionDescribe(); // VERY important - env->ExceptionClear(); - return; - } - - jclass javaClass = env.findClass("org/qtproject/example/appwaterLevelSensor/WaterLevelReceiver"); - env->RegisterNatives(javaClass, methods, 1); - - - QJniObject::callStaticMethod( - "org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter", - "startService", - "(Landroid/content/Context;)V", - context.object()); // pass the QtActivity instance ); - - // QJniObject activity = QNativeInterface::QAndroidApplication::context(); - // activity.callMethod("startWaterLevelService", "()V"); - // Intent serviceIntent(activity, serviceClass); - // activity.callMethod("startWaterLevelService"); - - // activity.callMethod<() - // QJniObject::callStaticMethod( - // "org/qtproject/example/appwaterLevelSensor/WaterLevelService", - // "startQtAndroidService", - // "(Landroid/content/Context;)V", - // activity.object() - // ); - - // activity.callMethod("startWaterLevelService", "()V"); // Intent serviceIntent(context, serviceClass); - // context.callMethod("startService", serviceIntent); - - // context.callMethod( - // "startForegroundService", - // serviceIntent - // ); - // QJniObject className = QJniObject::fromString( - // "org.qtproject.example.appwaterLevelSensor.WaterLevelService" - // ); - - // QJniObject serviceClassObj = QJniObject::callStaticObjectMethod( - // "java/lang/Class", - // "forName", - // "(Ljava/lang/String;)Ljava/lang/Class;", - // className.object() - // ); - - // QJniObject intent( - // "android/content/Intent", - // "(Landroid/content/Context;Ljava/lang/Class;)V", - // context.object(), - // serviceClassObj.object() - // ); - - // context.callObjectMethod( - // "startForegroundService", - // "(Landroid/content/Intent;)Landroid/content/ComponentName;", - // intent.object() - // ); // WaterLevelService::callStaticMethod("startQtAndroidService", context); + // // Context context = androidApp->context(); + // QJniObject context = QNativeInterface::QAndroidApplication::context(); + // auto serviceClass = env.findClass(Traits::className()); + // if (!serviceClass) { + // qDebug() << "FAILED to find WaterLevelService class!"; + // env->ExceptionDescribe(); // VERY important + // env->ExceptionClear(); + // return; + // } + // if (context.isValid()) { + // QJniObject::callStaticMethod< + // void>( + // "org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter", + // "startService", + // "(Landroid/content/Context;)V", + // context.object()); + // } } } + +void service::moveAppToBackground() +{ + QJniObject activity = QNativeInterface::QAndroidApplication::context(); + if (activity.isValid()) { + qDebug() << "Moved app to background"; + activity.callMethod("moveTaskToBack", "(Z)Z", true); + } + qDebug() << "Closing"; +} + +void service::begin() { + + + // Only call this once the UI is visible + QJniObject context = QNativeInterface::QAndroidApplication::context(); + QJniObject::callStaticMethod( + "org/qtproject/example/appwaterLevelSensor/WaterLevelServiceStarter", + "startService", + "(Landroid/content/Context;)V", + context.object() + ); +} + diff --git a/service.h b/service.h index 5b64765..1ba18a1 100644 --- a/service.h +++ b/service.h @@ -8,6 +8,8 @@ class service : public QObject Q_OBJECT public: explicit service(QObject *parent = nullptr); + void moveAppToBackground(); + void begin(); signals: }; diff --git a/udplistener.cpp b/udplistener.cpp index 52aaa8d..e44a9eb 100644 --- a/udplistener.cpp +++ b/udplistener.cpp @@ -1,15 +1,12 @@ #include "udplistener.h" #include -#ifdef Q_OS_ANDROID -#include "service.h" -#endif UDPListener::UDPListener(QObject *parent) : QObject{parent} { - worker = new Worker(this); - thread = new QThread(this); + worker = new Worker(); + thread = new QThread(); worker->moveToThread(thread); connect(worker, &Worker::lastUpdateddatagramReceived, this, &UDPListener::lastUpdateddatagramReceived); connect(worker, &Worker::datagramReceived, this, &UDPListener::datagramRecieved); @@ -23,11 +20,6 @@ UDPListener::UDPListener(QObject *parent) timer->setSingleShot(false); connect(timer, &QTimer::timeout, this, &UDPListener::connectionTimerTimedOut); connect(qApp, &QCoreApplication::aboutToQuit, this, &UDPListener::destroy); -#ifdef Q_OS_ANDROID - qDebug() << "Running on Android"; - service service; - qDebug() << "Ran service"; -#endif } double UDPListener::distance() const @@ -41,11 +33,29 @@ void UDPListener::setDistance(double newDistance) emit distanceChanged(); } +void UDPListener::moveAppToBackground() +{ +#ifdef Q_OS_ANDROID + service->moveAppToBackground(); +#endif +} + void UDPListener::lastUpdateddatagramReceived(QByteArray data) { QString s_data = QString::fromUtf8(data); qDebug() << "Main Class last updated on: " << s_data; - emit lastUpdatedChanged(s_data); + QDateTime time = QDateTime::fromString(s_data, "dd/MM/yy hh:mm:ss"); + QDateTime thres_time = QDateTime::currentDateTime().addSecs(-1800); + if(time.isValid() && time < thres_time) + { + emit lastUpdatedChanged(s_data, false); + } + else if(time > thres_time && time.isValid()) { + emit lastUpdatedChanged(s_data, true); + } + else if(!time.isValid()) { + emit lastUpdatedChanged("Invalid Date", true); + } } void UDPListener::continueDestruction() @@ -114,6 +124,9 @@ void UDPListener::setConfig(float distance, bool flip_bits, QString mode, QStrin emit changeWorkerMode(mode, url); } + else { + showError("Command Socket Bind Failed"); + } } averages.clear(); } @@ -129,6 +142,12 @@ void UDPListener::socketError(QString err) void UDPListener::startService() { +#ifdef Q_OS_ANDROID + qDebug() << "Running on Android"; + service = new class service(); + qDebug() << "Ran service"; + service->begin(); +#endif } @@ -163,25 +182,31 @@ void UDPListener::datagramRecieved(QByteArray data1, QByteArray data2) void UDPListener::destroy() { qDebug() << "Destroying UDP Listener"; + +#ifdef Q_OS_ANDROID + delete service; +#endif + disconnect(timer, nullptr, nullptr, nullptr); timer->stop(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); connect(this, &UDPListener::stopWorker, worker, &Worker::stop); connect(worker, &Worker::stopped, this, &UDPListener::continueDestruction); emit stopWorker(); while(!destroyed) { - QCoreApplication::processEvents(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); } thread->exit(0); - QCoreApplication::processEvents(); - QCoreApplication::processEvents(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); worker->deleteLater(); - QCoreApplication::processEvents(); - QCoreApplication::processEvents(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); + while(thread->isRunning()) { + qDebug() << "The thread is still running."; + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); + } thread->deleteLater(); - QCoreApplication::processEvents(); - QCoreApplication::processEvents(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); timer->deleteLater(); - QCoreApplication::processEvents(); - QCoreApplication::processEvents(); + QCoreApplication::processEvents(QEventLoop::AllEvents, 2000); qDebug() << "Deinitialized UDP Listener"; } @@ -234,6 +259,7 @@ void Worker::gotHttpReply(QNetworkReply *rep) void Worker::httpTimerTimeout() { + qDebug() << "Worker httpTimer Timeout"; QNetworkRequest req; req.setUrl(QUrl("https://unrealdragon.in/server/waterlevel")); disconnect(manager, nullptr, nullptr, nullptr); @@ -243,8 +269,16 @@ void Worker::httpTimerTimeout() void Worker::stop() { - disconnect(socket, nullptr, nullptr, nullptr); - delete socket; + if(socket_2) { + disconnect(socket_2, nullptr, nullptr, nullptr); + socket_2->close(); + delete socket_2; + } + if(socket) { + disconnect(socket, nullptr, nullptr, nullptr); + socket->close(); + delete socket; + } if(httpTimer) { disconnect(httpTimer, nullptr, nullptr, nullptr); delete httpTimer; @@ -265,8 +299,8 @@ void Worker::run() manager = new QNetworkAccessManager(this); #ifdef Q_OS_ANDROID // If it is android use it as a command socket; - bool bind = socket->bind(QHostAddress::Any, 8180); - bool bind2 = socket_2->bind(QHostAddress::Any, 8185); + bool bind = socket->bind(QHostAddress::Any, 8180, QUdpSocket::ReuseAddressHint); + bool bind2 = socket_2->bind(QHostAddress::Any, 8185, QUdpSocket::ReuseAddressHint); #else // Else use it as a receiver; bool bind = socket->bind(QHostAddress("192.168.1.255"), 8176); @@ -277,6 +311,7 @@ void Worker::run() qDebug() << "UI Worker socket bind failed" << bind << socket->errorString(); emit socketError(socket->errorString()); emit socketError(socket_2->errorString()); + qApp->exit(-1); } connect(httpTimer, &QTimer::timeout, this, &Worker::httpTimerTimeout); connect(socket, &QUdpSocket::readyRead, this, &Worker::socketDataReceived); diff --git a/udplistener.h b/udplistener.h index 0a950e5..8c123c8 100644 --- a/udplistener.h +++ b/udplistener.h @@ -16,6 +16,11 @@ #include #include "moving_container.h" +#ifdef Q_OS_ANDROID +#include "service.h" +#endif + + class Worker: public QObject { Q_OBJECT QUdpSocket *socket = nullptr; @@ -48,12 +53,15 @@ class UDPListener : public QObject QML_ELEMENT Q_PROPERTY(double distance READ distance WRITE setDistance NOTIFY distanceChanged FINAL) bool destroyed = false; +#ifdef Q_OS_ANDROID + service *service = nullptr; +#endif public: explicit UDPListener(QObject *parent = nullptr); double distance() const; void setDistance(double newDistance); signals: - void lastUpdatedChanged(QString last_updated); + void lastUpdatedChanged(QString last_updated, bool warn); void stopWorker(); void showError(QString err); void distanceChanged(); @@ -63,6 +71,7 @@ signals: void distanceReceived(double dist, QString timestamp); void changeWorkerMode(QString mode, QString url); public slots: + void moveAppToBackground(); void lastUpdateddatagramReceived(QByteArray data); void continueDestruction(); void resetAverages();