Android Notification - First Cut
the build was successful
Details
the build was successful
Details
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...
This commit is contained in:
parent
de56f8be15
commit
76df3c286d
|
@ -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
|
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:
|
issues might be helpful:
|
||||||
|
|
||||||
* https://bugreports.qt.io/browse/QTBUG-60022
|
* 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.
|
|
@ -23,20 +23,13 @@ import static android.app.Notification.CATEGORY_SERVICE;
|
||||||
public class CwtchActivity extends org.qtproject.qt5.android.bindings.QtActivity
|
public class CwtchActivity extends org.qtproject.qt5.android.bindings.QtActivity
|
||||||
{
|
{
|
||||||
private static NotificationManager m_notificationManager;
|
private static NotificationManager m_notificationManager;
|
||||||
private static Notification.Builder m_builder;
|
|
||||||
private static Notification.Builder m_builderOngoing;
|
private static Notification.Builder m_builderOngoing;
|
||||||
private static CwtchActivity m_instance;
|
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 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() {
|
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) {
|
if (m_notificationManager == null) {
|
||||||
m_notificationManager = (NotificationManager)m_instance.getSystemService(Context.NOTIFICATION_SERVICE);
|
m_notificationManager = (NotificationManager)m_instance.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
createNotificationChannel();
|
createNotificationChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_builder == null) {
|
// Apparently thr android documentation is just wrong and we need to provide a setGroupSummary
|
||||||
m_builder = new Notification.Builder(m_instance);
|
// notification regardless of targetted support version...
|
||||||
m_builder.setSmallIcon(R.drawable.ic_launcher);
|
Notification groupSummary =
|
||||||
m_builder.setContentTitle("Cwtch");
|
new Notification.Builder(m_instance)
|
||||||
m_builder.setPriority(PRIORITY_DEFAULT);
|
.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() {
|
private static void createNotificationChannel() {
|
||||||
// Create the NotificationChannel, but only on API 26+ because
|
// Create the NotificationChannel, but only on API 26+ because
|
||||||
// the NotificationChannel class is new and not in the support library
|
// the NotificationChannel class is new and not in the support library
|
||||||
if (SDK_INT >= 26) {
|
if (SDK_INT >= 26) {
|
||||||
String description = "Cwtch Ongoing Notification Channel";
|
String description = "Cwtch Notification Channel";
|
||||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, CONTENT_NOTIFICATION_ID_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
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);
|
|
||||||
channel.setDescription(description);
|
channel.setDescription(description);
|
||||||
// Register the channel with the system; you can't change the importance
|
// Register the channel with the system; you can't change the importance
|
||||||
// or other notification behaviors after this
|
// or other notification behaviors after this
|
||||||
|
|
|
@ -58,7 +58,6 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) {
|
||||||
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
|
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
|
||||||
ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampReceived])
|
ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampReceived])
|
||||||
uiManager.StoreAndNotify(peer, e.Data[event.RemotePeer], e.Data[event.Data], ts, onion)
|
uiManager.StoreAndNotify(peer, e.Data[event.RemotePeer], e.Data[event.Data], ts, onion)
|
||||||
|
|
||||||
case event.PeerAcknowledgement:
|
case event.PeerAcknowledgement:
|
||||||
uiManager.Acknowledge(e.Data[event.RemotePeer], e.Data[event.EventID])
|
uiManager.Acknowledge(e.Data[event.RemotePeer], e.Data[event.EventID])
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ type CwtchActivity struct {
|
||||||
|
|
||||||
_ func() `constructor:"init"`
|
_ func() `constructor:"init"`
|
||||||
|
|
||||||
|
_ string `property:"channel"`
|
||||||
_ string `property:"notification"`
|
_ string `property:"notification"`
|
||||||
|
|
||||||
_ func(string) `slot:"updateAndroidNotification"`
|
_ func(string) `slot:"updateAndroidNotification"`
|
||||||
|
@ -20,7 +21,6 @@ type CwtchActivity struct {
|
||||||
|
|
||||||
func (c *CwtchActivity) init() {
|
func (c *CwtchActivity) init() {
|
||||||
log.Debugln("CwtchActivity.init()")
|
log.Debugln("CwtchActivity.init()")
|
||||||
c.createOngoingNotification()
|
|
||||||
c.ConnectNotificationChanged(c.updateAndroidNotification)
|
c.ConnectNotificationChanged(c.updateAndroidNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ func (c *CwtchActivity) updateAndroidNotification(n string) {
|
||||||
var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught(
|
var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught(
|
||||||
"ca/openprivacy/cwtch/ui/CwtchActivity",
|
"ca/openprivacy/cwtch/ui/CwtchActivity",
|
||||||
"notify",
|
"notify",
|
||||||
"(Ljava/lang/String;)V",
|
"(Ljava/lang/String;Ljava/lang/String;)V",
|
||||||
n,
|
n, c.Channel(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
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() {
|
func (c *CwtchActivity) rootHomeButtonHandle() {
|
||||||
log.Infoln("CwtchActivity.rootHomeButtonHandle()!")
|
log.Infoln("CwtchActivity.rootHomeButtonHandle()!")
|
||||||
var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught(
|
var err = androidextras.QAndroidJniObject_CallStaticMethodVoid2Caught(
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
"cwtch.im/ui/go/constants"
|
"cwtch.im/ui/go/constants"
|
||||||
"cwtch.im/ui/go/features/groups"
|
"cwtch.im/ui/go/features/groups"
|
||||||
|
"cwtch.im/ui/go/ui/android"
|
||||||
"github.com/therecipe/qt/qml"
|
"github.com/therecipe/qt/qml"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -22,6 +23,7 @@ import (
|
||||||
type GrandCentralDispatcher struct {
|
type GrandCentralDispatcher struct {
|
||||||
core.QObject
|
core.QObject
|
||||||
|
|
||||||
|
AndroidCwtchActivity *android.CwtchActivity
|
||||||
QMLEngine *qml.QQmlApplicationEngine
|
QMLEngine *qml.QQmlApplicationEngine
|
||||||
Translator, OpaqueTranslator *core.QTranslator
|
Translator, OpaqueTranslator *core.QTranslator
|
||||||
|
|
||||||
|
@ -58,6 +60,7 @@ type GrandCentralDispatcher struct {
|
||||||
_ func() `signal:"ResetProfileList"`
|
_ func() `signal:"ResetProfileList"`
|
||||||
_ func(failed bool) `signal:"ChangePasswordResponse"`
|
_ func(failed bool) `signal:"ChangePasswordResponse"`
|
||||||
_ func(onion string, online bool) `signal:"UpdateProfileNetworkStatus"`
|
_ func(onion string, online bool) `signal:"UpdateProfileNetworkStatus"`
|
||||||
|
_ func(onion string) `signal:"Notify"`
|
||||||
|
|
||||||
// server management
|
// server management
|
||||||
_ func(handle, displayname, image string, status int, autostart bool, bundle string, messages int, key_types []string, keys []string) `signal:"AddServer"`
|
_ 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.SetTheme(this.GlobalSettings.Theme)
|
||||||
this.SetExperimentsEnabled(this.GlobalSettings.ExperimentsEnabled)
|
this.SetExperimentsEnabled(this.GlobalSettings.ExperimentsEnabled)
|
||||||
this.SetExperiments(this.GlobalSettings.Experiments)
|
this.SetExperiments(this.GlobalSettings.Experiments)
|
||||||
|
this.AndroidCwtchActivity = android.NewCwtchActivity(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUiManager gets (and creates if required) a ui Manager for the supplied profile id
|
// GetUiManager gets (and creates if required) a ui Manager for the supplied profile id
|
||||||
|
|
|
@ -301,6 +301,11 @@ func (this *manager) MessageJustAdded() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTxt string, sent time.Time, profileOnion string) {
|
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.DoIfProfileElse(this.profile, func() {
|
||||||
this.gcd.DoIfConversationElse(onion, func() {
|
this.gcd.DoIfConversationElse(onion, func() {
|
||||||
this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num())
|
this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num())
|
||||||
|
@ -314,6 +319,7 @@ func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTx
|
||||||
}, func() {
|
}, func() {
|
||||||
the.CwtchApp.GetPeer(profileOnion).StoreMessage(onion, messageTxt, sent)
|
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
|
// 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)
|
this.gcd.IncContactUnreadCount(handle)
|
||||||
})
|
})
|
||||||
|
if !fromMe {
|
||||||
|
this.gcd.Notify(handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *manager) ReloadProfiles() {
|
func (this *manager) ReloadProfiles() {
|
||||||
|
|
11
main.go
11
main.go
|
@ -11,7 +11,6 @@ import (
|
||||||
os2 "cwtch.im/ui/go/os"
|
os2 "cwtch.im/ui/go/os"
|
||||||
"cwtch.im/ui/go/the"
|
"cwtch.im/ui/go/the"
|
||||||
"cwtch.im/ui/go/ui"
|
"cwtch.im/ui/go/ui"
|
||||||
"cwtch.im/ui/go/ui/android"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"flag"
|
"flag"
|
||||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
"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)
|
log.Errorf("Could not access global ui config: %v\n", err)
|
||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
gcd := ui.NewGrandCentralDispatcher(nil)
|
gcd := ui.NewGrandCentralDispatcher(nil)
|
||||||
gcd.SetOs(runtime.GOOS)
|
gcd.SetOs(runtime.GOOS)
|
||||||
dir := core.QCoreApplication_ApplicationDirPath()
|
dir := core.QCoreApplication_ApplicationDirPath()
|
||||||
|
@ -186,8 +186,6 @@ func mainUi(flagLocal bool, flagClientUI bool) {
|
||||||
gcd.SetBuildDate("now")
|
gcd.SetBuildDate("now")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// this is to load local qml files quickly when developing
|
// this is to load local qml files quickly when developing
|
||||||
var qmlSource *core.QUrl
|
var qmlSource *core.QUrl
|
||||||
if flagLocal {
|
if flagLocal {
|
||||||
|
@ -223,13 +221,13 @@ func mainUi(flagLocal bool, flagClientUI bool) {
|
||||||
return nam
|
return nam
|
||||||
})
|
})
|
||||||
engine.SetNetworkAccessManagerFactory(factory)
|
engine.SetNetworkAccessManagerFactory(factory)
|
||||||
|
|
||||||
engine.RootContext().SetContextProperty("gcd", gcd)
|
engine.RootContext().SetContextProperty("gcd", gcd)
|
||||||
gcd.TimelineInterface = ui.NewMessageModel(nil)
|
gcd.TimelineInterface = ui.NewMessageModel(nil)
|
||||||
engine.RootContext().SetContextProperty("mm", gcd.TimelineInterface)
|
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)
|
engine.Load(qmlSource)
|
||||||
|
|
||||||
|
@ -263,7 +261,6 @@ func loadACN() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// generate a random socks and control port (not real random...these are port numbers...)
|
// generate a random socks and control port (not real random...these are port numbers...)
|
||||||
mrand.Seed(int64(time.Now().Nanosecond()))
|
mrand.Seed(int64(time.Now().Nanosecond()))
|
||||||
port := mrand.Intn(1000) + 9600
|
port := mrand.Intn(1000) + 9600
|
||||||
|
|
10
qml/main.qml
10
qml/main.qml
|
@ -419,6 +419,14 @@ ApplicationWindow {
|
||||||
parentStack.updateToolbar()
|
parentStack.updateToolbar()
|
||||||
statusbar.resetHeight()
|
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;
|
Component.onCompleted: Mutant.standard.imagePath = gcd.assetPath;
|
||||||
|
@ -433,4 +441,6 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue