~singpolyma/cheogram-android

24049a9b80d3259b8d0a8ebe55ae6252b9800237 — Stephen Paul Weber 8 months ago d559645 + e91b6ce
Merge tag '2.12.1'

* tag '2.12.1': (43 commits)
  version bump to 2.12.1
  Translated using Weblate (Galician)
  Translated using Weblate (Spanish)
  Added translation using Weblate (Albanian)
  Added translation using Weblate (Albanian)
  UP: null check transport verification
  version bump to 2.12.0
  Translated using Weblate (German)
  Translated using Weblate (Polish)
  Translated using Weblate (Polish)
  Translated using Weblate (Romanian)
  Translated using Weblate (Polish)
  Translated using Weblate (Russian)
  Translated using Weblate (Polish)
  Translated using Weblate (Romanian)
  add changelog ahead of release to allow translation
  version bump to 2.12.0-beta
  Translated using Weblate (Spanish)
  Translated using Weblate (Spanish)
  Translated using Weblate (Polish)
  ...
39 files changed, 1126 insertions(+), 167 deletions(-)

M CHANGELOG.md
A fastlane/metadata/android/de-DE/changelogs/42044.txt
A fastlane/metadata/android/de-DE/changelogs/42046.txt
M fastlane/metadata/android/de-DE/short_description.txt
A fastlane/metadata/android/en-US/changelogs/42046.txt
A fastlane/metadata/android/en-US/changelogs/42047.txt
A fastlane/metadata/android/pl-PL/changelogs/42044.txt
A fastlane/metadata/android/pl-PL/full_description.txt
A fastlane/metadata/android/pl-PL/short_description.txt
A fastlane/metadata/android/ro/short_description.txt
M src/conversations/res/values-pl/strings.xml
M src/conversations/res/values-ru/strings.xml
A src/conversations/res/values-sq/strings.xml
M src/main/AndroidManifest.xml
M src/main/java/eu/siacs/conversations/entities/Account.java
M src/main/java/eu/siacs/conversations/parser/AbstractParser.java
M src/main/java/eu/siacs/conversations/parser/IqParser.java
A src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java
A src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
A src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
M src/main/java/eu/siacs/conversations/utils/AccountUtils.java
M src/main/java/eu/siacs/conversations/xml/Namespace.java
D src/main/java/eu/siacs/conversations/xmpp/Patches.java
M src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
M src/main/res/values-de/strings.xml
M src/main/res/values-es/strings.xml
M src/main/res/values-gl/strings.xml
M src/main/res/values-ja/strings.xml
M src/main/res/values-pl/strings.xml
M src/main/res/values-ro-rRO/strings.xml
M src/main/res/values-zh-rCN/strings.xml
M src/main/res/values/defaults.xml
M src/main/res/values/strings.xml
M src/main/res/xml/preferences.xml
M src/quicksy/res/values-es/strings.xml
M src/quicksy/res/values-pl/strings.xml
A src/quicksy/res/values-sq/strings.xml
M CHANGELOG.md => CHANGELOG.md +8 -0
@@ 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

A fastlane/metadata/android/de-DE/changelogs/42044.txt => fastlane/metadata/android/de-DE/changelogs/42044.txt +3 -0
@@ 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

A fastlane/metadata/android/de-DE/changelogs/42046.txt => fastlane/metadata/android/de-DE/changelogs/42046.txt +1 -0
@@ 0,0 1,1 @@
* Integration eines UnifiedPush-Verteilers, um Push-Nachrichten für andere UnifiedPush-fähige Apps wie Tusky und Fedilab zu ermöglichen

M fastlane/metadata/android/de-DE/short_description.txt => fastlane/metadata/android/de-DE/short_description.txt +1 -1
@@ 1,1 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

A fastlane/metadata/android/en-US/changelogs/42046.txt => fastlane/metadata/android/en-US/changelogs/42046.txt +1 -0
@@ 0,0 1,1 @@
* Integrate UnifiedPush Distributor to facilitate push messages to other UnifiedPush enabled apps like Tusky and Fedilab

A fastlane/metadata/android/en-US/changelogs/42047.txt => fastlane/metadata/android/en-US/changelogs/42047.txt +1 -0
@@ 0,0 1,1 @@
* Fix crash in UnifiedPush Distributor

A fastlane/metadata/android/pl-PL/changelogs/42044.txt => fastlane/metadata/android/pl-PL/changelogs/42044.txt +3 -0
@@ 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ł.

A fastlane/metadata/android/pl-PL/full_description.txt => fastlane/metadata/android/pl-PL/full_description.txt +39 -0
@@ 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 <a href="http://conversations.im/omemo/">OMEMO</a> lub <a href="http://openpgp.org/about/">OpenPGP</a>;
* 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.

A fastlane/metadata/android/pl-PL/short_description.txt => fastlane/metadata/android/pl-PL/short_description.txt +1 -0
@@ 0,0 1,1 @@
Szyfrowany, prosty w użyciu komunikator XMPP dla Twojego urządzenia mobilnego

A fastlane/metadata/android/ro/short_description.txt => fastlane/metadata/android/ro/short_description.txt +1 -0
@@ 0,0 1,1 @@
Client de mesagerie XMPP ușor de folosit, criptat, și optimizat pentru mobile

M src/conversations/res/values-pl/strings.xml => src/conversations/res/values-pl/strings.xml +2 -2
@@ 2,7 2,7 @@
<resources>
    <string name="pick_a_server">Wybierz dostawcę XMPP</string>
    <string name="use_conversations.im">Użyj conversations.im</string>
    <string name="create_new_account">Stwórz nowe konto</string>
    <string name="create_new_account">Utwórz nowe konto</string>
    <string name="do_you_have_an_account">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.</string>
    <string name="server_select_text">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.</string>
    <string name="magic_create_text_on_x">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.</string>


@@ 12,5 12,5 @@
    <string name="tap_share_button_send_invite">Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s.</string>
    <string name="if_contact_is_nearby_use_qr">Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie.</string>
    <string name="easy_invite_share_text">Dołącz do %1$s aby porozmawiać ze mną: %2$s</string>
    <string name="share_invite_with">Udostępnij zaproszenie...</string>
    <string name="share_invite_with">Udostępnij zaproszenie…</string>
</resources>
\ No newline at end of file

M src/conversations/res/values-ru/strings.xml => src/conversations/res/values-ru/strings.xml +6 -3
@@ 3,10 3,13 @@
    <string name="pick_a_server">Выберите своего XMPP-провайдера</string>
    <string name="use_conversations.im">Использовать conversations.im</string>
    <string name="create_new_account">Создать новый аккаунт</string>
    <string name="do_you_have_an_account">У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас.\nНекоторые провайдеры электронной почты также регистрируют аккаунты XMPP. </string>
    <string name="do_you_have_an_account">У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас.
\nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP.</string>
    <string name="server_select_text">XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations.</string>
    <string name="magic_create_text_on_x">Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. Аккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.</string>
    <string name="magic_create_text_fixed">Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. Этот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.</string>
    <string name="magic_create_text_on_x">Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. 
\nАккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.</string>
    <string name="magic_create_text_fixed">Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. 
\nЭтот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес.</string>
    <string name="your_server_invitation">Ваше приглашение</string>
    <string name="improperly_formatted_provisioning">Неправильный формат кода</string>
    <string name="tap_share_button_send_invite">Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s.</string>

A src/conversations/res/values-sq/strings.xml => src/conversations/res/values-sq/strings.xml +2 -0
@@ 0,0 1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
\ No newline at end of file

M src/main/AndroidManifest.xml => src/main/AndroidManifest.xml +18 -0
@@ 67,6 67,9 @@
        <intent>
            <action android:name="android.intent.action.VIEW" />
        </intent>
        <intent>
            <action android:name="org.unifiedpush.android.connector.MESSAGE"/>
        </intent>
    </queries>




@@ 102,6 105,21 @@
            </intent-filter>
        </receiver>

        <receiver
            android:name=".services.UnifiedPushDistributor"
            android:enabled="false"
            android:exported="true">
            <intent-filter>
                <action android:name="org.unifiedpush.android.distributor.REGISTER" />
                <action android:name="org.unifiedpush.android.distributor.UNREGISTER" />
                <action android:name="org.unifiedpush.android.distributor.feature.BYTES_MESSAGE" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
                <data android:scheme="package"/>
            </intent-filter>
        </receiver>

        <activity
            android:name=".ui.ShareLocationActivity"
            android:label="@string/title_activity_share_location" />

M src/main/java/eu/siacs/conversations/entities/Account.java => src/main/java/eu/siacs/conversations/entities/Account.java +0 -8
@@ 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);
    }

