diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 593ccf6a3..5dc8d210e 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -79,6 +79,9 @@
+
+
+
@@ -128,6 +131,23 @@
android:scheme="package" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= 25 && timestamp.charAt(19) == '.') {
+ final String millis = timestamp.substring(19, timestamp.length() - 5);
+ try {
+ double fractions = Double.parseDouble("0" + millis);
+ return Math.round(1000 * fractions);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
protected void updateLastseen(final Account account, final Jid from) {
final Contact contact = account.getRoster().getContact(from);
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
index 1b3682796..a6881914d 100644
--- a/src/main/java/eu/siacs/conversations/parser/IqParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -452,6 +452,24 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
}
mXmppConnectionService.sendIqPacket(account, response, null);
+ } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) {
+ final Jid transport = packet.getFrom();
+ final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH);
+ final boolean success =
+ push != null
+ && mXmppConnectionService.processUnifiedPushMessage(
+ account, transport, push);
+ final IqPacket response;
+ if (success) {
+ response = packet.generateResponse(IqPacket.TYPE.RESULT);
+ } else {
+ response = packet.generateResponse(IqPacket.TYPE.ERROR);
+ final Element error = response.addChild("error");
+ error.setAttribute("type", "cancel");
+ error.setAttribute("code", "404");
+ error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
+ }
+ mXmppConnectionService.sendIqPacket(account, response, null);
} else {
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java
new file mode 100644
index 000000000..28f7e0077
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java
@@ -0,0 +1,244 @@
+package eu.siacs.conversations.persistance;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+
+public class UnifiedPushDatabase extends SQLiteOpenHelper {
+ private static final String DATABASE_NAME = "unified-push-distributor";
+ private static final int DATABASE_VERSION = 1;
+
+ private static UnifiedPushDatabase instance;
+
+ public static UnifiedPushDatabase getInstance(final Context context) {
+ synchronized (UnifiedPushDatabase.class) {
+ if (instance == null) {
+ instance = new UnifiedPushDatabase(context.getApplicationContext());
+ }
+ return instance;
+ }
+ }
+
+ private UnifiedPushDatabase(@Nullable Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase sqLiteDatabase) {
+ sqLiteDatabase.execSQL(
+ "CREATE TABLE push (account TEXT, transport TEXT, application TEXT NOT NULL, instance TEXT NOT NULL UNIQUE, endpoint TEXT, expiration NUMBER DEFAULT 0)");
+ }
+
+ public boolean register(final String application, final String instance) {
+ final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
+ sqLiteDatabase.beginTransaction();
+ final Optional existingApplication;
+ try (final Cursor cursor =
+ sqLiteDatabase.query(
+ "push",
+ new String[] {"application"},
+ "instance=?",
+ new String[] {instance},
+ null,
+ null,
+ null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ existingApplication = Optional.of(cursor.getString(0));
+ } else {
+ existingApplication = Optional.absent();
+ }
+ }
+ if (existingApplication.isPresent()) {
+ sqLiteDatabase.setTransactionSuccessful();
+ sqLiteDatabase.endTransaction();
+ return application.equals(existingApplication.get());
+ }
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put("application", application);
+ contentValues.put("instance", instance);
+ contentValues.put("expiration", 0);
+ final long inserted = sqLiteDatabase.insert("push", null, contentValues);
+ if (inserted > 0) {
+ Log.d(Config.LOGTAG, "inserted new application/instance tuple into unified push db");
+ }
+ sqLiteDatabase.setTransactionSuccessful();
+ sqLiteDatabase.endTransaction();
+ return true;
+ }
+
+ public List getRenewals(final String account, final String transport) {
+ final ImmutableList.Builder renewalBuilder = ImmutableList.builder();
+ final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
+ try (final Cursor cursor =
+ sqLiteDatabase.query(
+ "push",
+ new String[] {"application", "instance"},
+ "account <> ? OR transport <> ? OR expiration < "
+ + System.currentTimeMillis(),
+ new String[] {account, transport},
+ null,
+ null,
+ null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ renewalBuilder.add(
+ new PushTarget(
+ cursor.getString(cursor.getColumnIndexOrThrow("application")),
+ cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
+ }
+ }
+ return renewalBuilder.build();
+ }
+
+ public ApplicationEndpoint getEndpoint(
+ final String account, final String transport, final String instance) {
+ final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
+ try (final Cursor cursor =
+ sqLiteDatabase.query(
+ "push",
+ new String[] {"application", "endpoint"},
+ "account = ? AND transport = ? AND instance = ? ",
+ new String[] {account, transport, instance},
+ null,
+ null,
+ null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ return new ApplicationEndpoint(
+ cursor.getString(cursor.getColumnIndexOrThrow("application")),
+ cursor.getString(cursor.getColumnIndexOrThrow("endpoint")));
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onUpgrade(
+ final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {}
+
+ public boolean updateEndpoint(
+ final String instance,
+ final String account,
+ final String transport,
+ final String endpoint,
+ final long expiration) {
+ final SQLiteDatabase sqLiteDatabase = getWritableDatabase();
+ sqLiteDatabase.beginTransaction();
+ final String existingEndpoint;
+ try (final Cursor cursor =
+ sqLiteDatabase.query(
+ "push",
+ new String[] {"endpoint"},
+ "instance=?",
+ new String[] {instance},
+ null,
+ null,
+ null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ existingEndpoint = cursor.getString(0);
+ } else {
+ existingEndpoint = null;
+ }
+ }
+ final ContentValues contentValues = new ContentValues();
+ contentValues.put("account", account);
+ contentValues.put("transport", transport);
+ contentValues.put("endpoint", endpoint);
+ contentValues.put("expiration", expiration);
+ sqLiteDatabase.update("push", contentValues, "instance=?", new String[] {instance});
+ sqLiteDatabase.setTransactionSuccessful();
+ sqLiteDatabase.endTransaction();
+ return !endpoint.equals(existingEndpoint);
+ }
+
+ public List getPushTargets(final String account, final String transport) {
+ final ImmutableList.Builder renewalBuilder = ImmutableList.builder();
+ final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
+ try (final Cursor cursor =
+ sqLiteDatabase.query(
+ "push",
+ new String[] {"application", "instance"},
+ "account = ?",
+ new String[] {account},
+ null,
+ null,
+ null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ renewalBuilder.add(
+ new PushTarget(
+ cursor.getString(cursor.getColumnIndexOrThrow("application")),
+ cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
+ }
+ }
+ return renewalBuilder.build();
+ }
+
+ public boolean deleteInstance(final String instance) {
+ final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
+ final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance});
+ return rows >= 1;
+ }
+
+ public boolean deleteApplication(final String application) {
+ final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
+ final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application});
+ return rows >= 1;
+ }
+
+ public static class ApplicationEndpoint {
+ public final String application;
+ public final String endpoint;
+
+ public ApplicationEndpoint(String application, String endpoint) {
+ this.application = application;
+ this.endpoint = endpoint;
+ }
+ }
+
+ public static class PushTarget {
+ public final String application;
+ public final String instance;
+
+ public PushTarget(final String application, final String instance) {
+ this.application = application;
+ this.instance = instance;
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("application", application)
+ .add("instance", instance)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PushTarget that = (PushTarget) o;
+ return Objects.equal(application, that.application)
+ && Objects.equal(instance, that.instance);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(application, instance);
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
new file mode 100644
index 000000000..101a09fc3
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
@@ -0,0 +1,277 @@
+package eu.siacs.conversations.services;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.io.BaseEncoding;
+
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.parser.AbstractParser;
+import eu.siacs.conversations.persistance.UnifiedPushDatabase;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class UnifiedPushBroker {
+
+ private final XmppConnectionService service;
+
+ public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) {
+ this.service = xmppConnectionService;
+ }
+
+ public Optional renewUnifiedPushEndpoints() {
+ final Optional transportOptional = getTransport();
+ if (transportOptional.isPresent()) {
+ renewUnifiedEndpoint(transportOptional.get());
+ } else {
+ Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
+ }
+ return transportOptional;
+ }
+
+ private void renewUnifiedEndpoint(final Transport transport) {
+ final Account account = transport.account;
+ final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
+ final List renewals =
+ unifiedPushDatabase.getRenewals(
+ account.getUuid(), transport.transport.toEscapedString());
+ for (final UnifiedPushDatabase.PushTarget renewal : renewals) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
+ final String hashedApplication =
+ UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
+ final String hashedInstance =
+ UnifiedPushDistributor.hash(account.getUuid(), renewal.instance);
+ final IqPacket registration = new IqPacket(IqPacket.TYPE.SET);
+ registration.setTo(transport.transport);
+ final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
+ register.setAttribute("application", hashedApplication);
+ register.setAttribute("instance", hashedInstance);
+ this.service.sendIqPacket(
+ account,
+ registration,
+ (a, response) -> processRegistration(transport, renewal, response));
+ }
+ }
+
+ private void processRegistration(
+ final Transport transport,
+ final UnifiedPushDatabase.PushTarget renewal,
+ final IqPacket response) {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
+ if (registered == null) {
+ return;
+ }
+ final String endpoint = registered.getAttribute("endpoint");
+ if (Strings.isNullOrEmpty(endpoint)) {
+ Log.w(Config.LOGTAG, "endpoint was null in up registration");
+ return;
+ }
+ final long expiration;
+ try {
+ expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration"));
+ } catch (final IllegalArgumentException | ParseException e) {
+ Log.d(Config.LOGTAG, "could not parse expiration", e);
+ return;
+ }
+ renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration);
+ }
+ }
+
+ private void renewUnifiedPushEndpoint(
+ final Transport transport,
+ final UnifiedPushDatabase.PushTarget renewal,
+ final String endpoint,
+ final long expiration) {
+ Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
+ final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
+ final boolean modified =
+ unifiedPushDatabase.updateEndpoint(
+ renewal.instance,
+ transport.account.getUuid(),
+ transport.transport.toEscapedString(),
+ endpoint,
+ expiration);
+ if (modified) {
+ Log.d(
+ Config.LOGTAG,
+ "endpoint for "
+ + renewal.application
+ + "/"
+ + renewal.instance
+ + " was updated to "
+ + endpoint);
+ broadcastEndpoint(
+ renewal.instance,
+ new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint));
+ }
+ }
+
+ public boolean reconfigurePushDistributor() {
+ final boolean enabled = getTransport().isPresent();
+ setUnifiedPushDistributorEnabled(enabled);
+ return enabled;
+ }
+
+ private void setUnifiedPushDistributorEnabled(final boolean enabled) {
+ final PackageManager packageManager = service.getPackageManager();
+ final ComponentName componentName =
+ new ComponentName(service, UnifiedPushDistributor.class);
+ if (enabled) {
+ packageManager.setComponentEnabledSetting(
+ componentName,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP);
+ Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled");
+ } else {
+ packageManager.setComponentEnabledSetting(
+ componentName,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled");
+ }
+ }
+
+ public boolean processPushMessage(
+ final Account account, final Jid transport, final Element push) {
+ final String instance = push.getAttribute("instance");
+ final String application = push.getAttribute("application");
+ if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
+ return false;
+ }
+ final String content = push.getContent();
+ final byte[] payload;
+ if (Strings.isNullOrEmpty(content)) {
+ payload = new byte[0];
+ } else if (BaseEncoding.base64().canDecode(content)) {
+ payload = BaseEncoding.base64().decode(content);
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": received invalid unified push payload");
+ return false;
+ }
+ final Optional pushTarget =
+ getPushTarget(account, transport, application, instance);
+ if (pushTarget.isPresent()) {
+ final UnifiedPushDatabase.PushTarget target = pushTarget.get();
+ // TODO check if app is still installed?
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": broadcasting a "
+ + payload.length
+ + " bytes push message to "
+ + target.application);
+ broadcastPushMessage(target, payload);
+ return true;
+ } else {
+ Log.d(Config.LOGTAG, "could not find application for push");
+ return false;
+ }
+ }
+
+ public Optional getTransport() {
+ final SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
+ final String accountPreference =
+ sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
+ final String pushServerPreference =
+ sharedPreferences.getString(
+ UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
+ service.getString(R.string.default_push_server));
+ if (Strings.isNullOrEmpty(accountPreference)
+ || "none".equalsIgnoreCase(accountPreference)
+ || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
+ return Optional.absent();
+ }
+ final Jid transport;
+ final Jid jid;
+ try {
+ transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
+ jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
+ } catch (final IllegalArgumentException e) {
+ return Optional.absent();
+ }
+ final Account account = service.findAccountByJid(jid);
+ if (account == null) {
+ return Optional.absent();
+ }
+ return Optional.of(new Transport(account, transport));
+ }
+
+ private Optional getPushTarget(
+ final Account account,
+ final Jid transport,
+ final String application,
+ final String instance) {
+ final String uuid = account.getUuid();
+ final List pushTargets =
+ UnifiedPushDatabase.getInstance(service)
+ .getPushTargets(uuid, transport.toEscapedString());
+ return Iterables.tryFind(
+ pushTargets,
+ pt ->
+ UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
+ && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
+ }
+
+ private void broadcastPushMessage(
+ final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
+ final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
+ updateIntent.setPackage(target.application);
+ updateIntent.putExtra("token", target.instance);
+ updateIntent.putExtra("bytesMessage", payload);
+ updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
+ service.sendBroadcast(updateIntent);
+ }
+
+ private void broadcastEndpoint(
+ final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
+ Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
+ final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
+ updateIntent.setPackage(endpoint.application);
+ updateIntent.putExtra("token", instance);
+ updateIntent.putExtra("endpoint", endpoint.endpoint);
+ service.sendBroadcast(updateIntent);
+ }
+
+ public void rebroadcastEndpoint(final String instance, final Transport transport) {
+ final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
+ final UnifiedPushDatabase.ApplicationEndpoint endpoint =
+ unifiedPushDatabase.getEndpoint(
+ transport.account.getUuid(),
+ transport.transport.toEscapedString(),
+ instance);
+ if (endpoint != null) {
+ broadcastEndpoint(instance, endpoint);
+ }
+ }
+
+ public static class Transport {
+ public final Account account;
+ public final Jid transport;
+
+ public Transport(Account account, Jid transport) {
+ this.account = account;
+ this.transport = transport;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
new file mode 100644
index 000000000..64c16dbcd
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
@@ -0,0 +1,152 @@
+package eu.siacs.conversations.services;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.persistance.UnifiedPushDatabase;
+import eu.siacs.conversations.utils.Compatibility;
+
+public class UnifiedPushDistributor extends BroadcastReceiver {
+
+ public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER";
+ public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER";
+ public static final String ACTION_BYTE_MESSAGE =
+ "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
+ public static final String ACTION_REGISTRATION_FAILED =
+ "org.unifiedpush.android.connector.REGISTRATION_FAILED";
+ public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE";
+ public static final String ACTION_NEW_ENDPOINT =
+ "org.unifiedpush.android.connector.NEW_ENDPOINT";
+
+ public static final String PREFERENCE_ACCOUNT = "up_push_account";
+ public static final String PREFERENCE_PUSH_SERVER = "up_push_server";
+
+ public static final List PREFERENCES =
+ Arrays.asList(PREFERENCE_ACCOUNT, PREFERENCE_PUSH_SERVER);
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ final String action = intent.getAction();
+ final String application = intent.getStringExtra("application");
+ final String instance = intent.getStringExtra("token");
+ final List features = intent.getStringArrayListExtra("features");
+ switch (Strings.nullToEmpty(action)) {
+ case ACTION_REGISTER:
+ register(context, application, instance, features);
+ break;
+ case ACTION_UNREGISTER:
+ unregister(context, instance);
+ break;
+ case Intent.ACTION_PACKAGE_FULLY_REMOVED:
+ unregisterApplication(context, intent.getData());
+ break;
+ default:
+ Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
+ break;
+ }
+ }
+
+ private void register(
+ final Context context,
+ final String application,
+ final String instance,
+ final Collection features) {
+ if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
+ Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
+ return;
+ }
+ final List receivers = getBroadcastReceivers(context, application);
+ if (receivers.contains(application)) {
+ final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE);
+ Log.d(
+ Config.LOGTAG,
+ "received up registration from "
+ + application
+ + "/"
+ + instance
+ + " features: "
+ + features);
+ if (UnifiedPushDatabase.getInstance(context).register(application, instance)) {
+ Log.d(
+ Config.LOGTAG,
+ "successfully created UnifiedPush entry. waking up XmppConnectionService");
+ final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
+ serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
+ serviceIntent.putExtra("instance", instance);
+ Compatibility.startService(context, serviceIntent);
+ } else {
+ Log.d(Config.LOGTAG, "not successful. sending error message back to application");
+ final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
+ registrationFailed.setPackage(application);
+ registrationFailed.putExtra("token", instance);
+ context.sendBroadcast(registrationFailed);
+ }
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ "ignoring invalid UnifiedPush registration. Unknown application "
+ + application);
+ }
+ }
+
+ private List getBroadcastReceivers(final Context context, final String application) {
+ final Intent messageIntent = new Intent(ACTION_MESSAGE);
+ messageIntent.setPackage(application);
+ final List resolveInfo =
+ context.getPackageManager().queryBroadcastReceivers(messageIntent, 0);
+ return Lists.transform(
+ resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName);
+ }
+
+ private void unregister(final Context context, final String instance) {
+ if (Strings.isNullOrEmpty(instance)) {
+ Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration");
+ return;
+ }
+ final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
+ if (unifiedPushDatabase.deleteInstance(instance)) {
+ Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
+ }
+ }
+
+ private void unregisterApplication(final Context context, final Uri uri) {
+ if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) {
+ final String application = uri.getSchemeSpecificPart();
+ if (Strings.isNullOrEmpty(application)) {
+ return;
+ }
+ Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
+ final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
+ if (database.deleteApplication(application)) {
+ Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
+ }
+ }
+ }
+
+ public static String hash(String... components) {
+ return BaseEncoding.base64()
+ .encode(
+ Hashing.sha256()
+ .hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
+ .asBytes());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 5ed523a21..98ea59419 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -86,6 +86,10 @@ import org.conscrypt.Conscrypt;
import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
+import com.google.common.base.Optional;
+import java.text.ParseException;
+import eu.siacs.conversations.persistance.UnifiedPushDatabase;
+import eu.siacs.conversations.utils.AccountUtils;
import java.io.File;
import java.security.Security;
@@ -221,6 +225,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
+ public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
public static final String FDroid = "org.fdroid.fdroid";
public static final String PlayStore = "com.android.vending";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
@@ -257,6 +262,7 @@ public class XmppConnectionService extends Service {
public final FileBackend fileBackend = new FileBackend(this);
private MemorizingTrustManager mMemorizingTrustManager;
private final NotificationService mNotificationService = new NotificationService(this);
+ private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this);
private final ShortcutService mShortcutService = new ShortcutService(this);
private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
@@ -849,6 +855,13 @@ public class XmppConnectionService extends Service {
case ACTION_FCM_TOKEN_REFRESH:
refreshAllFcmTokens();
break;
+ case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
+ final String instance = intent.getStringExtra("instance");
+ final Optional transport = renewUnifiedPushEndpoints();
+ if (instance != null && transport.isPresent()) {
+ unifiedPushBroker.rebroadcastEndpoint(instance, transport.get());
+ }
+ break;
case ACTION_IDLE_PING:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
@@ -1033,6 +1046,9 @@ public class XmppConnectionService extends Service {
editor.putBoolean(ENABLE_MULTI_ACCOUNTS, true);
}
}
+ public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
+ return unifiedPushBroker.processPushMessage(account, transport, push);
+ }
public void reinitializeMuclumbusService() {
mChannelDiscoveryService.initializeMuclumbusService();
@@ -1450,6 +1466,7 @@ public class XmppConnectionService extends Service {
editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
editor.apply();
toggleSetProfilePictureActivity(hasEnabledAccounts);
+ reconfigurePushDistributor();
restoreFromDatabase();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
@@ -2754,9 +2771,16 @@ public class XmppConnectionService extends Service {
final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
} catch (IllegalStateException e) {
- Log.d(Config.LOGTAG, "unable to toggle profile picture actvitiy");
+ Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
}
}
+ public boolean reconfigurePushDistributor() {
+ return this.unifiedPushBroker.reconfigurePushDistributor();
+ }
+
+ public Optional renewUnifiedPushEndpoints() {
+ return this.unifiedPushBroker.renewUnifiedPushEndpoints();
+ }
private void provisionAccount(final String address, final String password) {
final Jid jid = Jid.ofEscaped(address);
@@ -5103,6 +5127,7 @@ public class XmppConnectionService extends Service {
}
}
+
private void sendOfflinePresence(final Account account) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index b569e1dcf..1c13c081c 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -3,6 +3,9 @@ package eu.siacs.conversations.ui;
import static eu.siacs.conversations.persistance.FileBackend.APP_DIRECTORY;
import static eu.siacs.conversations.utils.StorageHelper.getBackupDirectory;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.Intent;
@@ -49,6 +52,7 @@ import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xmpp.Jid;
import me.drakeet.support.toast.ToastCompat;
+import eu.siacs.conversations.services.UnifiedPushDistributor;
public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
@@ -130,7 +134,36 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
}
@Override
- void onBackendConnected() {}
+ void onBackendConnected() {
+ final Preference accountPreference =
+ mSettingsFragment.findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT);
+ reconfigureUpAccountPreference(accountPreference);
+ }
+
+ private void reconfigureUpAccountPreference(final Preference preference) {
+ final ListPreference listPreference;
+ if (preference instanceof ListPreference) {
+ listPreference = (ListPreference) preference;
+ } else {
+ return;
+ }
+ final List accounts =
+ ImmutableList.copyOf(
+ Lists.transform(
+ xmppConnectionService.getAccounts(),
+ a -> a.getJid().asBareJid().toEscapedString()));
+ final ImmutableList.Builder entries = new ImmutableList.Builder<>();
+ final ImmutableList.Builder entryValues = new ImmutableList.Builder<>();
+ entries.add(getString(R.string.no_account_deactivated));
+ entryValues.add("none");
+ entries.addAll(accounts);
+ entryValues.addAll(accounts);
+ listPreference.setEntries(entries.build().toArray(new CharSequence[0]));
+ listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0]));
+ if (!accounts.contains(listPreference.getValue())) {
+ listPreference.setValue("none");
+ }
+ }
@Override
public void onStart() {
@@ -651,6 +684,11 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
} else if (name.equals(USE_UNICOLORED_CHATBG)) {
xmppConnectionService.updateConversationUi();
}
+ else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
+ if (xmppConnectionService.reconfigurePushDistributor()) {
+ xmppConnectionService.renewUnifiedPushEndpoints();
+ }
+ }
}
@Override
diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java
index 55f45c6b5..b614251bd 100644
--- a/src/main/java/eu/siacs/conversations/xml/Namespace.java
+++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java
@@ -65,4 +65,5 @@ public final class Namespace {
public static final String PARS = "urn:xmpp:pars:0";
public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
+ public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push";
}
diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml
index 692e7d53f..74224975d 100644
--- a/src/main/res/values/defaults.xml
+++ b/src/main/res/values/defaults.xml
@@ -102,6 +102,8 @@
false
0
true
+ up.conversations.im
+ none