diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f9829f840eea5020472eb3b6238f70fbd7a58a..b5ef2e389ab0557028afff675640675948865bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### Version 2.12.1 + +* Fix crash in UnifiedPush Distributor + +### Version 2.12.0 + +* Integrate UnifiedPush Distributor to facilitate push messages to other UnifiedPush enabled apps like Tusky and Fedilab + ### Version 2.11.3 * Fix messages getting resend when using SASL2 diff --git a/fastlane/metadata/android/de-DE/changelogs/42044.txt b/fastlane/metadata/android/de-DE/changelogs/42044.txt new file mode 100644 index 0000000000000000000000000000000000000000..27548eeab8fbc55978368e700b3451a762413f87 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Nachrichten werden bei Verwendung von SASL2 nicht mehr erneut gesendet +* Schwarzes Video zwischen einigen Geräten behoben +* Absturz bei leeren Passwörtern behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/42046.txt b/fastlane/metadata/android/de-DE/changelogs/42046.txt new file mode 100644 index 0000000000000000000000000000000000000000..ed6e4bb38d9cb93ade5226bd92ec1c309e8d1354 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42046.txt @@ -0,0 +1 @@ +* Integration eines UnifiedPush-Verteilers, um Push-Nachrichten für andere UnifiedPush-fähige Apps wie Tusky und Fedilab zu ermöglichen diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index 52910ff20bea60419be42556559fa32c07b6698f..5249bddf27ae25cd426374a4eeb656d5fe88a2c4 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Ein verschlüsselter, benutzerfreundlicher XMPP-Instant-Messaging-Client, der für Smartphones optimiert ist +Verschlüsselter, benutzerfreundlicher XMPP-Instant-Messenger für dein Smartphone diff --git a/fastlane/metadata/android/en-US/changelogs/42046.txt b/fastlane/metadata/android/en-US/changelogs/42046.txt new file mode 100644 index 0000000000000000000000000000000000000000..2ca538cfbeeb24c3b783909e19a9cb4f1af0b286 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42046.txt @@ -0,0 +1 @@ +* Integrate UnifiedPush Distributor to facilitate push messages to other UnifiedPush enabled apps like Tusky and Fedilab diff --git a/fastlane/metadata/android/en-US/changelogs/42047.txt b/fastlane/metadata/android/en-US/changelogs/42047.txt new file mode 100644 index 0000000000000000000000000000000000000000..c44467f526305cc8bda306bf3934d61675817367 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42047.txt @@ -0,0 +1 @@ +* Fix crash in UnifiedPush Distributor diff --git a/fastlane/metadata/android/pl-PL/changelogs/42044.txt b/fastlane/metadata/android/pl-PL/changelogs/42044.txt new file mode 100644 index 0000000000000000000000000000000000000000..9afce457432a1a8eadf074c936a1142311479810 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Naprawiono ponowne wysyłanie wiadomości podczas używania SASL2. +* Naprawiono czarny obraz wideo pomiędzy niektórymi urządzeniami. +* Naprawiono awarię przy użyciu pustych haseł. diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/fastlane/metadata/android/pl-PL/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..bb1ca39195b43c0bb87d4e9a88c72ee6f5e9c3b6 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/full_description.txt @@ -0,0 +1,39 @@ +Łatwy w użyciu, godny zaufania, przyjazny dla baterii. Wbudowane wsparcie dla obrazków, rozmów grupowych i szyfrowania od nadawcy do odbiorcy. + +Zasady projektu: + +* ma być tak ładny i prosty w użyciu jak to możliwe bez uszczerbku na bezpieczeństwie lub prywatności; +* używa istniejących, dobrze znanych protokołów; +* nie wymaga Konta Google ani, w szczególności, Google Cloud Messaging (GCM); +* wymaga tylko naprawdę koniecznych uprawnień. + +Funkcjonalność: + +* szyfrowanie od nadawcy do odbiorcy (E2EE) z użyciem OMEMO lub OpenPGP; +* wysyłanie i odbieranie obrazków; +* szyfrowane rozmowy głosowe i wideo; +* intuicyjny interfejs użytkownika, zgodny z wytycznymi Android Design; +* obrazki/awatary dla Twoich kontaktów; +* synchronizacja z klientem desktopowym; +* konferencje (z obsługą zakładek); +* integracja z książką adresową; +* wiele kont, zintegrowana skrzynka odbiorcza; +* bardzo ograniczony wpływ na zużycie baterii. + +Conversations bardzo ułatwia rejestrację konta na darmowym serwerze conversations.im, jednak będzie działać również z każdym innym serwerem XMPP. Wiele serwerów jest uruchamianych przez wolontariuszy i są dostępne za bez opłat. + +Funkcjonalność XMPP: + +Conversations działa z każdym dostępnym serwerem XMPP, jednak XMPP to rozszerzalny protokół. Rozszerzenia są ustandaryzowane w tak zwanych XEP. Conversations obsługuje sporo z nich, dzięki czemu można go przyjemniej używać. Jest jednak możliwość, że Twój obecny serwer nie obsługuje tych rozszerzeń. Aby wyciągnąć jak najwięcej z Conversations rozważ przeniesienie się na taki serwer, który je obsługuje, lub — jeszcze lepiej — uruchom własny serwer dla Ciebie i Twoich przyjaciół. + +Obecnie są obsługiwane następujące rozszerzenia: + +* XEP-0065: SOCKS5 Bytestreams (lub mod_proxy65). Będzie używany do przesyłania plików jeżeli obie strony znajdują się za zaporą (NAT); +* XEP-0163: Personal Eventing Protocol dla awatarów; +* XEP-0191: Blocking Command umożliwia ochronę przed spamerami lub blokowanie bez usuwanie ich z rostera; +* XEP-0198: Stream Management pozwala na przetrwanie krótkich braków połączenia z siecią oraz zmian używanego połączenia TCP; +* XEP-0280: Message Carbons automatycznie synchronizuje wysyłane wiadomości z klientem desktopowym i w ten sposób pozwala na proste używanie zarówno klienta mobilnego, jak i desktopowego, w jednej konwersacji; +* XEP-0237: Roster Versioning, dzięki któremu można ograniczyć używanie sieci na słabych połączeniach komórkowych; +* XEP-0313: Message Archive Management synchronizuje historię wiadomości z serwerem. Bądź na bieżąco z wiadomości wysłanymi gdy Conversations był rozłączony; +* XEP-0352: Client State Indication informuje serwer o tym, czy Conversations działa w tle. Pozwala to na oszczędzanie łącza przez wstrzymywanie mniej ważnych komunikatów; +* XEP-0363: HTTP File Upload umożliwia udostępnianie plików w konferencjach oraz rozłączonym kontaktom. Wymaga dodatkowego komponentu na Twoim serwerze. diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..7869c1ba5ac150dfb0dd6d7c2e32d3eb9dac4a0f --- /dev/null +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -0,0 +1 @@ +Szyfrowany, prosty w użyciu komunikator XMPP dla Twojego urządzenia mobilnego diff --git a/fastlane/metadata/android/ro/short_description.txt b/fastlane/metadata/android/ro/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..143f7cb553d45a0c8e6e071289da4fe779414e69 --- /dev/null +++ b/fastlane/metadata/android/ro/short_description.txt @@ -0,0 +1 @@ +Client de mesagerie XMPP ușor de folosit, criptat, și optimizat pentru mobile diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index ac746f8e6c875559a08be758cd39331fe7fbe4f3..f3771aed2fe0f81499a51111cf16007fc668eab6 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -2,7 +2,7 @@ Wybierz dostawcę XMPP Użyj conversations.im - Stwórz nowe konto + Utwórz nowe konto Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP. XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP. @@ -12,5 +12,5 @@ Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s. Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie. Dołącz do %1$s aby porozmawiać ze mną: %2$s - Udostępnij zaproszenie... + Udostępnij zaproszenie… \ No newline at end of file diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml index a8194508990ecf49fbc3fe203de2d5d039a92efb..20b99a7b306327037393c73f51860fbdbd9d18e4 100644 --- a/src/conversations/res/values-ru/strings.xml +++ b/src/conversations/res/values-ru/strings.xml @@ -3,10 +3,13 @@ Выберите своего XMPP-провайдера Использовать conversations.im Создать новый аккаунт - У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас.\nНекоторые провайдеры электронной почты также регистрируют аккаунты XMPP. + У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. +\nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. - Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. Аккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. - Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. Этот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. + Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. +\nАккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. + Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. +\nЭтот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. Ваше приглашение Неправильный формат кода Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s. diff --git a/src/conversations/res/values-sq/strings.xml b/src/conversations/res/values-sq/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/conversations/res/values-sq/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 4a1ff2dfdad4cd8d669f360d939fd7e1e0b776b1..9f64fd245e06e721f74ba986c05cc046daab0014 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -67,6 +67,9 @@ + + + @@ -102,6 +105,21 @@ + + + + + + + + + + + + diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index ce5a3c0e07f6985a43df7866de057f7d169cbaf9..fc2ceed670422b0724dacbeb439d59734dedb3b7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -223,14 +223,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.displayName = displayName; } - public XmppConnection.Identity getServerIdentity() { - if (xmppConnection == null) { - return XmppConnection.Identity.UNKNOWN; - } else { - return xmppConnection.getServerIdentity(); - } - } - public Contact getSelfContact() { return getRoster().getContact(jid); } diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index f4b01b7d3199cf821732226b79cc1c998296ee89..5de637399fb571bfaff80675cee50a2275cc8d60 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -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()); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 4c16afb3a1e7b87a45692b993e55068069f83cda..1eb00d0e0b1fbf9b86d9881c0511ddf1df9c1d39 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -453,6 +453,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.getFrom() != null) { final Contact contact = account.getRoster().getContact(packet.getFrom()); final Conversation conversation = mXmppConnectionService.find(account, packet.getFrom()); 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 0000000000000000000000000000000000000000..f36506bd1db43a27fff4ad2264f226cd92e35d66 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java @@ -0,0 +1,262 @@ +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; +import eu.siacs.conversations.services.UnifiedPushBroker; + +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 long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW; + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"application", "instance"}, + "account <> ? OR transport <> ? OR expiration < " + expiration, + 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 long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW; + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.query( + "push", + new String[] {"application", "endpoint"}, + "account = ? AND transport = ? AND instance = ? AND endpoint IS NOT NULL AND expiration >= " + + expiration, + 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; + } + + public boolean hasEndpoints(final UnifiedPushBroker.Transport transport) { + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + try (final Cursor cursor = + sqLiteDatabase.rawQuery( + "SELECT EXISTS(SELECT endpoint FROM push WHERE account = ? AND transport = ?)", + new String[] { + transport.account.getUuid(), transport.transport.toEscapedString() + })) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0) > 0; + } + } + return false; + } + + @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 0000000000000000000000000000000000000000..7d2d90dd5777b4aebc028f2366fe90b77cbf0f60 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -0,0 +1,333 @@ +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 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; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class UnifiedPushBroker { + + // time to expiration before a renewal attempt is made (24 hours) + public static final long TIME_TO_RENEW = 86_400_000L; + + // interval for the 'cron tob' that attempts renewals for everything that expires is lass than + // `TIME_TO_RENEW` + public static final long RENEWAL_INTERVAL = 3_600_000L; + + private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1); + + private final XmppConnectionService service; + + public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) { + this.service = xmppConnectionService; + SCHEDULER.scheduleAtFixedRate( + this::renewUnifiedPushEndpoints, + RENEWAL_INTERVAL, + RENEWAL_INTERVAL, + TimeUnit.MILLISECONDS); + } + + public void renewUnifiedPushEndpointsOnBind(final Account account) { + final Optional transportOptional = getTransport(); + if (transportOptional.isPresent()) { + final Transport transport = transportOptional.get(); + final Account transportAccount = transport.account; + if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) { + final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service); + if (database.hasEndpoints(transport)) { + sendDirectedPresence(transportAccount, transport.transport); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": trigger endpoint renewal on bind"); + renewUnifiedEndpoint(transportOptional.get()); + } + } + } + + private void sendDirectedPresence(final Account account, Jid to) { + final PresencePacket presence = new PresencePacket(); + presence.setTo(to); + service.sendPresencePacket(account, presence); + } + + public Optional renewUnifiedPushEndpoints() { + final Optional transportOptional = getTransport(); + if (transportOptional.isPresent()) { + final Transport transport = transportOptional.get(); + if (transport.account.isEnabled()) { + renewUnifiedEndpoint(transportOptional.get()); + } else { + Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled"); + } + } 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()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + renewals.size() + + " UnifiedPush endpoints scheduled for renewal on " + + transport.transport); + 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); + } else { + Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition()); + } + } + + 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) { + if (transport == null || application == null || instance == null) { + return Optional.absent(); + } + 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 0000000000000000000000000000000000000000..64c16dbcdf7ff804741282d8e4c66f94907ba2a6 --- /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 57f47ae58c57148667a268b6a349427a202b482d..9598c04eb58e954faa1b5b983f99c51e0cde1fce 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -55,6 +55,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; @@ -63,10 +64,10 @@ import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import java.io.File; -import java.security.SecureRandom; 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; @@ -123,6 +124,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; @@ -130,6 +132,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; @@ -161,7 +164,6 @@ import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.Patches; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; @@ -194,6 +196,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"; @@ -226,6 +229,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); @@ -390,6 +394,7 @@ public class XmppConnectionService extends Service { connectMultiModeConversations(account); syncDirtyContacts(account); + unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); } }; private final AtomicLong mLastExpiryRun = new AtomicLong(0); @@ -831,6 +836,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(); @@ -843,7 +855,7 @@ public class XmppConnectionService extends Service { case Intent.ACTION_SEND: Uri uri = intent.getData(); if (uri != null) { - Log.d(Config.LOGTAG, "received uri permission for " + uri.toString()); + Log.d(Config.LOGTAG, "received uri permission for " + uri); } return START_STICKY; } @@ -960,6 +972,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(); } @@ -1206,6 +1222,7 @@ public class XmppConnectionService extends Service { editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); editor.apply(); toggleSetProfilePictureActivity(hasEnabledAccounts); + reconfigurePushDistributor(); restoreFromDatabase(); @@ -1558,9 +1575,7 @@ public class XmppConnectionService extends Service { } MessagePacket packet = null; - final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI - || !Patches.BAD_MUC_REFLECTION.contains(account.getServerIdentity())) - && !message.edited(); + final boolean addToConversation = !message.edited(); boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); @@ -2387,10 +2402,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 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); @@ -3716,7 +3739,7 @@ public class XmppConnectionService extends Service { } }); } else { - Log.d(Config.LOGTAG, "failed to request vcard " + response.toString()); + Log.d(Config.LOGTAG, "failed to request vcard " + response); callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support); } }); @@ -4573,6 +4596,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)); @@ -4750,7 +4775,7 @@ public class XmppConnectionService extends Service { mAvatarService.clear(account); sendIqPacket(account, request, (account1, packet) -> { if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet.toString()); + Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet); } }); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 67bda3d67920cec2e6093bce042e0dab3742b090..998693f8521cabfc380eff72c95405fffbee7236 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -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; @@ -41,6 +43,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; @@ -108,6 +111,34 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference Preference pref = mSettingsFragment.findPreference("dialler_integration_incoming"); if (cat != null && pref != null) cat.removePreference(pref); } + 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 @@ -493,6 +524,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(); + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index 8f04532189c656830454aadc8f75898400365285..b8f4855d0eec8856a71c2b1f91797e665861ec4b 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java @@ -8,6 +8,7 @@ import android.widget.Toast; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -34,6 +35,23 @@ public class AccountUtils { return false; } + public static String publicDeviceId(final Account account) { + final UUID uuid; + try { + uuid = UUID.fromString(account.getUuid()); + } catch (final IllegalArgumentException e) { + return account.getUuid(); + } + final UUID publicDeviceId = getUuid(uuid.getLeastSignificantBits(), uuid.getLeastSignificantBits()); + return publicDeviceId.toString(); + } + + protected static UUID getUuid(final long msb, final long lsb) { + final long msb0 = (msb & 0xffffffffffff0fffL) | 4; // set version + final long lsb0 = (lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // set variant + return new UUID(msb0, lsb0); + } + public static List getEnabledAccounts(final XmppConnectionService service) { ArrayList accounts = new ArrayList<>(); for (Account account : service.getAccounts()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 3351c96a7aae19ccd2b7b6cf192a8d7841e9a637..4570033e496cd2f6857293a476d69b44326d55fa 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/java/eu/siacs/conversations/xmpp/Patches.java b/src/main/java/eu/siacs/conversations/xmpp/Patches.java deleted file mode 100644 index a5b35e811c26a71a53a208aa58e22e3dfb596ec5..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/Patches.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.xmpp; - - -import java.util.Arrays; -import java.util.List; - -public class Patches { - public static final List DISCO_EXCEPTIONS = Arrays.asList( - "nimbuzz.com" - ); - public static final List BAD_MUC_REFLECTION = Arrays.asList( - XmppConnection.Identity.SLACK - ); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 461aec80b033bc3a000e1593ce6e3b2c416cfc52..a9db21ba7c12399d0602f57870729a485738cd9f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -77,6 +77,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PhoneHelper; @@ -1534,7 +1535,7 @@ public class XmppConnection implements Runnable { authenticate.addChild("initial-response").setContent(firstMessage); } final Element userAgent = authenticate.addChild("user-agent"); - userAgent.setAttribute("id", account.getUuid()); + userAgent.setAttribute("id", AccountUtils.publicDeviceId(account)); userAgent .addChild("software") .setContent(mXmppConnectionService.getString(R.string.app_name)); @@ -1907,16 +1908,7 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); - if (!waitForDisco - || Patches.DISCO_EXCEPTIONS.contains( - account.getJid().getDomain().toEscapedString())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": do not wait for service discovery"); - mWaitForDisco.set(false); - } else { - mWaitForDisco.set(true); - } + mWaitForDisco.set(waitForDisco); lastDiscoStarted = SystemClock.elapsedRealtime(); mXmppConnectionService.scheduleWakeUpCall( Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); @@ -2545,43 +2537,10 @@ public class XmppConnection implements Runnable { this.mInteractive = interactive; } - public Identity getServerIdentity() { - synchronized (this.disco) { - ServiceDiscoveryResult result = disco.get(account.getJid().getDomain()); - if (result == null) { - return Identity.UNKNOWN; - } - for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { - if (id.getType().equals("im") - && id.getCategory().equals("server") - && id.getName() != null) { - switch (id.getName()) { - case "Prosody": - return Identity.PROSODY; - case "ejabberd": - return Identity.EJABBERD; - case "Slack-XMPP": - return Identity.SLACK; - } - } - } - } - return Identity.UNKNOWN; - } - private IqGenerator getIqGenerator() { return mXmppConnectionService.getIqGenerator(); } - public enum Identity { - FACEBOOK, - SLACK, - EJABBERD, - PROSODY, - NIMBUZZ, - UNKNOWN - } - private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index ea1fbae833e02bf1b8e674f37118ed8d8071aff3..a0a783617c13a780786945e73f709be1c75ff271 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -545,7 +545,8 @@ Teile URI mit…
Du registrierst dich mit deiner Telefonnummer und Quicksy wird automatisch auf der Grundlage der Telefonnummern in deinem Adressbuch mögliche Kontakte vorschlagen.

