From b641df470c94eabb4cb2559c831a942b086bd3db Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sun, 23 Apr 2023 13:23:31 -0400 Subject: [PATCH] android: use node model state to update notification The AndroidNotifier class connects to the NodeModel's state signals and uses JNI to send callbacks to the Android service managing the foreground notification. --- configure.ac | 1 + src/Makefile.qt.include | 6 + src/qml/androidnotifier.cpp | 108 ++++++++++++++++++ src/qml/androidnotifier.h | 26 +++++ src/qml/bitcoin.cpp | 6 + .../org/bitcoincore/qt/BitcoinQtActivity.java | 6 +- .../org/bitcoincore/qt/BitcoinQtService.java | 87 +++++++++++++- 7 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 src/qml/androidnotifier.cpp create mode 100644 src/qml/androidnotifier.h diff --git a/configure.ac b/configure.ac index 68456e7874..0dfbdbf9b7 100644 --- a/configure.ac +++ b/configure.ac @@ -1869,6 +1869,7 @@ AM_CONDITIONAL([TARGET_DARWIN], [test "$TARGET_OS" = "darwin"]) AM_CONDITIONAL([BUILD_DARWIN], [test "$BUILD_OS" = "darwin"]) AM_CONDITIONAL([TARGET_LINUX], [test "$TARGET_OS" = "linux"]) AM_CONDITIONAL([TARGET_WINDOWS], [test "$TARGET_OS" = "windows"]) +AM_CONDITIONAL([TARGET_ANDROID], [test "$TARGET_OS" = "android"]) AM_CONDITIONAL([ENABLE_WALLET], [test "$enable_wallet" = "yes"]) AM_CONDITIONAL([USE_SQLITE], [test "$use_sqlite" = "yes"]) AM_CONDITIONAL([USE_BDB], [test "$use_bdb" = "yes"]) diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 0c9c127cad..0c40af6c05 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -391,6 +391,12 @@ QML_RES_QML = \ qml/pages/settings/SettingsProxy.qml \ qml/pages/settings/SettingsStorage.qml +if TARGET_ANDROID +BITCOIN_QT_H += qml/androidnotifier.h +BITCOIN_QML_BASE_CPP += qml/androidnotifier.cpp +QT_MOC_CPP += qml/moc_androidnotifier.cpp +endif + BITCOIN_QT_CPP = $(BITCOIN_QT_BASE_CPP) if TARGET_WINDOWS BITCOIN_QT_CPP += $(BITCOIN_QT_WINDOWS_CPP) diff --git a/src/qml/androidnotifier.cpp b/src/qml/androidnotifier.cpp new file mode 100644 index 0000000000..c3bcaad711 --- /dev/null +++ b/src/qml/androidnotifier.cpp @@ -0,0 +1,108 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +extern "C" { + JNIEXPORT jboolean JNICALL Java_org_bitcoincore_qt_BitcoinQtService_register(JNIEnv *env, jobject obj); +} + +static JavaVM * g_vm = nullptr; +static jobject g_obj; + +JNIEXPORT jboolean JNICALL Java_org_bitcoincore_qt_BitcoinQtService_register(JNIEnv * env, jobject obj) +{ + env->GetJavaVM(&g_vm); + g_obj = env->NewGlobalRef(obj); + + return (jboolean) true; +} + +namespace { + +JNIEnv* getJNIEnv(JavaVM* javaVM) { + JNIEnv* env; + jint result = javaVM->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + + if (result == JNI_EDETACHED) { + javaVM->AttachCurrentThread(&env, nullptr); + } else if (result != JNI_OK) { + // Error handling + return nullptr; + } + + return env; +} + +} + +AndroidNotifier::AndroidNotifier(const NodeModel & node_model, + QObject * parent) +: QObject(parent) +, m_node_model(node_model) +{ + QObject::connect(&node_model, &NodeModel::blockTipHeightChanged, + this, &AndroidNotifier::onBlockTipHeightChanged); + QObject::connect(&node_model, &NodeModel::numOutboundPeersChanged, + this, &AndroidNotifier::onNumOutboundPeersChanged); + QObject::connect(&node_model, &NodeModel::pauseChanged, + this, &AndroidNotifier::onPausedChanged); + QObject::connect(&node_model, &NodeModel::verificationProgressChanged, + this, &AndroidNotifier::onVerificationProgressChanged); +} + +void AndroidNotifier::onBlockTipHeightChanged() +{ + if (g_vm != nullptr) { + JNIEnv * env = getJNIEnv(g_vm); + if (env == nullptr) { + return; + } + jclass clazz = env->GetObjectClass(g_obj); + jmethodID mid = env->GetMethodID(clazz, "updateBlockTipHeight", "(I)V"); + env->CallVoidMethod(g_obj, mid, m_node_model.blockTipHeight()); + } +} + +void AndroidNotifier::onNumOutboundPeersChanged() +{ + if (g_vm != nullptr) { + JNIEnv * env = getJNIEnv(g_vm); + if (env == nullptr) { + return; + } + jclass clazz = env->GetObjectClass(g_obj); + jmethodID mid = env->GetMethodID(clazz, "updateNumberOfPeers", "(I)V"); + env->CallVoidMethod(g_obj, mid, m_node_model.numOutboundPeers()); + } +} + +void AndroidNotifier::onVerificationProgressChanged() +{ + if (g_vm != nullptr) { + JNIEnv * env = getJNIEnv(g_vm); + if (env == nullptr) { + return; + } + jclass clazz = env->GetObjectClass(g_obj); + jmethodID mid = env->GetMethodID(clazz, "updateVerificationProgress", "(D)V"); + env->CallVoidMethod(g_obj, mid, static_cast(m_node_model.verificationProgress())); + } +} + +void AndroidNotifier::onPausedChanged() +{ + if (g_vm != nullptr) { + JNIEnv * env = getJNIEnv(g_vm); + if (env == nullptr) { + return; + } + jclass clazz = env->GetObjectClass(g_obj); + jmethodID mid = env->GetMethodID(clazz, "updatePaused", "(Z)V"); + env->CallVoidMethod(g_obj, mid, static_cast(m_node_model.pause())); + } +} + diff --git a/src/qml/androidnotifier.h b/src/qml/androidnotifier.h new file mode 100644 index 0000000000..54642bf05b --- /dev/null +++ b/src/qml/androidnotifier.h @@ -0,0 +1,26 @@ +#ifndef BITCOIN_QML_ANDROIDNOTIFIER_H +#define BITCOIN_QML_ANDROIDNOTIFIER_H + +#include + +#include +#include + +class AndroidNotifier : public QObject +{ + Q_OBJECT + +public: + explicit AndroidNotifier(const NodeModel & node_model, QObject * parent = nullptr); + +public Q_SLOTS: + void onBlockTipHeightChanged(); + void onNumOutboundPeersChanged(); + void onVerificationProgressChanged(); + void onPausedChanged(); + +private: + const NodeModel & m_node_model; +}; + +#endif // BITCOIN_QML_ANDROIDNOTIFIER_H diff --git a/src/qml/bitcoin.cpp b/src/qml/bitcoin.cpp index 065e9b9bdd..0a2bf178d9 100644 --- a/src/qml/bitcoin.cpp +++ b/src/qml/bitcoin.cpp @@ -13,6 +13,9 @@ #include #include #include +#ifdef __ANDROID__ +#include +#endif #include #include #include @@ -233,6 +236,9 @@ int QmlGuiMain(int argc, char* argv[]) // QObject::connect(&init_executor, &InitExecutor::runawayException, &node_model, &NodeModel::handleRunawayException); NetworkTrafficTower network_traffic_tower{node_model}; +#ifdef __ANDROID__ + AndroidNotifier android_notifier{node_model}; +#endif ChainModel chain_model{*chain}; chain_model.setCurrentNetworkName(QString::fromStdString(gArgs.GetChainName())); diff --git a/src/qt/android/src/org/bitcoincore/qt/BitcoinQtActivity.java b/src/qt/android/src/org/bitcoincore/qt/BitcoinQtActivity.java index 529ec8ca18..97db345d4e 100644 --- a/src/qt/android/src/org/bitcoincore/qt/BitcoinQtActivity.java +++ b/src/qt/android/src/org/bitcoincore/qt/BitcoinQtActivity.java @@ -25,14 +25,14 @@ public void onCreate(Bundle savedInstanceState) Intent intent = new Intent(this, BitcoinQtService.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent); + startForegroundService(intent); } else { - startService(intent); + startService(intent); } getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_IMMERSIVE); + | View.SYSTEM_UI_FLAG_IMMERSIVE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); super.onCreate(savedInstanceState); diff --git a/src/qt/android/src/org/bitcoincore/qt/BitcoinQtService.java b/src/qt/android/src/org/bitcoincore/qt/BitcoinQtService.java index b26f35ccdc..5448839ce5 100644 --- a/src/qt/android/src/org/bitcoincore/qt/BitcoinQtService.java +++ b/src/qt/android/src/org/bitcoincore/qt/BitcoinQtService.java @@ -17,8 +17,17 @@ public class BitcoinQtService extends QtService { + private static final String TAG = "BitcoinQtService"; + private static final int NOTIFICATION_ID = 21000000; private PowerManager.WakeLock wakeLock; private WifiManager.WifiLock wifiLock; + private Notification.Builder notificationBuilder; + private boolean connected = false; + private boolean paused = false; + private boolean synced = false; + private int blockHeight = 0; + private double verificationProgress = 0.0; + @Override public void onCreate() { @@ -26,7 +35,8 @@ public void onCreate() { CharSequence name = "Bitcoin Core"; String description = "Bitcoin Core App notifications"; - int importance = NotificationManager.IMPORTANCE_DEFAULT; + // IMPORTANCE_LOW notifications won't make sound + int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = new NotificationChannel("bitcoin_channel_id", name, importance); channel.setDescription(description); @@ -36,14 +46,13 @@ public void onCreate() { Intent intent = new Intent(this, BitcoinQtActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); - Notification notification = new Notification.Builder(this, "bitcoin_channel_id") + notificationBuilder = new Notification.Builder(this, "bitcoin_channel_id") .setSmallIcon(R.drawable.bitcoin) .setContentTitle("Running bitcoin") .setOngoing(true) - .setContentIntent(pendingIntent) - .build(); + .setContentIntent(pendingIntent); - startForeground(1, notification); + startForeground(NOTIFICATION_ID, notificationBuilder.build()); PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "BitcoinCore::IBD"); @@ -56,6 +65,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); wakeLock.acquire(); wifiLock.acquire(); + if (register()) { + Log.d(TAG, "Registered JVM to native module"); + } else { + Log.e(TAG, "Failed to register JVM to native module"); + } return START_NOT_STICKY; } @@ -70,4 +84,67 @@ public void onDestroy() { wifiLock.release(); // Release the WiFi lock } } + + public void updateBlockTipHeight(int blockHeight) { + if (this.blockHeight != blockHeight) { + this.blockHeight = blockHeight; + if (synced && connected) { + updateNotification(); + } + } + } + + public void updateNumberOfPeers(int numPeers) { + boolean newConnectedState = numPeers > 0; + if (connected != newConnectedState) { + connected = newConnectedState; + updateNotification(); + } + } + + public void updatePaused(boolean paused) { + if (this.paused != paused) { + this.paused = paused; + updateNotification(); + } + } + + public void updateVerificationProgress(double progress) { + boolean newSyncedState = progress > 0.999; + boolean needNotificationUpdate = false; + if (synced != newSyncedState) { + synced = newSyncedState; + needNotificationUpdate = true; + } + double newProgress = Math.floor(progress * 10000) / 100.0; + if (verificationProgress != newProgress ) { + verificationProgress = newProgress; + needNotificationUpdate = true; + } + if (needNotificationUpdate) { + updateNotification(); + } + } + + private void updateNotification() { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (paused) { + notificationBuilder.setContentTitle("Paused"); + } else if (!connected) { + notificationBuilder.setContentTitle("Connecting..."); + } else if (!synced) { + if (verificationProgress < 0) { + notificationBuilder.setContentTitle(String.format("%.2f%% loaded...", verificationProgress)); + } else { + notificationBuilder.setContentTitle(String.format("%.0f%% loaded...", verificationProgress)); + } + } else { + // Synced and connected + notificationBuilder.setContentTitle(String.format("Blocktime %,d", blockHeight)); + } + + notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); + } + + public native boolean register(); }