Skip to content

Commit f581845

Browse files
committed
fix: Enrich android channel with nfc HW states
- Don't attempt to enable NFC if the app or HW is not ready - Notify clients whenever the HW adapter state changes
1 parent aede4e9 commit f581845

File tree

4 files changed

+120
-5
lines changed

4 files changed

+120
-5
lines changed

CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ if(IOS OR ANDROID)
1616
find_package(Qt6 REQUIRED COMPONENTS Nfc)
1717
endif()
1818

19+
# Android: this library may be built standalone (e.g. as a vendored dependency) and still needs
20+
# QtGui headers for QGuiApplication-based lifecycle gating in the NFC backend.
21+
if(ANDROID)
22+
find_package(Qt6 REQUIRED COMPONENTS Gui)
23+
endif()
24+
1925
# OpenSSL for secp256k1 ECDH
2026
# For Android and iOS, we use manually provided paths instead of find_package
2127
if((ANDROID OR IOS) AND OPENSSL_CRYPTO_LIBRARY AND OPENSSL_BUILD_INCLUDE_DIR AND OPENSSL_SOURCE_INCLUDE_DIR)
@@ -140,6 +146,10 @@ if(IOS OR ANDROID)
140146
target_link_libraries(keycard-qt PUBLIC Qt6::Nfc)
141147
endif()
142148

149+
if(ANDROID)
150+
target_link_libraries(keycard-qt PRIVATE Qt6::Gui)
151+
endif()
152+
143153
# Link OpenSSL (if found)
144154
if(OpenSSL_FOUND)
145155
# For Android and iOS with manually provided paths, link directly to the library file

include/keycard-qt/backends/keycard_channel_unified_qt_nfc.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "keycard_channel_backend.h"
77
#include <QNearFieldManager>
88
#include <QNearFieldTarget>
9+
#include <QMetaObject>
910
#include <QMutex>
1011

1112
namespace Keycard {
@@ -38,6 +39,9 @@ class KeycardChannelUnifiedQtNfc : public KeycardChannelBackend
3839
private slots:
3940
void onTargetDetected(QNearFieldTarget* target);
4041
void onTargetLost(QNearFieldTarget* target);
42+
#ifdef Q_OS_ANDROID
43+
void onAdapterStateChanged(QNearFieldManager::AdapterState state);
44+
#endif
4145

4246
private:
4347
QString describe(QNearFieldTarget::Error error);
@@ -50,6 +54,13 @@ private slots:
5054
// State management
5155
ChannelState m_state = ChannelState::Idle;
5256
bool m_detectionActive = false;
57+
58+
#ifdef Q_OS_ANDROID
59+
// Qt NFC on Android can fail if discovery is started before the Activity/app is fully active.
60+
// We defer startTargetDetection() until the app reaches Qt::ApplicationActive.
61+
bool m_waitingForAppActive = false;
62+
QMetaObject::Connection m_appStateConn;
63+
#endif
5364

5465
// Helper to update and emit channel state
5566
void emitChannelState(ChannelOperationalState newState);

src/channel/backends/keycard_channel_unified_qt_nfc.cpp

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
#include <QDebug>
66
#include <QThread>
77
#include <QCoreApplication>
8+
#ifdef Q_OS_ANDROID
9+
#include <QGuiApplication>
10+
#include <QTimer>
11+
#endif
812

913
namespace Keycard {
1014

11-
// Android NFC timeout extension is now handled via platform/android_nfc_utils.h
12-
// and called from CommandSet before long operations like GlobalPlatform factory reset
13-
1415
KeycardChannelUnifiedQtNfc::KeycardChannelUnifiedQtNfc(QObject* parent)
1516
: KeycardChannelBackend(parent)
1617
, m_manager(nullptr)
@@ -22,6 +23,15 @@ KeycardChannelUnifiedQtNfc::KeycardChannelUnifiedQtNfc(QObject* parent)
2223
this, &KeycardChannelUnifiedQtNfc::onTargetDetected, Qt::DirectConnection);
2324
connect(m_manager, &QNearFieldManager::targetLost,
2425
this, &KeycardChannelUnifiedQtNfc::onTargetLost, Qt::DirectConnection);
26+
connect(this, &KeycardChannelUnifiedQtNfc::channelStateChanged, this, [this](ChannelOperationalState state) {
27+
if (state == ChannelOperationalState::WaitingForKeycard) {
28+
m_manager->setUserInformation("Waiting for keycard. Please hold the keycard near the device.");
29+
} else if (state == ChannelOperationalState::Reading) {
30+
m_manager->setUserInformation("Reading keycard. Please hold the keycard near the device.");
31+
} else if (state == ChannelOperationalState::Error) {
32+
m_manager->setUserInformation("Error reading keycard. Please try again.");
33+
}
34+
});
2535
}
2636

