implement FCM push for group chats

Daniel Gultsch created

Change summary

src/free/java/eu/siacs/conversations/services/PushManagementService.java      |  11 
src/main/java/eu/siacs/conversations/entities/Conversation.java               |   1 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java               |  15 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      |  24 
src/main/java/eu/siacs/conversations/xml/Namespace.java                       |   1 
src/playstore/java/eu/siacs/conversations/services/PushManagementService.java | 225 
6 files changed, 185 insertions(+), 92 deletions(-)

Detailed changes

src/free/java/eu/siacs/conversations/services/PushManagementService.java 🔗

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.services;
 
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
 
 public class PushManagementService {
 
@@ -10,7 +11,15 @@ public class PushManagementService {
 		this.mXmppConnectionService = service;
 	}
 
-	public void registerPushTokenOnServer(Account account) {
+	void registerPushTokenOnServer(Account account) {
+		//stub implementation. only affects playstore flavor
+	}
+
+	void registerPushTokenOnServer(Conversation conversation) {
+		//stub implementation. only affects playstore flavor
+	}
+
+	void disablePushOnServer(Conversation conversation) {
 		//stub implementation. only affects playstore flavor
 	}
 

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -54,6 +54,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 
 	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 	public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
+	public static final String ATTRIBUTE_PUSH_NODE = "push_node";
 	public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
 	static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 	private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -423,14 +423,21 @@ public class IqGenerator extends AbstractGenerator {
 	}
 
 	public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
-		IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+		return pushTokenToAppServer(appServer, token, deviceId, null);
+	}
+
+	public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
+		final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
 		packet.setTo(appServer);
-		Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
+		final Element command = packet.addChild("command", Namespace.COMMANDS);
 		command.setAttribute("node", "register-push-fcm");
 		command.setAttribute("action", "execute");
-		Data data = new Data();
+		final Data data = new Data();
 		data.put("token", token);
 		data.put("android-id", deviceId);
+		if (muc != null) {
+			data.put("muc", muc.toEscapedString());
+		}
 		data.submit();
 		command.addChild(data);
 		return packet;
@@ -454,7 +461,7 @@ public class IqGenerator extends AbstractGenerator {
 	public IqPacket disablePush(final Jid jid, final String node) {
 		IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
 		Element disable = packet.addChild("disable", Namespace.PUSH);
-		disable.setAttribute("jid", jid.toString());
+		disable.setAttribute("jid", jid.toEscapedString());
 		disable.setAttribute("node", node);
 		return packet;
 	}

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -2583,31 +2583,35 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
-	private void enableMucPush(final Conversation conversation) {
-	    final Account account = conversation.getAccount();
-	    final Jid room = conversation.getJid().asBareJid();
+	private void enableDirectMucPush(final Conversation conversation) {
+        final Account account = conversation.getAccount();
+        final Jid room = conversation.getJid().asBareJid();
         final IqPacket enable = mIqGenerator.enablePush(conversation.getAccount().getJid(), conversation.getUuid(), null);
         enable.setTo(room);
         sendIqPacket(account, enable, (a, response) -> {
             if (response.getType() == IqPacket.TYPE.RESULT) {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": enabled push for muc "+room);
+                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": enabled direct push for muc "+room);
             } else if (response.getType() == IqPacket.TYPE.ERROR) {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": unable to enable push for muc "+room+" "+response.getError());
+                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": unable to enable direct push for muc "+room+" "+response.getError());
             }
         });
+    }
 
+	private void enableMucPush(final Conversation conversation) {
+	    enableDirectMucPush(conversation);
+        mPushManagementService.registerPushTokenOnServer(conversation);
     }
 
-    private void disableMucPush(final Conversation conversation) {
+    private void disableDirectMucPush(final Conversation conversation) {
         final Account account = conversation.getAccount();
         final Jid room = conversation.getJid().asBareJid();
         final IqPacket disable = mIqGenerator.disablePush(conversation.getAccount().getJid(), conversation.getUuid());
         disable.setTo(room);
         sendIqPacket(account, disable, (a, response) -> {
             if (response.getType() == IqPacket.TYPE.RESULT) {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": disabled push for muc "+room);
+                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": disabled direct push for muc "+room);
             } else if (response.getType() == IqPacket.TYPE.ERROR) {
-                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": unable to disable push for muc "+room+" "+response.getError());
+                Log.d(Config.LOGTAG,a.getJid().asBareJid()+": unable to disable direct push for muc "+room+" "+response.getError());
             }
         });
     }
