Update permission handling

This commit is contained in:
Michael Zanetti 2022-11-19 23:18:56 +01:00
parent a5bea39212
commit b847d5ba59
28 changed files with 670 additions and 244 deletions

View File

@ -227,7 +227,7 @@ public class NymeaAppControlService extends ControlsProviderService {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
intent.putExtra("nymeaId", nymeaId.toString());
intent.putExtra("thingId", thing.id.toString());
pi = PendingIntent.getActivity(context, intentId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
pi = PendingIntent.getActivity(context, intentId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Log.d(TAG, "Created pendingintent for " + thing.name + " with id " + intentId + " and extra " + thing.id);
Control.StatefulBuilder builder = new Control.StatefulBuilder(thing.id.toString(), pi)

View File

@ -36,7 +36,6 @@
#include "thingmanager.h"
#include "connection/nymeatransportinterface.h"
#include "jsonrpc/jsonrpcclient.h"
#include "wifisetup/bluetoothdiscovery.h"
class RuleManager;
class ScriptManager;

View File

@ -73,6 +73,8 @@
#include "configuration/serverconfigurations.h"
#include "configuration/mqttpolicy.h"
#include "configuration/mqttpolicies.h"
#include "wifisetup/bluetoothdeviceinfos.h"
#include "wifisetup/bluetoothdiscovery.h"
#include "wifisetup/btwifisetup.h"
#include "types/wirelessaccesspoint.h"
#include "types/wirelessaccesspoints.h"

View File

@ -45,6 +45,7 @@
#include "nfchelper.h"
#include "nfcthingactionwriter.h"
#include "platformhelper.h"
#include "platformintegration/platformpermissions.h"
#include "dashboard/dashboardmodel.h"
#include "dashboard/dashboarditem.h"
#include "mouseobserver.h"
@ -169,6 +170,7 @@ int main(int argc, char *argv[])
engine->rootContext()->setContextProperty("styleController", &styleController);
qmlRegisterSingletonType<PlatformHelper>("Nymea", 1, 0, "PlatformHelper", PlatformHelper::platformHelperProvider);
qmlRegisterSingletonType<PlatformPermissions>("Nymea", 1, 0, "PlatformPermissions", PlatformPermissions::qmlProvider);
qmlRegisterSingletonType<NfcHelper>("Nymea", 1, 0, "NfcHelper", NfcHelper::nfcHelperProvider);
qmlRegisterType<NfcThingActionWriter>("Nymea", 1, 0, "NfcThingActionWriter");

View File

@ -27,6 +27,7 @@ HEADERS += \
nfchelper.h \
nfcthingactionwriter.h \
platformintegration/generic/screenhelper.h \
platformintegration/platformpermissions.h \
stylecontroller.h \
pushnotifications.h \
platformhelper.h \
@ -39,6 +40,7 @@ SOURCES += main.cpp \
mouseobserver.cpp \
nfchelper.cpp \
nfcthingactionwriter.cpp \
platformintegration/platformpermissions.cpp \
stylecontroller.cpp \
pushnotifications.cpp \
platformhelper.cpp \
@ -73,11 +75,14 @@ android {
include(../3rdParty/android/android_openssl/openssl.pri)
ANDROID_MIN_SDK_VERSION = 21
ANDROID_TARGET_SDK_VERSION = 31
ANDROID_TARGET_SDK_VERSION = 33
QT += androidextras
HEADERS += platformintegration/android/platformhelperandroid.h
SOURCES += platformintegration/android/platformhelperandroid.cpp
HEADERS += platformintegration/android/platformhelperandroid.h \
platformintegration/android/platformpermissionsandroid.h \
SOURCES += platformintegration/android/platformhelperandroid.cpp \
platformintegration/android/platformpermissionsandroid.cpp \
# https://bugreports.qt.io/browse/QTBUG-83165
LIBS += -L$${top_builddir}/libnymea-app/$${ANDROID_TARGET_ARCH}
@ -131,13 +136,20 @@ macx: {
ios: {
message("iOS build")
HEADERS += platformintegration/ios/platformhelperios.h
SOURCES += platformintegration/ios/platformhelperios.cpp
HEADERS += platformintegration/ios/platformhelperios.h \
platformintegration/ios/platformpermissionsios.h \
SOURCES += platformintegration/ios/platformhelperios.cpp \
platformintegration/ios/platformpermissionsios.cpp \
OBJECTIVE_SOURCES += platformintegration/ios/platformhelperios.mm \
platformintegration/ios/pushnotifications.mm \
platformintegration/ios/platformpermissionsios.mm \
OTHER_FILES += $${OBJECTIVE_SOURCES}
LIBS += -framework CoreLocation \
# Add Firebase SDK
QMAKE_LFLAGS += -ObjC $(inherited)
firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist)

View File

@ -99,16 +99,6 @@ PlatformHelper *PlatformHelper::instance(bool create)
return s_instance;
}
bool PlatformHelper::hasPermissions() const
{
return true;
}
void PlatformHelper::requestPermissions()
{
emit permissionsRequestFinished();
}
void PlatformHelper::hideSplashScreen()
{
setSplashVisible(false);

View File

@ -43,7 +43,6 @@ class QJSEngine;
class PlatformHelper : public QObject
{
Q_OBJECT
Q_PROPERTY(bool hasPermissions READ hasPermissions NOTIFY permissionsRequestFinished)
Q_PROPERTY(QString platform READ platform CONSTANT)
Q_PROPERTY(QString deviceSerial READ deviceSerial CONSTANT)
Q_PROPERTY(QString device READ device CONSTANT)
@ -70,9 +69,6 @@ public:
static PlatformHelper* instance(bool create = true);
virtual ~PlatformHelper() = default;
virtual bool hasPermissions() const;
Q_INVOKABLE virtual void requestPermissions();
virtual QString platform() const;
virtual QString machineHostname() const;
virtual QString device() const;
@ -113,7 +109,6 @@ public:
void notificationActionReceived(const QString &nymeaData);
signals:
void permissionsRequestFinished();
void screenTimeoutChanged();
void screenBrightnessChanged();
void topPanelColorChanged();

View File

@ -50,7 +50,7 @@ public class NymeaAppNotificationService extends FirebaseMessagingService {
intent.setAction(Intent.ACTION_SEND);
intent.putExtra("notificationData", remoteMessage.getData().get("nymeaData"));
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
// We can't directly access R.drawable.notificationicon from here:
// When the package is branded, the package name is not "io.guh.nymeaapp" and resources in
@ -83,7 +83,7 @@ public class NymeaAppNotificationService extends FirebaseMessagingService {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("notify_001", "Channel human readable title", NotificationManager.IMPORTANCE_HIGH);
NotificationChannel channel = new NotificationChannel("notify_001", "Notifications from your nymea system", NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
}

View File

@ -84,11 +84,6 @@ PlatformHelperAndroid::PlatformHelperAndroid(QObject *parent) : PlatformHelper(p
}
}
void PlatformHelperAndroid::requestPermissions()
{
// Not using any fancy permissions in android yet...
}
void PlatformHelperAndroid::hideSplashScreen()
{
// Android's splash will flicker when fading out twice
@ -99,12 +94,6 @@ void PlatformHelperAndroid::hideSplashScreen()
}
}
bool PlatformHelperAndroid::hasPermissions() const
{
// Not using any fancy permissions in android yet...
return true;
}
QString PlatformHelperAndroid::machineHostname() const
{
// QSysInfo::machineHostname always gives "localhost" on android... best we can do here is:
@ -265,14 +254,6 @@ void PlatformHelperAndroid::shareFile(const QString &fileName)
);
}
void PlatformHelperAndroid::permissionRequestFinished(const QtAndroid::PermissionResultMap &result)
{
foreach (const QString &key, result.keys()) {
qDebug() << "Permission result:" << key << static_cast<int>(result.value(key));
}
emit m_instance->permissionsRequestFinished();
}
void PlatformHelperAndroid::darkModeEnabledChangedJNI()
{
if (m_instance) {

View File

@ -45,11 +45,8 @@ public:
explicit PlatformHelperAndroid(QObject *parent = nullptr);
Q_INVOKABLE void requestPermissions() override;
Q_INVOKABLE void hideSplashScreen() override;
bool hasPermissions() const override;
QString machineHostname() const override;
QString deviceSerial() const override;
QString device() const override;

View File

@ -0,0 +1,64 @@
#include "platformpermissionsandroid.h"
#include <QApplication>
#include <QDebug>
PlatformPermissionsAndroid * PlatformPermissionsAndroid::s_instance = nullptr;
QHash<PlatformPermissions::Permission, QStringList> permissionMap = {
// TODO: Once QtBluetooth does not request the COARSE_LOCATION for Bluetooth any more, remove it from here. The new Bluetooth permissions would be enough.
{PlatformPermissions::PermissionBluetooth, {"android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT", "android.permission.ACCESS_COARSE_LOCATION"}},
{PlatformPermissions::PermissionLocation, {"android.permission.ACCESS_FINE_LOCATION"}},
{PlatformPermissions::PermissionBackgroundLocation, {"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_BACKGROUND_LOCATION"}},
{PlatformPermissions::PermissionNotifications, {"android.permission.POST_NOTIFICATIONS"}},
};
PlatformPermissionsAndroid::PlatformPermissionsAndroid(QObject *parent)
: PlatformPermissions{parent}
{
s_instance = this;
// If the user switches to the settings app and changes permission settings there, we won't get notified
// in any way, so let's just refresh when we become active
connect(qApp, &QApplication::applicationStateChanged, this, [this](Qt::ApplicationState state){
if (state == Qt::ApplicationActive) {
emit bluetoothPermissionChanged();
emit locationPermissionChanged();
emit backgroundLocationPermissionChanged();
}
});
}
void PlatformPermissionsAndroid::requestPermission(PlatformPermissions::Permission permission)
{
qWarning() << "****** android permission request" << permission;
QtAndroid::requestPermissions({permissionMap.value(permission)}, &permissionResultCallback);
}
void PlatformPermissionsAndroid::openPermissionSettings()
{
QtAndroid::androidActivity().callMethod<void>("openPermissionSettings", "()V");
}
PlatformPermissions::PermissionStatus PlatformPermissionsAndroid::checkPermission(Permission permission) const
{
PermissionStatus status = PermissionStatusGranted;
QStringList androidPermissions = permissionMap.value(permission);
foreach (const QString androidPermission, androidPermissions) {
if (QtAndroid::shouldShowRequestPermissionRationale(androidPermission)) {
return PermissionStatusDenied;
}
if (QtAndroid::checkPermission(androidPermission) == QtAndroid::PermissionResult::Denied) {
status = PermissionStatusNotDetermined;
}
}
return status;
}
void PlatformPermissionsAndroid::permissionResultCallback(const QtAndroid::PermissionResultMap &/*results*/)
{
emit s_instance->bluetoothPermissionChanged();
emit s_instance->locationPermissionChanged();
emit s_instance->backgroundLocationPermissionChanged();
}

View File

@ -0,0 +1,28 @@
#ifndef PLATFORMPERMISSIONSANDROID_H
#define PLATFORMPERMISSIONSANDROID_H
#include "../platformpermissions.h"
#include <QtAndroidExtras/QtAndroid>
class PlatformPermissionsAndroid : public PlatformPermissions
{
Q_OBJECT
public:
explicit PlatformPermissionsAndroid(QObject *parent = nullptr);
PermissionStatus checkPermission(Permission permission) const override;
void requestPermission(Permission permission) override;
void openPermissionSettings() override;
signals:
private:
static PlatformPermissionsAndroid *s_instance;
static void permissionResultCallback(const QtAndroid::PermissionResultMap &results);
};
#endif // PLATFORMPERMISSIONSANDROID_H

View File

@ -44,13 +44,6 @@ PlatformHelperIOS::PlatformHelperIOS(QObject *parent) : PlatformHelper(parent)
QObject::connect(screen, &QScreen::orientationChanged, qApp, [this](Qt::ScreenOrientation) {
setBottomPanelColor(bottomPanelColor());
});
}
void PlatformHelperIOS::requestPermissions()
{
emit permissionsRequestFinished();
}
void PlatformHelperIOS::hideSplashScreen()
@ -58,11 +51,6 @@ void PlatformHelperIOS::hideSplashScreen()
// Nothing to be done
}
bool PlatformHelperIOS::hasPermissions() const
{
return true;
}
QString PlatformHelperIOS::machineHostname() const
{
return QSysInfo::machineHostName();

View File

@ -41,11 +41,8 @@ class PlatformHelperIOS : public PlatformHelper
public:
explicit PlatformHelperIOS(QObject *parent = nullptr);
Q_INVOKABLE virtual void requestPermissions() override;
Q_INVOKABLE void hideSplashScreen() override;
virtual bool hasPermissions() const override;
virtual QString machineHostname() const override;
virtual QString device() const override;
virtual QString deviceSerial() const override;

View File

@ -0,0 +1,69 @@
#include "platformpermissionsios.h"
#include <QSettings>
PlatformPermissionsIOS *PlatformPermissionsIOS::s_instance = nullptr;
PlatformPermissionsIOS::PlatformPermissionsIOS(QObject *parent)
: PlatformPermissions{parent}
{
s_instance = this;
initObjC();
}
PlatformPermissionsIOS *PlatformPermissionsIOS::instance()
{
return s_instance;
}
PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkPermission(Permission permission) const
{
switch (permission) {
case PermissionLocalNetwork:
return checkLocalNetworkPermission();
case PermissionNotifications:
return m_notificationPermissions;
case PermissionBackgroundLocation:
return checkBackgroundLocationPermission();
case PermissionLocation:
return checkLocationPermission();
case PermissionBluetooth:
return checkBluetoothPermission();
default:
return PermissionStatusGranted;
}
}
void PlatformPermissionsIOS::requestPermission(Permission permission)
{
switch (permission) {
case PermissionLocalNetwork:
requestLocalNetworkPermission();
break;
case PermissionNotifications:
requestNotificationPermission();
break;
case PermissionBackgroundLocation:
requestBackgroundLocationPermission();
break;
case PermissionLocation:
requestLocationPermission();
break;
case PermissionBluetooth:
requestBluetoothPermission();
break;
}
}
PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkLocalNetworkPermission() const
{
QSettings settings;
return settings.value("askedForLocalNetworkPermission", false).toBool() ? PermissionStatusGranted : PermissionStatusNotDetermined;
}
void PlatformPermissionsIOS::requestLocalNetworkPermission()
{
QSettings settings;
settings.setValue("askedForLocalNetworkPermission", true);
emit localNetworkPermissionChanged();
}

View File

@ -0,0 +1,51 @@
#ifndef PLATFORMPERMISSIONSIOS_H
#define PLATFORMPERMISSIONSIOS_H
#include <QObject>
#include "../platformpermissions.h"
#if __OBJC__
@class CLLocationManager;
@class CBCentralManager;
#else
typedef void CLLocationManager;
typedef void CBCentralManager;
#endif
class PlatformPermissionsIOS : public PlatformPermissions
{
Q_OBJECT
public:
explicit PlatformPermissionsIOS(QObject *parent = nullptr);
static PlatformPermissionsIOS *instance();
PermissionStatus checkPermission(Permission permission) const override;
void requestPermission(Permission permission) override;
void openPermissionSettings() override;
private:
void initObjC();
void refreshNotificationsPermission();
static PlatformPermissionsIOS *s_instance;
PermissionStatus checkLocalNetworkPermission() const;
PermissionStatus checkBluetoothPermission() const;
PermissionStatus checkLocationPermission() const;
PermissionStatus checkBackgroundLocationPermission() const;
void requestLocalNetworkPermission();
void requestNotificationPermission();
void requestBluetoothPermission();
void requestLocationPermission();
void requestBackgroundLocationPermission();
PermissionStatus m_notificationPermissions = PermissionStatusNotDetermined;
CLLocationManager *m_locationManager = nullptr;
CBCentralManager *m_bluetoothManager = nullptr;
};
#endif // PLATFORMPERMISSIONSIOS_H

View File

@ -0,0 +1,144 @@
#include "platformpermissionsios.h"
#import <UserNotifications/UNUserNotificationCenter.h>
#import <UserNotifications/UNNotificationSettings.h>
#import <CoreLocation/CoreLocation.h>
#import <CoreBluetooth/CoreBluetooth.h>
#import <UIKit/UIKit.h>
@interface LocationManagerPermissionDelegate : NSObject <CLLocationManagerDelegate>
@end
@implementation LocationManagerPermissionDelegate
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
emit PlatformPermissionsIOS::instance()->locationPermissionChanged();
}
@end
@interface BluetoothManagerDelegate: NSObject<CBCentralManagerDelegate>
@end
@implementation BluetoothManagerDelegate
- (void)centralManagerDidUpdateState:(CBCentralManager *)manager {
emit PlatformPermissionsIOS::instance()->bluetoothPermissionChanged();
}
@end
void PlatformPermissionsIOS::initObjC()
{
m_locationManager = [[CLLocationManager alloc] init];
m_locationManager.delegate = [[LocationManagerPermissionDelegate alloc] init];
// Refresh notification permissions right away as that can be retrieved async only. We wanna be ready when the app requests it.
refreshNotificationsPermission();
}
void PlatformPermissionsIOS::refreshNotificationsPermission()
{
// Notification permissions can be retrieved async only.
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
PermissionStatus newPermission;
switch (settings.authorizationStatus) {
case UNAuthorizationStatusNotDetermined:
newPermission = PermissionStatusNotDetermined;
break;
case UNAuthorizationStatusDenied:
newPermission = PermissionStatusDenied;
break;
case UNAuthorizationStatusAuthorized:
case UNAuthorizationStatusProvisional:
case UNAuthorizationStatusEphemeral:
newPermission = PermissionStatusGranted;
break;
}
if (newPermission != m_notificationPermissions) {
m_notificationPermissions = newPermission;
emit notificationsPermissionChanged();
}
}];
}
void PlatformPermissionsIOS::requestNotificationPermission()
{
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionBadge + UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
m_notificationPermissions = granted ? PermissionStatusGranted : PermissionStatusDenied;
emit notificationsPermissionChanged();
}];
}
PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkBluetoothPermission() const
{
// iOS 13.0 would have an api but it's more complicated and also deprecated... Ignoring...
if (@available(iOS 13.1, *)) {
switch (CBCentralManager.authorization) {
case CBManagerAuthorizationAllowedAlways:
case CBManagerAuthorizationRestricted:
return PermissionStatusGranted;
case CBManagerAuthorizationDenied:
return PermissionStatusDenied;
case CBManagerAuthorizationNotDetermined:
return PermissionStatusNotDetermined;
}
}
// Before iOS 13, Bluetooth permissions are not required
return PermissionStatusGranted;
}
void PlatformPermissionsIOS::requestBluetoothPermission()
{
// Instantiating a Bluetooth manager just trigger the popup...
if (!m_bluetoothManager) {
BluetoothManagerDelegate *delegate = [[BluetoothManagerDelegate alloc] init];
m_bluetoothManager = [[CBCentralManager alloc] initWithDelegate:delegate queue:nil];
}
}
PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkLocationPermission() const
{
switch ([CLLocationManager authorizationStatus]) {
case kCLAuthorizationStatusNotDetermined:
return PermissionStatusNotDetermined;
case kCLAuthorizationStatusAuthorizedAlways:
case kCLAuthorizationStatusAuthorizedWhenInUse:
return PermissionStatusGranted;
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted:
return PermissionStatusDenied;
}
return PermissionStatusGranted;
}
void PlatformPermissionsIOS::requestLocationPermission()
{
if ([m_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) {
[m_locationManager requestWhenInUseAuthorization];
}
}
PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkBackgroundLocationPermission() const
{
switch ([CLLocationManager authorizationStatus]) {
case kCLAuthorizationStatusNotDetermined:
return PermissionStatusNotDetermined;
case kCLAuthorizationStatusAuthorizedAlways:
return PermissionStatusGranted;
case kCLAuthorizationStatusAuthorizedWhenInUse:
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted:
return PermissionStatusDenied;
}
return PermissionStatusGranted;
}
void PlatformPermissionsIOS::requestBackgroundLocationPermission()
{
if ([m_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) {
[m_locationManager requestAlwaysAuthorization];
}
}
void PlatformPermissionsIOS::openPermissionSettings()
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}

View File

@ -8,9 +8,18 @@
#import "Firebase/Firebase.h"
@interface FirebaseDelegate: NSObject<FIRMessagingDelegate>
@end
@implementation FirebaseDelegate
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken {
qDebug() << "Firebase token received:" << QString::fromNSString(fcmToken);
dynamic_cast<PushNotifications*>(PushNotifications::instance())->setFirebaseRegistrationToken(QString::fromNSString(fcmToken));
}
@end
// This is hidden, so we declare it here to hook into it
@interface QIOSApplicationDelegate: UIResponder <UIApplicationDelegate,UNUserNotificationCenterDelegate,FIRMessagingDelegate>
@interface QIOSApplicationDelegate: UIResponder <UIApplicationDelegate,UNUserNotificationCenterDelegate>
@end
//add a category to QIOSApplicationDelegate
@ -23,50 +32,12 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Use Firebase library to configure APIs
[FIRApp configure];
[FIRMessaging messaging].delegate = self;
// Register to receive notifications from the system
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){
if(!error){
[[UIApplication sharedApplication] registerForRemoteNotifications];
}
}];
NSLog(@"registering for remote notifications");
qDebug() << "Registering for remote notifications";
return YES;
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSLog(@"Did Register for Remote Notifications with Device Token (%@)", deviceToken);
const unsigned *tokenBytes = (const unsigned*)[deviceToken bytes];
NSString *tokenStr = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
// We've switched to firebase... Not emitting the native APNS token
// qDebug() << "Registering for remote notifications";
// qDebug() << "Token description:" << QString::fromNSString(deviceToken.description);
// qDebug() << "Parsed token:" << QString::fromNSString(tokenStr);
PushNotifications::instance()->setAPNSRegistrationToken(QString::fromNSString(tokenStr));
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSLog(@"Did Fail to Register for Remote Notifications");
NSLog(@"%@, %@", error, error.localizedDescription);
qWarning() << "Failed to register for notifications:" << QString::fromNSString(error.localizedDescription);
}
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
NSLog(@"User Info : %@",notification.request.content.userInfo);
qDebug() << "willPresentNotification called!";
@ -91,20 +62,15 @@
}
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken {
NSLog(@"FCM registration token: %@", fcmToken);
// Notify about received token.
NSDictionary *dataDict = [NSDictionary dictionaryWithObject:fcmToken forKey:@"token"];
[[NSNotificationCenter defaultCenter] postNotificationName:
@"FCMToken" object:nil userInfo:dataDict];
// Note: This callback is fired at each app startup and whenever a new token is generated.
//qDebug() << "Firebase token received:" << QString::fromNSString(fcmToken);
PushNotifications::instance()->setFirebaseRegistrationToken(QString::fromNSString(fcmToken));
}
- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo {
qDebug() << "didReceiveRemoteNotification called";
}
@end
void PushNotifications::registerObjC()
{
[FIRApp configure];
[FIRMessaging messaging].delegate = [[FirebaseDelegate alloc] init];
[[UIApplication sharedApplication] registerForRemoteNotifications];
}

