integrate UnifiedPush distributor
This commit is contained in:
parent
1000d927a7
commit
b1f95d2e39
|
@ -67,6 +67,9 @@
|
|||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
|
@ -102,6 +105,21 @@
|
|||
</intent-filter>
|
||||
</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
|
||||
android:name=".ui.ShareLocationActivity"
|
||||
android:label="@string/title_activity_share_location" />
|
||||
|
|
|
@ -4,6 +4,7 @@ package eu.siacs.conversations.parser;
|
|||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -86,6 +87,37 @@ public abstract class AbstractParser {
|
|||
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) {
|
||||
final Contact contact = account.getRoster().getContact(from);
|
||||
contact.setLastResource(from.isBareJid() ? "" : from.getResource());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@ import androidx.core.app.RemoteInput;
|
|||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
|
@ -64,6 +65,7 @@ import java.io.File;
|
|||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -117,6 +119,7 @@ import eu.siacs.conversations.parser.MessageParser;
|
|||
import eu.siacs.conversations.parser.PresenceParser;
|
||||
import eu.siacs.conversations.persistance.DatabaseBackend;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
|
||||
import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
|
||||
import eu.siacs.conversations.ui.RtpSessionActivity;
|
||||
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.OnMediaLoaded;
|
||||
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
|
||||
import eu.siacs.conversations.utils.AccountUtils;
|
||||
import eu.siacs.conversations.utils.Compatibility;
|
||||
import eu.siacs.conversations.utils.ConversationsFileObserver;
|
||||
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_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";
|
||||
|
||||
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 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);
|
||||
|
@ -804,6 +810,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<UnifiedPushBroker.Transport> 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();
|
||||
|
@ -933,6 +946,10 @@ public class XmppConnectionService extends Service {
|
|||
return pingNow;
|
||||
}
|
||||
|
||||
public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
|
||||
return unifiedPushBroker.processPushMessage(account, transport, push);
|
||||
}
|
||||
|
||||
public void reinitializeMuclumbusService() {
|
||||
mChannelDiscoveryService.initializeMuclumbusService();
|
||||
}
|
||||
|
@ -1167,6 +1184,7 @@ public class XmppConnectionService extends Service {
|
|||
editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
|
||||
editor.apply();
|
||||
toggleSetProfilePictureActivity(hasEnabledAccounts);
|
||||
reconfigurePushDistributor();
|
||||
|
||||
restoreFromDatabase();
|
||||
|
||||
|
@ -2334,10 +2352,18 @@ 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<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
|
||||
return this.unifiedPushBroker.renewUnifiedPushEndpoints();
|
||||
}
|
||||
|
||||
private void provisionAccount(final String address, final String password) {
|
||||
final Jid jid = Jid.ofEscaped(address);
|
||||
final Account account = new Account(jid, password);
|
||||
|
@ -4499,6 +4525,8 @@ 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));
|
||||
|
|
|
@ -24,6 +24,8 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.io.File;
|
||||
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.MemorizingTrustManager;
|
||||
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.StyledAttributes;
|
||||
import eu.siacs.conversations.utils.GeoHelper;
|
||||
|
@ -88,7 +91,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<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
|
||||
public void onStart() {
|
||||
|
@ -472,6 +504,10 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
|
|||
}
|
||||
} else if (name.equals(PREVENT_SCREENSHOTS)) {
|
||||
SettingsUtils.applyScreenshotPreventionSetting(this);
|
||||
} else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
|
||||
if (xmppConnectionService.reconfigurePushDistributor()) {
|
||||
xmppConnectionService.renewUnifiedPushEndpoints();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -45,4 +45,6 @@
|
|||
<string name="video_compression">360</string>
|
||||
<string name="default_channel_discovery">JABBER_NETWORK</string>
|
||||
<bool name="prevent_screenshots">false</bool>
|
||||
<string name="default_push_server">up.conversations.im</string>
|
||||
<string name="default_push_account">none</string>
|
||||
</resources>
|
||||
|
|
|
@ -999,5 +999,11 @@
|
|||
<string name="audio_video_disabled_tor">Calls are disabled when using Tor</string>
|
||||
<string name="switch_to_video">Switch to video</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>
|
||||
|
|
|
@ -206,6 +206,21 @@
|
|||
android:summary="@string/pref_create_backup_summary"
|
||||
android:title="@string/pref_create_backup" />
|
||||
</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
|
||||
android:key="advanced"
|
||||
android:title="@string/pref_advanced_options">
|
||||
|
|
Loading…
Reference in a new issue