Compare commits

...

5 Commits

Author SHA1 Message Date
dranik 4cf0e477c1 merge dev 2026-05-18 18:43:12 +03:00
dranik 426a95c425 fixed compile file 2026-05-05 15:20:23 +03:00
dranik 5e52f7fcb0 fix path include 2026-05-05 15:05:07 +03:00
dranik c9a1b2e451 fix build iOS 2026-05-05 14:57:36 +03:00
dranik 6087375fb0 Fixed: push notifications on VPN connect/disconnect 2026-05-04 23:38:27 +03:00
18 changed files with 246 additions and 21 deletions
+3
View File
@@ -24,5 +24,8 @@
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string> <string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
<string name="openNotificationSettings">Открыть настройки уведомлений</string> <string name="openNotificationSettings">Открыть настройки уведомлений</string>
<string name="vpnStateEventChannelName">Уведомления о VPN</string>
<string name="vpnStateEventChannelDescription">Краткие оповещения при подключении и отключении VPN</string>
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string> <string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
</resources> </resources>
+3
View File
@@ -24,5 +24,8 @@
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string> <string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system settings</string>
<string name="openNotificationSettings">Open notification settings</string> <string name="openNotificationSettings">Open notification settings</string>
<string name="vpnStateEventChannelName">VPN connection alerts</string>
<string name="vpnStateEventChannelDescription">Brief alerts when VPN connects or disconnects</string>
<string name="tvNoFileBrowser">Please install a file management utility to browse files</string> <string name="tvNoFileBrowser">Please install a file management utility to browse files</string>
</resources> </resources>
@@ -1003,6 +1003,11 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused") @Suppress("unused")
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted() fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
@Suppress("unused")
fun showVpnStateNotification(title: String, message: String) {
ServiceNotification.showVpnStateEvent(applicationContext, title, message)
}
@Suppress("unused") @Suppress("unused")
fun requestNotificationPermission() { fun requestNotificationPermission() {
val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) val shouldShowPreRequest = shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
@@ -26,6 +26,9 @@ private const val OLD_NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notific
private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications" private const val NOTIFICATION_CHANNEL_ID: String = "org.amnezia.vpn.notifications"
const val NOTIFICATION_ID = 1337 const val NOTIFICATION_ID = 1337
const val VPN_STATE_EVENT_NOTIFICATION_ID = 1338
private const val VPN_STATE_EVENT_CHANNEL_ID = "org.amnezia.vpn.vpn_state_events"
private const val GET_ACTIVITY_REQUEST_CODE = 0 private const val GET_ACTIVITY_REQUEST_CODE = 0
private const val CONNECT_REQUEST_CODE = 1 private const val CONNECT_REQUEST_CODE = 1
private const val DISCONNECT_REQUEST_CODE = 2 private const val DISCONNECT_REQUEST_CODE = 2
@@ -162,8 +165,42 @@ class ServiceNotification(private val context: Context) {
.setDescription(context.resources.getString(R.string.notificationChannelDescription)) .setDescription(context.resources.getString(R.string.notificationChannelDescription))
.build() .build()
) )
createNotificationChannel(
Builder(VPN_STATE_EVENT_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setShowBadge(false)
.setSound(null, null)
.setVibrationEnabled(false)
.setLightsEnabled(false)
.setName(context.getString(R.string.vpnStateEventChannelName))
.setDescription(context.getString(R.string.vpnStateEventChannelDescription))
.build()
)
} }
} }
/** Brief alert when VPN connects or disconnects (invoked from Qt via AmneziaActivity). */
fun showVpnStateEvent(context: Context, title: String, message: String) {
if (!context.isNotificationPermissionGranted()) return
val nm = NotificationManagerCompat.from(context)
val notification = NotificationCompat.Builder(context, VPN_STATE_EVENT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_amnezia_round)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setContentIntent(
PendingIntent.getActivity(
context,
GET_ACTIVITY_REQUEST_CODE,
Intent(context, AmneziaActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
nm.notify(VPN_STATE_EVENT_NOTIFICATION_ID, notification)
}
} }
} }
+2
View File
@@ -31,6 +31,7 @@ set(LIBS ${LIBS}
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.h
@@ -42,6 +43,7 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_contro
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macos_ne_vpn_notification.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/ios_controller_wrapper.mm
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
+10
View File
@@ -85,6 +85,11 @@ if(NOT ANDROID)
set(HEADERS ${HEADERS} set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.h ${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.h
) )
else()
set(HEADERS ${HEADERS}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.h
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.h
)
endif() endif()
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
@@ -178,6 +183,11 @@ if(NOT ANDROID)
set(SOURCES ${SOURCES} set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.cpp ${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.cpp
) )
else()
set(SOURCES ${SOURCES}
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.cpp
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.cpp
)
endif() endif()
set(COMMON_FILES_H set(COMMON_FILES_H
+2 -6
View File
@@ -77,9 +77,7 @@
#include "ui/models/ipSplitTunnelingModel.h" #include "ui/models/ipSplitTunnelingModel.h"
#include "ui/models/newsModel.h" #include "ui/models/newsModel.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) class NotificationHandler;
#include "ui/utils/notificationHandler.h"
#endif
class CoreSignalHandlers; class CoreSignalHandlers;
class TestMultipleImports; class TestMultipleImports;
@@ -144,9 +142,7 @@ private:
SecureServersRepository* m_serversRepository; SecureServersRepository* m_serversRepository;
SecureAppSettingsRepository* m_appSettingsRepository; SecureAppSettingsRepository* m_appSettingsRepository;
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) NotificationHandler* m_notificationHandler {};
NotificationHandler* m_notificationHandler;
#endif
QMetaObject::Connection m_reloadConfigErrorOccurredConnection; QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
@@ -37,8 +37,8 @@
#include "ui/models/containersModel.h" #include "ui/models/containersModel.h"
#include "core/utils/containerEnum.h" #include "core/utils/containerEnum.h"
#include "ui/utils/notificationHandler.h"
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
#include "ui/utils/notificationHandler.h"
#include "ui/utils/systemTrayNotificationHandler.h" #include "ui/utils/systemTrayNotificationHandler.h"
#endif #endif
@@ -410,22 +410,23 @@ void CoreSignalHandlers::initIosSettingsHandler()
void CoreSignalHandlers::initNotificationHandler() void CoreSignalHandlers::initNotificationHandler()
{ {
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
m_coreController->m_notificationHandler = NotificationHandler::create(m_coreController); m_coreController->m_notificationHandler = NotificationHandler::create(m_coreController);
connect(m_coreController->m_connectionController, &ConnectionController::connectionStateChanged, m_coreController->m_notificationHandler, connect(m_coreController->m_connectionController, &ConnectionController::connectionStateChanged, m_coreController->m_notificationHandler,
&NotificationHandler::setConnectionState); &NotificationHandler::setConnectionState);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_notificationHandler, &NotificationHandler::onTranslationsUpdated);
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
connect(m_coreController->m_notificationHandler, &NotificationHandler::raiseRequested, m_coreController->m_pageController, &PageController::raiseMainWindow); connect(m_coreController->m_notificationHandler, &NotificationHandler::raiseRequested, m_coreController->m_pageController, &PageController::raiseMainWindow);
connect(m_coreController->m_notificationHandler, &NotificationHandler::connectRequested, m_coreController->m_connectionUiController, connect(m_coreController->m_notificationHandler, &NotificationHandler::connectRequested, m_coreController->m_connectionUiController,
static_cast<void (ConnectionUiController::*)()>(&ConnectionUiController::openConnection)); static_cast<void (ConnectionUiController::*)()>(&ConnectionUiController::openConnection));
connect(m_coreController->m_notificationHandler, &NotificationHandler::disconnectRequested, m_coreController->m_connectionUiController, connect(m_coreController->m_notificationHandler, &NotificationHandler::disconnectRequested, m_coreController->m_connectionUiController,
&ConnectionUiController::closeConnection); &ConnectionUiController::closeConnection);
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_notificationHandler, &NotificationHandler::onTranslationsUpdated);
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_coreController->m_notificationHandler); if (auto *trayHandler = qobject_cast<SystemTrayNotificationHandler *>(m_coreController->m_notificationHandler)) {
connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl); connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
#endif }
#endif
} }
void CoreSignalHandlers::initUpdateFoundHandler() void CoreSignalHandlers::initUpdateFoundHandler()
@@ -307,6 +307,16 @@ void AndroidController::requestNotificationPermission()
callActivityMethod("requestNotificationPermission", "()V"); callActivityMethod("requestNotificationPermission", "()V");
} }
void AndroidController::showVpnStateNotification(const QString &title, const QString &message)
{
if (!isNotificationPermissionGranted()) {
return;
}
callActivityMethod("showVpnStateNotification", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(title).object<jstring>(),
QJniObject::fromString(message).object<jstring>());
}
bool AndroidController::requestAuthentication() bool AndroidController::requestAuthentication()
{ {
QEventLoop wait; QEventLoop wait;
@@ -53,6 +53,7 @@ public:
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize); QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
bool isNotificationPermissionGranted(); bool isNotificationPermissionGranted();
void requestNotificationPermission(); void requestNotificationPermission();
void showVpnStateNotification(const QString &title, const QString &message);
bool requestAuthentication(); bool requestAuthentication();
void sendTouch(float x, float y); void sendTouch(float x, float y);
@@ -0,0 +1,19 @@
#include "android_notificationhandler.h"
#include "android_controller.h"
AndroidNotificationHandler::AndroidNotificationHandler(QObject *parent)
: NotificationHandler(parent)
{
}
void AndroidNotificationHandler::notify(Message type, const QString &title, const QString &message, int timerMsec)
{
Q_UNUSED(type);
Q_UNUSED(timerMsec);
// Permission is checked on the Kotlin side as well; avoid JNI if already denied.
if (!AndroidController::instance()->isNotificationPermissionGranted()) {
return;
}
AndroidController::instance()->showVpnStateNotification(title, message);
}
@@ -0,0 +1,16 @@
#ifndef ANDROID_NOTIFICATIONHANDLER_H
#define ANDROID_NOTIFICATIONHANDLER_H
#include "ui/utils/notificationHandler.h"
class AndroidNotificationHandler final : public NotificationHandler {
Q_OBJECT
public:
explicit AndroidNotificationHandler(QObject *parent = nullptr);
protected:
void notify(Message type, const QString &title, const QString &message, int timerMsec) override;
};
#endif // ANDROID_NOTIFICATIONHANDLER_H
+12 -6
View File
@@ -61,6 +61,7 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title, void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) { const QString& message, int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
Q_UNUSED(timerMsec);
if (!m_delegate) { if (!m_delegate) {
return; return;
@@ -71,11 +72,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString(); content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound]; content.sound = [UNNotificationSound defaultSound];
int timerSec = timerMsec / 1000; NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger* trigger = UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO]; [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn" NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld",
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
content:content content:content
trigger:trigger]; trigger:trigger];
@@ -143,6 +146,7 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title, void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
const QString& message, int timerMsec) { const QString& message, int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
Q_UNUSED(timerMsec);
if (!m_delegate) { if (!m_delegate) {
return; return;
@@ -153,11 +157,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
content.body = message.toNSString(); content.body = message.toNSString();
content.sound = [UNNotificationSound defaultSound]; content.sound = [UNNotificationSound defaultSound];
int timerSec = timerMsec / 1000; NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger* trigger = UNTimeIntervalNotificationTrigger* trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timerSec repeats:NO]; [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"amneziavpn" NSString* requestId = [NSString stringWithFormat:@"amneziavpn.vpnstate.%lld",
(long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:requestId
content:content content:content
trigger:trigger]; trigger:trigger];
@@ -0,0 +1,8 @@
#ifndef MACOS_NE_VPN_NOTIFICATION_H
#define MACOS_NE_VPN_NOTIFICATION_H
class QString;
void macosNePostVpnStateNotification(const QString &title, const QString &message);
#endif
@@ -0,0 +1,91 @@
#include "macos_ne_vpn_notification.h"
#include <QtGlobal>
#include <QString>
#import <Foundation/Foundation.h>
#import <UserNotifications/UserNotifications.h>
namespace {
@interface MacosNeVpnNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@end
@implementation MacosNeVpnNotificationDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(notification)
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
Q_UNUSED(center)
Q_UNUSED(response)
completionHandler();
}
@end
MacosNeVpnNotificationDelegate *delegateInstance()
{
static MacosNeVpnNotificationDelegate *d;
static dispatch_once_t once;
dispatch_once(&once, ^{
d = [[MacosNeVpnNotificationDelegate alloc] init];
});
return d;
}
void ensureNotificationCenterSetup()
{
static dispatch_once_t once;
dispatch_once(&once, ^{
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError *_Nullable error) {
Q_UNUSED(granted);
if (!error) {
center.delegate = delegateInstance();
}
}];
});
}
} // namespace
void macosNePostVpnStateNotification(const QString &title, const QString &message)
{
ensureNotificationCenterSetup();
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = title.toNSString();
content.body = message.toNSString();
content.sound = nil;
NSTimeInterval delay = 0.1;
UNTimeIntervalNotificationTrigger *trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO];
NSString *identifier =
[NSString stringWithFormat:@"amneziavpn.vpnstate.%lld", (long long)([[NSDate date] timeIntervalSince1970] * 1000.0)];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content
trigger:trigger];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request
withCompletionHandler:^(NSError *_Nullable error) {
if (error) {
NSLog(@"macosNePostVpnStateNotification failed: %@", error);
}
}];
}
@@ -16,6 +16,10 @@
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QTimer> #include <QTimer>
#ifdef Q_OS_IOS
#include "platforms/ios/ios_controller.h"
#endif
namespace namespace
{ {
namespace configKey namespace configKey
+6 -3
View File
@@ -5,16 +5,19 @@
#include <QDebug> #include <QDebug>
#include "notificationHandler.h" #include "notificationHandler.h"
#if defined(Q_OS_IOS) #if defined(Q_OS_ANDROID)
# include "platforms/android/android_notificationhandler.h"
#elif defined(Q_OS_IOS)
# include "platforms/ios/iosnotificationhandler.h" # include "platforms/ios/iosnotificationhandler.h"
#else #else
# include "systemTrayNotificationHandler.h" # include "systemTrayNotificationHandler.h"
#endif #endif
// static // static
NotificationHandler* NotificationHandler::create(QObject* parent) { NotificationHandler* NotificationHandler::create(QObject* parent) {
#if defined(Q_OS_IOS) #if defined(Q_OS_ANDROID)
return new AndroidNotificationHandler(parent);
#elif defined(Q_OS_IOS)
return new IOSNotificationHandler(parent); return new IOSNotificationHandler(parent);
#else #else
return new SystemTrayNotificationHandler(parent); return new SystemTrayNotificationHandler(parent);
@@ -10,6 +10,10 @@
# include "platforms/macos/macosutils.h" # include "platforms/macos/macosutils.h"
#endif #endif
#ifdef MACOS_NE
# include "platforms/macos/macos_ne_vpn_notification.h"
#endif
#include <QApplication> #include <QApplication>
#include <QDesktopServices> #include <QDesktopServices>
#include <QIcon> #include <QIcon>
@@ -152,6 +156,12 @@ void SystemTrayNotificationHandler::notify(NotificationHandler::Message type,
int timerMsec) { int timerMsec) {
Q_UNUSED(type); Q_UNUSED(type);
#ifdef MACOS_NE
Q_UNUSED(timerMsec);
macosNePostVpnStateNotification(title, message);
return;
#endif
QIcon icon(ConnectedTrayIconName); QIcon icon(ConnectedTrayIconName);
m_systemTrayIcon.showMessage(title, message, icon, timerMsec); m_systemTrayIcon.showMessage(title, message, icon, timerMsec);
} }