View File

@ -0,0 +1,64 @@
#include "platformpermissions.h"
#ifdef Q_OS_ANDROID
#include "android/platformpermissionsandroid.h"
#elif defined Q_OS_IOS
#include "ios/platformpermissionsios.h"
#endif
PlatformPermissions *PlatformPermissions::instance()
{
#ifdef Q_OS_ANDROID
return new PlatformPermissionsAndroid();
#elif defined Q_OS_IOS
return new PlatformPermissionsIOS();
#else
return new PlatformPermissions();
#endif
}
PlatformPermissions::PlatformPermissions(QObject *parent)
: QObject{parent}
{
}
QObject *PlatformPermissions::qmlProvider(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
Q_UNUSED(scriptEngine)
return instance();
}
PlatformPermissions::PermissionStatus PlatformPermissions::localNetworkPermission() const
{
return checkPermission(PermissionLocalNetwork);
}
PlatformPermissions::PermissionStatus PlatformPermissions::bluetoothPermission() const
{
return checkPermission(PermissionBluetooth);
}
PlatformPermissions::PermissionStatus PlatformPermissions::locationPermission() const
{
return checkPermission(PermissionLocation);
}
PlatformPermissions::PermissionStatus PlatformPermissions::backgroundLocationPermission() const
{
return checkPermission(PermissionBackgroundLocation);
}
PlatformPermissions::PermissionStatus PlatformPermissions::notificationsPermission() const
{
return checkPermission(PermissionNotifications);
}
PlatformPermissions::PermissionStatus PlatformPermissions::checkPermission(Permission permission) const
{
Q_UNUSED(permission)
return PermissionStatusGranted;
}