M src/main/java/eu/siacs/conversations/parser/AbstractParser.java => src/main/java/eu/siacs/conversations/parser/AbstractParser.java +32 -0
@@ 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());

M src/main/java/eu/siacs/conversations/parser/IqParser.java => src/main/java/eu/siacs/conversations/parser/IqParser.java +18 -0
@@ 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());

A src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java => src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java +262 -0
@@ 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<String> existingApplication;
        try (final Cursor cursor =
                sqLiteDatabase.query(
                        "push",
                        new String[] {"application"},
                        "instance=?",
                        new String[] {instance},
                        null,
                        null,
                        null)) {
            if (cursor != null && cursor.moveToFirst()) {
                existingApplication = Optional.of(cursor.getString(0));
            } else {
                existingApplication = Optional.absent();
            }
        }
        if (existingApplication.isPresent()) {
            sqLiteDatabase.setTransactionSuccessful();
            sqLiteDatabase.endTransaction();
            return application.equals(existingApplication.get());
        }
        final ContentValues contentValues = new ContentValues();
        contentValues.put("application", application);
        contentValues.put("instance", instance);
        contentValues.put("expiration", 0);
        final long inserted = sqLiteDatabase.insert("push", null, contentValues);
        if (inserted > 0) {
            Log.d(Config.LOGTAG, "inserted new application/instance tuple into unified push db");
        }
        sqLiteDatabase.setTransactionSuccessful();
        sqLiteDatabase.endTransaction();
        return true;
    }

    public List<PushTarget> getRenewals(final String account, final String transport) {
        final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
        final 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<PushTarget> getPushTargets(final String account, final String transport) {
        final ImmutableList.Builder<PushTarget> renewalBuilder = ImmutableList.builder();
        final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
        try (final Cursor cursor =
                sqLiteDatabase.query(
                        "push",
                        new String[] {"application", "instance"},
                        "account = ?",
                        new String[] {account},
                        null,
                        null,
                        null)) {
            while (cursor != null && cursor.moveToNext()) {
                renewalBuilder.add(
                        new PushTarget(
                                cursor.getString(cursor.getColumnIndexOrThrow("application")),
                                cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
            }
        }
        return renewalBuilder.build();
    }

    public boolean deleteInstance(final String instance) {
        final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
        final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance});
        return rows >= 1;
    }

    public boolean deleteApplication(final String application) {
        final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
        final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application});
        return rows >= 1;
    }

    public static class ApplicationEndpoint {
        public final String application;
        public final String endpoint;

        public ApplicationEndpoint(String application, String endpoint) {
            this.application = application;
            this.endpoint = endpoint;
        }
    }

    public static class PushTarget {
        public final String application;
        public final String instance;

        public PushTarget(final String application, final String instance) {
            this.application = application;
            this.instance = instance;
        }

        @NotNull
        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                    .add("application", application)
                    .add("instance", instance)
                    .toString();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            PushTarget that = (PushTarget) o;
            return Objects.equal(application, that.application)
                    && Objects.equal(instance, that.instance);
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(application, instance);
        }
    }
}

A src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java => src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +333 -0
@@ 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<Transport> 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<Transport> renewUnifiedPushEndpoints() {
        final Optional<Transport> 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<UnifiedPushDatabase.PushTarget> 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<UnifiedPushDatabase.PushTarget> pushTarget =
                getPushTarget(account, transport, application, instance);
        if (pushTarget.isPresent()) {
            final UnifiedPushDatabase.PushTarget target = pushTarget.get();
            // TODO check if app is still installed?
            Log.d(
                    Config.LOGTAG,
                    account.getJid().asBareJid()
                            + ": broadcasting a "
                            + payload.length
                            + " bytes push message to "
                            + target.application);
            broadcastPushMessage(target, payload);
            return true;
        } else {
            Log.d(Config.LOGTAG, "could not find application for push");
            return false;
        }
    }

    public Optional<Transport> getTransport() {
        final SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext());
        final String accountPreference =
                sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none");
        final String pushServerPreference =
                sharedPreferences.getString(
                        UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
                        service.getString(R.string.default_push_server));
        if (Strings.isNullOrEmpty(accountPreference)
                || "none".equalsIgnoreCase(accountPreference)
                || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) {
            return Optional.absent();
        }
        final Jid transport;
        final Jid jid;
        try {
            transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim());
            jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim());
        } catch (final IllegalArgumentException e) {
            return Optional.absent();
        }
        final Account account = service.findAccountByJid(jid);
        if (account == null) {
            return Optional.absent();
        }
        return Optional.of(new Transport(account, transport));
    }

    private Optional<UnifiedPushDatabase.PushTarget> getPushTarget(
            final Account account,
            final Jid transport,
            final String application,
            final String instance) {
        if (transport == null || application == null || instance == null) {
            return Optional.absent();
        }
        final String uuid = account.getUuid();
        final List<UnifiedPushDatabase.PushTarget> pushTargets =
                UnifiedPushDatabase.getInstance(service)
                        .getPushTargets(uuid, transport.toEscapedString());
        return Iterables.tryFind(
                pushTargets,
                pt ->
                        UnifiedPushDistributor.hash(uuid, pt.application).equals(application)
                                && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance));
    }

    private void broadcastPushMessage(
            final UnifiedPushDatabase.PushTarget target, final byte[] payload) {
        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE);
        updateIntent.setPackage(target.application);
        updateIntent.putExtra("token", target.instance);
        updateIntent.putExtra("bytesMessage", payload);
        updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
        service.sendBroadcast(updateIntent);
    }

    private void broadcastEndpoint(
            final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
        Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
        final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
        updateIntent.setPackage(endpoint.application);
        updateIntent.putExtra("token", instance);
        updateIntent.putExtra("endpoint", endpoint.endpoint);
        service.sendBroadcast(updateIntent);
    }

    public void rebroadcastEndpoint(final String instance, final Transport transport) {
        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
        final UnifiedPushDatabase.ApplicationEndpoint endpoint =
                unifiedPushDatabase.getEndpoint(
                        transport.account.getUuid(),
                        transport.transport.toEscapedString(),
                        instance);
        if (endpoint != null) {
            broadcastEndpoint(instance, endpoint);
        }
    }

    public static class Transport {
        public final Account account;
        public final Jid transport;

        public Transport(Account account, Jid transport) {
            this.account = account;
            this.transport = transport;
        }
    }
}

A src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java => src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java +152 -0
@@ 0,0 1,152 @@
package eu.siacs.conversations.services;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Log;

import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import eu.siacs.conversations.Config;
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
import eu.siacs.conversations.utils.Compatibility;

public class UnifiedPushDistributor extends BroadcastReceiver {

    public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER";
    public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER";
    public static final String ACTION_BYTE_MESSAGE =
            "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
    public static final String ACTION_REGISTRATION_FAILED =
            "org.unifiedpush.android.connector.REGISTRATION_FAILED";
    public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE";
    public static final String ACTION_NEW_ENDPOINT =
            "org.unifiedpush.android.connector.NEW_ENDPOINT";

    public static final String PREFERENCE_ACCOUNT = "up_push_account";
    public static final String PREFERENCE_PUSH_SERVER = "up_push_server";

    public static final List<String> PREFERENCES =
            Arrays.asList(PREFERENCE_ACCOUNT, PREFERENCE_PUSH_SERVER);

    @Override
    public void onReceive(final Context context, final Intent intent) {
        if (intent == null) {
            return;
        }
        final String action = intent.getAction();
        final String application = intent.getStringExtra("application");
        final String instance = intent.getStringExtra("token");
        final List<String> features = intent.getStringArrayListExtra("features");
        switch (Strings.nullToEmpty(action)) {
            case ACTION_REGISTER:
                register(context, application, instance, features);
                break;
            case ACTION_UNREGISTER:
                unregister(context, instance);
                break;
            case Intent.ACTION_PACKAGE_FULLY_REMOVED:
                unregisterApplication(context, intent.getData());
                break;
            default:
                Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
                break;
        }
    }

