diff --git a/packages/backend/src/mobileBackendManager.ts b/packages/backend/src/mobileBackendManager.ts index 4aa3cf8696..face074a99 100644 --- a/packages/backend/src/mobileBackendManager.ts +++ b/packages/backend/src/mobileBackendManager.ts @@ -3,6 +3,10 @@ import { ConnectionsManager } from './libp2p/connectionsManager' import { Command } from 'commander' export const runBackend = async (): Promise => { + // Enable triggering push notifications + process.env['BACKEND'] = 'mobile' + process.env['CONNECTION_TIME'] = new Date().getTime().toString() + const program = new Command() program diff --git a/packages/backend/src/storage/storage.ts b/packages/backend/src/storage/storage.ts index 393c8aebbe..a7a653763d 100644 --- a/packages/backend/src/storage/storage.ts +++ b/packages/backend/src/storage/storage.ts @@ -14,7 +14,8 @@ import { DownloadProgress, DownloadState, imagesExtensions, - User + User, + BASE_NOTIFICATION_CHANNEL } from '@quiet/state-manager' import * as IPFS from 'ipfs-core' import Libp2p from 'libp2p' @@ -305,6 +306,14 @@ export class Storage { this.io.loadMessages({ messages: [entry.payload.value] }) + // Display push notifications on mobile + if (process.env.BACKEND === 'mobile') { + const message = entry.payload.value + // Do not notify about old messages + if (parseInt(message.createdAt) < parseInt(process.env.CONNECTION_TIME)) return + const bridge = require('rn-bridge') + bridge.channel.send(BASE_NOTIFICATION_CHANNEL, JSON.stringify(message)) + } }) db.events.on('replicated', async address => { log('Replicated.', address) diff --git a/packages/mobile/android/app/src/main/java/com/zbaymobile/MainApplication.java b/packages/mobile/android/app/src/main/java/com/zbaymobile/MainApplication.java index 9da1f49446..dd1d333994 100644 --- a/packages/mobile/android/app/src/main/java/com/zbaymobile/MainApplication.java +++ b/packages/mobile/android/app/src/main/java/com/zbaymobile/MainApplication.java @@ -1,13 +1,19 @@ package com.zbaymobile; import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; +import android.os.Build; + import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.soloader.SoLoader; +import com.zbaymobile.Utils.Const; + import java.lang.reflect.InvocationTargetException; import java.util.List; @@ -29,6 +35,7 @@ protected List getPackages() { // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); packages.add(new TorModulePackage()); + packages.add(new NotificationModulePackage()); return packages; } @@ -49,6 +56,7 @@ public void onCreate() { SoLoader.init(this, /* native exopackage */ false); initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); PACKAGE_NAME = getApplicationContext().getPackageName(); + createNotificationChannel(); } /** @@ -81,4 +89,20 @@ private static void initializeFlipper( } } } + + private void createNotificationChannel() { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = getString(R.string.notification_channel_name); + String description = getString(R.string.notification_channel_description); + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(Const.INCOMING_MESSAGES_CHANNEL_ID, name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } } diff --git a/packages/mobile/android/app/src/main/java/com/zbaymobile/NotificationModule.java b/packages/mobile/android/app/src/main/java/com/zbaymobile/NotificationModule.java new file mode 100644 index 0000000000..001aef4247 --- /dev/null +++ b/packages/mobile/android/app/src/main/java/com/zbaymobile/NotificationModule.java @@ -0,0 +1,118 @@ +package com.zbaymobile; + +import android.app.ActivityManager; +import android.content.Context; +import android.util.Log; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.zbaymobile.Utils.Const; + +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class NotificationModule extends ReactContextBaseJavaModule { + + private static final String SYSTEM_CHANNEL = "_SYSTEM_"; + private static final String BASE_NOTIFICATION_CHANNEL = "_BASE_NOTIFICATION_"; + private static final String RICH_NOTIFICATION_CHANNEL = "_RICH_NOTIFICATION_"; + + private static ReactApplicationContext reactContext; + + @NonNull + @Override + public String getName() { + return "NotificationModule"; + } + + public NotificationModule(ReactApplicationContext reactContext) { + super(reactContext); + this.reactContext = reactContext; + } + + @ReactMethod + public static void notify(String channelName, String message) { + if (channelName.equals(SYSTEM_CHANNEL)) return; // Ignore system messages + if (!channelName.equals(RICH_NOTIFICATION_CHANNEL) && isAppOnForeground()) return; // Only RICH_NOTIFICATION can be shown in foreground + + JSONObject data; // Message payload + + String title = "Quiet"; + String text = ""; + + try { + JSONObject json = new JSONObject(message); + JSONArray payload = new JSONArray(json.getString("payload")); + data = new JSONObject(payload.getString(0)); + } catch (JSONException e) { + Log.e("NOTIFICATION", "unexpected JSON exception", e); + return; + } + + if (channelName.equals(BASE_NOTIFICATION_CHANNEL)) { + try { + String channelAddress = data.getString("channelAddress"); + title = "Quiet"; + text = String.format("You have a message in #%s", channelAddress); + } catch (JSONException e) { + Log.e("NOTIFICATION", "incorrect BASE_NOTIFICATION payload", e); + return; + } + } + + if(channelName.equals(RICH_NOTIFICATION_CHANNEL)) { + try { + String channelAddress = data.getString("channelAddress"); + String messageContent = data.getString("message"); + title = String.format("#%s", channelAddress); + text = truncate(messageContent, 32); + } catch (JSONException e) { + Log.e("NOTIFICATION", "incorrect RICH_NOTIFICATION payload", e); + return; + } + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(reactContext.getApplicationContext(), Const.INCOMING_MESSAGES_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(title) + .setContentText(text) + .setPriority(NotificationCompat.PRIORITY_DEFAULT); + + Integer notificationId = ThreadLocalRandom.current().nextInt(0, 9000 + 1); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(reactContext.getApplicationContext()); + notificationManager.notify(notificationId, builder.build()); + } + + private static boolean isAppOnForeground() { + ActivityManager activityManager = (ActivityManager) reactContext.getSystemService(Context.ACTIVITY_SERVICE); + List appProcesses = activityManager.getRunningAppProcesses(); + if (appProcesses == null) { + return false; + } + final String packageName = reactContext.getPackageName(); + for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) { + return true; + } + } + return false; + } + + private static String truncate(String message, Integer length) { + if (message.length() > length) { + return String.format("%s...", message.substring(0, length)); + } else { + return message; + } + } +} diff --git a/packages/mobile/android/app/src/main/java/com/zbaymobile/NotificationModulePackage.java b/packages/mobile/android/app/src/main/java/com/zbaymobile/NotificationModulePackage.java new file mode 100644 index 0000000000..001064912d --- /dev/null +++ b/packages/mobile/android/app/src/main/java/com/zbaymobile/NotificationModulePackage.java @@ -0,0 +1,28 @@ +package com.zbaymobile; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.bridge.JavaScriptModule; + +public class NotificationModulePackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new NotificationModule(reactContext)); + } + + // Deprecated from RN 0.47 + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/packages/mobile/android/app/src/main/java/com/zbaymobile/Utils/Const.kt b/packages/mobile/android/app/src/main/java/com/zbaymobile/Utils/Const.kt index ff6225600a..00b595ba50 100644 --- a/packages/mobile/android/app/src/main/java/com/zbaymobile/Utils/Const.kt +++ b/packages/mobile/android/app/src/main/java/com/zbaymobile/Utils/Const.kt @@ -8,5 +8,7 @@ object Const { const val TAG_TOR = "TOR" const val TAG_TOR_ERR = "TOR_ERR" + const val INCOMING_MESSAGES_CHANNEL_ID = "incoming.messages.channel" + val SERVICE_ACTION_EXECUTE = "$PACKAGE_NAME.service.execute" } diff --git a/packages/mobile/android/app/src/main/res/values/strings.xml b/packages/mobile/android/app/src/main/res/values/strings.xml index 2e4f3d4d8f..c69d1b9a5f 100644 --- a/packages/mobile/android/app/src/main/res/values/strings.xml +++ b/packages/mobile/android/app/src/main/res/values/strings.xml @@ -4,4 +4,8 @@ close + + + Incoming messages + Inform the user about incoming messages diff --git a/packages/mobile/enable-notifications.patch b/packages/mobile/enable-notifications.patch new file mode 100644 index 0000000000..3456575ce0 --- /dev/null +++ b/packages/mobile/enable-notifications.patch @@ -0,0 +1,14 @@ +--- node_modules/nodejs-mobile-react-native/android/src/main/cpp/native-lib.cpp.backup 2022-08-31 11:49:47.331103861 +0200 ++++ node_modules/nodejs-mobile-react-native/android/src/main/cpp/native-lib.cpp 2022-08-31 11:50:07.430981315 +0200 +@@ -67,9 +67,9 @@ + void rcv_message(const char* channel_name, const char* msg) { + JNIEnv *env=cacheEnvPointer; + if(!env) return; +- jclass cls2 = env->FindClass("com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule"); // try to find the class ++ jclass cls2 = env->FindClass("com/zbaymobile/NotificationModule"); // try to find the class + if(cls2 != nullptr) { +- jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "sendMessageToApplication", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method ++ jmethodID m_sendMessage = env->GetStaticMethodID(cls2, "notify", "(Ljava/lang/String;Ljava/lang/String;)V"); // find method + if(m_sendMessage != nullptr) { + jstring java_channel_name=env->NewStringUTF(channel_name); + jstring java_msg=env->NewStringUTF(msg); diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 1dcd2f2f98..b1ecf941d7 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -16,8 +16,9 @@ "provide-backend-assets": "rm -rf ./nodejs-assets/nodejs-project/* && rf-lerna zipbundle @quiet/backend --target ./nodejs-assets/nodejs-project/backend.zip && cd ./nodejs-assets/nodejs-project/ && unzip ./backend.zip && rm -rf ./backend.zip && cp ../parse-duration.patch ./ && patch -p0 --forward --binary < ./parse-duration.patch || true && cp ../libp2p.patch ./ && patch -p0 --forward --binary < ./libp2p.patch || true && cp ../orbit-db-keystore.patch ./ && patch -p0 --forward --binary < ./orbit-db-keystore.patch || true && cp ../orbit-db-storage-adapter.patch ./ && patch -p0 --forward --binary < ./orbit-db-storage-adapter.patch || true && rm -rf ./node_modules/electron && rm -rf ./node_modules/wrtc", "enable-backend-logging-android": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -p0 --forward --binary < ./enable-backend-logs-android.patch || true", "enable-backend-logging-ios": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -p0 --forward --binary < ./enable-backend-logs-ios.patch || true", + "enable-notifications": "patch -p0 --forward --binary < ./enable-notifications.patch || true", "patch-dependencies": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -d ../state-manager -p0 < ./factory-girl.patch || true && patch -d ../state-manager -p0 < ./state-manager.patch || true", - "prepare": "npm run provide-backend-assets && npm run enable-backend-logging-android && npm run enable-backend-logging-ios && npm run exclude-test-assets && npm run patch-dependencies && npm run build", + "prepare": "npm run provide-backend-assets && npm run enable-backend-logging-android && npm run enable-backend-logging-ios && npm run enable-notifications && npm run exclude-test-assets && npm run patch-dependencies && npm run build", "postversion": "react-native-version" }, "dependencies": { diff --git a/packages/state-manager/src/constants.ts b/packages/state-manager/src/constants.ts index 6ddee37d5d..caef467dbe 100644 --- a/packages/state-manager/src/constants.ts +++ b/packages/state-manager/src/constants.ts @@ -1,2 +1,6 @@ export const MAIN_CHANNEL = 'general' + export const AUTODOWNLOAD_SIZE_LIMIT = 20971520 // 20 MB + +export const BASE_NOTIFICATION_CHANNEL = '_BASE_NOTIFICATION_' +export const RICH_NOTIFICATION_CHANNEL = '_RICH_NOTIFICATION_' diff --git a/packages/state-manager/src/index.ts b/packages/state-manager/src/index.ts index 1d61982a9b..a7e1dbaa67 100644 --- a/packages/state-manager/src/index.ts +++ b/packages/state-manager/src/index.ts @@ -75,7 +75,7 @@ export { PublicChannelsTransform } from './sagas/publicChannels/publicChannels.t export { MessagesTransform } from './sagas/messages/messages.transform' export { FilesTransform } from './sagas/files/files.transform' -export { AUTODOWNLOAD_SIZE_LIMIT } from './constants' +export { AUTODOWNLOAD_SIZE_LIMIT, BASE_NOTIFICATION_CHANNEL, RICH_NOTIFICATION_CHANNEL } from './constants' export * from './sagas/identity/identity.types'