@@ -2792,7 +2796,8 @@ public class XmppConnectionService extends Service {
 		account.pendingConferenceLeaves.remove(conversation);
 		if (account.getStatus() == Account.State.ONLINE || now) {
 		    if (conversation.getMucOptions().push()) {
-		        disableMucPush(conversation);
+		        disableDirectMucPush(conversation);
+		        mPushManagementService.disablePushOnServer(conversation);
             }
 			sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions()));
 			conversation.getMucOptions().setOffline();
@@ -4063,6 +4068,7 @@ public class XmppConnectionService extends Service {
 		for (Account account : getAccounts()) {
 			if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
 				mPushManagementService.registerPushTokenOnServer(account);
+				//TODO renew mucs
 			}
 		}
 	}

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -29,4 +29,5 @@ public final class Namespace {
 	public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1";
 	public static final String PING = "urn:xmpp:ping";
 	public static final String PUSH = "urn:xmpp:push:0";
+	public static final String COMMANDS = "http://jabber.org/protocol/commands";
 }

src/playstore/java/eu/siacs/conversations/services/PushManagementService.java 🔗

@@ -5,13 +5,16 @@ import android.util.Log;
 import com.google.android.gms.common.ConnectionResult;
 import com.google.android.gms.common.GoogleApiAvailability;
 import com.google.firebase.iid.FirebaseInstanceId;