    private void register(
            final Context context,
            final String application,
            final String instance,
            final Collection<String> features) {
        if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
            Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
            return;
        }
        final List<String> receivers = getBroadcastReceivers(context, application);
        if (receivers.contains(application)) {
            final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE);
            Log.d(
                    Config.LOGTAG,
                    "received up registration from "
                            + application
                            + "/"
                            + instance
                            + " features: "
                            + features);
            if (UnifiedPushDatabase.getInstance(context).register(application, instance)) {
                Log.d(
                        Config.LOGTAG,
                        "successfully created UnifiedPush entry. waking up XmppConnectionService");
                final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
                serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
                serviceIntent.putExtra("instance", instance);
                Compatibility.startService(context, serviceIntent);
            } else {
                Log.d(Config.LOGTAG, "not successful. sending error message back to application");
                final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
                registrationFailed.setPackage(application);
                registrationFailed.putExtra("token", instance);
                context.sendBroadcast(registrationFailed);
            }
        } else {
            Log.d(
                    Config.LOGTAG,
                    "ignoring invalid UnifiedPush registration. Unknown application "
                            + application);
        }
    }

    private List<String> getBroadcastReceivers(final Context context, final String application) {
        final Intent messageIntent = new Intent(ACTION_MESSAGE);
        messageIntent.setPackage(application);
        final List<ResolveInfo> resolveInfo =
                context.getPackageManager().queryBroadcastReceivers(messageIntent, 0);
        return Lists.transform(
                resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName);
    }

    private void unregister(final Context context, final String instance) {
        if (Strings.isNullOrEmpty(instance)) {
            Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration");
            return;
        }
        final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
        if (unifiedPushDatabase.deleteInstance(instance)) {
            Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
        }
    }

    private void unregisterApplication(final Context context, final Uri uri) {
        if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) {
            final String application = uri.getSchemeSpecificPart();
            if (Strings.isNullOrEmpty(application)) {
                return;
            }
            Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
            final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
            if (database.deleteApplication(application)) {
                Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
            }
        }
    }

    public static String hash(String... components) {
        return BaseEncoding.base64()
                .encode(
                        Hashing.sha256()
                                .hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
                                .asBytes());
    }
}

M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +34 -9
@@ 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<UnifiedPushBroker.Transport> transport = renewUnifiedPushEndpoints();
                    if (instance != null && transport.isPresent()) {
                        unifiedPushBroker.rebroadcastEndpoint(instance, transport.get());
                    }
                    break;
                case ACTION_IDLE_PING:
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        scheduleNextIdlePing();


@@ 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<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
        return this.unifiedPushBroker.renewUnifiedPushEndpoints();
    }

    private void provisionAccount(final String address, final String password) {
        final Jid jid = Jid.ofEscaped(address);
        final Account account = new Account(jid, password);


@@ 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);
            }
        });
    }

M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java => src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +35 -0
@@ 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<CharSequence> accounts =
                ImmutableList.copyOf(
                        Lists.transform(
                                xmppConnectionService.getAccounts(),
                                a -> a.getJid().asBareJid().toEscapedString()));
        final ImmutableList.Builder<CharSequence> entries = new ImmutableList.Builder<>();
        final ImmutableList.Builder<CharSequence> entryValues = new ImmutableList.Builder<>();
        entries.add(getString(R.string.no_account_deactivated));
        entryValues.add("none");
        entries.addAll(accounts);
        entryValues.addAll(accounts);
        listPreference.setEntries(entries.build().toArray(new CharSequence[0]));
        listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0]));
        if (!accounts.contains(listPreference.getValue())) {
            listPreference.setValue("none");
        }
    }

    @Override


@@ 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();
            }
        }
    }