Mit der Anmeldung erklärst du dich mit unserer Datenschutzerklärung einverstanden.]]>
Zustimmen und fortfahren - Ein Guide hilft bei der Kontoerstellung auf conversations.im.¹\nWenn du conversations.im als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. + Ein Guide hilft bei der Kontoerstellung auf conversations.im. +\nWenn du conversations.im als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. Deine vollständige XMPP-Adresse lautet: %s Konto erstellen Nutze eigenen Provider @@ -612,7 +613,7 @@ Nutze die Kamera, um Barcodes deiner Kontakte zu scannen Bitte warten, bis die Schlüssel abgerufen werden Als Barcode teilen - Als XMPP URI teilen + Als XMPP-URI teilen Als HTTP Link teilen Blind vertrauen vor der Überprüfung Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen. @@ -998,4 +999,10 @@ Anrufe sind bei der Verwendung von Tor deaktiviert Umschalten auf Video Umschalten auf Video ablehnen + XMPP-Konto + Push-Server + Ein selbst gewählter Push-Server, der Push-Nachrichten über XMPP an dein Gerät weiterleitet. + Kein (deaktiviert) + UnifiedPush Verteiler + Das Konto, über das Push-Nachrichten empfangen werden sollen. \ No newline at end of file diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index b0398ec9dc888f6abfb3221dfa5ab482006614ac..bd23b79a2ec89e96993d5e9c57e2b74e65239d76 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -548,7 +548,7 @@ Compartir URI con… Quicksy es un derivado del popular cliente XMPP Conversations con detección automática de contactos.<br><br>El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.<br><br>Registrándote en Quicksy aceptas nuestra <a href=https://quicksy.im/#privacy>política de privacidad</a>. Aceptar y continuar - Una guía te ayudará en el proceso de creación de la cuenta en conversations.im.¹ + Una guía te ayudará en el proceso de creación de la cuenta en conversations.im. \nCuando selecciones conversations.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. Tu dirección XMPP completa será: %s Crear cuenta @@ -1013,4 +1013,10 @@ Las llamadas están deshabilitadas cuando se usa Tor Cambiar a vídeo Rechazar petición de cambiar a vídeo + Distribuidor de UnifiedPush + Cuenta XMPP + La cuenta a través de la cual se recibirán los mensajes push. + Servidor push + Un servidor push elegido por el usuario para transmitir mensajes push a través de XMPP a su dispositivo. + Ninguno (desactivado) \ No newline at end of file diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 77df6a8e0558c2c67a3ba20ee070db040b344bd5..0e20c589499f9184e35699153d6cb0f3394ede26 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -547,7 +547,8 @@ Compartir URI con…
Podes rexistrarte co teu número de teléfono e Quicksy suxerirache automáticamente —tomando os números da túa libreta de enderezos como referencia— posibles contactos para ti.

