notifications: enable local notifications
- only show notifications when app is in background - open the user chat upon tap on notification - remove chat from notifications on tap or dismiss notification - keep the service alive on host destroy - enable local notifications
This commit is contained in:
parent
277f65b94d
commit
5608d649b0
|
@ -17,4 +17,3 @@ SNOOPY=0
|
|||
RPC_NETWORKS_ONLY=1
|
||||
PARTITIONED_TOPIC=0
|
||||
MOBILE_UI_FOR_DESKTOP=1
|
||||
LOCAL_NOTIFICATIONS=0
|
||||
|
|
|
@ -3,6 +3,8 @@ package im.status.ethereum.module;
|
|||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.BroadcastReceiver;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -25,6 +27,7 @@ import android.graphics.BitmapFactory;
|
|||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
@ -35,7 +38,14 @@ import android.media.AudioAttributes;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
class NewMessageSignalHandler {
|
||||
public class NewMessageSignalHandler {
|
||||
//NOTE: currently we only show notifications for 1-1 chats, in the future we
|
||||
//will most likely extend to other kind of notifications. The first step will
|
||||
//be to define actions for these notifications, add it to the filter in
|
||||
//`registerBroadcastReceiver` method, and add some action specific code
|
||||
//in notificationActionReceiver.
|
||||
public static final String ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION";
|
||||
public static final String ACTION_TAP_NOTIFICATION = "im.status.ethereum.module.TAP_NOTIFICATION";
|
||||
private static final String GROUP_STATUS_MESSAGES = "im.status.notifications.message";
|
||||
private static final String CHANNEL_NAME = "Status";
|
||||
private static final String CHANNEL_DESCRIPTION = "Get notifications on new messages and mentions";
|
||||
|
@ -48,6 +58,64 @@ class NewMessageSignalHandler {
|
|||
private Intent serviceIntent;
|
||||
private Boolean shouldRefreshNotifications;
|
||||
|
||||
//NOTE: we use a dynamically created BroadcastReceiver here so that we can capture
|
||||
//intents from notifications and act on them. For instance when tapping/dismissing
|
||||
//a chat notification we want to clear the chat so that next messages don't show
|
||||
//the messages that we have seen again
|
||||
private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String chatId = intent.getExtras().getString("im.status.ethereum.chatId");
|
||||
if (intent.getAction() == ACTION_TAP_NOTIFICATION) {
|
||||
context.startActivity(getOpenAppIntent(chatId));
|
||||
}
|
||||
removeChat(chatId);
|
||||
// clean up the group notifications when there is no
|
||||
// more unread chats
|
||||
if (chats.size() == 0) {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
Log.e(TAG, "intent received: " + intent.getAction());
|
||||
}
|
||||
};
|
||||
|
||||
private void registerBroadcastReceiver() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_DELETE_NOTIFICATION);
|
||||
filter.addAction(ACTION_TAP_NOTIFICATION);
|
||||
context.registerReceiver(notificationActionReceiver, filter);
|
||||
Log.e(TAG, "Broadcast Receiver registered");
|
||||
}
|
||||
|
||||
//NOTE: this method takes a chatId and returns an intent that will open the app in that chat
|
||||
//Once we support other kind of notifications we will need to adapt it. The simplest method
|
||||
//is probably to pass the universal link as param instead of the chatId.
|
||||
public Intent getOpenAppIntent(String chatId) {
|
||||
Class intentClass;
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
intentClass = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
Intent intent = new Intent(context, intentClass);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
//NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well,
|
||||
//the answer is a simple as it can get in the Android native development world. I noticed
|
||||
//that my initial setup was opening the app but wasn't triggering any events on the js side, like
|
||||
//the links do from the browser. So I compared both intents and noticed that the link from
|
||||
//the browser produces an intent with the flag 0x14000000. I found out that it was the following
|
||||
//flags in this link:
|
||||
//https://stackoverflow.com/questions/52390129/android-intent-setflags-issue
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.setData(Uri.parse("status-im://chat/private/" + chatId));
|
||||
return intent;
|
||||
}
|
||||
|
||||
public NewMessageSignalHandler(Context context) {
|
||||
// NOTE: when instanciated the NewMessageSignalHandler class starts a foreground service
|
||||
// to keep the app running in the background in order to receive notifications
|
||||
|
@ -61,12 +129,17 @@ class NewMessageSignalHandler {
|
|||
Log.e(TAG, "Starting Foreground Service");
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.startService(serviceIntent);
|
||||
this.registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
Log.e(TAG, "Stopping Foreground Service");
|
||||
//NOTE: we cancel all the current notifications, because the intents can't be used anymore
|
||||
//since the broadcast receiver will be killed as well and won't be able to handle any intent
|
||||
notificationManager.cancelAll();
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.stopService(serviceIntent);
|
||||
context.unregisterReceiver(notificationActionReceiver);
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
|
@ -85,6 +158,22 @@ class NewMessageSignalHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private void removeChat(String chatId) {
|
||||
this.chats.remove(chatId);
|
||||
}
|
||||
|
||||
private PendingIntent createOnDismissedIntent(Context context, int notificationId, String chatId) {
|
||||
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
|
||||
intent.putExtra("im.status.ethereum.chatId", chatId);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent createOnTapIntent(Context context, int notificationId, String chatId) {
|
||||
Intent intent = new Intent(ACTION_TAP_NOTIFICATION);
|
||||
intent.putExtra("im.status.ethereum.chatId", chatId);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
}
|
||||
|
||||
public void notify(int notificationId, StatusChat chat) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me");
|
||||
ArrayList<StatusMessage> messages = chat.getMessages();
|
||||
|
@ -94,13 +183,16 @@ class NewMessageSignalHandler {
|
|||
message.getTimestamp(),
|
||||
message.getAuthor());
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setStyle(messagingStyle)
|
||||
.setGroup(GROUP_STATUS_MESSAGES);
|
||||
.setGroup(GROUP_STATUS_MESSAGES)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(createOnTapIntent(context, notificationId, chat.getId()))
|
||||
.setDeleteIntent(createOnDismissedIntent(context, notificationId, chat.getId()))
|
||||
.setAutoCancel(true);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
builder.setVibrate(new long[0]);
|
||||
}
|
||||
|
@ -109,33 +201,13 @@ class NewMessageSignalHandler {
|
|||
|
||||
public void refreshNotifications() {
|
||||
NotificationCompat.InboxStyle summaryStyle = new NotificationCompat.InboxStyle();
|
||||
String summary = "";
|
||||
int notificationId = 2; // we start at 2 because the service is using 1 and can't use 0
|
||||
int messageCounter = 0;
|
||||
Iterator<StatusChat> chatIterator = chats.values().iterator();
|
||||
while(chatIterator.hasNext()) {
|
||||
StatusChat chat = (StatusChat)chatIterator.next();
|
||||
notify(notificationId, chat);
|
||||
notificationId++;
|
||||
messageCounter += chat.getMessages().size();
|
||||
summaryStyle.addLine(chat.getSummary());
|
||||
summary += chat.getSummary() + "\n";
|
||||
}
|
||||
// NOTE: this is necessary for old versions of Android, newer versions are
|
||||
// building this group themselves and I was not able to make any change to
|
||||
// what this group displays
|
||||
NotificationCompat.Builder groupBuilder =
|
||||
new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentText(summary)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentTitle("You got " + messageCounter + " messages in " + chats.size() + " chats")
|
||||
.setStyle(summaryStyle
|
||||
.setBigContentTitle("You got " + messageCounter + " messages in " + chats.size() + " chats"))
|
||||
.setGroup(GROUP_STATUS_MESSAGES)
|
||||
.setGroupSummary(true);
|
||||
notificationManager.notify(notificationId, groupBuilder.build());
|
||||
}
|
||||
|
||||
void handleNewMessageSignal(JSONObject newMessageSignal) {
|
||||
|
|
|
@ -69,6 +69,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
private ReactApplicationContext reactContext;
|
||||
private boolean rootedDevice;
|
||||
private NewMessageSignalHandler newMessageSignalHandler;
|
||||
private boolean background;
|
||||
|
||||
StatusModule(ReactApplicationContext reactContext, boolean rootedDevice) {
|
||||
super(reactContext);
|
||||
|
@ -85,17 +86,18 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
@Override
|
||||
public void onHostResume() { // Activity `onResume`
|
||||
module = this;
|
||||
this.background = false;
|
||||
Statusgo.setMobileSignalHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
this.background = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
|
||||
getReactApplicationContext().stopService(intent);
|
||||
Log.d(TAG, "******************* ON HOST DESTROY *************************");
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
|
@ -145,7 +147,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
Log.d(TAG, "Signal event: " + jsonEventString);
|
||||
// NOTE: the newMessageSignalHandler is only instanciated if the user
|
||||
// enabled notifications in the app
|
||||
if (newMessageSignalHandler != null) {
|
||||
if (this.background && newMessageSignalHandler != null) {
|
||||
if (eventType.equals("messages.new")) {
|
||||
newMessageSignalHandler.handleNewMessageSignal(jsonEvent);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
;; TODO(yenda) investigate why `handle-universal-link` event is
|
||||
;; dispatched 7 times for the same link
|
||||
|
||||
(def private-chat-regex #".*/chat/private/(.*)$")
|
||||
(def public-chat-regex #"(?:https?://join\.)?status[.-]im(?::/)?/(?:chat/public/([a-z0-9\-]+)$|([a-z0-9\-]+))$")
|
||||
(def profile-regex #"(?:https?://join\.)?status[.-]im(?::/)?/(?:u/(0x.*)$|u/(.*)$|user/(.*))$")
|
||||
(def browse-regex #"(?:https?://join\.)?status[.-]im(?::/)?/(?:b/(.*)$|browse/(.*))$")
|
||||
|
@ -30,6 +31,7 @@
|
|||
:internal "status-im:/"})
|
||||
|
||||
(def links {:public-chat "%s/%s"
|
||||
:private-chat "%s/p/%s"
|
||||
:user "%s/u/%s"
|
||||
:browse "%s/b/%s"})
|
||||
|
||||
|
@ -71,6 +73,10 @@
|
|||
(when (security/safe-link? url)
|
||||
{:browser/show-browser-selection url}))
|
||||
|
||||
(fx/defn handle-private-chat [cofx chat-id]
|
||||
(log/info "universal-links: handling private chat" chat-id)
|
||||
(chat/start-chat cofx chat-id {}))
|
||||
|
||||
(fx/defn handle-public-chat [cofx public-chat]
|
||||
(log/info "universal-links: handling public chat" public-chat)
|
||||
(chat/start-public-chat cofx public-chat {}))
|
||||
|
@ -120,6 +126,9 @@
|
|||
[cofx url]
|
||||
(cond
|
||||
|
||||
(match-url url private-chat-regex)
|
||||
(handle-private-chat cofx (match-url url private-chat-regex))
|
||||
|
||||
(spec/valid? :global/public-key (match-url url profile-regex))
|
||||
(handle-view-profile cofx {:public-key (match-url url profile-regex)})
|
||||
|
||||
|
|
Loading…
Reference in New Issue