M src/main/java/eu/siacs/conversations/utils/AccountUtils.java => src/main/java/eu/siacs/conversations/utils/AccountUtils.java +18 -0
@@ 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<String> getEnabledAccounts(final XmppConnectionService service) {
        ArrayList<String> accounts = new ArrayList<>();
        for (Account account : service.getAccounts()) {

M src/main/java/eu/siacs/conversations/xml/Namespace.java => src/main/java/eu/siacs/conversations/xml/Namespace.java +1 -0
@@ 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";
}

D src/main/java/eu/siacs/conversations/xmpp/Patches.java => src/main/java/eu/siacs/conversations/xmpp/Patches.java +0 -14
@@ 1,14 0,0 @@
package eu.siacs.conversations.xmpp;


import java.util.Arrays;
import java.util.List;

public class Patches {
    public static final List<String> DISCO_EXCEPTIONS = Arrays.asList(
            "nimbuzz.com"
    );
    public static final List<XmppConnection.Identity> BAD_MUC_REFLECTION = Arrays.asList(
            XmppConnection.Identity.SLACK
    );
}

M src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java => src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +3 -44
@@ 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) {

M src/main/res/values-de/strings.xml => src/main/res/values-de/strings.xml +9 -2
@@ 545,7 545,8 @@
    <string name="share_uri_with">Teile URI mit…</string>
    <string name="welcome_text_quicksy"><![CDATA[Quicksy ist ein Ableger des beliebten XMPP-Clients Conversations mit automatischer Kontakterkennung.<br><br>Du registrierst dich mit deiner Telefonnummer und Quicksy wird automatisch auf der Grundlage der Telefonnummern in deinem Adressbuch mögliche Kontakte vorschlagen.<br><br>Mit der Anmeldung erklärst du dich mit unserer <a href="https://quicksy.im/#privacy">Datenschutzerklärung</a> einverstanden.]]></string>
    <string name="agree_and_continue">Zustimmen und fortfahren</string>
    <string name="magic_create_text">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.</string>
    <string name="magic_create_text">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.</string>
    <string name="your_full_jid_will_be">Deine vollständige XMPP-Adresse lautet: %s</string>
    <string name="create_account">Konto erstellen</string>
    <string name="use_own_provider">Nutze eigenen Provider</string>


@@ 612,7 613,7 @@
    <string name="use_camera_icon_to_scan_barcode">Nutze die Kamera, um Barcodes deiner Kontakte zu scannen</string>
    <string name="please_wait_for_keys_to_be_fetched">Bitte warten, bis die Schlüssel abgerufen werden</string>
    <string name="share_as_barcode">Als Barcode teilen</string>
    <string name="share_as_uri">Als XMPP URI teilen</string>
    <string name="share_as_uri">Als XMPP-URI teilen</string>
    <string name="share_as_http">Als HTTP Link teilen</string>
    <string name="pref_blind_trust_before_verification">Blind vertrauen vor der Überprüfung</string>
    <string name="pref_blind_trust_before_verification_summary">Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen.</string>


@@ 998,4 999,10 @@
    <string name="audio_video_disabled_tor">Anrufe sind bei der Verwendung von Tor deaktiviert</string>
    <string name="switch_to_video">Umschalten auf Video</string>
    <string name="reject_switch_to_video">Umschalten auf Video ablehnen</string>
    <string name="pref_up_push_account_title">XMPP-Konto</string>
    <string name="pref_up_push_server_title">Push-Server</string>
    <string name="pref_up_push_server_summary">Ein selbst gewählter Push-Server, der Push-Nachrichten über XMPP an dein Gerät weiterleitet.</string>
    <string name="no_account_deactivated">Kein (deaktiviert)</string>
    <string name="unified_push_distributor">UnifiedPush Verteiler</string>
    <string name="pref_up_push_account_summary">Das Konto, über das Push-Nachrichten empfangen werden sollen.</string>
</resources>
\ No newline at end of file

M src/main/res/values-es/strings.xml => src/main/res/values-es/strings.xml +7 -1
@@ 548,7 548,7 @@
    <string name="share_uri_with">Compartir URI con…</string>
    <string name="welcome_text_quicksy">Quicksy es un derivado del popular cliente XMPP Conversations con detección automática de contactos.&lt;br&gt;&lt;br&gt;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.&lt;br&gt;&lt;br&gt;Registrándote en Quicksy aceptas nuestra &lt;a href=https://quicksy.im/#privacy&gt;política de privacidad&lt;/a&gt;.</string>
    <string name="agree_and_continue">Aceptar y continuar</string>
    <string name="magic_create_text">Una guía te ayudará en el proceso de creación de la cuenta en conversations.im.¹
    <string name="magic_create_text">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.</string>
    <string name="your_full_jid_will_be">Tu dirección XMPP completa será: %s</string>
    <string name="create_account">Crear cuenta</string>


@@ 1013,4 1013,10 @@
    <string name="audio_video_disabled_tor">Las llamadas están deshabilitadas cuando se usa Tor</string>
    <string name="switch_to_video">Cambiar a vídeo</string>
    <string name="reject_switch_to_video">Rechazar petición de cambiar a vídeo</string>
    <string name="unified_push_distributor">Distribuidor de UnifiedPush</string>
    <string name="pref_up_push_account_title">Cuenta XMPP</string>
    <string name="pref_up_push_account_summary">La cuenta a través de la cual se recibirán los mensajes push.</string>
    <string name="pref_up_push_server_title">Servidor push</string>
    <string name="pref_up_push_server_summary">Un servidor push elegido por el usuario para transmitir mensajes push a través de XMPP a su dispositivo.</string>
    <string name="no_account_deactivated">Ninguno (desactivado)</string>
</resources>
\ No newline at end of file

M src/main/res/values-gl/strings.xml => src/main/res/values-gl/strings.xml +8 -1
@@ 547,7 547,8 @@
    <string name="share_uri_with">Compartir URI con…</string>
    <string name="welcome_text_quicksy"><![CDATA[Quicksy é un derivado do popular cliente XMPP Conversations con descubrimento automático de contactos.<br><br>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.<br><br>Ao rexistrarte aceptas a nosa <a href="https://quicksy.im/#privacy">política de privacidade</a>.]]></string>
    <string name="agree_and_continue">Aceptar e continuar</string>
    <string name="magic_create_text">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.</string>
    <string name="magic_create_text">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.</string>
    <string name="your_full_jid_will_be">O teu enderezo XMPP completo será: %s</string>
    <string name="create_account">Crear conta</string>
    <string name="use_own_provider">Utilizar o meu propio proveedor</string>


@@ 1001,4 1002,10 @@
    <string name="audio_video_disabled_tor">As chamadas están desactivadas cando usas Tor</string>
    <string name="switch_to_video">Cambiar a vídeo</string>
    <string name="reject_switch_to_video">Rexeitar a solicitude para cambiar a vídeo</string>
    <string name="unified_push_distributor">Distribuidor UnifiedPush</string>
    <string name="pref_up_push_account_title">Conta XMPP</string>
    <string name="pref_up_push_account_summary">A conta a través da cal se recibirán as mensaxes push.</string>
    <string name="pref_up_push_server_title">Servidor Push</string>
    <string name="pref_up_push_server_summary">O servidor elexido pola usuaria para obter as mensaxes push a través de XMPP.</string>
    <string name="no_account_deactivated">Ningún (desactivado)</string>
</resources>
\ No newline at end of file

M src/main/res/values-ja/strings.xml => src/main/res/values-ja/strings.xml +3 -4
@@ 31,7 31,6 @@
    <string name="minutes_ago">%d分前</string>
    <plurals name="x_unread_conversations">
        <item quantity="other">%d件の未読の会話</item>

    </plurals>
    <string name="sending">送信中…</string>
    <string name="message_decrypting">メッセージを復号しています。しばらくお待ちください…</string>


@@ 680,7 679,7 @@
    <string name="mtm_accept_cert">未知の証明書を受け入れますか?</string>
    <string name="mtm_trust_anchor">サーバー証明書が既知の認証局によって署名されていません。</string>
    <string name="mtm_accept_servername">不一致のサーバー名を受け入れますか?</string>
    <string name="mtm_hostname_mismatch">サーバーは\&quot;%s\&quot;として認証できませんでした。証明書は次の場合にのみ有効です:</string>
    <string name="mtm_hostname_mismatch">サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です:</string>
    <string name="mtm_connect_anyway">それでも接続を希望しますか?</string>
    <string name="mtm_cert_details">証明書の詳細:</string>
    <string name="once">一度だけ</string>


@@ 900,7 899,7 @@
    <string name="rtp_state_accepting_call">通話受入</string>
    <string name="rtp_state_ending_call">通話終了</string>
    <string name="answer_call">応答</string>
    <string name="dismiss_call">解散</string>
    <string name="dismiss_call">拒否</string>
    <string name="rtp_state_finding_device">デバイス発見</string>
    <string name="rtp_state_ringing">鳴動</string>
    <string name="rtp_state_declined_or_busy">取込中</string>


@@ 976,4 975,4 @@
    <string name="delete_avatar">アバターを削除</string>
    <string name="audio_video_disabled_tor">Tor使用中のため通話できません</string>
    <string name="switch_to_video">ビデオ通話切替</string>
    </resources>
</resources>

M src/main/res/values-pl/strings.xml => src/main/res/values-pl/strings.xml +27 -26
@@ 31,19 31,12 @@
    <string name="minutes_ago">%d minut temu</string>
    <plurals name="x_unread_conversations">
        <item quantity="one">%d nieprzeczytana konwersacja</item>

    
        <item quantity="few">%d nieprzeczytane konwersacje</item>

    
        <item quantity="many">%d nieprzeczytanych konwersacji</item>

    
        <item quantity="other">%d nieprzeczytanych konwersacji</item>

    </plurals>
    <string name="sending">wysyłanie...</string>
    <string name="message_decrypting">Odszyfrowywanie wiadomości. To zajmie tylko chwilę...</string>
    <string name="sending">wysyłanie…</string>
    <string name="message_decrypting">Odszyfrowywanie wiadomości. To zajmie tylko chwilę…</string>
    <string name="pgp_message">Wiadomość zaszyfrowana OpenPGP</string>
    <string name="nick_in_use">Nazwa jest już w użyciu</string>
    <string name="invalid_muc_nick">NIeprawidłowy pseudonim</string>


@@ 62,7 55,7 @@
    <string name="remove_bookmark_text">Czy chcesz usunąć zakładkę %s? Rozmowy z tą zakładką nie zostaną usunięte.</string>
    <string name="register_account">Zarejestruj nowe konto na serwerze</string>
    <string name="change_password_on_server">Zmień hasło na serwerze</string>
    <string name="share_with">Udostępnij...</string>
    <string name="share_with">Udostępnij…</string>
    <string name="start_conversation">Rozpocznij rozmowę</string>
    <string name="invite_contact">Zaproś kontakt</string>
    <string name="invite">Zaproś</string>


@@ 90,12 83,14 @@
    <string name="send_failed">wysyłanie nie powiodło się</string>
    <string name="preparing_image">Przygotowanie do wysłania obrazka</string>
    <string name="preparing_images">Przygotowanie do wysłania obrazków</string>
    <string name="sharing_files_please_wait">Udostępnianie plików. Proszę czekać...</string>
    <string name="sharing_files_please_wait">Udostępnianie plików. Proszę czekać…</string>
    <string name="action_clear_history">Wyczyść historię</string>
    <string name="clear_conversation_history">Wyczyść historię konwersacji</string>
    <string name="clear_histor_msg">Czy chcesz usunąć wszystkie wiadomości w tej rozmowie?\n\n<b>Ostrzeżenie:</b> To nie ma wpływu na wiadomości składowane na innych urządzeniach lub serwerach.</string>
    <string name="delete_file_dialog">Usuń plik</string>
    <string name="delete_file_dialog_msg">Czy na pewno usunąć ten plik?\n\n<b>Uwaga:</b> Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach.</string>
    <string name="delete_file_dialog_msg">Czy na pewno usunąć ten plik\?
\n
\n<b>Uwaga:</b> Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach. </string>
    <string name="also_end_conversation">Zamknij konwersację po zakończeniu</string>
    <string name="choose_presence">Wybierz urządzenie</string>
    <string name="send_unencrypted_message">Wyślij wiadomość bez szyfrowania</string>


@@ 112,8 107,8 @@
    <string name="restart">Zrestartuj</string>
    <string name="install">Zainstaluj</string>
    <string name="openkeychain_not_installed">Proszę zainstalować OpenKeychain</string>
    <string name="offering">oferowanie...</string>
    <string name="waiting">oczekiwanie...</string>
    <string name="offering">oferowanie…</string>
    <string name="waiting">oczekiwanie…</string>
    <string name="no_pgp_key">Nie znaleziono klucza OpenPGP</string>
    <string name="contact_has_no_pgp_key">Nie można zaszyfrować twojej wiadomości bo ten kontakt nie ogłasza swojego publicznego klucza.\n\n<small>Poproś kontakt aby ustawił OpenPGP.</small></string>
    <string name="no_pgp_keys">Nie znaleziono kluczy OpenPGP</string>


@@ 156,7 151,7 @@
    <string name="error_not_an_image_file">Wybrany plik nie jest obrazem</string>
    <string name="error_compressing_image">Błąd konwersji obrazu</string>
    <string name="error_file_not_found">Nie odnaleziono pliku</string>
    <string name="error_io_exception">Ogólny błąd wejścia/wyjścia</string>
    <string name="error_io_exception">Ogólny błąd wejścia/wyjścia. Być może skończyło się miejsce w pamięci\?</string>
    <string name="error_security_exception_during_image_copy">Aplikacja użyta do wyboru obrazu nie zezwoliła na odczyt pliku.\n\n<small>Wybierz obraz przy użyciu innego menedżera plików</small></string>
    <string name="error_security_exception">Aplikacja której użyłeś do udostępnienia pliku nie dostarczyła odpowiednich uprawnień. </string>
    <string name="account_status_unknown">Nieznany</string>


@@ 204,10 199,10 @@
    <string name="server_info_show_more">Informacje o serwerze</string>
    <string name="server_info_mam">XEP-0313: MAM</string>
    <string name="server_info_carbon_messages">XEP-0280: Kopie wiadomości</string>
    <string name="server_info_csi">XEP-0352: Client State Indication</string>
    <string name="server_info_blocking">XEP-0191: Blocking Command</string>
    <string name="server_info_csi">XEP-0352: Wskaźnik stanu klienta</string>
    <string name="server_info_blocking">XEP-0191: Polecenia Blokujące</string>
    <string name="server_info_roster_version">XEP-0237: Roster Versioning</string>
    <string name="server_info_stream_management">XEP-0198: Stream Management</string>
    <string name="server_info_stream_management">XEP-0198: Zarządzanie Strumieniem</string>
    <string name="server_info_external_service_discovery">XEP-0215: Wykrywanie Zewnętrznych Usług</string>
    <string name="server_info_pep">XEP-0163: PEP (Awatary / OMEMO)</string>
    <string name="server_info_http_upload">XEP-0363: Przesyłanie plików przez HTTP</string>


@@ 409,7 404,7 @@
    <string name="non_anonymous">Spraw aby adres XMPP był widoczny dla wszystkich</string>
    <string name="moderated">Włącz moderację na kanale</string>
    <string name="you_are_not_participating">Nie bierzesz udziału</string>
    <string name="modified_conference_options">Ustawienia konferencji zostały zmodyfikowane</string>
    <string name="modified_conference_options">Ustawienia konferencji zostały zmodyfikowane!</string>
    <string name="could_not_modify_conference_options">Nie można zmodyfikować ustawień konferencji</string>
    <string name="never">Nigdy</string>
    <string name="until_further_notice">Ręcznie</string>


@@ 468,7 463,7 @@
    <string name="search_contacts">Przeszukuj kontakty</string>
    <string name="search_bookmarks">Przeszukaj zakładki</string>
    <string name="send_private_message">Wyślij wiadomość prywatną</string>
    <string name="user_has_left_conference">%1$s opuścił konferencję!</string>
    <string name="user_has_left_conference">%1$s opuścił konferencję</string>
    <string name="username">Nazwa użytkownika</string>
    <string name="username_hint">Nazwa użytkownika</string>
    <string name="invalid_username">Błędna nazwa użytkownika</string>


@@ 503,8 498,8 @@
    <string name="jid_does_not_match_certificate">Adres XMPP nie pasuje do certyfikatu</string>
    <string name="action_renew_certificate">Odnów certyfikat</string>
    <string name="error_fetching_omemo_key">Błąd pobierania klucza OMEMO!</string>
    <string name="verified_omemo_key_with_certificate">Zweryfikowano klucz OMEMO z certyfikatem</string>
    <string name="device_does_not_support_certificates">Twoje urządzenie nie wspiera wyboru certyfikatów klienckich</string>
    <string name="verified_omemo_key_with_certificate">Zweryfikowano klucz OMEMO z certyfikatem!</string>
    <string name="device_does_not_support_certificates">Twoje urządzenie nie wspiera wyboru certyfikatów klienckich!</string>
    <string name="pref_connection_options">Połączenie</string>
    <string name="pref_use_tor">Połącz przez sieć TOR</string>
    <string name="pref_use_tor_summary">Tuneluj wszystkie połączenia przez sieć TOR. Wymaga zainstalowania aplikacji \"Orbot\"</string>


@@ 554,7 549,8 @@
    <string name="share_uri_with">Udostępnij URI za pomocą...</string>
    <string name="welcome_text_quicksy"><![CDATA[Quicksy to modyfikacja popularnego klienta XMPP Conversations, z automatycznym wykrywaniem kontaktów.<br><br>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.<br><br>Zapisując się zgadzasz się na naszą <a href="https://quicksy.im/#privacy">politykę prywatności</a>.]]></string>
    <string name="agree_and_continue">Zgoda i kontynuuj</string>
    <string name="magic_create_text">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.</string>
    <string name="magic_create_text">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.</string>
    <string name="your_full_jid_will_be">Twój pełen adres XMPP to: %s</string>
    <string name="create_account">Utwórz konto</string>
    <string name="use_own_provider">Użyj innego serwera</string>


@@ 812,7 808,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
    <string name="choose_a_country">Wybierz kraj</string>
    <string name="phone_number">numer telefonu</string>
    <string name="verify_your_phone_number">Zweryfikuj swój numer telefonu</string>
    <string name="enter_country_code_and_phone_number">Quicksy wyśle SMS (operator może naliczyć koszty) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu.</string>
    <string name="enter_country_code_and_phone_number">Quicksy wyśle wiadomość SMS (operator może naliczyć opłatę) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu:</string>
    <string name="we_will_be_verifying"><![CDATA[Zweryfikujemy numer telefonu <br/><br/><b>%s</b><br/><br/>Czy wszystko się zgadza czy też chciałbyś zmienić numer?]]></string>
    <string name="not_a_valid_phone_number">%s nie jest prawidłowym numerem telefonu</string>
    <string name="please_enter_your_phone_number">Proszę wpisać swój numer telefonu.</string>


@@ 1030,5 1026,10 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
    <string name="audio_video_disabled_tor">Dzwonienie jest wyłączone podczas używania Tora</string>
    <string name="switch_to_video">Przełącz na wideo</string>
    <string name="reject_switch_to_video">Odrzuć prośbę przełączenia na wideo</string>

</resources>
    <string name="unified_push_distributor">Dystrybutor UnifiedPush</string>
    <string name="pref_up_push_account_title">Konto XMPP</string>
    <string name="pref_up_push_account_summary">Konto, poprzez które będą odbierane powiadomienia push.</string>
    <string name="pref_up_push_server_title">Serwer push</string>
    <string name="pref_up_push_server_summary">Dowolnie wybrany serwer push do przekazywania wiadomości push przez XMPP na Twoje urządzenie.</string>
    <string name="no_account_deactivated">Brak (nieaktywne)</string>
</resources>
\ No newline at end of file

M src/main/res/values-ro-rRO/strings.xml => src/main/res/values-ro-rRO/strings.xml +58 -45
@@ 31,16 31,11 @@
    <string name="minutes_ago">acum %d minute</string>
    <plurals name="x_unread_conversations">
        <item quantity="one">%d conversație necitită</item>

    
        <item quantity="few">%d conversații necitite</item>

    
        <item quantity="other">%d de conversații necitite</item>

    </plurals>
    <string name="sending">trimitere...</string>
    <string name="message_decrypting">Decriptez mesaj. Te rog așteaptă...</string>
    <string name="sending">trimitere…</string>
    <string name="message_decrypting">Decriptare mesaj. Vă rugăm să așteptați…</string>
    <string name="pgp_message">Mesaj criptat cu OpenPGP</string>
    <string name="nick_in_use">Numele de utilizator este deja alocat</string>
    <string name="invalid_muc_nick">Nume invalid</string>


@@ 59,7 54,7 @@
    <string name="remove_bookmark_text">Ați dori să ștergeți %s din semne de carte? Conversațiile asociate cu acest semn de carte nu vor fi șterse.</string>
    <string name="register_account">Înregistrează un cont nou pe server</string>
    <string name="change_password_on_server">Schimbă parola pe server</string>
    <string name="share_with">Partajează cu...</string>
    <string name="share_with">Partajează cu…</string>
    <string name="start_conversation">Pornește o conversație</string>
    <string name="invite_contact">Invită contact</string>
    <string name="invite">Invită</string>


@@ 87,12 82,14 @@
    <string name="send_failed">trimitere eșuată</string>
    <string name="preparing_image">Se pregătește trimiterea imaginii</string>
    <string name="preparing_images">Se pregătește trimiterea imaginilor</string>
    <string name="sharing_files_please_wait">Trimitere fișiere. Te rog asteaptă...</string>
    <string name="sharing_files_please_wait">Trimitere fișiere. Vă rugăm să așteptați…</string>
    <string name="action_clear_history">Șterge istoric</string>
    <string name="clear_conversation_history">Șterge istoricul conversației</string>
    <string name="clear_histor_msg">Doriți să ștergeți toate mesajele din această conversație?\n\n<b>Atenție:</b> Această acțiune nu va afecta mesajele aflate pe alte dispozitive sau servere.</string>
    <string name="delete_file_dialog">Șterge fișierul</string>
    <string name="delete_file_dialog_msg">Sigur doriți să ștergeți acest fișier?\n\n<b>Atenție:</b> Această acțiune nu va șterge copiile acestui fișier care sunt stocate pe alte dispozitive sau servere.</string>
    <string name="delete_file_dialog_msg">Sigur doriți să ștergeți acest fișier\?
\n
\n<b>Atenție:</b> Această acțiune nu va șterge copiile acestui fișier care sunt stocate pe alte dispozitive sau servere. </string>
    <string name="also_end_conversation">Închide conversația după ștergere</string>
    <string name="choose_presence">Alege dispozitiv</string>
    <string name="send_unencrypted_message">Trimite mesaje necriptate</string>


@@ 105,19 102,19 @@
    <string name="send_unencrypted">Trimite necriptat</string>
    <string name="decryption_failed">Decriptarea a eșuat. Poate nu aveți cheia privată corectă.</string>
    <string name="openkeychain_required">OpenKeychain</string>
    <string name="openkeychain_required_long"><![CDATA[%1$s utilizează <b>OpenKeychain</b> 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<small>(Vă rugăm să reporniți %1$s după instalare.)</small>]]></string>
    <string name="openkeychain_required_long">%1$s utilizează &lt;b&gt;OpenKeychain&lt;/b&gt; pentru a cripta și decripta mesaje și a administra cheile publice.&lt;br&gt;&lt;br&gt;OpenKeychain este licențiat sub GPLv3+ și este disponibil în F-Droid și Google Play.&lt;br&gt;&lt;br&gt;&lt;small&gt;(Vă rugăm să reporniți %1$s după instalare.)&lt;/small&gt;</string>
    <string name="restart">Repornește</string>
    <string name="install">Instalare</string>
    <string name="openkeychain_not_installed">Va rugăm să instalați OpenKeychain</string>
    <string name="offering">transmit...</string>
    <string name="waiting">în așteptare...</string>
    <string name="offering">transmit…</string>
    <string name="waiting">în așteptare…</string>
    <string name="no_pgp_key">Nu am găsit cheia OpenPGP</string>
    <string name="contact_has_no_pgp_key">Nu s-au putut cripta mesajele deoarece contactul nu își anunță cheile publice.\n\n<small>Rugați-vă contactul să își configureze OpenPGP.</small></string>
    <string name="no_pgp_keys">Nu am găsit chei OpenPGP</string>
    <string name="contacts_have_no_pgp_keys">Nu s-au putut cripta mesajele deoarece contactele nu își anunță cheile publice.\n\n<small>Rugați-vă contactele să își configureze OpenPGP.</small></string>
    <string name="pref_general">General</string>
    <string name="pref_accept_files">Acceptă fișiere</string>
    <string name="pref_accept_files_summary">Mai mici decât...</string>
    <string name="pref_accept_files_summary">Mai mici decât…</string>
    <string name="pref_attachments">Atașamente</string>
    <string name="pref_notification_settings">Notificare</string>
    <string name="pref_vibrate">Vibrează</string>


@@ 154,7 151,9 @@
    <string name="error_compressing_image">Nu s-a putut face convertirea imaginii</string>
    <string name="error_file_not_found">Fișierul nu a fost găsit</string>
    <string name="error_io_exception">Eroare I/O generala. Poate ați rămas fără spațiu liber?</string>
    <string name="error_security_exception_during_image_copy">Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul.\n\n<small>Folosiți un alt manager de fișiere pentru a alege o imagine</small></string>
    <string name="error_security_exception_during_image_copy">Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul.
\n
\n<small>Folosiți un alt manager de fișiere pentru a alege o imagine</small>.</string>
    <string name="error_security_exception">Aplicația pe care ați folosit-o pentru a partaja acest fișier nu a furnizat suficiente permisiuni.</string>
    <string name="account_status_unknown">Necunoscut</string>
    <string name="account_status_disabled">Dezactivat temporar</string>


@@ 196,7 195,7 @@
    <string name="account_settings_example_jabber_id">numeutilizator@exemplu.ro</string>
    <string name="password">Parolă</string>
    <string name="invalid_jid">Aceasta nu este o adresă XMPP valabilă</string>
    <string name="error_out_of_memory">Memorie epuizată. Imaginea este prea mare.</string>
    <string name="error_out_of_memory">Memorie epuizată. Imaginea este prea mare</string>
    <string name="add_phone_book_text">Vreți să adăugați pe %s în lista de contacte?</string>
    <string name="server_info_show_more">Informații server</string>
    <string name="server_info_mam">XEP-0313: MAM</string>


@@ 228,7 227,7 @@
    <string name="omemo_fingerprint_x509_selected_message">v\\Amprenta OMEMO (originea mesajului)</string>
    <string name="other_devices">Alte dispozitive</string>
    <string name="trust_omemo_fingerprints">Amprente OMEMO de încredere</string>
    <string name="fetching_keys">Se preiau cheile...</string>
    <string name="fetching_keys">Se preiau cheile…</string>
    <string name="done">Gata</string>
    <string name="decrypt">Decriptează</string>
    <string name="bookmarks">Semne de carte</string>


@@ 254,7 253,7 @@
    <string name="could_not_destroy_channel">Nu s-a putut distruge canalul</string>
    <string name="action_edit_subject">Editează subiectul discuției de grup</string>
    <string name="topic">Subiect discuție</string>
    <string name="joining_conference">Vă alăturați discuției de grup...</string>
    <string name="joining_conference">Vă alăturați discuției de grup…</string>
    <string name="leave">Paraseste</string>
    <string name="contact_added_you">Contactul v-a adăugat în lista de contacte</string>
    <string name="add_back">Adaugă contact</string>


@@ 264,7 263,7 @@
    <string name="everyone_has_read_up_to_this_point">Toate persoanele au citit până aici</string>
    <string name="publish">Publică</string>
    <string name="touch_to_choose_picture">Atingeți avatarul pentru a selecta o poză din galerie</string>
    <string name="publishing">Se publică...</string>
    <string name="publishing">Se publică…</string>
    <string name="error_publish_avatar_server_reject">Acest server v-a refuzat publicarea</string>
    <string name="error_publish_avatar_converting">Nu s-a putut face convertirea pozei</string>
    <string name="error_saving_avatar">Nu s-a putut salva avatarul pe disc</string>


@@ 282,7 281,9 @@
    <string name="enable">Activează</string>
    <string name="conference_requires_password">Discuția de grup necesită o parolă</string>
    <string name="enter_password">Introduceți parola</string>
    <string name="request_presence_updates">Vă rugăm să cereți mai întâi actualizări de prezență de la acest contact.\n\n<small>Acestea vor fi folosite pentru a determina ce aplicații folosește contactul dumneavoastră.</small></string>
    <string name="request_presence_updates">Vă rugăm să cereți mai întâi actualizări de prezență de la acest contact.
\n
\n<small>Acestea vor fi folosite pentru a determina ce aplicații folosește contactul dumneavoastră.</small>.</string>
    <string name="request_now">Cere acum</string>
    <string name="ignore">Ignora</string>
    <string name="without_mutual_presence_updates"><b>Atenție:</b> Trimițând aceasta fără actualizări de prezență reciproce ar putea produce probleme neprevazute.\n\n<small>Mergeți la \"Detalii contact\" pentru a verifica abonările la actualizările de prezență.</small></string>


@@ 371,8 372,8 @@
    <string name="error_trustkeys_title">A apărut o problemă</string>
    <string name="fetching_history_from_server">Descarc istoric de pe server</string>
    <string name="no_more_history_on_server">Nu mai exista istoric pe server</string>
    <string name="updating">Actualizare...</string>
    <string name="password_changed">Parolă schimbată</string>
    <string name="updating">Actualizare…</string>
    <string name="password_changed">Parolă schimbată!</string>
    <string name="could_not_change_password">Nu s-a putut schimba parola</string>
    <string name="change_password">Schimbare parolă</string>
    <string name="current_password">Parola curentă</string>


@@ 430,9 431,9 @@
    <string name="sending_x_file">Trimit %s</string>
    <string name="offering_x_file">Ofer %s</string>
    <string name="hide_offline">Ascunde deconectat</string>
    <string name="contact_is_typing">%s tastează...</string>
    <string name="contact_is_typing">%s tastează…</string>
    <string name="contact_has_stopped_typing">%s s-a oprit din scris</string>
    <string name="contacts_are_typing">%s tastează...</string>
    <string name="contacts_are_typing">%s tastează…</string>
    <string name="contacts_have_stopped_typing">%s s-au oprit din scris</string>
    <string name="pref_chat_states">Notificare tastare</string>
    <string name="pref_chat_states_summary">Contactele sunt anunțate atunci când le scrieți un nou mesaj</string>


@@ 470,7 471,7 @@
    <string name="invalid_username">Acesta nu este un nume de utilizator valabil</string>
    <string name="download_failed_server_not_found">Descărcare eșuată: Serverul nu a fost găsit</string>
    <string name="download_failed_file_not_found">Descărcare eșuată: Fișierul nu a fost găsit</string>
    <string name="download_failed_could_not_connect">Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda.</string>
    <string name="download_failed_could_not_connect">Descărcare eșuată: Nu s-a putut realiza conexiunea cu gazda</string>
    <string name="download_failed_could_not_write_file">Descărcare eșuată: Nu s-a putut scrie fișierul</string>
    <string name="download_failed_invalid_file">Descărcarea a eșuat: Fișier invalid</string>
    <string name="account_status_tor_unavailable">Rețeaua Tor nu este disponibilă</string>


@@ 491,7 492,7 @@
    <string name="unable_to_parse_certificate">Nu s-a putut analiza certificatul</string>
    <string name="mam_prefs">Preferințe arhivare</string>
    <string name="server_side_mam_prefs">Preferințe arhivare pe server</string>
    <string name="fetching_mam_prefs">Se descarcă preferințe arhivare. Vă rugăm să așteptați...</string>
    <string name="fetching_mam_prefs">Se descarcă preferințe arhivare. Vă rugăm să așteptați…</string>
    <string name="unable_to_fetch_mam_prefs">Nu s-au putut descărca preferințele de arhivare</string>
    <string name="captcha_required">Text captcha de verificare necesar</string>
    <string name="captcha_hint">Introduceți textul din imaginea de mai sus</string>


@@ 499,7 500,7 @@
    <string name="jid_does_not_match_certificate">Adresa XMPP nu corespunde cu certificatul</string>
    <string name="action_renew_certificate">Înnoiește certificatul</string>
    <string name="error_fetching_omemo_key">Eroare la preluarea cheii OMEMO!</string>
    <string name="verified_omemo_key_with_certificate">Verifica cheia OMEMO cu un certificat</string>
    <string name="verified_omemo_key_with_certificate">Cheia OMEMO s-a verificat cu un certificat!</string>
    <string name="device_does_not_support_certificates">Dispozitivul nu permite selectia unui certificat pentru client!</string>
    <string name="pref_connection_options">Opțiuni conexiune</string>
    <string name="pref_use_tor">Conectare prin Tor</string>


@@ 523,7 524,10 @@
    <string name="no_storage_permission">Permiteți %1$s acces la stocarea externă</string>
    <string name="no_camera_permission">Permiteți %1$s acces la camera foto</string>
    <string name="sync_with_contacts">Sincronizează cu contactele</string>
    <string name="sync_with_contacts_long">%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ă.</string>
    <string name="sync_with_contacts_long">%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ă.</string>
    <string name="sync_with_contacts_quicksy"><![CDATA[Quicksy are nevoie să poată accesa numerele de telefon ale contactelor dumneavoastră pentru a putea sugera posibile contacte ce sunt deja pe Quicksy.<br><br>Nu vom stoca o copie a acestor numere the telefon.\n\nPentru mai multe informații puteți citii <a href="https://quicksy.im/#privacy">politica noastră de confidențialitate</a>.<br><br>Urmează să fiți întrebați dacă doriți să permiteți accesul la contacte.]]></string>
    <string name="notify_on_all_messages">Notifică la toate mesajele</string>
    <string name="notify_only_when_highlighted">Notifică doar atunci când cineva vă menționează numele</string>


@@ 534,8 538,11 @@
    <string name="always">Mereu</string>
    <string name="large_images_only">Doar imaginile mari</string>
    <string name="battery_optimizations_enabled">Optimizare baterie activată</string>
    <string name="battery_optimizations_enabled_explained">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.</string>
    <string name="battery_optimizations_enabled_dialog">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.</string>
    <string name="battery_optimizations_enabled_explained">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.</string>
    <string name="battery_optimizations_enabled_dialog">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.</string>
    <string name="disable">Dezactivează</string>
    <string name="selection_too_large">Zona selectată este prea mare</string>
    <string name="no_accounts">(Nici un cont activat)</string>


@@ 546,10 553,11 @@
    <string name="this_account_is_disabled">Ați dezactivat acest cont</string>
    <string name="security_error_invalid_file_access">Eroare de securitate.: Acces fișier invalid!</string>
    <string name="no_application_to_share_uri">Nu s-a găsit nici o aplicație care să partajeze adresa</string>
    <string name="share_uri_with">Partajează adresa cu...</string>
    <string name="share_uri_with">Partajează adresa cu…</string>
    <string name="welcome_text_quicksy"><![CDATA[Quicksy este o versiune a popularului client XMPP Conversations cu descoperire automată a contactelor.<br><br>Vă înscrieți cu numărul de telefon și Quicksy—pe baza numerelor de telefon din agenda dumneavoastră—vă va sugera automat posibile contacte.<br><br>Înscriindu-vă sunteți de acord cu <a href="https://quicksy.im/#privacy">politica noastră de confidențialitate</a>.]]></string>
    <string name="agree_and_continue">Sunt de acord și continuă</string>
    <string name="magic_create_text">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.</string>
    <string name="magic_create_text">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.</string>
    <string name="your_full_jid_will_be">Adresa dumneavoastră XMPP completă va fi: %s</string>
    <string name="create_account">Creează cont</string>
    <string name="use_own_provider">Folosește furnizorul meu</string>


@@ 567,7 575,7 @@
    <string name="registration_please_wait">Înregistrare eșuată: Încercați din nou mai târziu</string>
    <string name="registration_password_too_weak">Înregistrare eșuată: Parolă nesigură</string>
    <string name="choose_participants">Alegeți participanți</string>
    <string name="creating_conference">Se creează discuția de grup...</string>
    <string name="creating_conference">Se creează discuția de grup…</string>
    <string name="invite_again">Trimite din nou invitația</string>
    <string name="gp_disable">Dezactivată</string>
    <string name="gp_short">Scurtă</string>


@@ 582,7 590,7 @@
    <string name="pref_theme_light">Luminoasă</string>
    <string name="pref_theme_dark">Întunecată</string>
    <string name="unable_to_connect_to_keychain">Nu s-a putut face conectarea la OpenKeychain</string>
    <string name="this_device_is_no_longer_in_use">Acest dispozitiv nu mai este in uz</string>
    <string name="this_device_is_no_longer_in_use">Acest dispozitiv nu mai este în uz</string>
    <string name="type_pc">PC</string>
    <string name="type_phone">Telefon mobil</string>
    <string name="type_tablet">Tabletă</string>


@@ 614,7 622,7 @@
    <string name="barcode_does_not_contain_fingerprints_for_this_conversation">Codul de bare nu conține amprente pentru această conversație.</string>
    <string name="verified_fingerprints">Amprente verificate</string>
    <string name="use_camera_icon_to_scan_barcode">Folosește camera pentru a scana codul de bare al contactului</string>
    <string name="please_wait_for_keys_to_be_fetched">Asteptati cat se preiau cheile</string>
    <string name="please_wait_for_keys_to_be_fetched">Vă rugăm să așteptați până se preiau cheile</string>
    <string name="share_as_barcode">Partajează un cod de bare</string>
    <string name="share_as_uri">Partajează ca adresă XMPP</string>
    <string name="share_as_http">Partajează ca legatură HTTP</string>


@@ 745,7 753,7 @@
    <string name="title_activity_show_location">Arată locația</string>
    <string name="share">Partajare</string>
    <string name="unable_to_start_recording">Nu s-a putut pornii înregistrarea</string>
    <string name="please_wait">Vă rugăm să așteptați...</string>
    <string name="please_wait">Vă rugăm să așteptați…</string>
    <string name="no_microphone_permission">Permiteți %1$s acces la microfon</string>
    <string name="search_messages">Caută mesaje</string>
    <string name="gif">GIF</string>


@@ 800,7 808,7 @@
    <string name="choose_a_country">Alegeți o țară</string>
    <string name="phone_number">număr de telefon</string>
    <string name="verify_your_phone_number">Verificare număr de telefon</string>
    <string name="enter_country_code_and_phone_number">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:</string>
    <string name="enter_country_code_and_phone_number">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:</string>
    <string name="we_will_be_verifying"><![CDATA[Vă vom verifica numărul de telefon<br/><br/><b>%s</b><br/><br/>Este în regulă sau ați dori să editați numărul?]]></string>
    <string name="not_a_valid_phone_number">%s nu este un număr de telefon valid.</string>
    <string name="please_enter_your_phone_number">Vă rugăm să vă introduceți numărul de telefon.</string>


@@ 811,15 819,15 @@
    <string name="please_enter_pin_below">Vă rugăm să introduceți codul de 6 cifre mai jos.</string>
    <string name="resend_sms">Retrimitere SMS</string>
    <string name="resend_sms_in">Retrimite SMS (%s)</string>
    <string name="wait_x">%s</string>
    <string name="wait_x">Vă rugăm să așteptați (%s)</string>
    <string name="back">înapoi</string>
    <string name="possible_pin">S-a copiat automat un posibil cod din memorie</string>
    <string name="possible_pin">S-a copiat automat un posibil cod din memorie.</string>
    <string name="please_enter_pin">Vă rugăm să vă introduceți codul de 6 cifre.</string>
    <string name="abort_registration_procedure">Sigur doriți să anulați procedura de înregistrare?</string>
    <string name="yes">Da</string>
    <string name="no">Nu</string>
    <string name="verifying">Verificare...</string>
    <string name="requesting_sms">Se cere SMS...</string>
    <string name="verifying">Se verifică…</string>
    <string name="requesting_sms">Se cere SMS…</string>
    <string name="incorrect_pin">Codul introdus este incorect.</string>
    <string name="pin_expired">Codul pe care vi l-am trimis a expirat.</string>
    <string name="unknown_api_error_network">Eroare de rețea necunoscută.</string>


@@ 868,7 876,7 @@
    <string name="please_enter_name">Vă rugăm să furnizați un nume pentru canal</string>
    <string name="please_enter_xmpp_address">Vă rugăm să furnizați o adresă XMPP</string>
    <string name="this_is_an_xmpp_address">Aceasta este o adresă XMPP. Vă rugăm să furnizați un nume.</string>
    <string name="creating_channel">Se creează canalul public...</string>
    <string name="creating_channel">Se creează canalul public…</string>
    <string name="channel_already_exists">Acest canal există deja</string>
    <string name="joined_an_existing_channel">V-ați alăturat unui canal existent</string>
    <string name="unable_to_set_channel_configuration">Nu s-a putut salva configurația canalului</string>


@@ 905,7 913,7 @@
    <string name="account_already_setup">Acest cont a fost deja configurat</string>
    <string name="please_enter_password">Va rugăm să introduceți parola pentru acest cont</string>
    <string name="unable_to_perform_this_action">Nu s-a putut realiza această acțiune</string>
    <string name="open_join_dialog">Alătură-te unui canal public...</string>
    <string name="open_join_dialog">Alătură-te unui canal public…</string>
    <string name="sharing_application_not_grant_permission">Aplicația care a partajat nu a permis accesul la acest fișier.</string>
    <string name="group_chats_and_channels"><![CDATA[Discuții de grup & Canale]]></string>
    <string name="jabber_network">jabber.network</string>


@@ 1013,5 1021,10 @@
    <string name="audio_video_disabled_tor">Apelurile sunt dezactivate atunci când utilizați Tor</string>
    <string name="switch_to_video">Comută la video</string>
    <string name="reject_switch_to_video">Respinge solicitarea de comutare la video</string>

</resources>
    <string name="pref_up_push_account_title">Cont XMPP</string>
    <string name="pref_up_push_server_title">Server Push</string>
    <string name="pref_up_push_server_summary">Un server ales de utilizator pentru a intermedia mesajele push către dispozitivul vostru prin XMPP.</string>
    <string name="no_account_deactivated">Nici unul (dezactivat)</string>
    <string name="unified_push_distributor">Distribuitor UnifiedPush</string>
    <string name="pref_up_push_account_summary">Contul prin care vor fi primite notificările push.</string>
</resources>
\ No newline at end of file

M src/main/res/values-zh-rCN/strings.xml => src/main/res/values-zh-rCN/strings.xml +8 -1
@@ 547,7 547,8 @@
    <string name="share_uri_with">分享链接……</string>
    <string name="welcome_text_quicksy"><![CDATA[Quicksy是从受欢迎的XMPP客户端对话中分离出来的,具有自动联系人发现功能<br><br>您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人<br><br>签署即表示您同意我们的<a href="https://quicksy.im/#privacy">隐私政策</a>。]]></string>
    <string name="agree_and_continue">同意并继续</string>
    <string name="magic_create_text">此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。</string>
    <string name="magic_create_text">此向导将为您在conversations.im 上创建一个账户。
\n您的联系人可以通过您的XMPP完整地址与您聊天。</string>
    <string name="your_full_jid_will_be">您的XMPP完整地址将是:%s</string>
    <string name="create_account">创建账户</string>
    <string name="use_own_provider">使用我自己的服务器</string>


@@ 989,4 990,10 @@
    <string name="audio_video_disabled_tor">使用 Tor 时通话被禁用</string>
    <string name="switch_to_video">切换到视频</string>
    <string name="reject_switch_to_video">拒绝切换到视频的请求</string>
    <string name="pref_up_push_account_title">XMPP 账户</string>
    <string name="pref_up_push_server_title">推送服务器</string>
    <string name="no_account_deactivated">无(未激活)</string>
    <string name="unified_push_distributor">UnifiedPush 分发程序</string>
    <string name="pref_up_push_account_summary">将通过该账户接收推送消息。</string>
    <string name="pref_up_push_server_summary">用户选择的推送服务器,通过 XMPP 将推送消息传递到你的设备。</string>
</resources>
\ No newline at end of file

M src/main/res/values/defaults.xml => src/main/res/values/defaults.xml +2 -0
@@ 46,4 46,6 @@
    <string name="default_channel_discovery">JABBER_NETWORK</string>
    <bool name="prevent_screenshots">false</bool>
    <bool name="dialler_integration_incoming">true</bool>
    <string name="default_push_server">up.conversations.im</string>
    <string name="default_push_account">none</string>
</resources>

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

</resources>

M src/main/res/xml/preferences.xml => src/main/res/xml/preferences.xml +15 -0
@@ 212,6 212,21 @@
            android:title="@string/pref_create_backup" />
    </PreferenceCategory>
    <PreferenceCategory
        android:key="unified_push"
        android:title="@string/unified_push_distributor">
        <ListPreference
            android:defaultValue="@string/default_push_account"
            android:key="up_push_account"
            android:summary="@string/pref_up_push_account_summary"
            android:title="@string/pref_up_push_account_title" />
        <EditTextPreference
            android:defaultValue="@string/default_push_server"
            android:key="up_push_server"
            android:summary="@string/pref_up_push_server_summary"
            android:title="@string/pref_up_push_server_title" />

    </PreferenceCategory>
    <PreferenceCategory
        android:key="advanced"
        android:title="@string/pref_advanced_options">
        <CheckBoxPreference

M src/quicksy/res/values-es/strings.xml => src/quicksy/res/values-es/strings.xml +3 -3
@@ 1,10 1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pref_notification_grace_period_summary">Cuánto tiempo Quicksy permanece en silencio después de detectar una actividad en otro dispositivo</string>
    <string name="pref_never_send_crash_summary">Si elige enviar un informe de error, estará ayudando al desarrollo de Quicksy</string>
    <string name="pref_notification_grace_period_summary">Cuánto tiempo Quicksy permanece en silencio después de ver actividad en otros dispositivos</string>
    <string name="pref_never_send_crash_summary">Al enviar los seguimientos del registro, está ayudando al desarrollo de Quicksy</string>
    <string name="pref_broadcast_last_activity_summary">Informar a tus contactos cuando usas Quicksy</string>
    <string name="huawei_protected_apps_summary">Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas.</string>
    <string name="set_profile_picture">Foto de perfil de Quicksy</string>
    <string name="set_profile_picture">Imagen del perfil de Quicksy</string>
    <string name="not_available_in_your_country">Quicksy no está disponible en tu país.</string>
    <string name="unable_to_verify_server_identity">No se ha podido verificar la identidad del servidor.</string>
    <string name="unknown_security_error">Error de seguridad desconocido.</string>

M src/quicksy/res/values-pl/strings.xml => src/quicksy/res/values-pl/strings.xml +3 -3
@@ 1,12 1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="pref_notification_grace_period_summary">Ilość czasu kiedy Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu.</string>
    <string name="pref_notification_grace_period_summary">Czas, przez który Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu</string>
    <string name="pref_never_send_crash_summary">Wysyłając nam ślady stosu pomagasz w rozwoju Quicksy</string>
    <string name="pref_broadcast_last_activity_summary">Powiadom kontakty o tym że używasz Quicksy</string>
    <string name="huawei_protected_apps_summary">Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Quicksy do listy chronionych aplikacji.</string>
    <string name="set_profile_picture">Obrazek profilowy Quicksy</string>
    <string name="not_available_in_your_country">Quicksy nie jest dostępne w twoim kraju</string>
    <string name="not_available_in_your_country">Quicksy nie jest dostępne w Twoim kraju.</string>
    <string name="unable_to_verify_server_identity">Nie udało się sprawdzić tożsamości serwera.</string>
    <string name="unknown_security_error">Nieznany błąd bezpieczeństwa.</string>
    <string name="timeout_while_connecting_to_server">Błąd czasu oczekiwania na połączenie z serwerem.</string>
</resources>
</resources>
\ No newline at end of file

A src/quicksy/res/values-sq/strings.xml => src/quicksy/res/values-sq/strings.xml +2 -0
@@ 0,0 1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
\ No newline at end of file