View File

@ -0,0 +1,65 @@
#ifndef PLATFORMPERMISSIONS_H
#define PLATFORMPERMISSIONS_H
#include <QObject>
class QQmlEngine;
class QJSEngine;
class PlatformPermissions : public QObject
{
Q_OBJECT
Q_PROPERTY(PermissionStatus localNetworkPermission READ localNetworkPermission NOTIFY localNetworkPermissionChanged)
Q_PROPERTY(PermissionStatus bluetoothPermission READ bluetoothPermission NOTIFY bluetoothPermissionChanged)
Q_PROPERTY(PermissionStatus locationPermission READ locationPermission NOTIFY locationPermissionChanged)
Q_PROPERTY(PermissionStatus backgroundLocationPermission READ backgroundLocationPermission NOTIFY backgroundLocationPermissionChanged)
Q_PROPERTY(PermissionStatus notificationsPermission READ notificationsPermission NOTIFY notificationsPermissionChanged)
public:
enum Permission {
PermissionNone = 0x00,
PermissionLocalNetwork = 0x01,
PermissionBluetooth = 0x02,
PermissionLocation = 0x04,
PermissionBackgroundLocation = 0x08,
PermissionNotifications = 0x10
};
Q_ENUM(Permission)
Q_DECLARE_FLAGS(Permissions, Permission)
Q_FLAG(Permissions)
enum PermissionStatus {
PermissionStatusNotDetermined,
PermissionStatusGranted,
PermissionStatusDenied,
};
Q_ENUM(PermissionStatus)
static PlatformPermissions* instance();
static QObject *qmlProvider(QQmlEngine *engine, QJSEngine *scriptEngine);
virtual ~PlatformPermissions() = default;
PermissionStatus localNetworkPermission() const;
PermissionStatus bluetoothPermission() const;
PermissionStatus locationPermission() const;
PermissionStatus backgroundLocationPermission() const;
PermissionStatus notificationsPermission() const;
Q_INVOKABLE virtual PermissionStatus checkPermission(Permission permission) const;
Q_INVOKABLE virtual void requestPermission(Permission permission) { Q_UNUSED(permission) }
Q_INVOKABLE virtual void openPermissionSettings() {}
signals:
void localNetworkPermissionChanged();
void bluetoothPermissionChanged();
void locationPermissionChanged();
void backgroundLocationPermissionChanged();
void notificationsPermissionChanged();
protected:
explicit PlatformPermissions(QObject *parent = nullptr);
};
#endif // PLATFORMPERMISSIONS_H

