From 32da65f910207f08f50b57ba59af9474eaad75d8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 12 Feb 2016 11:39:27 +0100 Subject: [PATCH] client side support for XEP-0357: Push Notifications --- build.gradle | 37 ++++----- .../services/PushManagementService.java | 20 +++++ .../java/eu/siacs/conversations/Config.java | 1 - .../conversations/generator/IqGenerator.java | 16 +++- .../services/XmppConnectionService.java | 31 +++++++- .../conversations/ui/EditAccountActivity.java | 13 ++++ .../conversations/xmpp/XmppConnection.java | 14 ++-- src/main/res/layout/activity_edit_account.xml | 23 +++++- src/main/res/values/strings.xml | 1 + src/playstore/AndroidManifest.xml | 35 +++++++++ .../services/InstanceIdService.java | 15 ++++ .../services/PushManagementService.java | 78 +++++++++++++++++++ .../services/PushMessageReceiver.java | 20 +++++ 13 files changed, 267 insertions(+), 37 deletions(-) create mode 100644 src/free/java/eu/siacs/conversations/services/PushManagementService.java create mode 100644 src/playstore/AndroidManifest.xml create mode 100644 src/playstore/java/eu/siacs/conversations/services/InstanceIdService.java create mode 100644 src/playstore/java/eu/siacs/conversations/services/PushManagementService.java create mode 100644 src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java diff --git a/build.gradle b/build.gradle index 85be33055..4221d44ec 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.google.gms:google-services:1.5.0' } } @@ -21,12 +22,17 @@ allprojects { } apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' repositories { jcenter() mavenCentral() } +configurations { + playstoreCompile +} + dependencies { compile project(':libs:MemorizingTrustManager') compile 'org.sufficientlysecure:openpgp-api:10.0' @@ -44,6 +50,7 @@ dependencies { compile 'com.kyleduo.switchbutton:library:1.2.8' compile 'org.whispersystems:axolotl-android:1.3.4' compile 'com.makeramen:roundedimageview:2.2.0' + playstoreCompile 'com.google.android.gms:play-services-gcm:8.3.0' } android { @@ -55,7 +62,7 @@ android { targetSdkVersion 23 versionCode 123 versionName "1.9.4" - project.ext.set(archivesBaseName, archivesBaseName + "-" + versionName); + archivesBaseName += "-$versionName" } compileOptions { @@ -63,15 +70,10 @@ android { targetCompatibility JavaVersion.VERSION_1_7 } - // - // To sign release builds, create the file `gradle.properties` in - // $HOME/.gradle or in your project directory with this content: - // - // mStoreFile=/path/to/key.store - // mStorePassword=xxx - // mKeyAlias=alias - // mKeyPassword=xxx - // + productFlavors { + playstore + free + } if (project.hasProperty('mStoreFile') && project.hasProperty('mStorePassword') && project.hasProperty('mKeyAlias') && @@ -89,16 +91,6 @@ android { buildTypes.release.signingConfig = null } - applicationVariants.all { variant -> - if (variant.name.equals('release')) { - variant.outputs.each { output -> - if (output.zipAlign != null) { - output.zipAlign.outputFile = new File(output.outputFile.parent, rootProject.name + "-${variant.versionName}.apk") - } - } - } - } - lintOptions { disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource' } @@ -116,4 +108,9 @@ android { } } + + packagingOptions { + exclude 'META-INF/BCKEY.DSA' + exclude 'META-INF/BCKEY.SF' + } } diff --git a/src/free/java/eu/siacs/conversations/services/PushManagementService.java b/src/free/java/eu/siacs/conversations/services/PushManagementService.java new file mode 100644 index 000000000..48c576cec --- /dev/null +++ b/src/free/java/eu/siacs/conversations/services/PushManagementService.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.services; + +import eu.siacs.conversations.entities.Account; + +public class PushManagementService { + + protected final XmppConnectionService mXmppConnectionService; + + public PushManagementService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void registerPushTokenOnServer(Account account) { + //stub implementation. only affects playstore flavor + } + + public boolean available() { + return false; + } +} diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 3d32a2cb3..056dd7f00 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -109,6 +109,5 @@ public final class Config { }; private Config() { - } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 345f68ae3..dc9472c50 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -289,7 +289,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(host); - Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD); + Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD); request.addChild("filename").setContent(file.getName()); request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); if (mime != null) { @@ -307,4 +307,18 @@ public class IqGenerator extends AbstractGenerator { return register; } + + public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(appServer); + Element command = packet.addChild("command", "http://jabber.org/protocol/commands"); + command.setAttribute("node","register-push-gcm"); + command.setAttribute("action","execute"); + Data data = new Data(); + data.put("token", token); + data.put("device-id", deviceId); + data.submit(); + command.addChild(data); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 9c2aa50c0..e985fe07d 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -73,7 +73,6 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.Transferable; @@ -127,6 +126,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public static final String ACTION_TRY_AGAIN = "try_again"; public static final String ACTION_DISABLE_ACCOUNT = "disable_account"; private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh"; + public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received"; private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor(); private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor(); private final IBinder mBinder = new XmppConnectionBinder(); @@ -198,6 +199,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this); private AvatarService mAvatarService = new AvatarService(this); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); + private PushManagementService mPushManagementService = new PushManagementService(this); private OnConversationUpdate mOnConversationUpdate = null; private final FileObserver fileObserver = new FileObserver( FileBackend.getConversationsImageDirectory()) { @@ -265,7 +267,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private OnStatusChanged statusListener = new OnStatusChanged() { @Override - public void onStatusChanged(Account account) { + public void onStatusChanged(final Account account) { XmppConnection connection = account.getXmppConnection(); if (mOnAccountUpdate != null) { mOnAccountUpdate.onAccountUpdate(); @@ -296,6 +298,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } account.pendingConferenceJoins.clear(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); + + if (mPushManagementService.pushAvailable(account)) { + mPushManagementService.registerPushTokenOnServer(account); + } + } else if (account.getStatus() == Account.State.OFFLINE) { resetSendingToWaiting(account); if (!account.isOptionSet(Account.OPTION_DISABLED)) { @@ -512,6 +519,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa refreshAllPresences(); } break; + case ACTION_GCM_TOKEN_REFRESH: + refreshAllGcmTokens(); + break; + case ACTION_GCM_MESSAGE_RECEIVED: + Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras()); } } this.wakeLock.acquire(); @@ -572,7 +584,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa reconnectAccount(account, true, interactive); } } - } if (mOnAccountUpdate != null) { mOnAccountUpdate.onAccountUpdate(); @@ -2845,6 +2856,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + private void refreshAllGcmTokens() { + for(Account account : getAccounts()) { + if (account.isOnlineAndConnected() && mPushManagementService.pushAvailable(account)) { + mPushManagementService.registerPushTokenOnServer(account); + } + } + } + public void sendOfflinePresence(final Account account) { sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); } @@ -3005,7 +3024,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.insertDiscoveryResult(disco); injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco); } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid+" "+presence.getVer()+" vs "+disco.getVer()); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer()); } } account.inProgressDiscoFetches.remove(key); @@ -3041,6 +3060,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa }); } + public PushManagementService getPushManagementService() { + return mPushManagementService; + } + public interface OnMamPreferencesFetched { void onPreferencesFetched(Element prefs); void onPreferencesFetchFailed(); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index d30fbda2c..cfa1889a0 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -29,6 +29,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TableLayout; +import android.widget.TableRow; import android.widget.TextView; import android.widget.Toast; @@ -77,6 +78,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private TextView mServerInfoBlocking; private TextView mServerInfoPep; private TextView mServerInfoHttpUpload; + private TextView mServerInfoPush; private TextView mSessionEst; private TextView mOtrFingerprint; private TextView mAxolotlFingerprint; @@ -223,6 +225,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } }; private Toast mFetchingMamPrefsToast; + private TableRow mPushRow; public void refreshUiReal() { invalidateOptionsMenu(); @@ -422,6 +425,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); this.mServerInfoHttpUpload = (TextView) findViewById(R.id.server_info_http_upload); + this.mPushRow = (TableRow) findViewById(R.id.push_row); + this.mServerInfoPush = (TextView) findViewById(R.id.server_info_push); this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); @@ -680,6 +685,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mServerInfoHttpUpload.setText(R.string.server_info_unavailable); } + + this.mPushRow.setVisibility(xmppConnectionService.getPushManagementService().available() ? View.VISIBLE : View.GONE); + + if (features.push()) { + this.mServerInfoPush.setText(R.string.server_info_available); + } else { + this.mServerInfoPush.setText(R.string.server_info_unavailable); + } final String otrFingerprint = this.mAccount.getOtrFingerprint(); if (otrFingerprint != null) { this.mOtrFingerprintBox.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 8b7eae397..3a9c87a30 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1495,17 +1495,13 @@ public class XmppConnection implements Runnable { } public boolean mam() { - if (hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0")) { - return true; - } else { - return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); - } + return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0") + || hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); } - public boolean advancedStreamFeaturesLoaded() { - synchronized (XmppConnection.this.disco) { - return disco.containsKey(account.getServer()); - } + public boolean push() { + return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0") + || hasDiscoFeature(account.getServer(), "urn:xmpp:push:0"); } public boolean rosterVersioning() { diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index ed9358192..d308b4ce7 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -399,6 +399,26 @@ android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> + + + + + + @@ -416,8 +436,7 @@ android:layout_height="wrap_content" android:layout_gravity="right" android:textColor="@color/black87" - android:textSize="?attr/TextSizeBody" - tools:ignore="RtlHardcoded"/> + android:textSize="?attr/TextSizeBody"/> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8da14a4a3..0b726ae7c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -193,6 +193,7 @@ XEP-0198: Stream Management XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload + XEP-0357: Push available unavailable Missing public key announcements diff --git a/src/playstore/AndroidManifest.xml b/src/playstore/AndroidManifest.xml new file mode 100644 index 000000000..a1b91be35 --- /dev/null +++ b/src/playstore/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/playstore/java/eu/siacs/conversations/services/InstanceIdService.java b/src/playstore/java/eu/siacs/conversations/services/InstanceIdService.java new file mode 100644 index 000000000..dc0804305 --- /dev/null +++ b/src/playstore/java/eu/siacs/conversations/services/InstanceIdService.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.services; + +import android.content.Intent; + +import com.google.android.gms.iid.InstanceIDListenerService; + +public class InstanceIdService extends InstanceIDListenerService { + + @Override + public void onTokenRefresh() { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_GCM_TOKEN_REFRESH); + startService(intent); + } +} diff --git a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java new file mode 100644 index 000000000..d65170eec --- /dev/null +++ b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java @@ -0,0 +1,78 @@ +package eu.siacs.conversations.services; + +import android.provider.Settings; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.google.android.gms.iid.InstanceID; + +import java.io.IOException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class PushManagementService { + + private static final String APP_SERVER = "push.conversations.im"; + + protected final XmppConnectionService mXmppConnectionService; + + public PushManagementService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void registerPushTokenOnServer(final Account account) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": has push support"); + retrieveGcmInstanceToken(new OnGcmInstanceTokenRetrieved() { + @Override + public void onGcmInstanceTokenRetrieved(String token) { + try { + final String deviceId = Settings.Secure.getString(mXmppConnectionService.getContentResolver(), Settings.Secure.ANDROID_ID); + IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(Jid.fromString(APP_SERVER), token, deviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "push to app server result: " + packet.toString()); + } + }); + } catch (InvalidJidException ignored) { + + } + } + }); + } + + private void retrieveGcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { + new Thread(new Runnable() { + @Override + public void run() { + InstanceID instanceID = InstanceID.getInstance(mXmppConnectionService); + try { + String token = instanceID.getToken(mXmppConnectionService.getString(R.string.gcm_defaultSenderId), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + instanceTokenRetrieved.onGcmInstanceTokenRetrieved(token); + } catch (IOException e) { + } + } + }).start(); + + } + + public boolean available() { + return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS; + } + + public boolean pushAvailable(Account account) { + return account.getXmppConnection().getFeatures().push() && available(); + } + + interface OnGcmInstanceTokenRetrieved { + void onGcmInstanceTokenRetrieved(String token); + } +} diff --git a/src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java b/src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java new file mode 100644 index 000000000..37c95e135 --- /dev/null +++ b/src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.services; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.gcm.GcmListenerService; + +import eu.siacs.conversations.Config; + +public class PushMessageReceiver extends GcmListenerService { + + @Override + public void onMessageReceived(String from, Bundle data) { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_GCM_MESSAGE_RECEIVED); + intent.replaceExtras(data); + startService(intent); + } +}