2737
KeycardChannelUnifiedQtNfc::~KeycardChannelUnifiedQtNfc()
@@ -34,6 +44,48 @@ void KeycardChannelUnifiedQtNfc::startDetection()
3444
{
3545
qDebug() << "KeycardChannelUnifiedQtNfc::startDetection()";
3646

47+
#ifdef Q_OS_ANDROID
48+
// Android-only: emitted when the OS NFC adapter is toggled on/off (or transitioning).
49+
QObject::connect(m_manager, &QNearFieldManager::adapterStateChanged,
50+
this, &KeycardChannelUnifiedQtNfc::onAdapterStateChanged, Qt::UniqueConnection);
51+
// Qt 6.9 Android NFC uses foreground dispatch and may fail if started before the Activity
52+
// is fully active/resumed. If we call too early, Qt may consider discovery enabled while
53+
// the platform never actually starts delivering tag intents until a background→foreground cycle.
54+
//
55+
// Mitigation: defer startTargetDetection() until the Qt app is ApplicationActive.
56+
auto *app = QCoreApplication::instance();
57+
auto *guiApp = qobject_cast<QGuiApplication *>(app);
58+
qDebug() << "KeycardChannelUnifiedQtNfc:Verifying app state=" << static_cast<int>(guiApp->applicationState());
59+
60+
if (guiApp && guiApp->applicationState() != Qt::ApplicationActive) {
61+
qDebug() << "KeycardChannelUnifiedQtNfc: App not active yet, deferring NFC start. state="
62+
<< static_cast<int>(guiApp->applicationState());
63+
64+
if (!m_waitingForAppActive) {
65+
m_waitingForAppActive = true;
66+
m_appStateConn = connect(guiApp, &QGuiApplication::applicationStateChanged,
67+
this, [this](Qt::ApplicationState state) {
68+
if (state != Qt::ApplicationActive) {
69+
return;
70+
}
71+
72+
// One-shot: once active, drop the guard and retry immediately on the event loop.
73+
m_waitingForAppActive = false;
74+
QObject::disconnect(m_appStateConn);
75+
m_appStateConn = {};
76+
77+
QTimer::singleShot(0, this, [this]() {
78+
this->startDetection();
79+
});
80+
});
81+
}
82+
83+
// Keep UX consistent: we are logically waiting for a keycard, just not starting NFC yet.
84+
emitChannelState(ChannelOperationalState::WaitingForKeycard);
85+
return;
86+
}
87+
#endif
88+
3789
if (!m_manager->isSupported(QNearFieldTarget::TagTypeSpecificAccess)) {
3890
emitChannelState(ChannelOperationalState::NotSupported);
3991
emit readerAvailabilityChanged(false);
@@ -49,7 +101,7 @@ void KeycardChannelUnifiedQtNfc::startDetection()
49101
m_detectionActive = false;
50102
return;
51103
}
52-
// Desktop: Start continuous detection immediately
104+
53105
m_manager->startTargetDetection(QNearFieldTarget::TagTypeSpecificAccess);
54106
m_detectionActive = true;
55107
emit readerAvailabilityChanged(true);
@@ -58,6 +110,11 @@ void KeycardChannelUnifiedQtNfc::startDetection()
58110

59111
void KeycardChannelUnifiedQtNfc::stopDetection()
60112
{
113+
#ifdef Q_OS_ANDROID
114+
QObject::disconnect(m_manager, &QNearFieldManager::adapterStateChanged,
115+
this, &KeycardChannelUnifiedQtNfc::onAdapterStateChanged);
116+
#endif
117+
// iOS NFC session management can be sensitive to threading; serialize with transmit.
61118
#ifdef Q_OS_IOS
62119
QMutexLocker locker(&m_transmitMutex);
63120
qDebug() << "KeycardChannelUnifiedQtNfc::stopDetection()";
@@ -160,6 +217,44 @@ void KeycardChannelUnifiedQtNfc::onTargetLost(QNearFieldTarget* target)
160217
emit cardRemoved();
161218
}
162219

220+
#ifdef Q_OS_ANDROID
221+
void KeycardChannelUnifiedQtNfc::onAdapterStateChanged(QNearFieldManager::AdapterState state)
222+
{
223+
qDebug() << "KeycardChannelUnifiedQtNfc::onAdapterStateChanged() state=" << static_cast<int>(state);
224+
225+
switch (state) {
226+
case QNearFieldManager::AdapterState::Offline:
227+
case QNearFieldManager::AdapterState::TurningOff: {
228+
m_manager->stopTargetDetection();
229+
m_detectionActive = false;
230+
disconnect();
231+
// NFC is going away: stop scanning and drop any current target.
232+
// Do NOT emit Idle in-between (avoid flicker); go straight to NotAvailable.
233+
emit readerAvailabilityChanged(false);
234+
emitChannelState(ChannelOperationalState::NotAvailable);
235+
break;
236+
}
237+
238+
case QNearFieldManager::AdapterState::Online: {
239+
emit readerAvailabilityChanged(true);
240+
241+
// If higher-level logic expects us to be scanning, (re)start now.
242+
// Use event-loop deferral to avoid re-entrancy if this signal fires during Qt NFC internals.
243+
QTimer::singleShot(0, this, [this]() { this->setState(ChannelState::WaitingForCard); });
244+
break;
245+
}
246+
247+
case QNearFieldManager::AdapterState::TurningOn: {
248+
// Transitional state: avoid emitting errors; keep UX in "waiting" if applicable.
249+
if (m_state == ChannelState::WaitingForCard) {
250+
emitChannelState(ChannelOperationalState::WaitingForKeycard);
251+
}
252+
break;
253+
}
254+
}
255+
}
256+
#endif
257+
163258
QString KeycardChannelUnifiedQtNfc::describe(QNearFieldTarget::Error error)
164259
{
165260
switch(error) {

src/command_set.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
#include "keycard-qt/pairing_storage.h"
55
#include "keycard-qt/globalplatform/gp_command_set.h"
66
#include "keycard-qt/globalplatform/gp_constants.h"
7-
#include "keycard-qt/platform/android_nfc_utils.h"
87
#include <QDebug>
98
#include <QCryptographicHash>
109
#include <QMessageAuthenticationCode>

0 commit comments

Comments
 (0)