View File

@ -42,33 +42,7 @@ static PushNotifications *m_client_pointer;
PushNotifications::PushNotifications(QObject *parent) : QObject(parent)
{
#if defined Q_OS_ANDROID && defined WITH_FIREBASE
qDebug() << "Checking for play services";
jboolean playServicesAvailable = QAndroidJniObject::callStaticMethod<jboolean>("io.guh.nymeaapp.NymeaAppNotificationService", "checkPlayServices", "()Z");
if (playServicesAvailable) {
qDebug() << "Setting up firebase";
m_client_pointer = this;
m_firebaseApp = ::firebase::App::Create(::firebase::AppOptions(), QAndroidJniEnvironment(), QtAndroid::androidActivity().object());
m_firebase_initializer.Initialize(m_firebaseApp, nullptr, [](::firebase::App * fapp, void *) {
return ::firebase::messaging::Initialize( *fapp, (::firebase::messaging::Listener *)m_client_pointer);
});
} else {
qDebug() << "Google Play Services not available. Cannot connect to push client.";
}
#endif
#ifdef UBPORTS
m_pushClient = new PushClient(this);
m_pushClient->setAppId("io.guh.nymeaapp_nymea-app");
connect(m_pushClient, &PushClient::tokenChanged, this, [this](const QString &token) {
// On UBPorts, core and cloud use the same token
m_coreToken = token;
emit coreTokenChanged();
m_cloudToken = m_coreToken;
emit cloudTokenChanged();
});
#endif
}
PushNotifications::~PushNotifications()
@ -91,6 +65,57 @@ PushNotifications *PushNotifications::instance()
return pushNotifications;
}
bool PushNotifications::enabled() const
{
return m_enabled;
}
void PushNotifications::setEnabled(bool enabled)
{
if (m_enabled == enabled) {
return;
}
m_enabled = enabled;
if (enabled) {
registerForPush();
}
}
void PushNotifications::registerForPush()
{
#if defined Q_OS_ANDROID && defined WITH_FIREBASE
qDebug() << "Checking for play services";
jboolean playServicesAvailable = QAndroidJniObject::callStaticMethod<jboolean>("io.guh.nymeaapp.NymeaAppNotificationService", "checkPlayServices", "()Z");
if (playServicesAvailable) {
qDebug() << "Setting up firebase";
m_client_pointer = this;
m_firebaseApp = ::firebase::App::Create(::firebase::AppOptions(), QAndroidJniEnvironment(), QtAndroid::androidActivity().object());
m_firebase_initializer.Initialize(m_firebaseApp, nullptr, [](::firebase::App * fapp, void *) {
return ::firebase::messaging::Initialize( *fapp, (::firebase::messaging::Listener *)m_client_pointer);
});
} else {
qDebug() << "Google Play Services not available. Cannot connect to push client.";
}
#endif
#ifdef UBPORTS
m_pushClient = new PushClient(this);
m_pushClient->setAppId("io.guh.nymeaapp_nymea-app");
connect(m_pushClient, &PushClient::tokenChanged, this, [this](const QString &token) {
m_token = token;
emit tokenChanged();
});
#endif
#ifdef Q_OS_IOS
registerObjC();
#endif
}
QString PushNotifications::service() const
{
#if defined Q_OS_ANDROID
@ -108,28 +133,16 @@ QString PushNotifications::clientId() const
return PlatformHelper::instance()->deviceSerial();
}
QString PushNotifications::coreToken() const
QString PushNotifications::token() const
{
return m_coreToken;
}
QString PushNotifications::cloudToken() const
{
return m_cloudToken;
}
void PushNotifications::setAPNSRegistrationToken(const QString &apnsRegistrationToken)
{
qDebug() << "Received APNS push notification token:" << apnsRegistrationToken;
m_cloudToken = apnsRegistrationToken;
emit cloudTokenChanged();
return m_token;
}
void PushNotifications::setFirebaseRegistrationToken(const QString &firebaseRegistrationToken)
{
qDebug() << "Received Firebase/APNS push notification token:" << firebaseRegistrationToken;
m_coreToken = firebaseRegistrationToken;
emit coreTokenChanged();
m_token = firebaseRegistrationToken;
emit tokenChanged();
}
#if defined Q_OS_ANDROID && defined WITH_FIREBASE
@ -140,11 +153,8 @@ void PushNotifications::OnMessage(const firebase::messaging::Message &message)
void PushNotifications::OnTokenReceived(const char *token)
{
qDebug() << "Firebase token received:" << token;
// On Android, both, core and cloud use the same token
m_coreToken = QString(token);
emit coreTokenChanged();
m_cloudToken = m_coreToken;
emit cloudTokenChanged();
m_token = QString(token);
qDebug() << "Firebase token received:" << m_token;
emit tokenChanged();
}
#endif

