mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
Fixed: push notifications on VPN connect/disconnect
This commit is contained in:
@@ -24,5 +24,8 @@
|
||||
<string name="notificationSettingsDialogMessage">Для показа уведомлений необходимо включить уведомления в системных настройках</string>
|
||||
<string name="openNotificationSettings">Открыть настройки уведомлений</string>
|
||||
|
||||
<string name="vpnStateEventChannelName">Уведомления о VPN</string>
|
||||
<string name="vpnStateEventChannelDescription">Краткие оповещения при подключении и отключении VPN</string>
|
||||
|
||||
<string name="tvNoFileBrowser">Пожалуйста, установите приложение для просмотра файлов</string>
|
||||
</resources>
|
||||
@@ -24,5 +24,8 @@
|
||||
<string name="notificationSettingsDialogMessage">To show notifications, you must enable notifications in the system 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>
|
||||
</resources>
|
||||
@@ -1003,6 +1003,11 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun isNotificationPermissionGranted(): Boolean = applicationContext.isNotificationPermissionGranted()
|
||||
|
||||
@Suppress("unused")
|
||||
fun showVpnStateNotification(title: String, message: String) {
|
||||
ServiceNotification.showVpnStateEvent(applicationContext, title, message)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun requestNotificationPermission() {
|
||||
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"
|
||||
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 CONNECT_REQUEST_CODE = 1
|
||||
private const val DISCONNECT_REQUEST_CODE = 2
|
||||
@@ -162,8 +165,42 @@ class ServiceNotification(private val context: Context) {
|
||||
.setDescription(context.resources.getString(R.string.notificationChannelDescription))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ set(LIBS ${LIBS}
|
||||
|
||||
|
||||
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_wrapper.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}
|
||||
${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_wrapper.mm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios/iosnotificationhandler.mm
|
||||
|
||||
@@ -84,6 +84,10 @@ if(NOT ANDROID)
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.h
|
||||
)
|
||||
else()
|
||||
set(HEADERS ${HEADERS}
|
||||
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.h
|
||||
)
|
||||
endif()
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
@@ -174,6 +178,10 @@ if(NOT ANDROID)
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/ui/utils/notificationHandler.cpp
|
||||
)
|
||||
else()
|
||||
set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/platforms/android/android_notificationhandler.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
set(COMMON_FILES_H
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
#include <QQmlContext>
|
||||
#include <QThread>
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
#include "ui/utils/systemTrayNotificationHandler.h"
|
||||
#endif
|
||||
#include "ui/utils/systemTrayNotificationHandler.h"
|
||||
|
||||
#include "ui/controllers/api/subscriptionUiController.h"
|
||||
#include "ui/controllers/api/apiNewsUiController.h"
|
||||
@@ -141,9 +139,7 @@ private:
|
||||
SecureServersRepository* m_serversRepository;
|
||||
SecureAppSettingsRepository* m_appSettingsRepository;
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
NotificationHandler* m_notificationHandler;
|
||||
#endif
|
||||
|
||||
QMetaObject::Connection m_reloadConfigErrorOccurredConnection;
|
||||
|
||||
|
||||
@@ -407,9 +407,12 @@ void CoreSignalHandlers::initNotificationHandler()
|
||||
&ConnectionUiController::closeConnection);
|
||||
connect(m_coreController, &CoreController::translationsUpdated, m_coreController->m_notificationHandler, &NotificationHandler::onTranslationsUpdated);
|
||||
|
||||
auto* trayHandler = qobject_cast<SystemTrayNotificationHandler*>(m_coreController->m_notificationHandler);
|
||||
connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
|
||||
#endif
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
if (auto *trayHandler = qobject_cast<SystemTrayNotificationHandler *>(m_coreController->m_notificationHandler)) {
|
||||
connect(m_coreController, &CoreController::websiteUrlChanged, trayHandler, &SystemTrayNotificationHandler::updateWebsiteUrl);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
|
||||
@@ -307,6 +307,16 @@ void AndroidController::requestNotificationPermission()
|
||||
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()
|
||||
{
|
||||
QEventLoop wait;
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
QPixmap getAppIcon(const QString &package, QSize *size, const QSize &requestedSize);
|
||||
bool isNotificationPermissionGranted();
|
||||
void requestNotificationPermission();
|
||||
void showVpnStateNotification(const QString &title, const QString &message);
|
||||
bool requestAuthentication();
|
||||
void sendTouch(float x, float y);
|
||||
|
||||
|
||||
19
client/platforms/android/android_notificationhandler.cpp
Normal file
19
client/platforms/android/android_notificationhandler.cpp
Normal file
@@ -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);
|
||||
}
|
||||
16
client/platforms/android/android_notificationhandler.h
Normal file
16
client/platforms/android/android_notificationhandler.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef ANDROID_NOTIFICATIONHANDLER_H
|
||||
#define ANDROID_NOTIFICATIONHANDLER_H
|
||||
|
||||
#include "ui/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
|
||||
@@ -61,6 +61,7 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
|
||||
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
|
||||
const QString& message, int timerMsec) {
|
||||
Q_UNUSED(type);
|
||||
Q_UNUSED(timerMsec);
|
||||
|
||||
if (!m_delegate) {
|
||||
return;
|
||||
@@ -71,11 +72,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
|
||||
content.body = message.toNSString();
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
int timerSec = timerMsec / 1000;
|
||||
NSTimeInterval delay = 0.1;
|
||||
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
|
||||
trigger:trigger];
|
||||
|
||||
@@ -143,6 +146,7 @@ IOSNotificationHandler::~IOSNotificationHandler() { }
|
||||
void IOSNotificationHandler::notify(NotificationHandler::Message type, const QString& title,
|
||||
const QString& message, int timerMsec) {
|
||||
Q_UNUSED(type);
|
||||
Q_UNUSED(timerMsec);
|
||||
|
||||
if (!m_delegate) {
|
||||
return;
|
||||
@@ -153,11 +157,13 @@ void IOSNotificationHandler::notify(NotificationHandler::Message type, const QSt
|
||||
content.body = message.toNSString();
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
int timerSec = timerMsec / 1000;
|
||||
NSTimeInterval delay = 0.1;
|
||||
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
|
||||
trigger:trigger];
|
||||
|
||||
|
||||
8
client/platforms/macos/macos_ne_vpn_notification.h
Normal file
8
client/platforms/macos/macos_ne_vpn_notification.h
Normal file
@@ -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
|
||||
91
client/platforms/macos/macos_ne_vpn_notification.mm
Normal file
91
client/platforms/macos/macos_ne_vpn_notification.mm
Normal file
@@ -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);
|
||||
}
|
||||
}];
|
||||
}
|
||||
@@ -5,16 +5,19 @@
|
||||
#include <QDebug>
|
||||
#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"
|
||||
#else
|
||||
# include "systemTrayNotificationHandler.h"
|
||||
#endif
|
||||
|
||||
|
||||
// static
|
||||
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);
|
||||
#else
|
||||
return new SystemTrayNotificationHandler(parent);
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
# include "platforms/macos/macosutils.h"
|
||||
#endif
|
||||
|
||||
#ifdef MACOS_NE
|
||||
# include "platforms/macos/macos_ne_vpn_notification.h"
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QIcon>
|
||||
@@ -152,6 +156,12 @@ void SystemTrayNotificationHandler::notify(NotificationHandler::Message type,
|
||||
int timerMsec) {
|
||||
Q_UNUSED(type);
|
||||
|
||||
#ifdef MACOS_NE
|
||||
Q_UNUSED(timerMsec);
|
||||
macosNePostVpnStateNotification(title, message);
|
||||
return;
|
||||
#endif
|
||||
|
||||
QIcon icon(ConnectedTrayIconName);
|
||||
m_systemTrayIcon.showMessage(title, message, icon, timerMsec);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user