integrate UnifiedPush distributor

This commit is contained in:
Daniel Gultsch 2023-01-04 10:23:20 +01:00
parent 1000d927a7
commit b1f95d2e39
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
12 changed files with 831 additions and 2 deletions

View file

@ -67,6 +67,9 @@
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
</intent> </intent>
<intent>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
</intent>
</queries> </queries>
@ -102,6 +105,21 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".services.UnifiedPushDistributor"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
<action android:name="org.unifiedpush.android.distributor.UNREGISTER" />
<action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package"/>
</intent-filter>
</receiver>
<activity <activity
android:name=".ui.ShareLocationActivity" android:name=".ui.ShareLocationActivity"
android:label="@string/title_activity_share_location" /> android:label="@string/title_activity_share_location" />

View file

@ -4,6 +4,7 @@ package eu.siacs.conversations.parser;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -86,6 +87,37 @@ public abstract class AbstractParser {
return Math.min(dateFormat.parse(timestamp).getTime()+ms, System.currentTimeMillis()); return Math.min(dateFormat.parse(timestamp).getTime()+ms, System.currentTimeMillis());
} }
public static long getTimestamp(final String input) throws ParseException {
if (input == null) {
throw new IllegalArgumentException("timestamp should not be null");
}
final String timestamp = input.replace("Z", "+0000");
final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
final long milliseconds = getMilliseconds(timestamp);
final String formatted =
timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5);
final Date date = simpleDateFormat.parse(formatted);
if (date == null) {
throw new IllegalArgumentException("Date was null");
}
return date.getTime() + milliseconds;
}
private static long getMilliseconds(final String timestamp) {
if (timestamp.length() >= 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) { protected void updateLastseen(final Account account, final Jid from) {
final Contact contact = account.getRoster().getContact(from); final Contact contact = account.getRoster().getContact(from);
contact.setLastResource(from.isBareJid() ? "" : from.getResource()); contact.setLastResource(from.isBareJid() ? "" : from.getResource());

View file

@ -452,6 +452,24 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
} }
mXmppConnectionService.sendIqPacket(account, response, null); 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 { } else {
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);

View file

@ -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<String> 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<PushTarget> getRenewals(final String account, final String transport) {
final ImmutableList.Builder<PushTarget> 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<PushTarget> getPushTargets(final String account, final String transport) {
final ImmutableList.Builder<PushTarget> 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);
}
}
}

View file

@ -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<Transport> renewUnifiedPushEndpoints() {
final Optional<Transport> 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<UnifiedPushDatabase.PushTarget> 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<UnifiedPushDatabase.PushTarget> 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<Transport> 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<UnifiedPushDatabase.PushTarget> getPushTarget(
final Account account,
final Jid transport,
final String application,
final String instance) {
final String uuid = account.getUuid();
final List<UnifiedPushDatabase.PushTarget> 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;
}
}
}

View file

@ -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<String> 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<String> 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<String> features) {
if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
return;
}
final List<String> 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<String> getBroadcastReceivers(final Context context, final String application) {
final Intent messageIntent = new Intent(ACTION_MESSAGE);
messageIntent.setPackage(application);
final List<ResolveInfo> 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());
}
}

View file

@ -53,6 +53,7 @@ import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
@ -64,6 +65,7 @@ import java.io.File;
import java.security.Security; import java.security.Security;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -117,6 +119,7 @@ import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.parser.PresenceParser;
import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity; import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.ui.RtpSessionActivity;
import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.SettingsActivity;
@ -124,6 +127,7 @@ import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ConversationsFileObserver; import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
@ -185,6 +189,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_END_CALL = "end_call"; public static final String ACTION_END_CALL = "end_call";
public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; 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";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
@ -217,6 +222,7 @@ public class XmppConnectionService extends Service {
private final FileBackend fileBackend = new FileBackend(this); private final FileBackend fileBackend = new FileBackend(this);
private MemorizingTrustManager mMemorizingTrustManager; private MemorizingTrustManager mMemorizingTrustManager;
private final NotificationService mNotificationService = new NotificationService(this); private final NotificationService mNotificationService = new NotificationService(this);
private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this); private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this);
private final ShortcutService mShortcutService = new ShortcutService(this); private final ShortcutService mShortcutService = new ShortcutService(this);
private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
@ -804,6 +810,13 @@ public class XmppConnectionService extends Service {
case ACTION_FCM_TOKEN_REFRESH: case ACTION_FCM_TOKEN_REFRESH:
refreshAllFcmTokens(); refreshAllFcmTokens();
break; break;
case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
final String instance = intent.getStringExtra("instance");
final Optional<UnifiedPushBroker.Transport> transport = renewUnifiedPushEndpoints();
if (instance != null && transport.isPresent()) {
unifiedPushBroker.rebroadcastEndpoint(instance, transport.get());
}
break;
case ACTION_IDLE_PING: case ACTION_IDLE_PING:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing(); scheduleNextIdlePing();
@ -933,6 +946,10 @@ public class XmppConnectionService extends Service {
return pingNow; return pingNow;
} }
public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
return unifiedPushBroker.processPushMessage(account, transport, push);
}
public void reinitializeMuclumbusService() { public void reinitializeMuclumbusService() {
mChannelDiscoveryService.initializeMuclumbusService(); mChannelDiscoveryService.initializeMuclumbusService();
} }
@ -1167,6 +1184,7 @@ public class XmppConnectionService extends Service {
editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
editor.apply(); editor.apply();
toggleSetProfilePictureActivity(hasEnabledAccounts); toggleSetProfilePictureActivity(hasEnabledAccounts);
reconfigurePushDistributor();
restoreFromDatabase(); restoreFromDatabase();
@ -2334,10 +2352,18 @@ public class XmppConnectionService extends Service {
final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP); getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
} catch (IllegalStateException e) { } 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<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
return this.unifiedPushBroker.renewUnifiedPushEndpoints();
}
private void provisionAccount(final String address, final String password) { private void provisionAccount(final String address, final String password) {
final Jid jid = Jid.ofEscaped(address); final Jid jid = Jid.ofEscaped(address);
final Account account = new Account(jid, password); final Account account = new Account(jid, password);
@ -4499,6 +4525,8 @@ public class XmppConnectionService extends Service {
} }
} }
private void sendOfflinePresence(final Account account) { private void sendOfflinePresence(final Account account) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));

