~singpolyma/cheogram-android

32da65f910207f08f50b57ba59af9474eaad75d8 — Daniel Gultsch 8 years ago 93dad9b
client side support for XEP-0357: Push Notifications
M build.gradle => build.gradle +17 -20
@@ 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'
	}
}

A src/free/java/eu/siacs/conversations/services/PushManagementService.java => src/free/java/eu/siacs/conversations/services/PushManagementService.java +20 -0
@@ 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;
	}
}

M src/main/java/eu/siacs/conversations/Config.java => src/main/java/eu/siacs/conversations/Config.java +0 -1
@@ 109,6 109,5 @@ public final class Config {
	};

	private Config() {

	}
}

M src/main/java/eu/siacs/conversations/generator/IqGenerator.java => src/main/java/eu/siacs/conversations/generator/IqGenerator.java +15 -1
@@ 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;
	}
}

M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +27 -4
@@ 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();

M src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java => src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +13 -0
@@ 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);

M src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java => src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +5 -9
@@ 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() {

M src/main/res/layout/activity_edit_account.xml => src/main/res/layout/activity_edit_account.xml +21 -2
@@ 400,6 400,26 @@
                            tools:ignore="RtlHardcoded"/>
                    </TableRow>
                    <TableRow
                        android:id="@+id/push_row"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="@string/server_info_push"
                            android:textColor="@color/black87"
                            android:textSize="?attr/TextSizeBody"/>

                        <TextView
                            android:id="@+id/server_info_push"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_gravity="right"
                            android:textColor="@color/black87"
                            android:textSize="?attr/TextSizeBody"/>
                    </TableRow>
                    <TableRow
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content">



@@ 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"/>
                    </TableRow>
                </TableLayout>


M src/main/res/values/strings.xml => src/main/res/values/strings.xml +1 -0
@@ 193,6 193,7 @@
	<string name="server_info_stream_management">XEP-0198: Stream Management</string>
	<string name="server_info_pep">XEP-0163: PEP (Avatars / OMEMO)</string>
	<string name="server_info_http_upload">XEP-0363: HTTP File Upload</string>
	<string name="server_info_push">XEP-0357: Push</string>
	<string name="server_info_available">available</string>
	<string name="server_info_unavailable">unavailable</string>
	<string name="missing_public_keys">Missing public key announcements</string>

A src/playstore/AndroidManifest.xml => src/playstore/AndroidManifest.xml +35 -0
@@ 0,0 1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
    package="eu.siacs.conversations"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <permission android:name="eu.siacs.conversations.permission.C2D_MESSAGE"
                android:protectionLevel="signature"/>
    <uses-permission android:name="eu.siacs.conversations.permission.C2D_MESSAGE"/>

    <application>

        <receiver
            android:name="com.google.android.gms.gcm.GcmReceiver"
            android:exported="true"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="com.example.gcm" />
            </intent-filter>
        </receiver>
        <service
            android:name=".services.PushMessageReceiver"
            android:exported="false" >
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            </intent-filter>
        </service>

        <service android:name=".services.InstanceIdService" android:exported="false">
            <intent-filter>
                <action android:name="com.google.android.gms.iid.InstanceID"/>
            </intent-filter>
        </service>
    </application>
</manifest>

A src/playstore/java/eu/siacs/conversations/services/InstanceIdService.java => src/playstore/java/eu/siacs/conversations/services/InstanceIdService.java +15 -0
@@ 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);
	}
}

A src/playstore/java/eu/siacs/conversations/services/PushManagementService.java => src/playstore/java/eu/siacs/conversations/services/PushManagementService.java +78 -0
@@ 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);
	}
}

A src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java => src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java +20 -0
@@ 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);
	}
}