+import com.google.firebase.iid.InstanceIdResult;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.forms.Data;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
@@ -19,82 +22,148 @@ import rocks.xmpp.addr.Jid;
 
 public class PushManagementService {
 
-	protected final XmppConnectionService mXmppConnectionService;
-
-	PushManagementService(XmppConnectionService service) {
-		this.mXmppConnectionService = service;
-	}
-
-	void registerPushTokenOnServer(final Account account) {
-		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support");
-		retrieveFcmInstanceToken(token -> {
-			final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
-			final Jid appServer = Jid.of(mXmppConnectionService.getString(R.string.app_server));
-			IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(appServer, token, androidId);
-			mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> {
-				Element command = p.findChild("command", "http://jabber.org/protocol/commands");
-				if (p.getType() == IqPacket.TYPE.RESULT && command != null) {
-					Element x = command.findChild("x", Namespace.DATA);
-					if (x != null) {
-						Data data = Data.parse(x);
-						try {
-							String node = data.getValue("node");
-							String secret = data.getValue("secret");
-							Jid jid = Jid.of(data.getValue("jid"));
-							if (node != null && secret != null) {
-								enablePushOnServer(a, jid, node, secret);
-							}
-						} catch (IllegalArgumentException e) {
-							e.printStackTrace();
-						}
-					}
-				} else {
-					Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server");
-				}
-			});
-		});
-	}
-
-	private void enablePushOnServer(final Account account, final Jid jid, final String node, final String secret) {
-		IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(jid, node, secret);
-		mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> {
-			if (p.getType() == IqPacket.TYPE.RESULT) {
-				Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server");
-			} else if (p.getType() == IqPacket.TYPE.ERROR) {
-				Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed");
-			}
-		});
-	}
-
-	private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
-		new Thread(() -> {
-			try {
-				instanceTokenRetrieved.onGcmInstanceTokenRetrieved(FirebaseInstanceId.getInstance().getToken());
-			} catch (Exception e) {
-				Log.d(Config.LOGTAG, "unable to get push token",e);
-			}
-		}).start();
-
-	}
-
-
-	public boolean available(Account account) {
-		final XmppConnection connection = account.getXmppConnection();
-		return connection != null
-				&& connection.getFeatures().sm()
-				&& connection.getFeatures().push()
-				&& playServicesAvailable();
-	}
-
-	private boolean playServicesAvailable() {
-		return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS;
-	}
-
-	public boolean isStub() {
-		return false;
-	}
-
-	interface OnGcmInstanceTokenRetrieved {
-		void onGcmInstanceTokenRetrieved(String token);
-	}
+    protected final XmppConnectionService mXmppConnectionService;
+
+    PushManagementService(XmppConnectionService service) {
+        this.mXmppConnectionService = service;
+    }
+
+    private static Data findResponseData(IqPacket response) {
+        final Element command = response.findChild("command", Namespace.COMMANDS);
+        final Element x = command == null ? null : command.findChild("x", Namespace.DATA);
+        return x == null ? null : Data.parse(x);
+    }
+
+    private Jid getAppServer() {
+        return Jid.of(mXmppConnectionService.getString(R.string.app_server));
+    }
+
+    void registerPushTokenOnServer(final Account account) {
+        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support");
+        retrieveFcmInstanceToken(token -> {
+            final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
+            final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId);
+            mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> {
+                final Data data = findResponseData(response);
+                if (response.getType() == IqPacket.TYPE.RESULT && data != null) {
+                    try {
+                        String node = data.getValue("node");
+                        String secret = data.getValue("secret");
+                        Jid jid = Jid.of(data.getValue("jid"));
+                        if (node != null && secret != null) {
+                            enablePushOnServer(a, jid, node, secret);
+                        }
+                    } catch (IllegalArgumentException e) {
+                        e.printStackTrace();
+                    }
+                } else {
+                    Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server");
+                }
+            });
+        });
+    }
+
+    void registerPushTokenOnServer(final Conversation conversation) {
+        Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": room "+conversation.getJid().asBareJid()+" has push support");
+        retrieveFcmInstanceToken(token -> {
+            final Jid muc = conversation.getJid().asBareJid();
+            final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
+            final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId, muc);
+            packet.setTo(muc);
+            mXmppConnectionService.sendIqPacket(conversation.getAccount(), packet, (a, response) -> {
+                final Data data = findResponseData(response);
+                if (response.getType() == IqPacket.TYPE.RESULT && data != null) {
+                    try {
+                        final String node = data.getValue("node");
+                        final String secret = data.getValue("secret");
+                        final Jid jid = Jid.of(data.getValue("jid"));
+                        if (node != null && secret != null) {
+                            enablePushOnServer(conversation, jid, node, secret);
+                        }
+                    } catch (IllegalArgumentException e) {
+                        e.printStackTrace();
+                    }
+                } else {
+                    Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server");
+                }
+            });
+        });
+    }
+
+    private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) {
+        final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
+        mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> {
+            if (p.getType() == IqPacket.TYPE.RESULT) {
+                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server");
+            } else if (p.getType() == IqPacket.TYPE.ERROR) {
+                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed");
+            }
+        });
+    }
+
+    private void enablePushOnServer(final Conversation conversation, final Jid appServer, final String node, final String secret) {
+        final Jid muc = conversation.getJid().asBareJid();
+        final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
+        enable.setTo(muc);
+        mXmppConnectionService.sendIqPacket(conversation.getAccount(), enable, (a, p) -> {
+            if (p.getType() == IqPacket.TYPE.RESULT) {
+                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on " + muc);
+                if (conversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, node)) {
+                    mXmppConnectionService.updateConversation(conversation);
+                }
+            } else if (p.getType() == IqPacket.TYPE.ERROR) {
+                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on " + muc + " failed");
+            }
+        });
+    }
+
+    public void disablePushOnServer(final Conversation conversation) {
+        final Jid muc = conversation.getJid().asBareJid();
+        final String node = conversation.getAttribute(Conversation.ATTRIBUTE_PUSH_NODE);
+        if (node != null) {
+            final IqPacket disable = mXmppConnectionService.getIqGenerator().disablePush(getAppServer(), node);
+            disable.setTo(muc);
+            mXmppConnectionService.sendIqPacket(conversation.getAccount(), disable, (account, response) -> {
+                if (response.getType() == IqPacket.TYPE.ERROR) {
+                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to disable push for room "+muc);
+                }
+            });
+        } else {
+            Log.d(Config.LOGTAG,conversation.getAccount().getJid().asBareJid()+": room "+muc+" has no stored node. unable to disable push");
+        }
+    }
+
+    private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
+        FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(task -> {
+            if (!task.isSuccessful()) {
+                Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException());
+            }
+            final InstanceIdResult result = task.getResult();
+            if (result != null) {
+                instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result.getToken());
+            }
+        });
+
+    }
+
+
+    public boolean available(Account account) {
+        final XmppConnection connection = account.getXmppConnection();
+        return connection != null
+                && connection.getFeatures().sm()
+                && connection.getFeatures().push()
+                && playServicesAvailable();
+    }
+
+    private boolean playServicesAvailable() {
+        return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS;
+    }
+
+    public boolean isStub() {
+        return false;
+    }
+
+    interface OnGcmInstanceTokenRetrieved {
+        void onGcmInstanceTokenRetrieved(String token);
+    }
 }