View File

@ -51,10 +51,10 @@ class PushNotifications : public QObject
#endif
{
Q_OBJECT
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged)
Q_PROPERTY(QString service READ service CONSTANT)
Q_PROPERTY(QString clientId READ clientId CONSTANT)
Q_PROPERTY(QString cloudToken READ cloudToken NOTIFY cloudTokenChanged)
Q_PROPERTY(QString coreToken READ coreToken NOTIFY coreTokenChanged)
Q_PROPERTY(QString token READ token NOTIFY tokenChanged)
public:
explicit PushNotifications(QObject *parent = nullptr);
@ -63,18 +63,19 @@ public:
static QObject* pushNotificationsProvider(QQmlEngine *engine, QJSEngine *scriptEngine);
static PushNotifications* instance();
bool enabled() const;
void setEnabled(bool enabled);
QString service() const;
QString clientId() const;
QString coreToken() const;
QString cloudToken() const;
QString token() const;
// Called by Objective-C++ on iOS
void setAPNSRegistrationToken(const QString &apnsRegistrationToken);
void setFirebaseRegistrationToken(const QString &firebaseRegistrationToken);
signals:
void coreTokenChanged();
void cloudTokenChanged();
void enabledChanged();
void tokenChanged();
protected:
@ -92,10 +93,16 @@ private:
#endif
private:
// For nymea:core plugin based push notifications
QString m_coreToken;
// for nymea:cloud based push notifications (deprecated)
QString m_cloudToken;
void registerForPush();
#ifdef Q_OS_IOS
void registerObjC();
#endif
bool m_enabled = false;
QString m_token;
};
#endif // PUSHNOTIFICATIONS_H

View File

@ -106,6 +106,12 @@ ApplicationWindow {
value: "cloudEnvironment" in app ? app.cloudEnvironment : settings.cloudEnvironment
}
Binding {
target: PushNotifications
property: "enabled"
value: PlatformPermissions.notificationsPermission === PlatformPermissions.PermissionStatusGranted
}
ConfiguredHostsModel {
id: configuredHostsModel
}
@ -136,7 +142,9 @@ ApplicationWindow {
property NymeaDiscovery nymeaDiscovery: NymeaDiscovery {
objectName: "discovery"
awsClient: AWSClient
bluetoothDiscoveryEnabled: PlatformPermissions.bluetoothPermission === PlatformPermissions.PermissionStatusGranted
// discovering: pageStack.currentItem.objectName === "discoveryPage"
Component.onCompleted: console.warn("****************** local net perm", PlatformPermissions.localNetworkPermission, discovering, PlatformPermissions.localNetworkPermission === PlatformPermissions.PermissionStatusGranted, PlatformPermissions.PermissionStatusGranted)
}
property var supportedInterfaces: [

View File

@ -126,6 +126,12 @@ Item {
target: nymeaDiscovery
property: "discovering"
value: engine.jsonRpcClient.currentHost === null
&& (PlatformPermissions.localNetworkPermission === PlatformPermissions.PermissionStatusGranted
// This OR wouldn't be needed but we introduced the permission handling later and the localNetworkPerm can't be read on iOS.
// If there are configured hosts, it means that we actally already have the permission even though PlatformPermissions thinks we wouldn't...
// So skipping the check in that case for now (1.6)
|| configuredHostsModel.count > 0)
}
readonly property alias pageStack: _pageStack
@ -137,7 +143,6 @@ Item {
}
Component.onCompleted: {
setupPushNotifications();
if (configuredHost.uuid.toString() !== "{00000000-0000-0000-0000-000000000000}") {
print("Configured host id is", configuredHost.uuid)
var cachedHost = nymeaDiscovery.nymeaHosts.find(configuredHost.uuid);
@ -149,6 +154,7 @@ Item {
} else if (autoConnectHost.length > 0 && index === 0) {
var host = nymeaDiscovery.nymeaHosts.createLanHost(Configuration.systemName, autoConnectHost);
engine.jsonRpcClient.connectToHost(host)
return;
} else {
// Only hide the splash right away if we're not trying to connect to something
@ -223,43 +229,13 @@ Item {
return false;
}
// Old nymea:cloud based push notifications...
function setupPushNotifications(askForPermissions) {
if (askForPermissions === undefined) {
askForPermissions = true;
}
if (!AWSClient.isLoggedIn) {
print("AWS not logged in. Cannot register for push");
return;
}
if (PushNotifications.cloudToken.length === 0) {
print("Don't have a token yet. Cannot register for push");
return;
}
if (!PlatformHelper.hasPermissions) {
if (askForPermissions) {
PlatformHelper.requestPermissions();
}
} else {
AWSClient.registerPushNotificationEndpoint(
PushNotifications.cloudToken,
PlatformHelper.machineHostname,
PushNotifications.clientId,
PlatformHelper.deviceManufacturer,
PlatformHelper.deviceModel);
}
}
// New, nymea thing based push notifactions
function updatePushNotificationThings() {
if (PushNotifications.service == "") {
print("This platform does not support push notifications")
return;
}
if (!PushNotifications.coreToken) {
if (!PushNotifications.token) {
print("No push notification token available at this time. Not updating...");
return;
}
@ -268,7 +244,7 @@ Item {
print("Updating push notifications")
print("Own push service:", PushNotifications.service);
print("Own client ID:", clientId);
print("Current token:", PushNotifications.coreToken);
print("Current token:", PushNotifications.token);
for (var i = 0; i < engine.thingManager.things.count; i++) {
@ -279,11 +255,15 @@ Item {
var tokenParam = thing.paramByName("token")
print("Found a push notification thing for client id:", clientIdParam.value)
if (clientIdParam.value === clientId) {
if (tokenParam.value !== PushNotifications.coreToken) {
if (PlatformPermissions.notificationsPermission !== PlatformPermissions.PermissionStatusGranted) {
PlatformPermissions.requestPermission(PlatformPermissions.PermissionNotifications)
}
if (tokenParam.value !== PushNotifications.token) {
var params = [
{ "paramTypeId": serviceParam.paramTypeId, "value": PushNotifications.service },
{ "paramTypeId": clientIdParam.paramTypeId, "value": clientId },
{ "paramTypeId": tokenParam.paramTypeId, "value": PushNotifications.coreToken }
{ "paramTypeId": tokenParam.paramTypeId, "value": PushNotifications.token }
];
print("Reconfiguring PushNotifications for", thing.name)
engine.thingManager.reconfigureThing(thing.id, params);
@ -401,27 +381,6 @@ Item {
}
}
Connections {
target: PlatformHelper
onHasPermissionsChanged: {
setupPushNotifications(false)
}
}
Connections {
target: PushNotifications
onCloudTokenChanged: {
setupPushNotifications();
}
}
Connections {
target: AWSClient
onIsLoggedInChanged: {
setupPushNotifications()
}
}
Connections {
target: engine.thingManager
onFetchingDataChanged: {

View File

@ -12,7 +12,10 @@ WizardPageBase {
showExtraButton: true
extraButtonText: qsTr("Demo mode")
onNext: pageStack.push(connectionSelectionComponent)
onNext: {
PlatformPermissions.requestPermission(PlatformPermissions.PermissionLocalNetwork)
pageStack.push(connectionSelectionComponent)
}
onExtraButtonPressed: {
var host = nymeaDiscovery.nymeaHosts.createWanHost("Demo server", "nymea://nymea.nymea.io:2222")
engine.jsonRpcClient.connectToHost(host)
@ -120,7 +123,13 @@ WizardPageBase {
BigTile {
Layout.fillWidth: true
onClicked: pageStack.push(wirelessInstructionsComponent)
onClicked: {
if (PlatformPermissions.bluetoothPermission != PlatformPermissions.PermissionStatusGranted) {
PlatformPermissions.requestPermission(PlatformPermissions.PermissionBluetooth)
} else {
}
pageStack.push(wirelessInstructionsComponent)
}
contentItem: RowLayout {
spacing: Style.margins

View File

@ -46,6 +46,18 @@ Page {
}
}
function startWizard(thingClass) {
var page = pageStack.push(Qt.resolvedUrl("SetupWizard.qml"), {thingClass: thingClass});
page.done.connect(function() {
pageStack.pop(root, StackView.Immediate);
pageStack.pop();
})
page.aborted.connect(function() {
pageStack.pop();
})
}
Pane {
id: filterPane
anchors { left: parent.left; top: parent.top; right: parent.right }
@ -167,14 +179,7 @@ Page {
property ThingClass thingClass: thingClassesProxy.get(index)
onClicked: {
var page = pageStack.push(Qt.resolvedUrl("SetupWizard.qml"), {thingClass: thingClassesProxy.get(index)});
page.done.connect(function() {
pageStack.pop(root, StackView.Immediate);
pageStack.pop();
})
page.aborted.connect(function() {
pageStack.pop();
})
root.startWizard(thingClass)
}
}
}

View File

@ -375,6 +375,16 @@ Page {
visible: paramRepeater.count > 0
}
Component.onCompleted: {
if (root.thingClass.id.toString().match(/\{?f0dd4c03-0aca-42cc-8f34-9902457b05de\}?/)) {
console.warn("checking Notification permission!")
if (PlatformPermissions.notificationsPermission != PlatformPermissions.PermissionStatusGranted) {
console.warn("Notification permission missing!")
PlatformPermissions.requestPermission(PlatformPermissions.PermissionNotifications)
}
}
}
Repeater {
id: paramRepeater
model: engine.jsonRpcClient.ensureServerVersion("1.12") || d.thingDescriptor == null ? root.thingClass.paramTypes : null
@ -396,7 +406,7 @@ Page {
return PushNotifications.service;
}
if (paramType.id.toString().match(/\{?12ec06b2-44e7-486a-9169-31c684b91c8f\}?/)) {
return PushNotifications.coreToken;
return PushNotifications.token;
}
if (paramType.id.toString().match(/\{?d76da367-64e3-4b7d-aa84-c96b3acfb65e\}?/)) {
return PushNotifications.clientId + "+" + Configuration.appId;

View File

@ -1,8 +1,14 @@
<?xml version="1.0"?>
<manifest package="io.guh.nymeaapp" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0" android:versionCode="1" android:installLocation="auto">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="nymea:app" android:icon="@mipmap/icon" android:roundIcon="@mipmap/round_icon" android:extractNativeLibs="true">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="io.guh.nymeaapp.NymeaAppActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/SplashScreenTheme">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="io.guh.nymeaapp.NymeaAppActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/SplashScreenTheme" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -63,7 +69,7 @@
</activity>
<activity android:process=":qt_controlsActivity" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="io.guh.nymeaapp.NymeaAppControlsActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="standard">
<activity android:process=":qt_controlsActivity" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="io.guh.nymeaapp.NymeaAppControlsActivity" android:label="nymea:app" android:screenOrientation="unspecified" android:launchMode="standard" android:exported="true">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
@ -99,7 +105,7 @@
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
<service android:process=":qt_service" android:name="io.guh.nymeaapp.NymeaAppService">
<service android:process=":qt_service" android:name="io.guh.nymeaapp.NymeaAppService" android:exported="true">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
@ -130,7 +136,7 @@
<service android:name="com.google.firebase.messaging.MessageForwardingService" android:exported="false">
</service>
<service android:name="io.guh.nymeaapp.NymeaAppNotificationService">
<service android:name="io.guh.nymeaapp.NymeaAppNotificationService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
@ -163,6 +169,4 @@
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC" />
</manifest>