From 76df3c286dc7721f7293c0db400ea6dabf4e45a1 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 24 Nov 2020 16:45:50 -0800 Subject: [PATCH] Android Notification - First Cut This commit has the basics of notifications on Android working again, updated to the latest Android SDK way of doing things (with channel IDs and grouping). Android users will get notified when the app is open for new Peer Messages across all profiles. In the future, this should be extended to add notifications for new peer invites, actual have actionable actions (accept/block) and maybe even work when the app isn't open... --- ANDROID_DEBUGGING.md | 9 ++- .../openprivacy/cwtch/ui/CwtchActivity.java | 80 +++++++------------ go/handlers/peerHandler.go | 1 - go/ui/android/CwtchActivity.go | 20 +---- go/ui/gcd.go | 4 + go/ui/manager.go | 9 +++ main.go | 11 +-- qml/main.qml | 10 +++ 8 files changed, 65 insertions(+), 79 deletions(-) diff --git a/ANDROID_DEBUGGING.md b/ANDROID_DEBUGGING.md index 9378bffc..bf59e2c3 100644 --- a/ANDROID_DEBUGGING.md +++ b/ANDROID_DEBUGGING.md @@ -79,4 +79,11 @@ Theoretically speaking it should be possible to use `ANDROID_EXTRA_PLUGINS` to i SVG images on Android. However, we have been unable to make it work. If you would like to try, the following issues might be helpful: -* https://bugreports.qt.io/browse/QTBUG-60022 \ No newline at end of file +* https://bugreports.qt.io/browse/QTBUG-60022 + +## Notifications + +- Android 8 (API Level 26) forces you to call setChannelId() +- Android 9 "Do Not Disturb" mode also hides all notifications +- Setting up notification channels only seems possible *once* per install. any changes you need to make +require that the app is reinstalled, or the actual channel deleted and changed. \ No newline at end of file diff --git a/android/src/ca/openprivacy/cwtch/ui/CwtchActivity.java b/android/src/ca/openprivacy/cwtch/ui/CwtchActivity.java index 736c10ca..b99dce53 100644 --- a/android/src/ca/openprivacy/cwtch/ui/CwtchActivity.java +++ b/android/src/ca/openprivacy/cwtch/ui/CwtchActivity.java @@ -23,20 +23,13 @@ import static android.app.Notification.CATEGORY_SERVICE; public class CwtchActivity extends org.qtproject.qt5.android.bindings.QtActivity { private static NotificationManager m_notificationManager; - private static Notification.Builder m_builder; private static Notification.Builder m_builderOngoing; private static CwtchActivity m_instance; - private static int PRIORITY_MIN = -2; // From NotificationCompat - private static int PRIORITY_DEFAULT = 0; // From NotificationCompat - private static String NOTIFICATION_CHANNEL_ID = "cwtch_notification_channel"; + private static int CONTENT_NOTIFICATION_ID = 2; + private static String CONTENT_NOTIFICATION_ID_NAME = "Notifications from Peers"; - private static int ONGOING_NOTIFICATION_ID = 0; - private static String ONGOING_NOTIFICATION_ID_NAME = "ongoing"; - - private static int CONTENT_NOTIFICATION_ID = 1; - private static String CONTENT_NOTIFICATION_ID_NAME = "content"; public CwtchActivity() { @@ -57,66 +50,47 @@ public class CwtchActivity extends org.qtproject.qt5.android.bindings.QtActivity } } - public static void notify(String s) + public static void notify(String s, String o) { if (m_notificationManager == null) { m_notificationManager = (NotificationManager)m_instance.getSystemService(Context.NOTIFICATION_SERVICE); createNotificationChannel(); } - if (m_builder == null) { - m_builder = new Notification.Builder(m_instance); - m_builder.setSmallIcon(R.drawable.ic_launcher); - m_builder.setContentTitle("Cwtch"); - m_builder.setPriority(PRIORITY_DEFAULT); + // Apparently thr android documentation is just wrong and we need to provide a setGroupSummary + // notification regardless of targetted support version... + Notification groupSummary = + new Notification.Builder(m_instance) + .setContentTitle("Cwtch") + .setContentText("New Message from Peer: " + o) + .setGroupSummary(true) + .setWhen(System.currentTimeMillis()) + .setSmallIcon(R.drawable.ic_launcher) + .setGroup(NOTIFICATION_CHANNEL_ID) + .setChannelId(NOTIFICATION_CHANNEL_ID) + .build(); + m_notificationManager.notify(1, groupSummary); - } + Notification.Builder m_builder = new Notification.Builder(m_instance) + .setSmallIcon(R.drawable.ic_launcher) + .setChannelId(NOTIFICATION_CHANNEL_ID) + .setGroup(NOTIFICATION_CHANNEL_ID) + .setWhen(System.currentTimeMillis()) + .setAutoCancel(true) + .setContentTitle("New Message from Peer: " + o) + .setContentText("[redacted: Open Cwtch App to see the Message]"); + m_notificationManager.notify(CONTENT_NOTIFICATION_ID++, m_builder.build()); - m_builder.setContentText(s); - m_notificationManager.notify(CONTENT_NOTIFICATION_ID, m_builder.build()); - } - public static void ongoingNotify(String s) - { - if (m_notificationManager == null) { - m_notificationManager = (NotificationManager)m_instance.getSystemService(Context.NOTIFICATION_SERVICE); - createNotificationChannel(); - } - if (m_builderOngoing == null) { - m_builderOngoing = new Notification.Builder(m_instance); - m_builderOngoing.setSmallIcon(R.drawable.ic_launcher); - m_builderOngoing.setContentTitle("Cwtch"); - m_builderOngoing.setPriority(PRIORITY_MIN); - - m_builderOngoing.setWhen(0); // Don't show the time - m_builderOngoing.setOngoing(true); - if (SDK_INT >= 21) { - m_builderOngoing.setCategory(CATEGORY_SERVICE); - //m_builder.setVisibility(VISIBILITY_SECRET); - } - - } - - m_builderOngoing.setContentText(s); - m_notificationManager.notify(ONGOING_NOTIFICATION_ID, m_builderOngoing.build()); } private static void createNotificationChannel() { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (SDK_INT >= 26) { - String description = "Cwtch Ongoing Notification Channel"; - int importance = NotificationManager.IMPORTANCE_LOW; - NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, ONGOING_NOTIFICATION_ID_NAME, importance); - channel.setDescription(description); - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - m_notificationManager.createNotificationChannel(channel); - - description = "Cwtch Content Notification Channel"; - importance = NotificationManager.IMPORTANCE_DEFAULT; - channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, CONTENT_NOTIFICATION_ID_NAME, importance); + String description = "Cwtch Notification Channel"; + NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, CONTENT_NOTIFICATION_ID_NAME, NotificationManager.IMPORTANCE_HIGH); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go index 83ce5fa3..e6aba32c 100644 --- a/go/handlers/peerHandler.go +++ b/go/handlers/peerHandler.go @@ -58,7 +58,6 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampReceived]) uiManager.StoreAndNotify(peer, e.Data[event.RemotePeer], e.Data[event.Data], ts, onion) - case event.PeerAcknowledgement: uiManager.Acknowledge(e.Data[event.RemotePeer], e.Data[event.EventID]) diff --git a/go/ui/android/CwtchActivity.go b/go/ui/android/CwtchActivity.go index 0bfaa2c6..e8750b4b 100644 --- a/go/ui/android/CwtchActivity.go +++ b/go/ui/android/CwtchActivity.go @@ -11,6 +11,7 @@ type CwtchActivity struct { _ func() `constructor:"init"` + _ string `property:"channel"` _ string `property:"notification"` _ func(string) `slot:"updateAndroidNotification"` @@ -20,7 +21,6 @@ type CwtchActivity struct { func (c *CwtchActivity) init() { log.Debugln("CwtchActivity.init()") - c.createOngoingNotification() c.ConnectNotificationChanged(c.updateAndroidNotification) } @@ -29,8 +29,8 @@ func (c *CwtchActivity) updateAndroidNotification(n string) { var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught( "ca/openprivacy/cwtch/ui/CwtchActivity", "notify", - "(Ljava/lang/String;)V", - n, + "(Ljava/lang/String;Ljava/lang/String;)V", + n, c.Channel(), ) if err != nil { @@ -38,20 +38,6 @@ func (c *CwtchActivity) updateAndroidNotification(n string) { } } -func (c *CwtchActivity) createOngoingNotification() { - - var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught( - "ca/openprivacy/cwtch/ui/CwtchActivity", - "ongoingNotify", - "(Ljava/lang/String;)V", - "Cwtch is running", - ) - - if err != nil { - log.Errorf("Error calling Java CwtchActivity.ongoingNotify(): %v\n", err.Error()) - } -} - func (c *CwtchActivity) rootHomeButtonHandle() { log.Infoln("CwtchActivity.rootHomeButtonHandle()!") var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught( diff --git a/go/ui/gcd.go b/go/ui/gcd.go index 3dbf9d2a..39458ab9 100644 --- a/go/ui/gcd.go +++ b/go/ui/gcd.go @@ -8,6 +8,7 @@ import ( "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" "cwtch.im/ui/go/features/groups" + "cwtch.im/ui/go/ui/android" "github.com/therecipe/qt/qml" "strconv" "sync" @@ -22,6 +23,7 @@ import ( type GrandCentralDispatcher struct { core.QObject + AndroidCwtchActivity *android.CwtchActivity QMLEngine *qml.QQmlApplicationEngine Translator, OpaqueTranslator *core.QTranslator @@ -58,6 +60,7 @@ type GrandCentralDispatcher struct { _ func() `signal:"ResetProfileList"` _ func(failed bool) `signal:"ChangePasswordResponse"` _ func(onion string, online bool) `signal:"UpdateProfileNetworkStatus"` + _ func(onion string) `signal:"Notify"` // server management _ func(handle, displayname, image string, status int, autostart bool, bundle string, messages int, key_types []string, keys []string) `signal:"AddServer"` @@ -147,6 +150,7 @@ func (this *GrandCentralDispatcher) init() { this.SetTheme(this.GlobalSettings.Theme) this.SetExperimentsEnabled(this.GlobalSettings.ExperimentsEnabled) this.SetExperiments(this.GlobalSettings.Experiments) + this.AndroidCwtchActivity = android.NewCwtchActivity(nil) } // GetUiManager gets (and creates if required) a ui Manager for the supplied profile id diff --git a/go/ui/manager.go b/go/ui/manager.go index 279d1520..0828df4e 100644 --- a/go/ui/manager.go +++ b/go/ui/manager.go @@ -301,6 +301,11 @@ func (this *manager) MessageJustAdded() { } func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTxt string, sent time.Time, profileOnion string) { + + // Send a New Message from Peer Notification + this.gcd.AndroidCwtchActivity.SetChannel(onion) + this.gcd.AndroidCwtchActivity.NotificationChanged("New Message from Peer") + this.gcd.DoIfProfileElse(this.profile, func() { this.gcd.DoIfConversationElse(onion, func() { this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) @@ -314,6 +319,7 @@ func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTx }, func() { the.CwtchApp.GetPeer(profileOnion).StoreMessage(onion, messageTxt, sent) }) + this.gcd.Notify(onion) } // AddMessage adds a message to the message pane for the supplied conversation if it is active @@ -331,6 +337,9 @@ func (this *manager) AddMessage(handle string, from string, message string, from }) this.gcd.IncContactUnreadCount(handle) }) + if !fromMe { + this.gcd.Notify(handle) + } } func (this *manager) ReloadProfiles() { diff --git a/main.go b/main.go index ff02d7db..13cef305 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,6 @@ import ( os2 "cwtch.im/ui/go/os" "cwtch.im/ui/go/the" "cwtch.im/ui/go/ui" - "cwtch.im/ui/go/ui/android" "encoding/base64" "flag" "git.openprivacy.ca/openprivacy/connectivity/tor" @@ -161,6 +160,7 @@ func mainUi(flagLocal bool, flagClientUI bool) { log.Errorf("Could not access global ui config: %v\n", err) os.Exit(-1) } + gcd := ui.NewGrandCentralDispatcher(nil) gcd.SetOs(runtime.GOOS) dir := core.QCoreApplication_ApplicationDirPath() @@ -186,8 +186,6 @@ func mainUi(flagLocal bool, flagClientUI bool) { gcd.SetBuildDate("now") } - - // this is to load local qml files quickly when developing var qmlSource *core.QUrl if flagLocal { @@ -223,13 +221,13 @@ func mainUi(flagLocal bool, flagClientUI bool) { return nam }) engine.SetNetworkAccessManagerFactory(factory) - engine.RootContext().SetContextProperty("gcd", gcd) gcd.TimelineInterface = ui.NewMessageModel(nil) engine.RootContext().SetContextProperty("mm", gcd.TimelineInterface) - var androidCwtchActivity = android.NewCwtchActivity(nil) - engine.RootContext().SetContextProperty("androidCwtchActivity", androidCwtchActivity) + + engine.RootContext().SetContextProperty("androidCwtchActivity", gcd.AndroidCwtchActivity) + engine.Load(qmlSource) @@ -263,7 +261,6 @@ func loadACN() { } } - // generate a random socks and control port (not real random...these are port numbers...) mrand.Seed(int64(time.Now().Nanosecond())) port := mrand.Intn(1000) + 9600 diff --git a/qml/main.qml b/qml/main.qml index d50a40c4..b2c17069 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -419,6 +419,14 @@ ApplicationWindow { parentStack.updateToolbar() statusbar.resetHeight() } + + onNotify: function(onion) { + // If we are processing QML it means the app is open, and as such we don't want to + // Send a notification - in the future we should probably use an API like this to Cancel notifications + // Until then I am leaving this here for documentation. + // androidCwtchActivity.channel = onion + // androidCwtchActivity.notification = "Message from " + onion; + } } Component.onCompleted: Mutant.standard.imagePath = gcd.assetPath; @@ -433,4 +441,6 @@ ApplicationWindow { } } } + + }