Ao rexistrarte aceptas a nosa política de privacidade.]]>
Aceptar e continuar - Tes unha guía para crear unha conta en conversations.im¹\nAo escoller conversations.im como provedor poderás comunicarte con outras usuarias de outros provedores con só darlles o teu enderezo XMPP completo. + Tes unha guía para crear unha conta en conversations.im +\nAo escoller conversations.im como provedor poderás comunicarte con outras usuarias de outros provedores con só darlles o teu enderezo XMPP completo. O teu enderezo XMPP completo será: %s Crear conta Utilizar o meu propio proveedor @@ -1001,4 +1002,10 @@ As chamadas están desactivadas cando usas Tor Cambiar a vídeo Rexeitar a solicitude para cambiar a vídeo + Distribuidor UnifiedPush + Conta XMPP + A conta a través da cal se recibirán as mensaxes push. + Servidor Push + O servidor elexido pola usuaria para obter as mensaxes push a través de XMPP. + Ningún (desactivado) \ No newline at end of file diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index f99c7884b79c0863e79d8e5120f3be7e6635fd2c..ff862a3833e4ff0593bd7f24311f7f3700b616c0 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -31,7 +31,6 @@ %d分前 %d件の未読の会話 - 送信中… メッセージを復号しています。しばらくお待ちください… @@ -680,7 +679,7 @@ 未知の証明書を受け入れますか? サーバー証明書が既知の認証局によって署名されていません。 不一致のサーバー名を受け入れますか? - サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です: + サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です: それでも接続を希望しますか? 証明書の詳細: 一度だけ @@ -900,7 +899,7 @@ 通話受入 通話終了 応答 - 解散 + 拒否 デバイス発見 鳴動 取込中 @@ -976,4 +975,4 @@ アバターを削除 Tor使用中のため通話できません ビデオ通話切替 - + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 8caf9deed2e93f9a8a97ea1996b14178af94f44e..d3769ebafd845f5a8b15bc3da3aacd437c7d9e79 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -31,19 +31,12 @@ %d minut temu %d nieprzeczytana konwersacja - - %d nieprzeczytane konwersacje - - %d nieprzeczytanych konwersacji - - %d nieprzeczytanych konwersacji - - wysyłanie... - Odszyfrowywanie wiadomości. To zajmie tylko chwilę... + wysyłanie… + Odszyfrowywanie wiadomości. To zajmie tylko chwilę… Wiadomość zaszyfrowana OpenPGP Nazwa jest już w użyciu NIeprawidłowy pseudonim @@ -62,7 +55,7 @@ Czy chcesz usunąć zakładkę %s? Rozmowy z tą zakładką nie zostaną usunięte. Zarejestruj nowe konto na serwerze Zmień hasło na serwerze - Udostępnij... + Udostępnij… Rozpocznij rozmowę Zaproś kontakt Zaproś @@ -90,12 +83,14 @@ wysyłanie nie powiodło się Przygotowanie do wysłania obrazka Przygotowanie do wysłania obrazków - Udostępnianie plików. Proszę czekać... + Udostępnianie plików. Proszę czekać… Wyczyść historię Wyczyść historię konwersacji Czy chcesz usunąć wszystkie wiadomości w tej rozmowie?\n\nOstrzeżenie: To nie ma wpływu na wiadomości składowane na innych urządzeniach lub serwerach. Usuń plik - Czy na pewno usunąć ten plik?\n\nUwaga: Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach. + Czy na pewno usunąć ten plik\? +\n +\nUwaga: Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach. Zamknij konwersację po zakończeniu Wybierz urządzenie Wyślij wiadomość bez szyfrowania @@ -112,8 +107,8 @@ Zrestartuj Zainstaluj Proszę zainstalować OpenKeychain - oferowanie... - oczekiwanie... + oferowanie… + oczekiwanie… Nie znaleziono klucza OpenPGP Nie można zaszyfrować twojej wiadomości bo ten kontakt nie ogłasza swojego publicznego klucza.\n\nPoproś kontakt aby ustawił OpenPGP. Nie znaleziono kluczy OpenPGP @@ -156,7 +151,7 @@ Wybrany plik nie jest obrazem Błąd konwersji obrazu Nie odnaleziono pliku - Ogólny błąd wejścia/wyjścia + Ogólny błąd wejścia/wyjścia. Być może skończyło się miejsce w pamięci\? Aplikacja użyta do wyboru obrazu nie zezwoliła na odczyt pliku.\n\nWybierz obraz przy użyciu innego menedżera plików Aplikacja której użyłeś do udostępnienia pliku nie dostarczyła odpowiednich uprawnień. Nieznany @@ -204,10 +199,10 @@ Informacje o serwerze XEP-0313: MAM XEP-0280: Kopie wiadomości - XEP-0352: Client State Indication - XEP-0191: Blocking Command + XEP-0352: Wskaźnik stanu klienta + XEP-0191: Polecenia Blokujące XEP-0237: Roster Versioning - XEP-0198: Stream Management + XEP-0198: Zarządzanie Strumieniem XEP-0215: Wykrywanie Zewnętrznych Usług XEP-0163: PEP (Awatary / OMEMO) XEP-0363: Przesyłanie plików przez HTTP @@ -409,7 +404,7 @@ Spraw aby adres XMPP był widoczny dla wszystkich Włącz moderację na kanale Nie bierzesz udziału - Ustawienia konferencji zostały zmodyfikowane + Ustawienia konferencji zostały zmodyfikowane! Nie można zmodyfikować ustawień konferencji Nigdy Ręcznie @@ -468,7 +463,7 @@ Przeszukuj kontakty Przeszukaj zakładki Wyślij wiadomość prywatną - %1$s opuścił konferencję! + %1$s opuścił konferencję Nazwa użytkownika Nazwa użytkownika Błędna nazwa użytkownika @@ -503,8 +498,8 @@ Adres XMPP nie pasuje do certyfikatu Odnów certyfikat Błąd pobierania klucza OMEMO! - Zweryfikowano klucz OMEMO z certyfikatem - Twoje urządzenie nie wspiera wyboru certyfikatów klienckich + Zweryfikowano klucz OMEMO z certyfikatem! + Twoje urządzenie nie wspiera wyboru certyfikatów klienckich! Połączenie Połącz przez sieć TOR Tuneluj wszystkie połączenia przez sieć TOR. Wymaga zainstalowania aplikacji \"Orbot\" @@ -554,7 +549,8 @@ Udostępnij URI za pomocą...
Zapisujesz się przy użyciu numeru telefonu i Quicksy automatycznie - na podstawie numerów telefonów w książce adresowej - zasugeruje potencjalne kontakty dla ciebie.

Zapisując się zgadzasz się na naszą politykę prywatności.]]>
Zgoda i kontynuuj - Poprowadzimy ciebie przez proces tworzenia konta na conversations.im.¹\nKiedy wybierzesz conversations.im jako dostawcę będziesz mógł komunikować się z innymi osobami jeśli podasz im swój pełen adres XMPP. + Poprowadzimy cię przez proces tworzenia konta na conversations.im. +\nKiedy wybierzesz conversations.im jako dostawcę będziesz mógł komunikować się z innymi osobami jeśli podasz im swój pełen adres XMPP. Twój pełen adres XMPP to: %s Utwórz konto Użyj innego serwera @@ -812,7 +808,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wybierz kraj numer telefonu Zweryfikuj swój numer telefonu - Quicksy wyśle SMS (operator może naliczyć koszty) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu. + Quicksy wyśle wiadomość SMS (operator może naliczyć opłatę) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu:
%s

Czy wszystko się zgadza czy też chciałbyś zmienić numer?]]>
%s nie jest prawidłowym numerem telefonu Proszę wpisać swój numer telefonu. @@ -1030,5 +1026,10 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dzwonienie jest wyłączone podczas używania Tora Przełącz na wideo Odrzuć prośbę przełączenia na wideo - - + Dystrybutor UnifiedPush + Konto XMPP + Konto, poprzez które będą odbierane powiadomienia push. + Serwer push + Dowolnie wybrany serwer push do przekazywania wiadomości push przez XMPP na Twoje urządzenie. + Brak (nieaktywne) + \ No newline at end of file diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 847143911c46f88c567ba29add30cd328f3b0154..f41b8b9c217d516ff1a50fea6501cfad32a39475 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -31,16 +31,11 @@ acum %d minute %d conversație necitită - - %d conversații necitite - - %d de conversații necitite - - trimitere... - Decriptez mesaj. Te rog așteaptă... + trimitere… + Decriptare mesaj. Vă rugăm să așteptați… Mesaj criptat cu OpenPGP Numele de utilizator este deja alocat Nume invalid @@ -59,7 +54,7 @@ Ați dori să ștergeți %s din semne de carte? Conversațiile asociate cu acest semn de carte nu vor fi șterse. Înregistrează un cont nou pe server Schimbă parola pe server - Partajează cu... + Partajează cu… Pornește o conversație Invită contact Invită @@ -87,12 +82,14 @@ trimitere eșuată Se pregătește trimiterea imaginii Se pregătește trimiterea imaginilor - Trimitere fișiere. Te rog asteaptă... + Trimitere fișiere. Vă rugăm să așteptați… Șterge istoric Șterge istoricul conversației Doriți să ștergeți toate mesajele din această conversație?\n\nAtenție: Această acțiune nu va afecta mesajele aflate pe alte dispozitive sau servere. Șterge fișierul - Sigur doriți să ștergeți acest fișier?\n\nAtenție: Această acțiune nu va șterge copiile acestui fișier care sunt stocate pe alte dispozitive sau servere. + Sigur doriți să ștergeți acest fișier\? +\n +\nAtenție: Această acțiune nu va șterge copiile acestui fișier care sunt stocate pe alte dispozitive sau servere. Închide conversația după ștergere Alege dispozitiv Trimite mesaje necriptate @@ -105,19 +102,19 @@ Trimite necriptat Decriptarea a eșuat. Poate nu aveți cheia privată corectă. OpenKeychain - OpenKeychain pentru a cripta și decripta mesaje și a administra cheile publice.\n\nOpenKeychain este licențiat sub GPLv3+ și este disponibil în F-Droid și Google Play.\n\n(Vă rugăm să reporniți %1$s după instalare.)]]> + %1$s utilizează <b>OpenKeychain</b> pentru a cripta și decripta mesaje și a administra cheile publice.<br><br>OpenKeychain este licențiat sub GPLv3+ și este disponibil în F-Droid și Google Play.<br><br><small>(Vă rugăm să reporniți %1$s după instalare.)</small> Repornește Instalare Va rugăm să instalați OpenKeychain - transmit... - în așteptare... + transmit… + în așteptare… Nu am găsit cheia OpenPGP Nu s-au putut cripta mesajele deoarece contactul nu își anunță cheile publice.\n\nRugați-vă contactul să își configureze OpenPGP. Nu am găsit chei OpenPGP Nu s-au putut cripta mesajele deoarece contactele nu își anunță cheile publice.\n\nRugați-vă contactele să își configureze OpenPGP. General Acceptă fișiere - Mai mici decât... + Mai mici decât… Atașamente Notificare Vibrează @@ -154,7 +151,9 @@ Nu s-a putut face convertirea imaginii Fișierul nu a fost găsit Eroare I/O generala. Poate ați rămas fără spațiu liber? - Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul.\n\nFolosiți un alt manager de fișiere pentru a alege o imagine + Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul. +\n +\nFolosiți un alt manager de fișiere pentru a alege o imagine. Aplicația pe care ați folosit-o pentru a partaja acest fișier nu a furnizat suficiente permisiuni. Necunoscut Dezactivat temporar @@ -196,7 +195,7 @@ numeutilizator@exemplu.ro Parolă Aceasta nu este o adresă XMPP valabilă - Memorie epuizată. Imaginea este prea mare. + Memorie epuizată. Imaginea este prea mare Vreți să adăugați pe %s în lista de contacte? Informații server XEP-0313: MAM @@ -228,7 +227,7 @@ v\\Amprenta OMEMO (originea mesajului) Alte dispozitive Amprente OMEMO de încredere - Se preiau cheile... + Se preiau cheile… Gata Decriptează Semne de carte @@ -254,7 +253,7 @@ Nu s-a putut distruge canalul Editează subiectul discuției de grup Subiect discuție - Vă alăturați discuției de grup... + Vă alăturați discuției de grup… Paraseste Contactul v-a adăugat în lista de contacte Adaugă contact @@ -264,7 +263,7 @@ Toate persoanele au citit până aici Publică Atingeți avatarul pentru a selecta o poză din galerie - Se publică... + Se publică… Acest server v-a refuzat publicarea Nu s-a putut face convertirea pozei Nu s-a putut salva avatarul pe disc @@ -282,7 +281,9 @@ Activează Discuția de grup necesită o parolă Introduceți parola - Vă rugăm să cereți mai întâi actualizări de prezență de la acest contact.\n\nAcestea vor fi folosite pentru a determina ce aplicații folosește contactul dumneavoastră. + Vă rugăm să cereți mai întâi actualizări de prezență de la acest contact. +\n +\nAcestea vor fi folosite pentru a determina ce aplicații folosește contactul dumneavoastră.. Cere acum Ignora Atenție: Trimițând aceasta fără actualizări de prezență reciproce ar putea produce probleme neprevazute.\n\nMergeți la \"Detalii contact\" pentru a verifica abonările la actualizările de prezență. @@ -371,8 +372,8 @@ A apărut o problemă Descarc istoric de pe server Nu mai exista istoric pe server - Actualizare... - Parolă schimbată + Actualizare… + Parolă schimbată! Nu s-a putut schimba parola Schimbare parolă Parola curentă @@ -430,9 +431,9 @@ Trimit %s Ofer %s Ascunde deconectat - %s tastează... + %s tastează… %s s-a oprit din scris - %s tastează... + %s tastează… %s s-au oprit din scris Notificare tastare Contactele sunt anunțate atunci când le scrieți un nou mesaj @@ -470,7 +471,7 @@ Acesta nu este un nume de utilizator valabil Descărcare eșuată: Serverul nu a fost găsit Descărcare eșuată: Fișierul nu a fost găsit - Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda. + Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda Descărcare eșuată: Nu s-a putut scrie fișierul Descărcarea a eșuat: Fișier invalid Rețeaua Tor nu este disponibilă @@ -491,7 +492,7 @@ Nu s-a putut analiza certificatul Preferințe arhivare Preferințe arhivare pe server - Se descarcă preferințe arhivare. Vă rugăm să așteptați... + Se descarcă preferințe arhivare. Vă rugăm să așteptați… Nu s-au putut descărca preferințele de arhivare Text captcha de verificare necesar Introduceți textul din imaginea de mai sus @@ -499,7 +500,7 @@ Adresa XMPP nu corespunde cu certificatul Înnoiește certificatul Eroare la preluarea cheii OMEMO! - Verifica cheia OMEMO cu un certificat + Cheia OMEMO s-a verificat cu un certificat! Dispozitivul nu permite selectia unui certificat pentru client! Opțiuni conexiune Conectare prin Tor @@ -523,7 +524,10 @@ Permiteți %1$s acces la stocarea externă Permiteți %1$s acces la camera foto Sincronizează cu contactele - %1$s dorește permisiunea de a vă accesa contactele pentru a putea potrivi lista de contacte XMPP cu cea din dispozitiv și a afișa numele lor complete și avatarele.\n\n%1$s va citi și potrivi local fără a fi încărcate pe serverul dumneavoastră. + %1$s dorește permisiunea de a vă accesa contactele pentru a putea potrivi lista de contacte XMPP cu cea din dispozitiv. +\nAșa v-a afișa numele lor complete și avatarele. +\n +\n%1$s va citi și potrivi local fără a fi încărcate pe serverul dumneavoastră.
Nu vom stoca o copie a acestor numere the telefon.\n\nPentru mai multe informații puteți citii politica noastră de confidențialitate.

Urmează să fiți întrebați dacă doriți să permiteți accesul la contacte.]]>
Notifică la toate mesajele Notifică doar atunci când cineva vă menționează numele @@ -534,8 +538,11 @@ Mereu Doar imaginile mari Optimizare baterie activată - Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor.\nEste recomandat sa le dezactivați. - Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să le dezactivați. + Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor. +\nEste recomandat să dezactivați optimizarea. + Dispozitivul dumneavoastră încearcă să optimizeze agresiv consumul bateriei pentru %1$s, aceasta poate duce la notificări întârziate sau chiar pierderea mesajelor. +\n +\nÎn continuare veți fi rugați să dezactivați optimizarea. Dezactivează Zona selectată este prea mare (Nici un cont activat) @@ -546,10 +553,11 @@ Ați dezactivat acest cont Eroare de securitate.: Acces fișier invalid! Nu s-a găsit nici o aplicație care să partajeze adresa - Partajează adresa cu... + Partajează adresa cu…
Vă înscrieți cu numărul de telefon și Quicksy—pe baza numerelor de telefon din agenda dumneavoastră—vă va sugera automat posibile contacte.

Înscriindu-vă sunteți de acord cu politica noastră de confidențialitate.]]>
Sunt de acord și continuă - Ghidul va configura un cont pe conversations.im.¹\nCând alegeți conversations.im ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. + Ghidul va configura un cont pe conversations.im. +\nCând alegeți conversations.im ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. Adresa dumneavoastră XMPP completă va fi: %s Creează cont Folosește furnizorul meu @@ -567,7 +575,7 @@ Înregistrare eșuată: Încercați din nou mai târziu Înregistrare eșuată: Parolă nesigură Alegeți participanți - Se creează discuția de grup... + Se creează discuția de grup… Trimite din nou invitația Dezactivată Scurtă @@ -582,7 +590,7 @@ Luminoasă Întunecată Nu s-a putut face conectarea la OpenKeychain - Acest dispozitiv nu mai este in uz + Acest dispozitiv nu mai este în uz PC Telefon mobil Tabletă @@ -614,7 +622,7 @@ Codul de bare nu conține amprente pentru această conversație. Amprente verificate Folosește camera pentru a scana codul de bare al contactului - Asteptati cat se preiau cheile + Vă rugăm să așteptați până se preiau cheile Partajează un cod de bare Partajează ca adresă XMPP Partajează ca legatură HTTP @@ -745,7 +753,7 @@ Arată locația Partajare Nu s-a putut pornii înregistrarea - Vă rugăm să așteptați... + Vă rugăm să așteptați… Permiteți %1$s acces la microfon Caută mesaje GIF @@ -800,7 +808,7 @@ Alegeți o țară număr de telefon Verificare număr de telefon - Quicksy va trimite un mesaj SMS (pot exista costuri în funcție de furnizor) pentru a vă verifica numărul de telefon. Introduceți codul țării dumneavoastră si numărul de telefon: + Quicksy va trimite un mesaj SMS (pot exista costuri în funcție de furnizor) pentru a vă verifica numărul de telefon. Introduceți codul țării dumneavoastră și numărul de telefon:
%s

Este în regulă sau ați dori să editați numărul?]]>
%s nu este un număr de telefon valid. Vă rugăm să vă introduceți numărul de telefon. @@ -811,15 +819,15 @@ Vă rugăm să introduceți codul de 6 cifre mai jos. Retrimitere SMS Retrimite SMS (%s) - %s + Vă rugăm să așteptați (%s) înapoi - S-a copiat automat un posibil cod din memorie + S-a copiat automat un posibil cod din memorie. Vă rugăm să vă introduceți codul de 6 cifre. Sigur doriți să anulați procedura de înregistrare? Da Nu - Verificare... - Se cere SMS... + Se verifică… + Se cere SMS… Codul introdus este incorect. Codul pe care vi l-am trimis a expirat. Eroare de rețea necunoscută. @@ -868,7 +876,7 @@ Vă rugăm să furnizați un nume pentru canal Vă rugăm să furnizați o adresă XMPP Aceasta este o adresă XMPP. Vă rugăm să furnizați un nume. - Se creează canalul public... + Se creează canalul public… Acest canal există deja V-ați alăturat unui canal existent Nu s-a putut salva configurația canalului @@ -905,7 +913,7 @@ Acest cont a fost deja configurat Va rugăm să introduceți parola pentru acest cont Nu s-a putut realiza această acțiune - Alătură-te unui canal public... + Alătură-te unui canal public… Aplicația care a partajat nu a permis accesul la acest fișier. jabber.network @@ -1013,5 +1021,10 @@ Apelurile sunt dezactivate atunci când utilizați Tor Comută la video Respinge solicitarea de comutare la video - - + Cont XMPP + Server Push + Un server ales de utilizator pentru a intermedia mesajele push către dispozitivul vostru prin XMPP. + Nici unul (dezactivat) + Distribuitor UnifiedPush + Contul prin care vor fi primite notificările push. + \ No newline at end of file diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 94044436e8a95d348c76e14d76a85f1884b2ab22..a3d52ff47f42a585c76755f6ace001ea4d7e5785 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -547,7 +547,8 @@ 分享链接……
您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人

签署即表示您同意我们的隐私政策。]]>
同意并继续 - 此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。 + 此向导将为您在conversations.im 上创建一个账户。 +\n您的联系人可以通过您的XMPP完整地址与您聊天。 您的XMPP完整地址将是:%s 创建账户 使用我自己的服务器 @@ -989,4 +990,10 @@ 使用 Tor 时通话被禁用 切换到视频 拒绝切换到视频的请求 + XMPP 账户 + 推送服务器 + 无(未激活) + UnifiedPush 分发程序 + 将通过该账户接收推送消息。 + 用户选择的推送服务器,通过 XMPP 将推送消息传递到你的设备。 \ No newline at end of file diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index 9485344ead4a839af54c6abdfa1e0a3673dcbb78..bbe9f906d8e5b14bdfa5b9a04edcc3ba548a9370 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -46,4 +46,6 @@ JABBER_NETWORK false true + up.conversations.im + none diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3910eea37f1e8000528ca7393128f7797b3440df..6e71f6e40d45dab01f936fc0d40772bcf0fae46a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1007,5 +1007,11 @@ Calls are disabled when using Tor Switch to video Reject switch to video request + UnifiedPush Distributor + XMPP Account + The account through which push messages will be received. + Push Server + A user-chosen push server to relay push messages via XMPP to your device. + None (deactivated) diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 8f849cdecd23c9a604f34f1207ef85d2abd94235..d4bddeae0026552c836bd48274b3667e65515dcc 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -211,6 +211,21 @@ android:summary="@string/pref_create_backup_summary" android:title="@string/pref_create_backup" /> + + + + + diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index fb93a9971a8f1e3a31fca5597a4454a9c1550335..9b9f07ad18fa04307cbc3ae9031eb11f272b5686 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -1,10 +1,10 @@ - Cuánto tiempo Quicksy permanece en silencio después de detectar una actividad en otro dispositivo - Si elige enviar un informe de error, estará ayudando al desarrollo de Quicksy + Cuánto tiempo Quicksy permanece en silencio después de ver actividad en otros dispositivos + Al enviar los seguimientos del registro, está ayudando al desarrollo de Quicksy Informar a tus contactos cuando usas Quicksy Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. - Foto de perfil de Quicksy + Imagen del perfil de Quicksy Quicksy no está disponible en tu país. No se ha podido verificar la identidad del servidor. Error de seguridad desconocido. diff --git a/src/quicksy/res/values-pl/strings.xml b/src/quicksy/res/values-pl/strings.xml index 4794987b4b7d4b114fda0e0f434219b62cefa66d..d94b0c7750eb36d77ecbb2162c694b42fe5ba038 100644 --- a/src/quicksy/res/values-pl/strings.xml +++ b/src/quicksy/res/values-pl/strings.xml @@ -1,12 +1,12 @@ - Ilość czasu kiedy Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu. + Czas, przez który Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu Wysyłając nam ślady stosu pomagasz w rozwoju Quicksy Powiadom kontakty o tym że używasz Quicksy Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Quicksy do listy chronionych aplikacji. Obrazek profilowy Quicksy - Quicksy nie jest dostępne w twoim kraju + Quicksy nie jest dostępne w Twoim kraju. Nie udało się sprawdzić tożsamości serwera. Nieznany błąd bezpieczeństwa. Błąd czasu oczekiwania na połączenie z serwerem. - + \ No newline at end of file diff --git a/src/quicksy/res/values-sq/strings.xml b/src/quicksy/res/values-sq/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/quicksy/res/values-sq/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file