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