View file

@ -24,6 +24,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.File; import java.io.File;
import java.security.KeyStoreException; import java.security.KeyStoreException;
@ -40,6 +42,7 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.ExportBackupService; import eu.siacs.conversations.services.ExportBackupService;
import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.UnifiedPushDistributor;
import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.GeoHelper;
@ -88,7 +91,36 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
} }
@Override @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<CharSequence> accounts =
ImmutableList.copyOf(
Lists.transform(
xmppConnectionService.getAccounts(),
a -> a.getJid().asBareJid().toEscapedString()));
final ImmutableList.Builder<CharSequence> entries = new ImmutableList.Builder<>();
final ImmutableList.Builder<CharSequence> 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 @Override
public void onStart() { public void onStart() {
@ -472,6 +504,10 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
} }
} else if (name.equals(PREVENT_SCREENSHOTS)) { } else if (name.equals(PREVENT_SCREENSHOTS)) {
SettingsUtils.applyScreenshotPreventionSetting(this); SettingsUtils.applyScreenshotPreventionSetting(this);
} else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
if (xmppConnectionService.reconfigurePushDistributor()) {
xmppConnectionService.renewUnifiedPushEndpoints();
}
} }
} }

View file

@ -65,4 +65,5 @@ public final class Namespace {
public static final String PARS = "urn:xmpp:pars:0"; public static final String PARS = "urn:xmpp:pars:0";
public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; 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 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";
} }

View file

@ -45,4 +45,6 @@
<string name="video_compression">360</string> <string name="video_compression">360</string>
<string name="default_channel_discovery">JABBER_NETWORK</string> <string name="default_channel_discovery">JABBER_NETWORK</string>
<bool name="prevent_screenshots">false</bool> <bool name="prevent_screenshots">false</bool>
<string name="default_push_server">up.conversations.im</string>
<string name="default_push_account">none</string>
</resources> </resources>

View file

@ -999,5 +999,11 @@
<string name="audio_video_disabled_tor">Calls are disabled when using Tor</string> <string name="audio_video_disabled_tor">Calls are disabled when using Tor</string>
<string name="switch_to_video">Switch to video</string> <string name="switch_to_video">Switch to video</string>
<string name="reject_switch_to_video">Reject switch to video request</string> <string name="reject_switch_to_video">Reject switch to video request</string>
<string name="unified_push_distributor">UnifiedPush Distributor</string>
<string name="pref_up_push_account_title">XMPP Account</string>
<string name="pref_up_push_account_summary">The account through which push messages will be received.</string>
<string name="pref_up_push_server_title">Push Server</string>
<string name="pref_up_push_server_summary">A user-chosen push server to relay push messages via XMPP to your device.</string>
<string name="no_account_deactivated">None (deactivated)</string>
</resources> </resources>

View file

@ -206,6 +206,21 @@
android:summary="@string/pref_create_backup_summary" android:summary="@string/pref_create_backup_summary"
android:title="@string/pref_create_backup" /> android:title="@string/pref_create_backup" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="unified_push"
android:title="@string/unified_push_distributor">
<ListPreference
android:defaultValue="@string/default_push_account"
android:key="up_push_account"
android:summary="@string/pref_up_push_account_summary"
android:title="@string/pref_up_push_account_title" />
<EditTextPreference
android:defaultValue="@string/default_push_server"
android:key="up_push_server"
android:summary="@string/pref_up_push_server_summary"
android:title="@string/pref_up_push_server_title" />
</PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="advanced" android:key="advanced"
android:title="@string/pref_advanced_options"> android:title="@string/pref_advanced_options">